Repository: hyperium/tonic Branch: master Commit: d9a52ef67970 Files: 479 Total size: 49.9 MB Directory structure: gitextract_d8h12mxx/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── CI.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── codegen/ │ ├── Cargo.toml │ └── src/ │ └── main.rs ├── examples/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── data/ │ │ ├── gcp/ │ │ │ └── roots.pem │ │ ├── route_guide_db.json │ │ └── tls/ │ │ ├── ca.key │ │ ├── ca.pem │ │ ├── client1.key │ │ ├── client1.pem │ │ ├── client2.key │ │ ├── client2.pem │ │ ├── client_ca.key │ │ ├── client_ca.pem │ │ ├── create.sh │ │ ├── openssl.cnf │ │ ├── server.key │ │ ├── server.pem │ │ ├── server2.key │ │ └── server2.pem │ ├── helloworld-tutorial.md │ ├── proto/ │ │ ├── attrs/ │ │ │ └── attrs.proto │ │ ├── echo/ │ │ │ └── echo.proto │ │ ├── googleapis/ │ │ │ └── google/ │ │ │ ├── api/ │ │ │ │ ├── annotations.proto │ │ │ │ ├── client.proto │ │ │ │ ├── field_behavior.proto │ │ │ │ ├── http.proto │ │ │ │ └── resource.proto │ │ │ └── pubsub/ │ │ │ └── v1/ │ │ │ ├── pubsub.proto │ │ │ └── schema.proto │ │ ├── helloworld/ │ │ │ └── helloworld.proto │ │ ├── routeguide/ │ │ │ └── route_guide.proto │ │ └── unaryecho/ │ │ └── echo.proto │ ├── routeguide-tutorial.md │ └── src/ │ ├── authentication/ │ │ ├── client.rs │ │ └── server.rs │ ├── autoreload/ │ │ └── server.rs │ ├── blocking/ │ │ ├── client.rs │ │ └── server.rs │ ├── cancellation/ │ │ ├── client.rs │ │ └── server.rs │ ├── codec_buffers/ │ │ ├── client.rs │ │ ├── common.rs │ │ └── server.rs │ ├── compression/ │ │ ├── client.rs │ │ └── server.rs │ ├── dynamic/ │ │ └── server.rs │ ├── dynamic_load_balance/ │ │ ├── client.rs │ │ └── server.rs │ ├── gcp/ │ │ ├── README.md │ │ └── client.rs │ ├── grpc-web/ │ │ ├── client.rs │ │ └── server.rs │ ├── h2c/ │ │ ├── client.rs │ │ └── server.rs │ ├── health/ │ │ ├── README.md │ │ └── server.rs │ ├── helloworld/ │ │ ├── client.rs │ │ └── server.rs │ ├── interceptor/ │ │ ├── client.rs │ │ └── server.rs │ ├── json-codec/ │ │ ├── client.rs │ │ ├── common.rs │ │ └── server.rs │ ├── load_balance/ │ │ ├── client.rs │ │ └── server.rs │ ├── mock/ │ │ └── mock.rs │ ├── multiplex/ │ │ ├── client.rs │ │ └── server.rs │ ├── reflection/ │ │ └── server.rs │ ├── richer-error/ │ │ ├── client.rs │ │ ├── client_vec.rs │ │ ├── server.rs │ │ └── server_vec.rs │ ├── routeguide/ │ │ ├── client.rs │ │ ├── data.rs │ │ └── server.rs │ ├── streaming/ │ │ ├── client.rs │ │ └── server.rs │ ├── tls/ │ │ ├── client.rs │ │ └── server.rs │ ├── tls_client_auth/ │ │ ├── client.rs │ │ └── server.rs │ ├── tls_rustls/ │ │ ├── client.rs │ │ └── server.rs │ ├── tower/ │ │ ├── client.rs │ │ └── server.rs │ ├── tracing/ │ │ ├── client.rs │ │ └── server.rs │ └── uds/ │ ├── client_standard.rs │ ├── client_with_connector.rs │ └── server.rs ├── flake.nix ├── grpc/ │ ├── Cargo.toml │ ├── examples/ │ │ └── inmemory.rs │ ├── proto/ │ │ └── echo/ │ │ └── echo.proto │ └── src/ │ ├── attributes/ │ │ ├── linked_list.rs │ │ └── mod.rs │ ├── byte_str.rs │ ├── client/ │ │ ├── channel.rs │ │ ├── interceptor.rs │ │ ├── load_balancing/ │ │ │ ├── child_manager.rs │ │ │ ├── graceful_switch.rs │ │ │ ├── mod.rs │ │ │ ├── pick_first.rs │ │ │ ├── registry.rs │ │ │ ├── round_robin.rs │ │ │ └── test_utils.rs │ │ ├── mod.rs │ │ ├── name_resolution/ │ │ │ ├── backoff.rs │ │ │ ├── dns/ │ │ │ │ ├── mod.rs │ │ │ │ └── test.rs │ │ │ ├── mod.rs │ │ │ └── registry.rs │ │ ├── service_config.rs │ │ ├── stream_util.rs │ │ ├── subchannel.rs │ │ └── transport/ │ │ ├── mod.rs │ │ ├── registry.rs │ │ └── tonic/ │ │ ├── mod.rs │ │ └── test.rs │ ├── core/ │ │ └── mod.rs │ ├── credentials/ │ │ ├── call.rs │ │ ├── client.rs │ │ ├── dyn_wrapper.rs │ │ ├── insecure.rs │ │ ├── local.rs │ │ ├── mod.rs │ │ ├── rustls/ │ │ │ ├── client/ │ │ │ │ ├── mod.rs │ │ │ │ └── test.rs │ │ │ ├── key_log.rs │ │ │ ├── mod.rs │ │ │ ├── server/ │ │ │ │ ├── mod.rs │ │ │ │ └── test.rs │ │ │ └── tls_stream.rs │ │ └── server.rs │ ├── generated/ │ │ ├── echo_fds.rs │ │ └── grpc_examples_echo.rs │ ├── inmemory/ │ │ └── mod.rs │ ├── lib.rs │ ├── macros.rs │ ├── rt/ │ │ ├── hyper_wrapper.rs │ │ ├── mod.rs │ │ └── tokio/ │ │ ├── hickory_resolver.rs │ │ └── mod.rs │ ├── send_future.rs │ ├── server/ │ │ └── mod.rs │ ├── status/ │ │ ├── server_status.rs │ │ └── status_code.rs │ └── status.rs ├── interop/ │ ├── Cargo.toml │ ├── bin/ │ │ ├── client_darwin_amd64 │ │ ├── client_linux_amd64 │ │ ├── server_darwin_amd64 │ │ └── server_linux_amd64 │ ├── build.rs │ ├── data/ │ │ ├── README.md │ │ ├── ca.pem │ │ ├── cert-generator/ │ │ │ ├── .gitignore │ │ │ ├── ca.tf │ │ │ └── server_certs.tf │ │ ├── server1.key │ │ └── server1.pem │ ├── proto/ │ │ └── grpc/ │ │ └── testing/ │ │ ├── empty.proto │ │ ├── messages.proto │ │ └── test.proto │ ├── src/ │ │ ├── bin/ │ │ │ ├── client.rs │ │ │ └── server.rs │ │ ├── client.rs │ │ ├── client_prost.rs │ │ ├── client_protobuf.rs │ │ ├── lib.rs │ │ ├── server_prost.rs │ │ └── server_protobuf.rs │ ├── test.sh │ └── update_binaries.sh ├── prepare-release.sh ├── protoc-gen-rust-grpc/ │ ├── .bazelrc │ ├── .gitignore │ ├── CMakeLists.txt │ ├── README.md │ ├── cmake/ │ │ └── FetchProtobuf.cmake │ └── src/ │ ├── BUILD │ ├── grpc_rust_generator.cc │ ├── grpc_rust_generator.h │ └── grpc_rust_plugin.cc ├── publish-release.sh ├── tests/ │ ├── compile/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── proto/ │ │ │ ├── ambiguous_methods.proto │ │ │ ├── includee.proto │ │ │ ├── includer.proto │ │ │ ├── result.proto │ │ │ ├── root_crate_path.proto │ │ │ ├── same_name.proto │ │ │ ├── service.proto │ │ │ ├── skip_debug.proto │ │ │ ├── stream.proto │ │ │ └── use_arc_self.proto │ │ ├── src/ │ │ │ └── lib.rs │ │ └── tests/ │ │ ├── ui/ │ │ │ ├── ambiguous_methods.rs │ │ │ ├── includer.rs │ │ │ ├── result.rs │ │ │ ├── root_file_path.rs │ │ │ ├── same_name.rs │ │ │ ├── service.rs │ │ │ ├── skip_debug.rs │ │ │ ├── stream.rs │ │ │ └── use_arc_self.rs │ │ └── ui.rs │ ├── compression/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── proto/ │ │ │ └── test.proto │ │ └── src/ │ │ ├── bidirectional_stream.rs │ │ ├── client_stream.rs │ │ ├── compressing_request.rs │ │ ├── compressing_response.rs │ │ ├── lib.rs │ │ ├── server_stream.rs │ │ └── util.rs │ ├── default_stubs/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── proto/ │ │ │ ├── test.proto │ │ │ └── test_default.proto │ │ ├── src/ │ │ │ └── lib.rs │ │ └── tests/ │ │ └── default.rs │ ├── deprecated_methods/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── proto/ │ │ │ └── test.proto │ │ ├── src/ │ │ │ └── lib.rs │ │ └── tests/ │ │ └── deprecated_methods.rs │ ├── disable_comments/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── proto/ │ │ │ └── test.proto │ │ ├── src/ │ │ │ └── lib.rs │ │ └── tests/ │ │ └── disable_comments.rs │ ├── extern_path/ │ │ ├── my_application/ │ │ │ ├── Cargo.toml │ │ │ ├── build.rs │ │ │ └── src/ │ │ │ └── main.rs │ │ ├── proto/ │ │ │ ├── my_application/ │ │ │ │ └── service.proto │ │ │ └── uuid/ │ │ │ └── uuid.proto │ │ └── uuid/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ └── lib.rs │ ├── integration_tests/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── proto/ │ │ │ ├── stream.proto │ │ │ └── test.proto │ │ ├── src/ │ │ │ └── lib.rs │ │ └── tests/ │ │ ├── client_layer.rs │ │ ├── complex_tower_middleware.rs │ │ ├── connect_info.rs │ │ ├── connection.rs │ │ ├── extensions.rs │ │ ├── http2_keep_alive.rs │ │ ├── http2_max_header_list_size.rs │ │ ├── interceptor.rs │ │ ├── load_shed.rs │ │ ├── max_message_size.rs │ │ ├── origin.rs │ │ ├── routes_builder.rs │ │ ├── status.rs │ │ ├── streams.rs │ │ ├── timeout.rs │ │ └── user_agent.rs │ ├── web/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── proto/ │ │ │ └── test.proto │ │ ├── src/ │ │ │ └── lib.rs │ │ └── tests/ │ │ ├── grpc.rs │ │ └── grpc_web.rs │ ├── wellknown/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── proto/ │ │ │ └── wellknown.proto │ │ └── src/ │ │ └── lib.rs │ └── wellknown-compiled/ │ ├── Cargo.toml │ ├── build.rs │ ├── proto/ │ │ ├── google.proto │ │ └── test.proto │ └── src/ │ └── lib.rs ├── tonic/ │ ├── Cargo.toml │ ├── benches/ │ │ └── decode.rs │ ├── benches-disabled/ │ │ ├── README.md │ │ ├── bench_main.rs │ │ ├── benchmarks/ │ │ │ ├── compiled_protos/ │ │ │ │ ├── diverse_types.rs │ │ │ │ ├── helloworld.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── request_response.rs │ │ │ ├── request_response_diverse_types.rs │ │ │ └── utils.rs │ │ └── proto/ │ │ ├── diverse_types/ │ │ │ └── diverse_types.proto │ │ └── helloworld/ │ │ └── helloworld.proto │ └── src/ │ ├── body.rs │ ├── client/ │ │ ├── grpc.rs │ │ ├── mod.rs │ │ └── service.rs │ ├── codec/ │ │ ├── buffer.rs │ │ ├── compression.rs │ │ ├── decode.rs │ │ ├── encode.rs │ │ └── mod.rs │ ├── codegen.rs │ ├── extensions.rs │ ├── lib.rs │ ├── macros.rs │ ├── metadata/ │ │ ├── encoding.rs │ │ ├── key.rs │ │ ├── map.rs │ │ ├── mod.rs │ │ └── value.rs │ ├── request.rs │ ├── response.rs │ ├── server/ │ │ ├── grpc.rs │ │ ├── mod.rs │ │ └── service.rs │ ├── service/ │ │ ├── interceptor.rs │ │ ├── layered.rs │ │ ├── mod.rs │ │ ├── recover_error.rs │ │ └── router.rs │ ├── status.rs │ ├── transport/ │ │ ├── channel/ │ │ │ ├── endpoint.rs │ │ │ ├── mod.rs │ │ │ ├── service/ │ │ │ │ ├── add_origin.rs │ │ │ │ ├── connection.rs │ │ │ │ ├── connector.rs │ │ │ │ ├── discover.rs │ │ │ │ ├── executor.rs │ │ │ │ ├── io.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── reconnect.rs │ │ │ │ ├── tls.rs │ │ │ │ └── user_agent.rs │ │ │ ├── tls.rs │ │ │ └── uds_connector.rs │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── server/ │ │ │ ├── conn.rs │ │ │ ├── display_error_stack.rs │ │ │ ├── incoming.rs │ │ │ ├── io_stream.rs │ │ │ ├── mod.rs │ │ │ ├── service/ │ │ │ │ ├── io.rs │ │ │ │ ├── mod.rs │ │ │ │ └── tls.rs │ │ │ ├── tls.rs │ │ │ └── unix.rs │ │ ├── service/ │ │ │ ├── grpc_timeout.rs │ │ │ ├── mod.rs │ │ │ └── tls.rs │ │ └── tls.rs │ └── util.rs ├── tonic-build/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── client.rs │ ├── code_gen.rs │ ├── lib.rs │ ├── manual.rs │ └── server.rs ├── tonic-health/ │ ├── Cargo.toml │ ├── README.md │ ├── proto/ │ │ └── health.proto │ └── src/ │ ├── generated/ │ │ ├── grpc_health_v1.rs │ │ └── grpc_health_v1_fds.rs │ ├── lib.rs │ └── server.rs ├── tonic-prost/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── codec.rs │ └── lib.rs ├── tonic-prost-build/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── lib.rs │ └── tests.rs ├── tonic-protobuf/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── tonic-protobuf-build/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ └── src/ │ └── lib.rs ├── tonic-reflection/ │ ├── Cargo.toml │ ├── README.md │ ├── proto/ │ │ ├── reflection_v1.proto │ │ └── reflection_v1alpha.proto │ ├── src/ │ │ ├── generated/ │ │ │ ├── grpc_reflection_v1.rs │ │ │ ├── grpc_reflection_v1alpha.rs │ │ │ ├── reflection_v1_fds.rs │ │ │ └── reflection_v1alpha1_fds.rs │ │ ├── lib.rs │ │ └── server/ │ │ ├── mod.rs │ │ ├── v1.rs │ │ └── v1alpha.rs │ └── tests/ │ ├── server.rs │ └── versions.rs ├── tonic-types/ │ ├── Cargo.toml │ ├── README.md │ ├── proto/ │ │ ├── error_details.proto │ │ └── status.proto │ └── src/ │ ├── generated/ │ │ ├── google_rpc.rs │ │ └── types_fds.rs │ ├── lib.rs │ └── richer_error/ │ ├── error_details/ │ │ ├── mod.rs │ │ └── vec.rs │ ├── mod.rs │ └── std_messages/ │ ├── bad_request.rs │ ├── debug_info.rs │ ├── error_info.rs │ ├── help.rs │ ├── loc_message.rs │ ├── mod.rs │ ├── prec_failure.rs │ ├── quota_failure.rs │ ├── request_info.rs │ ├── resource_info.rs │ └── retry_info.rs ├── tonic-web/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── call.rs │ ├── client.rs │ ├── layer.rs │ ├── lib.rs │ └── service.rs ├── tonic-xds/ │ ├── Cargo.toml │ ├── examples/ │ │ └── gen_test_proto.rs │ ├── proto/ │ │ └── test/ │ │ └── helloworld.proto │ └── src/ │ ├── client/ │ │ ├── channel.rs │ │ ├── cluster.rs │ │ ├── endpoint.rs │ │ ├── lb.rs │ │ ├── mod.rs │ │ └── route.rs │ ├── common/ │ │ ├── async_util.rs │ │ └── mod.rs │ ├── lib.rs │ ├── testutil/ │ │ ├── grpc.rs │ │ ├── mod.rs │ │ └── proto/ │ │ ├── helloworld.rs │ │ └── mod.rs │ └── xds/ │ ├── bootstrap.rs │ ├── mod.rs │ ├── resource/ │ │ ├── cluster.rs │ │ ├── endpoints.rs │ │ ├── listener.rs │ │ ├── mod.rs │ │ └── route_config.rs │ ├── routing.rs │ ├── uri.rs │ └── xds_manager.rs └── xds-client/ ├── Cargo.toml ├── examples/ │ └── basic.rs └── src/ ├── client/ │ ├── config.rs │ ├── mod.rs │ ├── retry.rs │ ├── watch.rs │ └── worker.rs ├── codec/ │ ├── mod.rs │ └── prost.rs ├── error.rs ├── lib.rs ├── message.rs ├── resource/ │ ├── mod.rs │ └── prost.rs ├── runtime/ │ ├── mod.rs │ └── tokio.rs └── transport/ ├── mod.rs └── tonic.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: 🐛 Bug Report about: If something isn't working as expected 🤔. --- ## Bug Report ### Version ### Platform ### Crates ### Description ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: 💡 Feature Request about: I have a suggestion (and may want to implement it 🙂)! --- ## Feature Request ### Crates ### Motivation ### Proposal ### Alternatives ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Motivation ## Solution ================================================ FILE: .github/workflows/CI.yml ================================================ name: CI on: push: pull_request: {} merge_group: branches: [ "master" ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: rustfmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: hecrj/setup-rust-action@v2 with: components: rustfmt - run: cargo fmt --all --check build-protoc-plugin: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] outputs: cache-hit: ${{ steps.cache-plugin.outputs.cache-hit }} steps: - uses: actions/checkout@v6 - name: Cache protoc and plugin id: cache-plugin uses: actions/cache@v4 with: path: ${{ runner.temp }}/protoc-plugin # The key changes only when plugin source files or CMake files change key: ${{ runner.os }}-protoc-plugin-cmake-v4-${{ hashFiles('protoc-gen-rust-grpc/src/**', 'protoc-gen-rust-grpc/CMakeLists.txt', 'protoc-gen-rust-grpc/cmake/**') }} - name: Install CMake if: steps.cache-plugin.outputs.cache-hit != 'true' uses: lukka/get-cmake@latest with: cmakeVersion: "~3.28.0" # Building the protoc plugin from scratch takes 6–14 minutes, depending on # the OS. This delays the execution of workflows that use the plugin in # build.rs files. We try to avoid rebuilding the plugin if it hasn't # changed. - name: Build protoc plugin if: steps.cache-plugin.outputs.cache-hit != 'true' working-directory: ./protoc-gen-rust-grpc shell: bash run: | set -e # Create build directory mkdir -p build cd build # Configure with CMake cmake .. -DCMAKE_BUILD_TYPE=Release -DPROTOBUF_VERSION=33.0 # Build with limited parallelism to avoid OOM cmake --build . --parallel 2 # The target path needs to match the cache config. TARGET_PATH="${{ runner.temp }}/protoc-plugin" mkdir -p "${TARGET_PATH}" # Copy both protoc and the plugin # First, find and copy protoc PROTOC_FOUND=false for protoc_path in "bin/protoc" "bin/protoc.exe" "bin/Release/protoc.exe" "bin/Debug/protoc.exe" "_deps/protobuf-build/protoc" "_deps/protobuf-build/protoc.exe" "_deps/protobuf-build/Release/protoc.exe" "_deps/protobuf-build/Debug/protoc.exe"; do if [ -f "$protoc_path" ]; then echo "Found protoc at: $protoc_path" # Copy with explicit name to ensure it's called 'protoc' or 'protoc.exe' if [[ "$protoc_path" == *.exe ]]; then cp "$protoc_path" "${TARGET_PATH}/protoc.exe" echo "Copied to: ${TARGET_PATH}/protoc.exe" else cp "$protoc_path" "${TARGET_PATH}/protoc" chmod +x "${TARGET_PATH}/protoc" echo "Copied to: ${TARGET_PATH}/protoc" fi PROTOC_FOUND=true break fi done if [ "$PROTOC_FOUND" = "false" ]; then echo "Error: protoc not found in expected locations" echo "Searching for protoc in build directory:" find . -name "protoc" -o -name "protoc.exe" | head -20 exit 1 fi # Copy protoc with its standard installation structure echo "Setting up protoc installation..." # protoc expects to find includes relative to its binary location # Standard structure: bin/protoc and include/google/protobuf/*.proto # First check if CMake created an install directory if [ -d "install" ] && [ -f "install/bin/protoc" -o -f "install/bin/protoc.exe" ]; then echo "Found CMake install directory" cp -r install/* "${TARGET_PATH}/" else # Manual setup if no install directory echo "Creating manual protoc installation structure..." # The protoc binary should already be copied to TARGET_PATH # Now find and copy the include files to the correct relative location mkdir -p "${TARGET_PATH}/include" # Find the protobuf include files INCLUDE_FOUND=false for include_path in "_deps/protobuf-src/src" "_deps/protobuf-build/include" "include"; do if [ -d "$include_path/google/protobuf" ] && [ -f "$include_path/google/protobuf/descriptor.proto" ]; then echo "Found protobuf includes at: $include_path" cp -r "$include_path/google" "${TARGET_PATH}/include/" INCLUDE_FOUND=true break fi done if [ "$INCLUDE_FOUND" = "false" ]; then echo "Warning: Could not find protobuf include files" echo "Searching for descriptor.proto:" find . -name "descriptor.proto" -type f | grep -v "test" | head -10 fi fi # Then copy the plugin (handle different output locations) if [ -f "bin/protoc-gen-rust-grpc" ]; then cp bin/protoc-gen-rust-grpc "${TARGET_PATH}/" elif [ -f "bin/protoc-gen-rust-grpc.exe" ]; then cp bin/protoc-gen-rust-grpc.exe "${TARGET_PATH}/" elif [ -f "bin/Release/protoc-gen-rust-grpc.exe" ]; then # Windows Release build cp bin/Release/protoc-gen-rust-grpc.exe "${TARGET_PATH}/" elif [ -f "bin/Debug/protoc-gen-rust-grpc.exe" ]; then # Windows Debug build (shouldn't happen with Release config, but just in case) cp bin/Debug/protoc-gen-rust-grpc.exe "${TARGET_PATH}/" else echo "Error: protoc-gen-rust-grpc not found" echo "Looking for binary in common locations..." find . -name "protoc-gen-rust-grpc*" -type f 2>/dev/null | head -10 exit 1 fi clippy: runs-on: ubuntu-latest needs: build-protoc-plugin steps: - uses: actions/checkout@v6 - uses: hecrj/setup-rust-action@v2 with: components: clippy - name: Restore protoc and plugin from cache id: cache-plugin uses: actions/cache@v4 with: path: ${{ runner.temp }}/protoc-plugin key: ${{ runner.os }}-protoc-plugin-cmake-v4-${{ hashFiles('protoc-gen-rust-grpc/src/**', 'protoc-gen-rust-grpc/CMakeLists.txt', 'protoc-gen-rust-grpc/cmake/**') }} - name: Add protoc and plugin to PATH shell: bash run: | # Use forward slashes for all paths in bash, even on Windows PROTOC_DIR="${{ runner.temp }}/protoc-plugin" PROTOC_DIR="${PROTOC_DIR//\\/\/}" # Convert backslashes to forward slashes echo "${PROTOC_DIR}" >> $GITHUB_PATH # Also set PROTOC for build scripts if [ "${{ runner.os }}" = "Windows" ]; then echo "PROTOC=${PROTOC_DIR}/protoc.exe" >> $GITHUB_ENV else echo "PROTOC=${PROTOC_DIR}/protoc" >> $GITHUB_ENV fi # Set the protoc include path only if it exists if [ -d "${PROTOC_DIR}/include" ]; then echo "PROTOC_INCLUDE=${PROTOC_DIR}/include" >> $GITHUB_ENV fi - uses: Swatinem/rust-cache@v2 - run: cargo clippy --workspace --all-features --all-targets codegen: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: hecrj/setup-rust-action@v2 - uses: Swatinem/rust-cache@v2 - run: cargo run --package codegen - run: git diff --exit-code udeps: runs-on: ubuntu-latest needs: build-protoc-plugin steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly-2026-02-22 - uses: taiki-e/install-action@cargo-hack - uses: taiki-e/install-action@cargo-udeps - name: Restore protoc and plugin from cache id: cache-plugin uses: actions/cache@v4 with: path: ${{ runner.temp }}/protoc-plugin key: ${{ runner.os }}-protoc-plugin-cmake-v4-${{ hashFiles('protoc-gen-rust-grpc/src/**', 'protoc-gen-rust-grpc/CMakeLists.txt', 'protoc-gen-rust-grpc/cmake/**') }} - name: Add protoc and plugin to PATH shell: bash run: | # Use forward slashes for all paths in bash, even on Windows PROTOC_DIR="${{ runner.temp }}/protoc-plugin" PROTOC_DIR="${PROTOC_DIR//\\/\/}" # Convert backslashes to forward slashes echo "${PROTOC_DIR}" >> $GITHUB_PATH # Also set PROTOC for build scripts if [ "${{ runner.os }}" = "Windows" ]; then echo "PROTOC=${PROTOC_DIR}/protoc.exe" >> $GITHUB_ENV else echo "PROTOC=${PROTOC_DIR}/protoc" >> $GITHUB_ENV fi # Set the protoc include path only if it exists if [ -d "${PROTOC_DIR}/include" ]; then echo "PROTOC_INCLUDE=${PROTOC_DIR}/include" >> $GITHUB_ENV fi - uses: Swatinem/rust-cache@v2 - run: cargo hack udeps --workspace --exclude-features=_tls-any,tls,tls-aws-lc,tls-ring,tls-connect-info --each-feature - run: cargo udeps --package tonic --features tls-ring,transport - run: cargo udeps --package tonic --features tls-ring,server - run: cargo udeps --package tonic --features tls-ring,channel - run: cargo udeps --package tonic --features tls-aws-lc,transport - run: cargo udeps --package tonic --features tls-aws-lc,server - run: cargo udeps --package tonic --features tls-aws-lc,channel - run: cargo udeps --package tonic --features tls-connect-info check: runs-on: ${{ matrix.os }} needs: build-protoc-plugin strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] env: RUSTFLAGS: "-D warnings" steps: - uses: actions/checkout@v6 - uses: hecrj/setup-rust-action@v2 - uses: taiki-e/install-action@cargo-hack - name: Restore protoc and plugin from cache id: cache-plugin uses: actions/cache@v4 with: path: ${{ runner.temp }}/protoc-plugin key: ${{ runner.os }}-protoc-plugin-cmake-v4-${{ hashFiles('protoc-gen-rust-grpc/src/**', 'protoc-gen-rust-grpc/CMakeLists.txt', 'protoc-gen-rust-grpc/cmake/**') }} - name: Add protoc and plugin to PATH shell: bash run: | # Use forward slashes for all paths in bash, even on Windows PROTOC_DIR="${{ runner.temp }}/protoc-plugin" PROTOC_DIR="${PROTOC_DIR//\\/\/}" # Convert backslashes to forward slashes echo "${PROTOC_DIR}" >> $GITHUB_PATH # Also set PROTOC for build scripts if [ "${{ runner.os }}" = "Windows" ]; then echo "PROTOC=${PROTOC_DIR}/protoc.exe" >> $GITHUB_ENV else echo "PROTOC=${PROTOC_DIR}/protoc" >> $GITHUB_ENV fi # Set the protoc include path only if it exists if [ -d "${PROTOC_DIR}/include" ]; then echo "PROTOC_INCLUDE=${PROTOC_DIR}/include" >> $GITHUB_ENV fi - uses: Swatinem/rust-cache@v2 - name: Check features run: cargo hack check --workspace --no-private --each-feature --no-dev-deps - name: Check tonic feature powerset run: cargo hack check --package tonic --feature-powerset --depth 2 - name: Check all targets run: cargo check --workspace --all-targets --all-features msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: hecrj/setup-rust-action@v2 - name: Resolve MSRV aware dependencies run: cargo update env: CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback - name: Get MSRV from manifest file id: msrv run: echo "version=$(yq '.workspace.package.rust-version' Cargo.toml)" >> "$GITHUB_OUTPUT" - uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ steps.msrv.outputs.version }} - uses: taiki-e/install-action@cargo-no-dev-deps - uses: Swatinem/rust-cache@v2 # we exlude crates that do not use rust-version = { workspace = true } - run: cargo no-dev-deps --no-private check --all-features --workspace --exclude grpc --exclude tonic-protobuf\* doc: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@nightly - uses: dtolnay/install@cargo-docs-rs - uses: taiki-e/install-action@cargo-hack - uses: Swatinem/rust-cache@v2 - run: cargo +nightly hack --no-private docs-rs env: RUSTDOCFLAGS: "-D warnings" test: runs-on: ${{ matrix.os }} needs: build-protoc-plugin strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v6 - uses: hecrj/setup-rust-action@v2 - name: Restore protoc and plugin from cache id: cache-plugin uses: actions/cache@v4 with: path: ${{ runner.temp }}/protoc-plugin key: ${{ runner.os }}-protoc-plugin-cmake-v4-${{ hashFiles('protoc-gen-rust-grpc/src/**', 'protoc-gen-rust-grpc/CMakeLists.txt', 'protoc-gen-rust-grpc/cmake/**') }} - name: Check cache status if: steps.cache-plugin.outputs.cache-hit != 'true' run: | echo "ERROR: Cache miss! The protoc plugin was not found in cache." echo "This means the build-protoc-plugin job either failed or didn't run." exit 1 - name: Add protoc and plugin to PATH shell: bash run: | # Use forward slashes for all paths in bash, even on Windows PROTOC_DIR="${{ runner.temp }}/protoc-plugin" PROTOC_DIR="${PROTOC_DIR//\\/\/}" # Convert backslashes to forward slashes echo "${PROTOC_DIR}" >> $GITHUB_PATH # Also set PROTOC for build scripts if [ "${{ runner.os }}" = "Windows" ]; then echo "PROTOC=${PROTOC_DIR}/protoc.exe" >> $GITHUB_ENV else echo "PROTOC=${PROTOC_DIR}/protoc" >> $GITHUB_ENV fi # Set the protoc include path only if it exists if [ -d "${PROTOC_DIR}/include" ]; then echo "PROTOC_INCLUDE=${PROTOC_DIR}/include" >> $GITHUB_ENV fi - uses: taiki-e/install-action@cargo-hack - uses: taiki-e/install-action@cargo-nextest - uses: Swatinem/rust-cache@v2 - run: cargo nextest run --workspace --all-features env: QUICKCHECK_TESTS: 1000 # run a lot of quickcheck iterations doc-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: hecrj/setup-rust-action@v2 - uses: taiki-e/install-action@cargo-hack - uses: Swatinem/rust-cache@v2 - run: cargo hack --no-private test --doc --all-features interop: name: Interop Tests runs-on: ${{ matrix.os }} needs: build-protoc-plugin strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v6 - uses: hecrj/setup-rust-action@v2 - name: Restore protoc and plugin from cache id: cache-plugin uses: actions/cache@v4 with: path: ${{ runner.temp }}/protoc-plugin key: ${{ runner.os }}-protoc-plugin-cmake-v4-${{ hashFiles('protoc-gen-rust-grpc/src/**', 'protoc-gen-rust-grpc/CMakeLists.txt', 'protoc-gen-rust-grpc/cmake/**') }} - name: Add protoc and plugin to PATH shell: bash run: | # Use forward slashes for all paths in bash, even on Windows PROTOC_DIR="${{ runner.temp }}/protoc-plugin" PROTOC_DIR="${PROTOC_DIR//\\/\/}" # Convert backslashes to forward slashes echo "${PROTOC_DIR}" >> $GITHUB_PATH # Also set PROTOC for build scripts if [ "${{ runner.os }}" = "Windows" ]; then echo "PROTOC=${PROTOC_DIR}/protoc.exe" >> $GITHUB_ENV else echo "PROTOC=${PROTOC_DIR}/protoc" >> $GITHUB_ENV fi # Set the protoc include path only if it exists if [ -d "${PROTOC_DIR}/include" ]; then echo "PROTOC_INCLUDE=${PROTOC_DIR}/include" >> $GITHUB_ENV fi - uses: Swatinem/rust-cache@v2 - name: Run interop tests run: ./interop/test.sh shell: bash - name: Run interop tests with Rustls run: ./interop/test.sh --use_tls tls_rustls shell: bash semver: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: obi1kenobi/cargo-semver-checks-action@v2 with: feature-group: all-features external-types: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: nightly-2025-10-18 - name: Install cargo-check-external-types uses: taiki-e/cache-cargo-install-action@v2 with: tool: cargo-check-external-types@0.4.0 - uses: taiki-e/install-action@cargo-hack - uses: Swatinem/rust-cache@v2 - run: cargo hack --no-private check-external-types --all-features env: RUSTFLAGS: "-D warnings" ================================================ FILE: .gitignore ================================================ target/ **/*.rs.bk Cargo.lock tags ================================================ FILE: CHANGELOG.md ================================================ # NOTE: ths changelog is no longer used and from version `v0.13.0` onward we will be using github releases and the changes can be found [here](https://github.com/hyperium/tonic/releases). # [0.12.3](https://github.com/hyperium/tonic/compare/v0.12.2...v0.12.3) (2024-08-29) ### Features * **server:** Added support for grpc max_connection_age (#1865) * **build:** Add `#[deprecated]` to deprecated client methods (#1879) * **build:** plumb skip_debug through prost Builder and add test (#1900) ### Bug Fixes * **build:** Revert "fix tonic-build cargo build script outputs (#1821)" which accidentally increases MSRV (#1898) * **server:** ignore more error kinds in incoming socket stream (#1885) * **transport**: do not shutdown server on broken connections (#1948) # [0.12.2](https://github.com/hyperium/tonic/compare/v0.12.1...v0.12.2) (2024-08-23) ### Features * Move TimeoutExpired out of transport (#1826) * Move ConnectError type from transport (#1828) * **channel:** allow setting max_header_list_size (#1835) * **router:** Add RoutesBuilder constructor (#1855) * **tls:** Rename tls-roots feature with tls-native-roots (#1860) * **router:** Rename Routes::into_router with into_axum_router (#1862) * **router:** Implement from axum::Router for Routes (#1863) * **channel:** Re-enable TLS based on Cargo features in generated clients (#1866) * **server:** allow setting max_header_list_size (#1870) * **build:** Expose formatted service name (#1684) * **reflection:** add back support for v1alpha reflection protocol (#1888) ### Bug Fixes * **router:** Add missing unimplemented fallback to RoutesBuilder (#1864) * **server:** Prevent server from exiting on ECONNABORTED (#1874) * **web:** fix panic in trailer parsing on multiple trailers (#1880) * **web:** fix empty trailer parsing causing infinite parser loop (#1883) # [0.12.1](https://github.com/hyperium/tonic/compare/v0.12.0...v0.12.1) (2024-07-17) ### Bug Fixes * Reduce tokio-stream feature (#1795) # [0.12.0](https://github.com/hyperium/tonic/compare/v0.11.0...v0.12.0) (2024-07-08) This breaking release updates tonic to the hyper `1.0` ecosystem and also updates to prost `v0.13.0`. ### Features * **build:** Custom codecs for generated code ([#1599](https://github.com/hyperium/tonic/issues/1599)) ([18a2b30](https://github.com/hyperium/tonic/commit/18a2b30922460be02829706cf9dd0cd1ec6a19c1)) * **channel:** Make channel feature additive ([#1574](https://github.com/hyperium/tonic/issues/1574)) ([b947e1a](https://github.com/hyperium/tonic/commit/b947e1ac0727ceb0a0267a30854ada4ba18931db)) * **codec:** Make error when not utf8 value in compression encoding ([#1768](https://github.com/hyperium/tonic/issues/1768)) ([f8e1f87](https://github.com/hyperium/tonic/commit/f8e1f87eb862676147fd6215b58c9090d259104d)) * Implement http_body::Body::size_hint for custom body ([#1713](https://github.com/hyperium/tonic/issues/1713)) ([9728c01](https://github.com/hyperium/tonic/commit/9728c01132bd64dca046675198edc751c4547966)) * Make boxed function public ([#1754](https://github.com/hyperium/tonic/issues/1754)) ([2cc868f](https://github.com/hyperium/tonic/commit/2cc868f80b20379d6635ac182f523b4971d016b7)) * Relax GrpcMethod lifetime ([#1598](https://github.com/hyperium/tonic/issues/1598)) ([68bf17d](https://github.com/hyperium/tonic/commit/68bf17d67ad71af44c34d565566c3dd58ea3ab87)) * **tls:** Add ability to add multiple ca certificates ([#1724](https://github.com/hyperium/tonic/issues/1724)) ([3457f92](https://github.com/hyperium/tonic/commit/3457f9203226f88524b31bf5d64ce6e5ec7c993c)) * **tls:** Use rustls_pki_types::CertificateDer to describe DER encoded certificate ([#1707](https://github.com/hyperium/tonic/issues/1707)) ([96a8cbc](https://github.com/hyperium/tonic/commit/96a8cbc04d0cad6d30d2944dba6b32aac8975f91)) * **tls:** Remove tls roots implicit configuration ([#1731](https://github.com/hyperium/tonic/issues/1731)) ([de73617](https://github.com/hyperium/tonic/commit/de736171f20ec5d485c26ee5eda4a9ccf5fc75e5)) * **transport:** Make service router independent from transport ([#1572](https://github.com/hyperium/tonic/issues/1572)) ([da48235](https://github.com/hyperium/tonic/commit/da482359933f52e84c0263b28a5a83ab1efe6c33)) * **transport:** Make transport server and channel independent ([#1630](https://github.com/hyperium/tonic/issues/1630)) ([654289f](https://github.com/hyperium/tonic/commit/654289fdc24f56d6845ec0ceb233deb46b640fac)) * **transport:** Rename reexported axum body ([#1752](https://github.com/hyperium/tonic/issues/1752)) ([5d7bfc2](https://github.com/hyperium/tonic/commit/5d7bfc22c590982463f2d93464b0a7fb90e17083)) * Use http::Extensions directly ([#1710](https://github.com/hyperium/tonic/issues/1710)) ([ed95d27](https://github.com/hyperium/tonic/commit/ed95d2762146f001970b74941f3bad77b7560426)) ### Bug Fixes * **tonic:** flush accumulated ready messages when status received ([#1756](https://github.com/hyperium/tonic/issues/1756)) ([d312dcc](https://github.com/hyperium/tonic/commit/d312dcc0ec362cb12f6e54072622761d7466a650)), closes [#1423](https://github.com/hyperium/tonic/issues/1423) ### BREAKING CHANGES * `tonic` and crates updated to hyper 1.0 (#1670) * `tonic` and crates updated to prost 0.13 (#1779) * `tonic_reflection::server` is updated to use the generated `tonic_reflection::pb::v1` code. * Make compression encoding configuration more malleable (#1757) * Removed implicit configuration of client TLS roots setup (#1731) [v1.8.8]: https://github.com/fullstorydev/grpcurl/releases/tag/v1.8.8 [proto]: https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1/reflection.proto [grpcurl]: https://github.com/fullstorydev/grpcurl # [0.11.0](https://github.com/hyperium/tonic/compare/v0.10.2...v0.11.0) (2024-02-08) BREAKING CHANGES: - Removed `NamedService` from the `transport` module, please import it via `tonic::server::NamedService`. - MSRV bumped to `1.70`. ### Features - Added `zstd` compression support. - Added connection timeout for `connecto_with_connector_lazy`. - Upgrade rustls to `v0.22` - Feature gate server implementation for `tonic-reflection`. # [0.10.2](https://github.com/hyperium/tonic/compare/v0.10.1...v0.10.2) (2023-09-28) ### Bug Fixes * **web:** Client decoding incomplete buffer bug ([#1540](https://github.com/hyperium/tonic/issues/1540)) ([83e363a](https://github.com/hyperium/tonic/commit/83e363ace6cbe20ccc2efbe1eb10a4236e4b8065)) # [0.10.1](https://github.com/hyperium/tonic/compare/v0.10.0...v0.10.1) (2023-09-21) ### Bug Fixes * **web:** Buffer incomplete messages ([#1528](https://github.com/hyperium/tonic/issues/1528)) ([fe6f8d9](https://github.com/hyperium/tonic/commit/fe6f8d9b4953a266eb32945a67edce9558bd05b3)) # [0.10.0](https://github.com/hyperium/tonic/compare/v0.9.2...v0.10.0) (2023-09-08) ### Bug Fixes * **codegen:** Use stream type from codegen mod ([#1446](https://github.com/hyperium/tonic/issues/1446)) ([60d776b](https://github.com/hyperium/tonic/commit/60d776b019854b6a6881d69823a36dcc18b1b4ce)) * **examples:** Use https scheme when using tls ([#1466](https://github.com/hyperium/tonic/issues/1466)) ([388b177](https://github.com/hyperium/tonic/commit/388b177d99e7b0a2c8d5eab1dee65c4dbb671db4)) * **tls:** Don't use tls w/ `http` scheme ([#1454](https://github.com/hyperium/tonic/issues/1454)) ([95e81f5](https://github.com/hyperium/tonic/commit/95e81f51fbbc32a5cf2b94ac0d7005d56b44a8d3)) ### Features * **build:** Add optional default unimplemented stubs ([#1344](https://github.com/hyperium/tonic/issues/1344)) ([aff1daf](https://github.com/hyperium/tonic/commit/aff1daf65d9a0d55b92719318eba2b5a4769c4e1)) * **core:** amortize many ready messages into fewer, larger buffers ([#1423](https://github.com/hyperium/tonic/issues/1423)) ([76eedc1](https://github.com/hyperium/tonic/commit/76eedc13d0dd891892301afa38c3dd8ae6646edf)) * **tonic-types:** add ability to extract rich error details from `google.rpc.Status` ([#1430](https://github.com/hyperium/tonic/issues/1430)) ([5fd635a](https://github.com/hyperium/tonic/commit/5fd635a30568ff629c4197c603c45b6b94750e88)) * **transport:** Add `Router::into_router` ([#1442](https://github.com/hyperium/tonic/issues/1442)) ([ea06a1b](https://github.com/hyperium/tonic/commit/ea06a1bb30bc325c7f6d7763fe48bf8b88c1c3ed)) * **transport:** Expose TcpConnectInfo fields ([#1449](https://github.com/hyperium/tonic/issues/1449)) ([74b079c](https://github.com/hyperium/tonic/commit/74b079ce752311fbe760d748804d801c385a5e7a)) * **web:** Add `GrpcWebClientService` ([#1472](https://github.com/hyperium/tonic/issues/1472)) ([dc29c17](https://github.com/hyperium/tonic/commit/dc29c17ae3ef729024e1f80c66566b09d7a01051)) ## [0.9.2](https://github.com/hyperium/tonic/compare/v0.9.1...v0.9.2) (2023-04-17) ## [0.9.1](https://github.com/hyperium/tonic/compare/v0.9.0...v0.9.1) (2023-04-03) # [0.9.0](https://github.com/hyperium/tonic/compare/v0.8.4...v0.9.0) (2023-03-31) ### Bug Fixes * **build:** Allow Services to be named Result ([#1203](https://github.com/hyperium/tonic/issues/1203)) ([a562a3c](https://github.com/hyperium/tonic/commit/a562a3ce329a38696dfcb0d82b7102d93fb30a5c)), closes [#1156](https://github.com/hyperium/tonic/issues/1156) * **codec:** Cancelled client streaming handling ([#1315](https://github.com/hyperium/tonic/issues/1315)) ([c8027a1](https://github.com/hyperium/tonic/commit/c8027a1385dd5d3fb6abdce7be49c46a43d4f3c2)), closes [#848](https://github.com/hyperium/tonic/issues/848) * MetadataKey::from_bytes returns an error ([#1246](https://github.com/hyperium/tonic/issues/1246)) ([930c805](https://github.com/hyperium/tonic/commit/930c805127cada70e4e4ab03c7680214b5c2a4f5)) * **web:** Fix `enable` and update docs ([#1326](https://github.com/hyperium/tonic/issues/1326)) ([a9db219](https://github.com/hyperium/tonic/commit/a9db219e50b7d27e48cd44e76941113a36b72e26)) ### Features * add GrpcMethod extension into request for client ([#1275](https://github.com/hyperium/tonic/issues/1275)) ([7a6b20d](https://github.com/hyperium/tonic/commit/7a6b20d8ef5d31c9cc01f0cf697df1f3e28cb421)) * **build:** Builder: add {enum,message}_attributes ([#1234](https://github.com/hyperium/tonic/issues/1234)) ([ff642f9](https://github.com/hyperium/tonic/commit/ff642f9233beab322333745f9edfa9c62ae18ca4)) * **codec:** Configure max request message size ([#1274](https://github.com/hyperium/tonic/issues/1274)) ([9f716d8](https://github.com/hyperium/tonic/commit/9f716d841184b8521720c6ed941af137ca2ee6a0)), closes [#1097](https://github.com/hyperium/tonic/issues/1097) * **core:** Default encoding/decoding limits ([#1335](https://github.com/hyperium/tonic/issues/1335)) ([ff33119](https://github.com/hyperium/tonic/commit/ff331199e45c8b53e93f1bd51ccd74dafc2146ac)) * **reflection:** Add dummy implementation for extension ([#1209](https://github.com/hyperium/tonic/issues/1209)) ([fdff111](https://github.com/hyperium/tonic/commit/fdff11115b44c4cc7e3de59ea045a193fa6881bc)) * Rename api related to protobuf ([#1224](https://github.com/hyperium/tonic/issues/1224)) ([d2542dc](https://github.com/hyperium/tonic/commit/d2542dc034e89383bd182a25a0d3235859fb10f9)) * **tls:** add an option for optional TLS client authentication ([#1163](https://github.com/hyperium/tonic/issues/1163)) ([773e4e1](https://github.com/hyperium/tonic/commit/773e4e1749daf023222f2294816b1f09d9e916a0)), closes [#687](https://github.com/hyperium/tonic/issues/687) * **tonic:** Use NamedService without transport feature ([#1273](https://github.com/hyperium/tonic/issues/1273)) ([5acde56](https://github.com/hyperium/tonic/commit/5acde56176d928ffddbf1076e922764fb151f959)) * **transport:** Add`local_addr` to `Request o` ([#1327](https://github.com/hyperium/tonic/issues/1327)) ([b54ce23](https://github.com/hyperium/tonic/commit/b54ce2321a5cba1c32261f4eda2b27d1110b893d)) * **transport:** added support for EC keys ([#1145](https://github.com/hyperium/tonic/issues/1145)) ([17d6a4b](https://github.com/hyperium/tonic/commit/17d6a4b576c1571bb149d3e935e9a835265a80dd)), closes [#1143](https://github.com/hyperium/tonic/issues/1143) * **types:** Add gRPC Richer Error Model support (Docs) ([#1317](https://github.com/hyperium/tonic/issues/1317)) ([69ce71e](https://github.com/hyperium/tonic/commit/69ce71efa6f4601c9e8060e87d0641a51251e9ab)) * **types:** Add gRPC Richer Error Model support (Examples) ([#1300](https://github.com/hyperium/tonic/issues/1300)) ([d471212](https://github.com/hyperium/tonic/commit/d471212ee8264ca6c5169a9893f361187e9378c9)) * **types:** Add gRPC Richer Error Model support (Help) ([#1293](https://github.com/hyperium/tonic/issues/1293)) ([d6041a9](https://github.com/hyperium/tonic/commit/d6041a99c2a216a2ebc83b7bc5a0947ba7ca869c)) * **types:** Add gRPC Richer Error Model support (LocalizedMessage) ([#1295](https://github.com/hyperium/tonic/issues/1295)) ([d54d02d](https://github.com/hyperium/tonic/commit/d54d02d3ed8bf221c0c54494b7ce692d412391a4)) * **types:** Add gRPC Richer Error Model support (PreconditionFailure) ([#1276](https://github.com/hyperium/tonic/issues/1276)) ([2378581](https://github.com/hyperium/tonic/commit/2378581850483f26fd7c1dee0a797d936b73e881)) * **types:** Add gRPC Richer Error Model support (QuotaFailure) ([#1204](https://github.com/hyperium/tonic/issues/1204)) ([03b4735](https://github.com/hyperium/tonic/commit/03b4735bb4ba7c6e84842d0515d1fd3be9d1cc13)) * **types:** Add gRPC Richer Error Model support (ResourceInfo) ([#1282](https://github.com/hyperium/tonic/issues/1282)) ([7eeda24](https://github.com/hyperium/tonic/commit/7eeda24350c5a61cae7c8e56cc0439d9c40cc77d)) * **types:** Add gRPC Richer Error Model support (RetryInfo) ([#1095](https://github.com/hyperium/tonic/issues/1095)) ([6cdb3d4](https://github.com/hyperium/tonic/commit/6cdb3d4685966b71f051e4cd67c50e1d2db402f5)) * **types:** add support for `DebugInfo` error message type ([#1179](https://github.com/hyperium/tonic/issues/1179)) ([3076e82](https://github.com/hyperium/tonic/commit/3076e8251e602ed6e98a8b3029070b33e3459109)) * **types:** Expose FILE_DESCRIPTOR_SET ([#1210](https://github.com/hyperium/tonic/issues/1210)) ([cc42d1f](https://github.com/hyperium/tonic/commit/cc42d1f88c39d87b244f863daf4ff625f6ff36df)) ## [0.8.4](https://github.com/hyperium/tonic/compare/v0.8.3...v0.8.4) (2022-11-29) ### Bug Fixes * **build:** Fix CodeGen8uilder typo ([#1165](https://github.com/hyperium/tonic/issues/1165)) ([#1166](https://github.com/hyperium/tonic/issues/1166)) ([c7476ff](https://github.com/hyperium/tonic/commit/c7476fff425b972c7966228fd38a9191e8d2ddc9)) ## [0.8.3](https://github.com/hyperium/tonic/compare/v0.8.2...v0.8.3) (2022-11-28) ### Bug Fixes * do not panic while encoding oversized bodies ([#1142](https://github.com/hyperium/tonic/issues/1142)) ([33e22bb](https://github.com/hyperium/tonic/commit/33e22bbc5ef1b74de82394c3ebfea27382419620)), closes [#1141](https://github.com/hyperium/tonic/issues/1141) * **reflection, health:** Remove transport feature ([#1112](https://github.com/hyperium/tonic/issues/1112)) ([7153289](https://github.com/hyperium/tonic/commit/7153289b51f7770cdd00cefeceddacc4cf36df97)) ### Features * **build:** Add `build_transport` builder option ([#1130](https://github.com/hyperium/tonic/issues/1130)) ([1f5bc9b](https://github.com/hyperium/tonic/commit/1f5bc9b9d55f814d1cb83de6f43239e275122265)) * **build:** Add `CodeGenBuilder` ([#1154](https://github.com/hyperium/tonic/issues/1154)) ([c4525ba](https://github.com/hyperium/tonic/commit/c4525ba6ad21cf9db8d1857931f430cbe924aeb5)) * **build:** Add disable_comments option ([#1127](https://github.com/hyperium/tonic/issues/1127)) ([e188521](https://github.com/hyperium/tonic/commit/e1885211495e63d962bc1d00f9be6eeaab2bb901)) * Expose `Request#into_parts` and `Request#from_parts` ([#1118](https://github.com/hyperium/tonic/issues/1118)) ([b409ddd](https://github.com/hyperium/tonic/commit/b409ddd478959e239aeef3cb8715cd3ace470a8f)) * **transport:** add `from_listener` for `TcpIncoming` ([#1093](https://github.com/hyperium/tonic/issues/1093)) ([0b03b30](https://github.com/hyperium/tonic/commit/0b03b30cccc67d517b05587614405d63d942b1bb)) * **web:** Implement tower::Layer for tonic_web::Config ([#1119](https://github.com/hyperium/tonic/issues/1119)) ([40536dc](https://github.com/hyperium/tonic/commit/40536dc13428f6338610d74f7b45a5f9c87d9335)) * **web:** Removed Cors impl and replaced with tower-http's CorsLayer ([#1123](https://github.com/hyperium/tonic/issues/1123)) ([a98d719](https://github.com/hyperium/tonic/commit/a98d719fb4b0a88127504a1ab3eb472e842c6b71)), closes [#1122](https://github.com/hyperium/tonic/issues/1122) ## [0.8.2](https://github.com/hyperium/tonic/compare/v0.8.0...v0.8.2) (2022-09-28) ### Bug Fixes * **transport:** Bump axum for CVE-2022-3212 ([#1088](https://github.com/hyperium/tonic/issues/1088)) ([cddd992](https://github.com/hyperium/tonic/commit/cddd99266682127a3fa0e5d601f56a6346369814)) ### Features * add `Result` type alias for `std::result::Result` ([#1085](https://github.com/hyperium/tonic/issues/1085)) ([56ff45d](https://github.com/hyperium/tonic/commit/56ff45d9a36040c429753d0d118ad980fbfe3eb8)) * **build:** add `cleanup-markdown` feature flag ([#1086](https://github.com/hyperium/tonic/issues/1086)) ([c1b08df](https://github.com/hyperium/tonic/commit/c1b08dffacb67e13ce7e94a002eee8999ca7c0e5)) * **tonic:** impl `Clone` for `Status` using `Arc` ([#1076](https://github.com/hyperium/tonic/issues/1076)) ([ee3d0df](https://github.com/hyperium/tonic/commit/ee3d0dfe7556fc7e996764f650ee3351097e7309)) * **transport:** Expose hyper's H2 adaptive window on server ([#1071](https://github.com/hyperium/tonic/issues/1071)) ([919d28b](https://github.com/hyperium/tonic/commit/919d28b2b96c7c803cec131a9e36e80d2b071701)) * **types:** Add gRPC Richer Error Model support (BadRequest) ([#1068](https://github.com/hyperium/tonic/issues/1068)) ([3e40d81](https://github.com/hyperium/tonic/commit/3e40d819cfbd3d5e4e078b79e3c95a43d14d489e)), closes [/github.com/hyperium/tonic/pull/1068#discussion_r956117520](https://github.com//github.com/hyperium/tonic/pull/1068/issues/discussion_r956117520) # [0.8.0](https://github.com/hyperium/tonic/compare/v0.7.2...v0.8.0) (2022-07-29) ### Features * Add `Grpc::with_origin` for clients ([#1017](https://github.com/hyperium/tonic/issues/1017)) ([10f6d2f](https://github.com/hyperium/tonic/commit/10f6d2f1a9fa3969599ebd674f7be27f4f458754)) * **build:** Add option to emit rerun-if-changed instructions ([#1021](https://github.com/hyperium/tonic/issues/1021)) ([1d2083a](https://github.com/hyperium/tonic/commit/1d2083a1a690edcb3f95343edfe229339c4257b7)) * **build:** Better support for custom codecs ([#999](https://github.com/hyperium/tonic/issues/999)) ([de2e4ac](https://github.com/hyperium/tonic/commit/de2e4ac077c076736dc451f3415ea7da1a61a560)) * Decouple `NamedService` from the `transport` feature ([#969](https://github.com/hyperium/tonic/issues/969)) ([feae96c](https://github.com/hyperium/tonic/commit/feae96c5be1247af368e6ce665c8df757d298e35)) ### BREAKING CHANGES * **build:** `CODEC_PATH` moved from const to fn ## [0.7.2](https://github.com/hyperium/tonic/compare/v0.7.1...v0.7.2) (2022-05-05) ### Bug Fixes * **build:** Reduce `Default` bound requirement ([#974](https://github.com/hyperium/tonic/issues/974)) ([4533a6e](https://github.com/hyperium/tonic/commit/4533a6e20eb889f8f13446c0edf39613fa4fe9f6)) * don't enable default features in tower ([#972](https://github.com/hyperium/tonic/issues/972)) ([b4f9634](https://github.com/hyperium/tonic/commit/b4f96343afe6106db80f41f49e576a687bfcd633)) * **transport:** Emit `HttpsUriWithoutTlsSupport` only w/ tls feat ([#996](https://github.com/hyperium/tonic/issues/996)) ([1dd5ad2](https://github.com/hyperium/tonic/commit/1dd5ad2b07810fc6eb5015c152ec737b5f0ca39c)) ### Features * Add TryFrom implementations for MetadataValue ([#990](https://github.com/hyperium/tonic/issues/990)) ([edc5a0d](https://github.com/hyperium/tonic/commit/edc5a0d88d4a392effe065dfcc1c005b6bb55b5d)) ## [0.7.1](https://github.com/hyperium/tonic/compare/v0.7.0...v0.7.1) (2022-04-04) ### Features * **transport:** Add `channel` feature flag ([#960](https://github.com/hyperium/tonic/issues/960)) ([f1ca90f](https://github.com/hyperium/tonic/commit/f1ca90f2882925c30f96ef60ccfd4fe39bc2c93b)) # [0.7.0](https://github.com/hyperium/tonic/compare/v0.6.2...v0.7.0) (2022-04-01) ### Bug Fixes * **build:** clippy warning for must_use ([#892](https://github.com/hyperium/tonic/issues/892)) ([a337f13](https://github.com/hyperium/tonic/commit/a337f132a57dfcc262b70537cf31686519e0f73c)) * **codec:** Remove `Default` bound on `Codec` ([#894](https://github.com/hyperium/tonic/issues/894)) ([d574cfd](https://github.com/hyperium/tonic/commit/d574cfda3a692d300db02f486a1792a99b3f9f6d)) * **codec:** Return None after poll_data error ([#921](https://github.com/hyperium/tonic/issues/921)) ([d7cae70](https://github.com/hyperium/tonic/commit/d7cae702fc2284473846db7c946baf87977b7b48)) * Handle interceptor errors as responses ([#840](https://github.com/hyperium/tonic/issues/840)) ([#842](https://github.com/hyperium/tonic/issues/842)) ([bf44940](https://github.com/hyperium/tonic/commit/bf44940f9b73709a83b31e4595a3d8ad262797a3)) * **health:** Correctly implement spec for overall health ([#897](https://github.com/hyperium/tonic/issues/897)) ([2b0ffee](https://github.com/hyperium/tonic/commit/2b0ffee62034f5983f8d6dcdafccd66f780559f2)) * Return error on non https uri instead of panic ([#838](https://github.com/hyperium/tonic/issues/838)) ([ef6e245](https://github.com/hyperium/tonic/commit/ef6e245180936097e56f5f95ed8b182674f3131b)) * **tonic:** Expose h2 error instead of reason ([#883](https://github.com/hyperium/tonic/issues/883)) ([a33e15a](https://github.com/hyperium/tonic/commit/a33e15a387a6ca1844748346904d28cb4caae84b)) * **tonic:** Preserve HTTP method in interceptor ([#912](https://github.com/hyperium/tonic/issues/912)) ([e623562](https://github.com/hyperium/tonic/commit/e6235623c4707f97e9b9f7c3ba88745050a884e5)) * **transport:** connect w/ connector infailable ([#922](https://github.com/hyperium/tonic/issues/922)) ([a197c20](https://github.com/hyperium/tonic/commit/a197c20469a666164c5cba280679e55b9e9e2b6c)) * **transport:** Endpoint returns transport error ([#920](https://github.com/hyperium/tonic/issues/920)) ([ee6e726](https://github.com/hyperium/tonic/commit/ee6e726707a6839c6cabe672eb296c6118a2a1cd)) * **transport:** Make `Server::layer()` support more than one layer ([#932](https://github.com/hyperium/tonic/issues/932)) ([e30bb7e](https://github.com/hyperium/tonic/commit/e30bb7ede7e107a3181cd786533c250ba09a2fcf)) * **transport:** Make server builder more consitient ([#901](https://github.com/hyperium/tonic/issues/901)) ([6763d19](https://github.com/hyperium/tonic/commit/6763d191d267c1b9f861b96ad0f4b850e0264f4d)) * **web:** Fix error tonic-web doc url ([#928](https://github.com/hyperium/tonic/issues/928)) ([37cd483](https://github.com/hyperium/tonic/commit/37cd48304f07adf09ab61b74b6ba3c91a24d2729)) ### Features * **build:** add constructor `from_arc` for gRPC servers ([#875](https://github.com/hyperium/tonic/issues/875)) ([7179f7a](https://github.com/hyperium/tonic/commit/7179f7ae6a5186bb64e4c120302084f56c053206)) * **build:** Expose Prost generation plugin ([#947](https://github.com/hyperium/tonic/issues/947)) ([d4bd475](https://github.com/hyperium/tonic/commit/d4bd4758dd80135f89d3e559c5d7f42ccbbab504)) * **build:** use prettyplease to format output ([#890](https://github.com/hyperium/tonic/issues/890)) ([#904](https://github.com/hyperium/tonic/issues/904)) ([d6c0fc1](https://github.com/hyperium/tonic/commit/d6c0fc112b2288a080fd0a727453b24d666e3d79)) * **health:** Expose `HealthService` publically ([#930](https://github.com/hyperium/tonic/issues/930)) ([097e7e8](https://github.com/hyperium/tonic/commit/097e7e85a9079bb76bef54921f03c6f7e0ee0744)) * Implement hash for `Code` ([#917](https://github.com/hyperium/tonic/issues/917)) ([6bc7dab](https://github.com/hyperium/tonic/commit/6bc7dab8e099c8ce226a6261e545d8d131c604f0)) * **tls:** upgrade to tokio-rustls 0.23 (rustls 0.20) ([#859](https://github.com/hyperium/tonic/issues/859)) ([4548997](https://github.com/hyperium/tonic/commit/4548997080c9c34f12dc0ff83ab0e2bb35ceca9c)) * **transport:** add unix socket support in server ([#861](https://github.com/hyperium/tonic/issues/861)) ([dee2ab5](https://github.com/hyperium/tonic/commit/dee2ab52ff4a2995156a3baf5ea916b479fd1d14)) * **transport:** port router to axum ([#830](https://github.com/hyperium/tonic/issues/830)) ([6dfc20e](https://github.com/hyperium/tonic/commit/6dfc20e1db455be12b0a647533c65bbfd6ae78f2)) * **transport:** support customizing `Channel`'s async executor ([#935](https://github.com/hyperium/tonic/issues/935)) ([0859d82](https://github.com/hyperium/tonic/commit/0859d82e577fb024e39ce9b5b7356b95dcb66562)) * Update prost to 0.10 ([#948](https://github.com/hyperium/tonic/issues/948)) ([c78274e](https://github.com/hyperium/tonic/commit/c78274e3fe5763cba291a605979cd7175ad6c38f)) ## [0.6.2](https://github.com/hyperium/tonic/compare/v0.6.1...v0.6.2) (2021-12-08) ### Bug Fixes * **examples:** Fix autoreload example ([#798](https://github.com/hyperium/tonic/issues/798)) ([#818](https://github.com/hyperium/tonic/issues/818)) ([8508f36](https://github.com/hyperium/tonic/commit/8508f369c2c12b09bcd6c099a7915566603911fd)) ## [0.6.1](https://github.com/hyperium/tonic/compare/v0.6.0...v0.6.1) (2021-10-27) ### Bug Fixes * **transport:** Bump hyper to 0.14.14 ([#813](https://github.com/hyperium/tonic/issues/813)) ([2a3e9b2](https://github.com/hyperium/tonic/commit/2a3e9b2f6fa459b065c5a4ebeab5f447a3515707)) # [0.6.0](https://github.com/hyperium/tonic/compare/v0.5.2...v0.6.0) (2021-10-25) ### Bug Fixes * **build:** Correctly convert `Empty` to `()` ([#734](https://github.com/hyperium/tonic/issues/734)) ([ff6a690](https://github.com/hyperium/tonic/commit/ff6a690cec9daca33984cabea66f9d370ac63462)) * **build:** split path types in compile ([#721](https://github.com/hyperium/tonic/issues/721)) ([53ecc1f](https://github.com/hyperium/tonic/commit/53ecc1f85e7f7eeb0dce4ab23432d6c36d8a46b0)) * **tonic:** change `connect_lazy` to be infallible ([#712](https://github.com/hyperium/tonic/issues/712)) ([2e47154](https://github.com/hyperium/tonic/commit/2e471548d89be98d26b2332d059a24a3fc15ec23)) * **tonic:** fix extensions disappearing during streaming requests ([5c1bb90](https://github.com/hyperium/tonic/commit/5c1bb90ce82ecf90843a7c959edd7ef8fc280f62)), closes [#770](https://github.com/hyperium/tonic/issues/770) * **tonic:** Remove `Sync` requirement for streams ([#804](https://github.com/hyperium/tonic/issues/804)) ([23c1392](https://github.com/hyperium/tonic/commit/23c1392fb7e0ac50bcdedc35509917061bc858e1)) * **tonic:** Status code to set correct source on unkown error ([#799](https://github.com/hyperium/tonic/issues/799)) ([4054d61](https://github.com/hyperium/tonic/commit/4054d61e14b9794a72b48de1a051c26129ec36b1)) * **transport:** AddOrigin panic on invalid uri ([#801](https://github.com/hyperium/tonic/issues/801)) ([3ab00f3](https://github.com/hyperium/tonic/commit/3ab00f304dd204fccf00d1995e635fa6b2f8503b)) * **transport:** Correctly map hyper errors ([#629](https://github.com/hyperium/tonic/issues/629)) ([4947b07](https://github.com/hyperium/tonic/commit/4947b076f5b0b5149ee7f6144515535b85f65db5)) ### Features * **build:** Support prost's include_file option ([#774](https://github.com/hyperium/tonic/issues/774)) ([3f9ab80](https://github.com/hyperium/tonic/commit/3f9ab801f7ee50ec04ab0f73cd457898dc687e61)) * Update `prost` and friends to 0.9 ([#791](https://github.com/hyperium/tonic/issues/791)) ([09805ec](https://github.com/hyperium/tonic/commit/09805ece453047bf609b1a69c72931eae6e1144a)) ## [0.5.2](https://github.com/hyperium/tonic/compare/v0.5.1...v0.5.2) (2021-08-10) ## [0.5.1](https://github.com/hyperium/tonic/compare/v0.5.0...v0.5.1) (2021-08-09) ### Bug Fixes * **build:** allow services to be named `Service` ([#709](https://github.com/hyperium/tonic/issues/709)) ([380d81d](https://github.com/hyperium/tonic/commit/380d81dd86a4d4ab2a23a7d9c072eab67631c331)) * **build:** remove unnecessary `Debug` constraint for client streams ([#719](https://github.com/hyperium/tonic/issues/719)) ([167e8cb](https://github.com/hyperium/tonic/commit/167e8cb5b212338b0d668f5304ab19823ab94529)) ### Features * **examples:** add grpc-web example ([#710](https://github.com/hyperium/tonic/issues/710)) ([5aa8ae1](https://github.com/hyperium/tonic/commit/5aa8ae1fec27377cd4c2a41d309945d7e38087d0)) * **health:** Expose grpc_health_v1 file descriptor set ([#620](https://github.com/hyperium/tonic/issues/620)) ([6ee638d](https://github.com/hyperium/tonic/commit/6ee638d9409144dc1c587283f47994ba9f4b8efd)) * **tonic:** add `Interceptor` trait ([#713](https://github.com/hyperium/tonic/issues/713)) ([8c8f4d1](https://github.com/hyperium/tonic/commit/8c8f4d12515643050f47227894c98e226b01f924)) * **transport:** Add `Connected` impl for `DuplexStream` ([#722](https://github.com/hyperium/tonic/issues/722)) ([0e33a02](https://github.com/hyperium/tonic/commit/0e33a0241e642b402a2215d30a8bfc0de2b168d2)) # [0.5.0](https://github.com/hyperium/tonic/compare/v0.4.3...v0.5.0) (2021-07-08) ### Bug Fixes * **build:** fix `with_interceptor` not building on Rust 1.51 ([#669](https://github.com/hyperium/tonic/issues/669)) ([9478fac](https://github.com/hyperium/tonic/commit/9478fac97984cf8291bf89c55eb9a02a06889e03)) * **codec:** Fix streaming reponses w/ many status ([#689](https://github.com/hyperium/tonic/issues/689)) ([737ace3](https://github.com/hyperium/tonic/commit/737ace393d3d11fb179af939e5f1a5d16ebc2b82)), closes [#681](https://github.com/hyperium/tonic/issues/681) * **codec:** improve error message for invalid compression flag ([#663](https://github.com/hyperium/tonic/issues/663)) ([9cc14b7](https://github.com/hyperium/tonic/commit/9cc14b79fba9e789e215f7ea3fa40ccfaecc8e59)) * **examples:** Fix tower examples ([#624](https://github.com/hyperium/tonic/issues/624)) ([4a917a3](https://github.com/hyperium/tonic/commit/4a917a32f05c70c99d608be5ae3fc58f130ee4df)) * **tonic:** don't include error's cause in Display impl ([#633](https://github.com/hyperium/tonic/issues/633)) ([31a3468](https://github.com/hyperium/tonic/commit/31a34681c7ba606e27615859d4b65dfcdcaa6f38)) * **tonic:** don't remove reserved headers in interceptor ([#701](https://github.com/hyperium/tonic/issues/701)) ([6711b80](https://github.com/hyperium/tonic/commit/6711b8067457ed31f1844e3ec6571ef0c4589325)) * **tonic:** make `Interceptor` `UnwindSafe` ([#641](https://github.com/hyperium/tonic/issues/641)) ([57509d3](https://github.com/hyperium/tonic/commit/57509d321ba49e6e9189efef345d59089875dff8)) * **transport:** remove needless `BoxFuture` ([#644](https://github.com/hyperium/tonic/issues/644)) ([74ad0a9](https://github.com/hyperium/tonic/commit/74ad0a998fedb2507f6b2f035b961eb9bac5b494)) * **web:** fix compilation ([#670](https://github.com/hyperium/tonic/issues/670)) ([e199387](https://github.com/hyperium/tonic/commit/e1993877c430906500aeda9ab1e3413e68ed483d)) ### Features * **build:** support adding attributes to clients and servers ([#684](https://github.com/hyperium/tonic/issues/684)) ([a948a8f](https://github.com/hyperium/tonic/commit/a948a8f884705b9f2a6df5c86d07cc6eb0bb1b7c)) * **codec:** compression support ([#692](https://github.com/hyperium/tonic/issues/692)) ([0583cff](https://github.com/hyperium/tonic/commit/0583cff80f57ba071295416ee8828c3430851d0d)) * **metadata:** expose `IterMut` and `ValuesMut` ([#639](https://github.com/hyperium/tonic/issues/639)) ([b0ec3ea](https://github.com/hyperium/tonic/commit/b0ec3ead344df44fc17e5ad22398ed2464768e63)) * **metadata:** remove manual `Send + Sync` impls for metadata types ([#640](https://github.com/hyperium/tonic/issues/640)) ([e97f518](https://github.com/hyperium/tonic/commit/e97f5180250a567aead16fe9a8644216edc4bbb3)) * **tonic-web:** implement grpc <-> grpc-web protocol translation ([#455](https://github.com/hyperium/tonic/issues/455)) ([c309063](https://github.com/hyperium/tonic/commit/c309063254dff42fd05afc5e56b0b0371b905758)) * **tonic:** add `h2::Error` as a `source` for `Status` ([#612](https://github.com/hyperium/tonic/issues/612)) ([b90bb7b](https://github.com/hyperium/tonic/commit/b90bb7bbc012207451fe2788a8efd69023312425)) * **tonic:** add `Request` and `Response` extensions ([#642](https://github.com/hyperium/tonic/issues/642)) ([352b0f5](https://github.com/hyperium/tonic/commit/352b0f584be33bc49ca266698c9224d16a6825ff)) * **tonic:** expose setting for `http2_adaptive_window` ([#657](https://github.com/hyperium/tonic/issues/657)) ([12815d0](https://github.com/hyperium/tonic/commit/12815d0a1d558eb9f661a85354336b04df1f5bab)) * **tonic:** implement `From` for `i32` ([f33316d](https://github.com/hyperium/tonic/commit/f33316d5b32f6a44fa23ea12851f502c48bac5ea)) * **tonic:** make it easier to add tower middleware to servers ([#651](https://github.com/hyperium/tonic/issues/651)) ([4d2667d](https://github.com/hyperium/tonic/commit/4d2667d1cb1b938756d20dafa3cccae1db23a831)) * **tonic:** pass `trace_fn` the request rather than just the headers ([#634](https://github.com/hyperium/tonic/issues/634)) ([7862a22](https://github.com/hyperium/tonic/commit/7862a2259db8dc1af440604c6c582487a59a2709)) * **tonic:** Use `BoxBody` from `http-body` crate ([#622](https://github.com/hyperium/tonic/issues/622)) ([4dda4cb](https://github.com/hyperium/tonic/commit/4dda4cbcca88fa46a7d8a6e4eabfb6d7c333617a)) * **transport:** Add `connect_with_connector_lazy` ([#696](https://github.com/hyperium/tonic/issues/696)) ([2a46ff5](https://github.com/hyperium/tonic/commit/2a46ff5c96415b217700353dadba74a80e5ad88c)), closes [#695](https://github.com/hyperium/tonic/issues/695) * **transport:** Add a tls-webpki-roots feature to add trust roots from webpki-roots ([#660](https://github.com/hyperium/tonic/issues/660)) ([32173dc](https://github.com/hyperium/tonic/commit/32173dc7f6521bad8f26b055b6a86d807348f151)) * **transport:** add connect timeout to `Endpoint` ([#662](https://github.com/hyperium/tonic/issues/662)) ([2b60a00](https://github.com/hyperium/tonic/commit/2b60a00614c5c4260ce0acaaa599da89bebfd267)) * **transport:** provide generic access to connect info ([#647](https://github.com/hyperium/tonic/issues/647)) ([e5e3118](https://github.com/hyperium/tonic/commit/e5e311853bff347355722bc829d40f54e8954aee)) ## [0.4.3](https://github.com/hyperium/tonic/compare/v0.4.2...v0.4.3) (2021-04-29) ### Features * **tonic:** Add `Request::set_timeout` ([#615](https://github.com/hyperium/tonic/issues/615)) ([dae31d0](https://github.com/hyperium/tonic/commit/dae31d0e1cfafaaad9d634d7c0022c65ab76d7e1)) * **transport:** Support timeouts with "grpc-timeout" header ([#606](https://github.com/hyperium/tonic/issues/606)) ([9ff4f7b](https://github.com/hyperium/tonic/commit/9ff4f7b8e418278a923a86bb925e3f3e189ca7e0)) ## [0.4.2](https://github.com/hyperium/tonic/compare/v0.4.1...v0.4.2) (2021-04-13) ### Bug Fixes * **codec:** Allocate inbound buffer once ([#578](https://github.com/hyperium/tonic/issues/578)) ([1d2754f](https://github.com/hyperium/tonic/commit/1d2754feba6b49bfc813f41e8e8e42ffaf8ab0dd)) * **reflection:** Depend on correct version of build ([#582](https://github.com/hyperium/tonic/issues/582)) ([db09093](https://github.com/hyperium/tonic/commit/db0909382b8ab1a385c1352feeea663844b7d799)) ### Features * **build:** Add `prostoc_args` ([#577](https://github.com/hyperium/tonic/issues/577)) ([480a794](https://github.com/hyperium/tonic/commit/480a79409c4cb9a1c680e57d0f74ad1d4f18beaa)) * Expose status constructors ([#579](https://github.com/hyperium/tonic/issues/579)) ([0d05aa0](https://github.com/hyperium/tonic/commit/0d05aa0d02bd3037e81c72dcf7fa5168d5a62097)) * **health:** Expose proto and client ([#471](https://github.com/hyperium/tonic/issues/471)) ([#602](https://github.com/hyperium/tonic/issues/602)) ([49f6137](https://github.com/hyperium/tonic/commit/49f613767341656cad1cc4883ff0e89b03d378ae)) ### Reverts * Revert "Remove grpc-timeout header from reserved headers (#603)" ([7aaa2f8](https://github.com/hyperium/tonic/commit/7aaa2f85d991d875673825fd76931d0a4f3c86b0)), closes [#603](https://github.com/hyperium/tonic/issues/603) ## [0.4.1](https://github.com/hyperium/tonic/compare/v0.4.0...v0.4.1) (2021-03-16) ### Bug Fixes * Depend on at least tower 0.4.4 ([#554](https://github.com/hyperium/tonic/issues/554)) ([ca3b9a1](https://github.com/hyperium/tonic/commit/ca3b9a1df12f32a425926a6cd7d04e1692f8f503)), closes [#553](https://github.com/hyperium/tonic/issues/553) [#552](https://github.com/hyperium/tonic/issues/552) [#553](https://github.com/hyperium/tonic/issues/553) [#552](https://github.com/hyperium/tonic/issues/552) ### Features * **build:** Add disable_package_emission option to tonic-build ([#556](https://github.com/hyperium/tonic/issues/556)) ([4f5e160](https://github.com/hyperium/tonic/commit/4f5e160679bf1ac37c7d3094a65690ce59986fc3)) * **build:** Support compiling well-known protobuf types ([#522](https://github.com/hyperium/tonic/issues/522)) ([61555ff](https://github.com/hyperium/tonic/commit/61555ff2b5b76e4e3172717354aed1e6f31d6611)) * **build:** Use `RUSTFMT` to find `rustfmt` binary ([#566](https://github.com/hyperium/tonic/issues/566)) ([ea56e2e](https://github.com/hyperium/tonic/commit/ea56e2e2b89d45c95c60152cbe5e4338e1c997fd)) * Implement gRPC Reflection Service ([#340](https://github.com/hyperium/tonic/issues/340)) ([c54f247](https://github.com/hyperium/tonic/commit/c54f24721c669f0784694568f387bba6bec98e12)) # [0.4.0](https://github.com/hyperium/tonic/compare/v0.3.1...v0.4.0) (2021-01-15) ### Bug Fixes * **build:** Add content-type for generated unimplemented service ([#441](https://github.com/hyperium/tonic/issues/441)) ([62c1230](https://github.com/hyperium/tonic/commit/62c1230117bcaa6f45cb0fa0697b89b9255a94a5)) * **build:** Match namespace code with other generated packages ([#472](https://github.com/hyperium/tonic/issues/472)) ([1b03ece](https://github.com/hyperium/tonic/commit/1b03ece2a81cb7e8b1922b3c3c1f496bd402d76c)) * gracefully handle bad native certs ([#520](https://github.com/hyperium/tonic/issues/520)) ([fe4d5b9](https://github.com/hyperium/tonic/commit/fe4d5b9d9a0fdcf414bbe31c2fcad59e8cc03da8)), closes [#519](https://github.com/hyperium/tonic/issues/519) * **transport:** Add content-type for Unimplemented ([#434](https://github.com/hyperium/tonic/issues/434)) ([594a542](https://github.com/hyperium/tonic/commit/594a542b8a9e8f9f4c3bd1d0a08e87ce74a850e5)) * **transport:** reconnect lazy connections after first failure ([#458](https://github.com/hyperium/tonic/issues/458)) ([e9910d1](https://github.com/hyperium/tonic/commit/e9910d10a7c1287a2247a236b45dbf31eceb08bd)), closes [#452](https://github.com/hyperium/tonic/issues/452) * **transport:** return Poll::ready until error is consumed ([#536](https://github.com/hyperium/tonic/issues/536)) ([dafea9a](https://github.com/hyperium/tonic/commit/dafea9adeec5626ee780bc3ad7dc69691db51a82)) * fix(transport) Do not panic when building and Endpoint with an invali… (#438) ([26ce9d1](https://github.com/hyperium/tonic/commit/26ce9d12bf1765e5a7acb07cab05b6bd75bd4e4d)), closes [#438](https://github.com/hyperium/tonic/issues/438) ### Features * **tonic:** implement From for Status ([#500](https://github.com/hyperium/tonic/issues/500)) ([fc86563](https://github.com/hyperium/tonic/commit/fc86563b369d0b73a79d3e8dc9a84d5ce1513303)) * **transport:** Add `Router::into_service` ([#419](https://github.com/hyperium/tonic/issues/419)) ([37f6733](https://github.com/hyperium/tonic/commit/37f6733f85a42e828c124026c3a0f21919549b12)) * **transport:** add max http2 frame size to server. ([#529](https://github.com/hyperium/tonic/issues/529)) ([31936e0](https://github.com/hyperium/tonic/commit/31936e0513a41e83c8137786bd417fe57ecd05eb)), closes [#264](https://github.com/hyperium/tonic/issues/264) * **transport:** add user-agent header to client requests. ([#457](https://github.com/hyperium/tonic/issues/457)) ([d4899df](https://github.com/hyperium/tonic/commit/d4899df83287a4eb1a91754c2e2955000d13c5f4)), closes [#453](https://github.com/hyperium/tonic/issues/453) * **transport:** Connect lazily in the load balanced channel ([#493](https://github.com/hyperium/tonic/issues/493)) ([2e964c7](https://github.com/hyperium/tonic/commit/2e964c78c666ecd6e6cfc37689d30300cad81f4c)) * **transport:** expose HTTP2 server keepalive interval and timeout ([#486](https://github.com/hyperium/tonic/issues/486)) ([2b9cdb9](https://github.com/hyperium/tonic/commit/2b9cdb9779eb5cb7d3862e1ce95ab63f847ec223)), closes [#474](https://github.com/hyperium/tonic/issues/474) * **transport:** Fix TLS accept w/ peer certs ([#535](https://github.com/hyperium/tonic/issues/535)) ([41c51f1](https://github.com/hyperium/tonic/commit/41c51f1c61ac957e439ced4302f09160c850787e)) * **transport:** Move error! to debug! ([#537](https://github.com/hyperium/tonic/issues/537)) ([a7778ad](https://github.com/hyperium/tonic/commit/a7778ad16611b7ade64c33256eecf9825408f06a)) ### BREAKING CHANGES * `TryFrom` API has been changed. ## [0.3.1](https://github.com/hyperium/tonic/compare/v0.3.0...v0.3.1) (2020-08-20) ### Bug Fixes * **transport:** Return connection error on `Channel::connect` ([#413](https://github.com/hyperium/tonic/issues/413)) ([2ea17b2](https://github.com/hyperium/tonic/commit/2ea17b2ecfc40a20f4d9608f807b3d099a8f415d)), closes [#403](https://github.com/hyperium/tonic/issues/403) # [0.3.0](https://github.com/hyperium/tonic/compare/v0.2.1...v0.3.0) (2020-07-13) ### Bug Fixes * `Status::details` leaking base64 encoding ([#395](https://github.com/hyperium/tonic/issues/395)) ([2c4c544](https://github.com/hyperium/tonic/commit/2c4c544d902c588fc0654910fba1f0d21d78eab3)), closes [#379](https://github.com/hyperium/tonic/issues/379) * **build:** Allow empty packages ([#382](https://github.com/hyperium/tonic/issues/382)) ([f085aba](https://github.com/hyperium/tonic/commit/f085aba302001986fd04219d2843f659f73c4031)), closes [#381](https://github.com/hyperium/tonic/issues/381) * **build:** Make generated server service public ([#347](https://github.com/hyperium/tonic/issues/347)) ([8cd6f05](https://github.com/hyperium/tonic/commit/8cd6f0506429cfbe59e63b0216f208482d12358a)) * Remove uses of pin_project::project attribute ([#367](https://github.com/hyperium/tonic/issues/367)) ([5bda615](https://github.com/hyperium/tonic/commit/5bda6156328bd2c94bc274588871b666f1b72d6e)) * **transport:** Propagate errors in tls_config instead of unwrap/panic ([#385](https://github.com/hyperium/tonic/issues/385)) ([3b9d6a6](https://github.com/hyperium/tonic/commit/3b9d6a6262b62f30b8c9953f0da8e403be53216e)) ### Features * Add `Display` implementation for `Code` ([#386](https://github.com/hyperium/tonic/issues/386)) ([ab1de44](https://github.com/hyperium/tonic/commit/ab1de44771f3fa6ac283485bdbf1035d6407ac1a)) * Add `Status::to_http` ([#376](https://github.com/hyperium/tonic/issues/376)) ([327b4ff](https://github.com/hyperium/tonic/commit/327b4fffa3381345ee4620df7e9998efe2aa9454)) * Add metadata to error responses ([#348](https://github.com/hyperium/tonic/issues/348)) ([372da52](https://github.com/hyperium/tonic/commit/372da52e96114ca76cc221f3c598be82bfae970c)) * add new method get_uri for Endpoint ([#371](https://github.com/hyperium/tonic/issues/371)) ([54d7a7a](https://github.com/hyperium/tonic/commit/54d7a7af6b6530b80353c5741586c38cca8382c9)) * **codec:** Improve compression flag log ([#374](https://github.com/hyperium/tonic/issues/374)) ([d68dd36](https://github.com/hyperium/tonic/commit/d68dd365321764aceaf4e37a106a519797926495)) * **transport:** Add Endpoint::connect_lazy method ([#392](https://github.com/hyperium/tonic/issues/392)) ([ec9046d](https://github.com/hyperium/tonic/commit/ec9046dfc23d63828363d9555cd7b96811ad442d)), closes [#167](https://github.com/hyperium/tonic/issues/167) * **transport:** Add optional service methods ([#275](https://github.com/hyperium/tonic/issues/275)) ([2b997b0](https://github.com/hyperium/tonic/commit/2b997b0c5f37d69f3cd8b5b566b64df110d9f4eb)) * **transport:** Dynamic load balancing ([#341](https://github.com/hyperium/tonic/issues/341)) ([85ae0a4](https://github.com/hyperium/tonic/commit/85ae0a4733b9e99edaa05e65160d98f21f288fc1)) * **types:** Add `tonic-types` crate ([#391](https://github.com/hyperium/tonic/issues/391)) ([ea7fe66](https://github.com/hyperium/tonic/commit/ea7fe66b145e01891f1c1f16d247e02524d98fae)) ## [0.2.1](https://github.com/hyperium/tonic/compare/v0.2.0...v0.2.1) (2020-05-07) ### Bug Fixes * base64 encode details header ([#345](https://github.com/hyperium/tonic/issues/345)) ([e683ffe](https://github.com/hyperium/tonic/commit/e683ffef1fcbe0ace9cc696232489f5f6600e83f)) * **build:** Remove ambiguity in service method call ([#327](https://github.com/hyperium/tonic/issues/327)) ([5d56daa](https://github.com/hyperium/tonic/commit/5d56daa721cfb18edc74cf50db4270e2c8461fc9)) * **transport:** Apply tls-connector for discovery when applicable ([#334](https://github.com/hyperium/tonic/issues/334)) ([#338](https://github.com/hyperium/tonic/issues/338)) ([99fbe22](https://github.com/hyperium/tonic/commit/99fbe22e7c1340d6be9ee5d3ae9738850881af61)) ### Features * **transport:** Add AsRef impl for Certificate ([#326](https://github.com/hyperium/tonic/issues/326)) ([d2ad8df](https://github.com/hyperium/tonic/commit/d2ad8df629a349cc151a0a4ede96f04356f73839)) # [0.2.0](https://github.com/hyperium/tonic/compare/v0.1.1...v0.2.0) (2020-04-01) ### Bug Fixes * **build:** Allow non_camel_case_types on codegen structs ([224280d](https://github.com/hyperium/tonic/commit/224280dfff8944e9e553337416d23d6e5a050945)), closes [#295](https://github.com/hyperium/tonic/issues/295) * **build:** Don't replace extern_paths ([#261](https://github.com/hyperium/tonic/issues/261)) ([1b3d107](https://github.com/hyperium/tonic/commit/1b3d107206136312a2536d3b72748c52191d99b1)) * **build:** Ignore non `.rs` files with rustfmt ([#284](https://github.com/hyperium/tonic/issues/284)) ([7dfa2a2](https://github.com/hyperium/tonic/commit/7dfa2a277b593e008cea53eef7163ca59a06c56a)), closes [#283](https://github.com/hyperium/tonic/issues/283) * **build:** Implement Debug for client struct ([6dbe88d](https://github.com/hyperium/tonic/commit/6dbe88d445e378fff48d05083c23baeb2020cb2d)), closes [#298](https://github.com/hyperium/tonic/issues/298) * **build:** Remove debug println! ([#287](https://github.com/hyperium/tonic/issues/287)) ([e2c2be2](https://github.com/hyperium/tonic/commit/e2c2be2f084b7c1ef4e93f6994cb9c728de0c1ed)) * **build:** Server service uses generic body bound ([#306](https://github.com/hyperium/tonic/issues/306)) ([5758b75](https://github.com/hyperium/tonic/commit/5758b758b2d44059b0149a31542d11589999a789)) * **health:** Set referenced version of tonic ([59c7788](https://github.com/hyperium/tonic/commit/59c77888464a0302993dbe07fed7c1848b415f8f)) * **metadata:** Remove deprecated error description ([61e0429](https://github.com/hyperium/tonic/commit/61e0429ae810354363835c36a046b5113b3c74b4)) * **transport:** Handle tls accepting on task ([#320](https://github.com/hyperium/tonic/issues/320)) ([04a8c0c](https://github.com/hyperium/tonic/commit/04a8c0c82a4007f48c3bf3539a3f2312746fedd1)) ### Features * Add Status with Details Constructor ([#308](https://github.com/hyperium/tonic/issues/308)) ([cfd59db](https://github.com/hyperium/tonic/commit/cfd59dbb342a8b7d216f4856e13d24b564c606f3)) * **build:** Add support for custom prost config ([#318](https://github.com/hyperium/tonic/issues/318)) ([202093c](https://github.com/hyperium/tonic/commit/202093c31715b52997c6c206c758924ff5f69bc8)) * **build:** Decouple codgen from `prost` ([#170](https://github.com/hyperium/tonic/issues/170)) ([f65cda1](https://github.com/hyperium/tonic/commit/f65cda1ea0a190fe07c4f8d91473baad9a6f1f77)) * **health:** Add tonic-health server impl ([da92dbf](https://github.com/hyperium/tonic/commit/da92dbf8aa885ea0ea05755e9432532fc980e353)), closes [#135](https://github.com/hyperium/tonic/issues/135) [#135](https://github.com/hyperium/tonic/issues/135) * **transport:** Expose http2 keep-alive support ([#307](https://github.com/hyperium/tonic/issues/307)) ([012fa3c](https://github.com/hyperium/tonic/commit/012fa3cb4a0e010dafa28305416fab6c4278fc7b)) ## [0.1.1](https://github.com/hyperium/tonic/compare/v0.1.0...v0.1.1) (2020-01-20) ### Bug Fixes * **build:** Typo with client mod docstring ([#237](https://github.com/hyperium/tonic/issues/237)) ([5fc6762](https://github.com/hyperium/tonic/commit/5fc6762435494d8df023bea8e35a5d20d81f2f3b)) * **transport:** Add Connected impl for TcpStream ([#245](https://github.com/hyperium/tonic/issues/245)) ([cfdf0af](https://github.com/hyperium/tonic/commit/cfdf0aff549196af0c3b7f6e531dbeacfb6990dc)) * **transport:** Use Uri host if no domain for tls ([#244](https://github.com/hyperium/tonic/issues/244)) ([6de0b4d](https://github.com/hyperium/tonic/commit/6de0b4d26fd82b4d1303080b0ba8c4db2d4f0fd1)) # [0.1.0](https://github.com/hyperium/tonic/compare/v0.1.0-beta.1...v0.1.0) (2020-01-14) ### Bug Fixes * **build:** Remove default impl for Server traits ([#229](https://github.com/hyperium/tonic/issues/229)) ([a41f55a](https://github.com/hyperium/tonic/commit/a41f55ab9dfe77fca920b3c2e89343c7ce963225)) * **transport:** Improve `Error` type ([#217](https://github.com/hyperium/tonic/issues/217)) ([ec1f37e](https://github.com/hyperium/tonic/commit/ec1f37e4b46279d20f4fadafa5bf30cfb729fa42)) ### chore * rename ServiceName -> NamedService ([#233](https://github.com/hyperium/tonic/issues/233)) ([6ee2ed9](https://github.com/hyperium/tonic/commit/6ee2ed9b4ff30c0517d70908c6348a633dab5b91)) ### Features * Add gRPC interceptors ([#232](https://github.com/hyperium/tonic/issues/232)) ([eba7ec7](https://github.com/hyperium/tonic/commit/eba7ec7b32fb96938cbdc3d2dfd91c238afda0dc)) * **build:** Add extern_path config support ([#223](https://github.com/hyperium/tonic/issues/223)) ([e034288](https://github.com/hyperium/tonic/commit/e034288c3739467238aee54fdbe0a2a3a87bf824)) * **codec:** Introduce `Decoder/Encoder` traits ([#208](https://github.com/hyperium/tonic/issues/208)) ([0fa2bf1](https://github.com/hyperium/tonic/commit/0fa2bf1cea9d1166d49e40f2211268611b6993de)) * **transport:** Add `serve_with_incoming_shutdown` ([#220](https://github.com/hyperium/tonic/issues/220)) ([a66595b](https://github.com/hyperium/tonic/commit/a66595bfe3c146daaa437bddd5ce3db4542b1bf6)) * **transport:** Add server side peer cert support ([#228](https://github.com/hyperium/tonic/issues/228)) ([af807c3](https://github.com/hyperium/tonic/commit/af807c3ccd283cee0e424e75298cd176424767ca)) ### BREAKING CHANGES * Rename `ServiceName` to `NamedService`. * removed `interceptor_fn` and `intercep_headers_fn` from `transport` in favor of using `tonic::Interceptor`. * **codec:** Add new `Decoder/Encoder` traits and use `EncodeBuf/DecodeBuf` over `BytesMut` directly. * **build:** remove default implementations for server traits. # [0.1.0-beta.1](https://github.com/hyperium/tonic/compare/v0.1.0-alpha.5...v0.1.0-beta.1) (2019-12-19) ### Bug Fixes * **build:** Allow creating multiple services in the same package ([#173](https://github.com/hyperium/tonic/issues/173)) ([0847b67](https://github.com/hyperium/tonic/commit/0847b67c4eb66a814c8c447a57fade2552e64a85)) * **build:** Prevent duplicated client/server generated code ([#121](https://github.com/hyperium/tonic/issues/121)) ([b02b4b2](https://github.com/hyperium/tonic/commit/b02b4b238bfee96b886609396b957e2592477ecb)) * **build:** Remove async ready ([#185](https://github.com/hyperium/tonic/issues/185)) ([97d5363](https://github.com/hyperium/tonic/commit/97d5363e2b2aee456edc5db4b5b53316c8b40745)) * **build:** snake_case service names ([#190](https://github.com/hyperium/tonic/issues/190)) ([3a5c66d](https://github.com/hyperium/tonic/commit/3a5c66d5f236eaece05dbd9fd1e1a00a3ab98259)) * **docs:** typo in lib.rs ([#142](https://github.com/hyperium/tonic/issues/142)) ([c63c107](https://github.com/hyperium/tonic/commit/c63c107560db165303c369487006b3507a0e7e07)) * **examples:** Remove use of VecDeque as a placeholder type ([#143](https://github.com/hyperium/tonic/issues/143)) ([354d4fd](https://github.com/hyperium/tonic/commit/354d4fdc35dc51575f4c685fc04354f2058061ff)) * Sanitize custom metadata ([#138](https://github.com/hyperium/tonic/issues/138)) ([f9502df](https://github.com/hyperium/tonic/commit/f9502dfd7ef306fff86c83b711bc96623555ef5c)) * **transport:** Fix infinite recursion in `poll_ready` ([#192](https://github.com/hyperium/tonic/issues/192)) ([c99d13c](https://github.com/hyperium/tonic/commit/c99d13c6e669be3a6ecf428ae32d4b937393738a)), closes [#184](https://github.com/hyperium/tonic/issues/184) [#191](https://github.com/hyperium/tonic/issues/191) * **transport:** Fix lazily reconnecting ([#187](https://github.com/hyperium/tonic/issues/187)) ([0505dff](https://github.com/hyperium/tonic/commit/0505dff65a18c162c3ae398d42ed20ac54351439)), closes [#167](https://github.com/hyperium/tonic/issues/167) * **transport:** Load balance connecting panic ([#128](https://github.com/hyperium/tonic/issues/128)) ([23e7695](https://github.com/hyperium/tonic/commit/23e7695800d8f22ee8e0ba7456f5ffc4b19430c3)), closes [#127](https://github.com/hyperium/tonic/issues/127) * **transport:** Remove support for OpenSSL ([#141](https://github.com/hyperium/tonic/issues/141)) ([8506050](https://github.com/hyperium/tonic/commit/85060500f3a8f91ed47c632e07896c9e5567629a)) * **transport:** Remove with_rustls for tls config ([#188](https://github.com/hyperium/tonic/issues/188)) ([502491a](https://github.com/hyperium/tonic/commit/502491a59031dc0aa6e51a764f8edab04ab85581)) * **transport:** Update builders to move self ([#132](https://github.com/hyperium/tonic/issues/132)) ([85ef18f](https://github.com/hyperium/tonic/commit/85ef18f8b7f91047ca5bcfe5fc90e3c510c7936a)) ### Features * Add `Status` constructors ([#137](https://github.com/hyperium/tonic/issues/137)) ([997241c](https://github.com/hyperium/tonic/commit/997241c43fdb390caad19a41dc6bf67724de521a)) * expose tcp_nodelay for clients and servers ([#145](https://github.com/hyperium/tonic/issues/145)) ([0eb9991](https://github.com/hyperium/tonic/commit/0eb9991b9fcd4a688904788966d1e5ab74918571)) * **transport:** Add `remote_addr` to `Request` on the server si… ([#186](https://github.com/hyperium/tonic/issues/186)) ([3eb76ab](https://github.com/hyperium/tonic/commit/3eb76abf9fdce5f903de1a7f05b8afc8694fa0ce)) * **transport:** Add server graceful shutdown ([#169](https://github.com/hyperium/tonic/issues/169)) ([393a57e](https://github.com/hyperium/tonic/commit/393a57eadebb8e2e6d3633f70141edba647b5f65)) * **transport:** Add system root anchors for TLS ([#114](https://github.com/hyperium/tonic/issues/114)) ([ac0e333](https://github.com/hyperium/tonic/commit/ac0e333b39f60f9c304d7798a49e07e9f08a16d4)), closes [#101](https://github.com/hyperium/tonic/issues/101) * **transport:** Add tracing support to server ([#175](https://github.com/hyperium/tonic/issues/175)) ([f46a454](https://github.com/hyperium/tonic/commit/f46a45401d42f6c8b6ab449f7462735a9aea0bfc)) * **transport:** Allow custom IO and UDS example ([#184](https://github.com/hyperium/tonic/issues/184)) ([b90c340](https://github.com/hyperium/tonic/commit/b90c3408001f762a32409f7e2cf688ebae39d89e)), closes [#136](https://github.com/hyperium/tonic/issues/136) * **transport:** Enable TCP_NODELAY. ([#120](https://github.com/hyperium/tonic/issues/120)) ([0299509](https://github.com/hyperium/tonic/commit/029950904a5e1398bb508446b660c1863e9f631c)) * **transport:** Expose tcp keepalive to clients & servers ([#151](https://github.com/hyperium/tonic/issues/151)) ([caccfad](https://github.com/hyperium/tonic/commit/caccfad7e7b03d42aa1679c00a270c92a621bb0f)) ### BREAKING CHANGES * **build:** Build will now generate each service client and server into their own modules. * **transport:** Remove support for OpenSSL within the transport. # [0.1.0-alpha.5](https://github.com/hyperium/tonic/compare/v0.1.0-alpha.4...v0.1.0-alpha.5) (2019-10-31) ### Bug Fixes * **build:** Fix missing argument in generate_connect ([#95](https://github.com/hyperium/tonic/issues/95)) ([eea3c0f](https://github.com/hyperium/tonic/commit/eea3c0f99ac292efb7b8d4956fa014108af871ac)) * **codec:** Enforce encoders/decoders are `Sync` ([#84](https://github.com/hyperium/tonic/issues/84)) ([3ce61d9](https://github.com/hyperium/tonic/commit/3ce61d9860528dd4a13f719774d5c649198fb55c)), closes [#81](https://github.com/hyperium/tonic/issues/81) * **codec:** Remove custom content-type ([#104](https://github.com/hyperium/tonic/issues/104)) ([a17049f](https://github.com/hyperium/tonic/commit/a17049f1f72c9655a72fef8021072d56b3f4e543)) ### Features * Add `IntoRequest` and `IntoStreamingRequest` traits ([#66](https://github.com/hyperium/tonic/issues/66)) ([4bb087b](https://github.com/hyperium/tonic/commit/4bb087b5ff19636a20e10a669ba3b46f99c84358)) * **transport:** Add service multiplexing/routing ([#99](https://github.com/hyperium/tonic/issues/99)) ([5b4f468](https://github.com/hyperium/tonic/commit/5b4f4689a253ccca34f34bb5329b420efb9159c1)), closes [#29](https://github.com/hyperium/tonic/issues/29) * **transport:** Change channel connect to be async ([#107](https://github.com/hyperium/tonic/issues/107)) ([5c2f4db](https://github.com/hyperium/tonic/commit/5c2f4dba322b28e8132b21acfa184309de791d12)) ### BREAKING CHANGES * **transport:** `Endpoint::channel` was removed in favor of an async `Endpoint::connect`. # [0.1.0-alpha.4](https://github.com/hyperium/tonic/compare/v0.1.0-alpha.3...v0.1.0-alpha.4) (2019-10-23) ### Bug Fixes * **build:** Fix service and rpc name conflict ([#92](https://github.com/hyperium/tonic/issues/92)) ([1dbde95](https://github.com/hyperium/tonic/commit/1dbde95d844378121af54f16d9f8aa9f0f7fc2f2)), closes [#89](https://github.com/hyperium/tonic/issues/89) * **client:** Use `Stream` instead of `TrySteam` for client calls ([#61](https://github.com/hyperium/tonic/issues/61)) ([7eda823](https://github.com/hyperium/tonic/commit/7eda823c9cbe6054c39b42f8f3e7efce4698aebe)) * **codec:** Properly decode partial DATA frames ([#83](https://github.com/hyperium/tonic/issues/83)) ([9079e0f](https://github.com/hyperium/tonic/commit/9079e0f66bc75d2ce49a5537bf66c9ff5effbdab)) * **transport:** Rename server tls config method ([#73](https://github.com/hyperium/tonic/issues/73)) ([2a4bdb2](https://github.com/hyperium/tonic/commit/2a4bdb24f62bb3bbceb73e9551ba70512f94c187)) ### Features * **docs:** Add routeguide tutorial ([#21](https://github.com/hyperium/tonic/issues/21)) ([5d0a795](https://github.com/hyperium/tonic/commit/5d0a7955541509d2dbfdb9b689fb57cd2b842172)) * **transport:** Add support client mTLS ([#77](https://github.com/hyperium/tonic/issues/77)) ([335a373](https://github.com/hyperium/tonic/commit/335a373a403615a9737b2e19d0089c89bcaa3c4e)) ### BREAKING CHANGES * **transport:** `rustls_client_config` for the server has been renamed to `rustls_server_config`. # [0.1.0-alpha.3](https://github.com/hyperium/tonic/compare/v0.1.0-alpha.2...v0.1.0-alpha.3) (2019-10-09) ### Features * **build:** Expose prost-build type_attributes and field_attribu… ([#60](https://github.com/hyperium/tonic/issues/60)) ([06ff619](https://github.com/hyperium/tonic/commit/06ff619944a2f44d3aea60e653b39157c392f541)) * **transport:** Expose more granular control of TLS configuration ([#48](https://github.com/hyperium/tonic/issues/48)) ([8db3961](https://github.com/hyperium/tonic/commit/8db3961491c35955c76bf2da6a17bf8a60e3b146)) # [0.1.0-alpha.2](https://github.com/hyperium/tonic/compare/2670b349f96666c8d30d9d5d6ac2e611bb4584e2...v0.1.0-alpha.2) (2019-10-08) ### Bug Fixes * **codec:** Fix buffer decode panic on full ([#43](https://github.com/hyperium/tonic/issues/43)) ([ed3e7e9](https://github.com/hyperium/tonic/commit/ed3e7e95a5401b9b224640e17908c2182286197d)) * **codegen:** Fix Empty protobuf type and add unimplemented ([#26](https://github.com/hyperium/tonic/issues/26)) ([2670b34](https://github.com/hyperium/tonic/commit/2670b349f96666c8d30d9d5d6ac2e611bb4584e2)) * **codegen:** Use wellknown types from `prost-types` ([#49](https://github.com/hyperium/tonic/issues/49)) ([4e1fcec](https://github.com/hyperium/tonic/commit/4e1fcece150fb1f373b0ccbb69d302463ed6bcfd)) * **transport:** Attempt to load RSA private keys in rustls ([#39](https://github.com/hyperium/tonic/issues/39)) ([2c5c3a2](https://github.com/hyperium/tonic/commit/2c5c3a282a1ccc9288bc0f6fb138fc123f45dd09)) * **transport:** Avoid exit after bad TLS handshake ([#51](https://github.com/hyperium/tonic/issues/51)) ([412a0bd](https://github.com/hyperium/tonic/commit/412a0bd697b4822b94c55cb18d2373a6ed75b690)) ### Features * **codgen:** Add default implementations for the generated serve… ([#27](https://github.com/hyperium/tonic/issues/27)) ([4559613](https://github.com/hyperium/tonic/commit/4559613c37f75dde67981ee38a7f5af5947ef0be)) * **transport:** Expose http/2 settings ([#28](https://github.com/hyperium/tonic/issues/28)) ([0218d58](https://github.com/hyperium/tonic/commit/0218d58c282d6de6f300229677c99369d3ea20ed)) ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Tonic :balloon: Thanks for your help improving the project! We are so happy to have you! There are opportunities to contribute to `tonic` at any level. It doesn't matter if you are just getting started with Rust or are the most weathered expert, we can use your help. **No contribution is too small and all contributions are valued.** This guide will help you get started. **Do not let this guide intimidate you**. It should be considered a map to help you navigate the process. You may also get help with contributing in the `dev` channel, please join us! Tonic is a part of the [Tokio][tokio] and [Hyperium][hyperium] project, and follows the project's guidelines for contributing. This document is based on the [`CONTRIBUTING.md` file][tokio-contrib] in the `tokio-rs/tokio` repository. [dev]: https://gitter.im/tokio-rs/dev [tokio]: https://tokio.rs [hyperium]: https://github.com/hyperium [tokio-contrib]: https://github.com/tokio-rs/tokio/blob/master/CONTRIBUTING.md ## Conduct The `tonic` project adheres to the [Rust Code of Conduct][coc]. This describes the _minimum_ behavior expected from all contributors. [coc]: https://github.com/rust-lang/rust/blob/master/CODE_OF_CONDUCT.md ## Contributing in Issues For any issue, there are fundamentally three ways an individual can contribute: 1. By opening the issue for discussion: For instance, if you believe that you have uncovered a bug in a `tonic` crate, creating a new issue in the hyperium/tonic [issue tracker][issues] is the way to report it. 2. By helping to triage the issue: This can be done by providing supporting details (a test case that demonstrates a bug), providing suggestions on how to address the issue, or ensuring that the issue is tagged correctly. 3. By helping to resolve the issue: Typically this is done either in the form of demonstrating that the issue reported is not a problem after all, or more often, by opening a Pull Request that changes some bit of something in Tokio in a concrete and reviewable manner. **Anybody can participate in any stage of contribution**. We urge you to participate in the discussion around bugs and participate in reviewing PRs. [issues]: https://github.com/hyperium/tonic/issues ### Asking for General Help If you have reviewed existing documentation and still have questions or are having problems, you can open an issue asking for help. In exchange for receiving help, we ask that you contribute back a documentation PR that helps others avoid the problems that you encountered. ### Submitting a Bug Report When opening a new issue in the `tonic` issue tracker, users will be presented with a [basic template][template] that should be filled in. If you believe that you have uncovered a bug, please fill out this form, following the template to the best of your ability. Do not worry if you cannot answer every detail, just fill in what you can. The two most important pieces of information we need in order to properly evaluate the report is a description of the behavior you are seeing and a simple test case we can use to recreate the problem on our own. If we cannot recreate the issue, it becomes impossible for us to fix. In order to rule out the possibility of bugs introduced by userland code, test cases should be limited, as much as possible, to using only Tokio APIs. See [How to create a Minimal, Complete, and Verifiable example][mcve]. [mcve]: https://stackoverflow.com/help/mcve [template]: .github/ISSUE_TEMPLATE/bug_report.md ### Triaging a Bug Report Once an issue has been opened, it is not uncommon for there to be discussion around it. Some contributors may have differing opinions about the issue, including whether the behavior being seen is a bug or a feature. This discussion is part of the process and should be kept focused, helpful, and professional. Short, clipped responses—that provide neither additional context nor supporting detail—are not helpful or professional. To many, such responses are simply annoying and unfriendly. Contributors are encouraged to help one another make forward progress as much as possible, empowering one another to solve issues collaboratively. If you choose to comment on an issue that you feel either is not a problem that needs to be fixed, or if you encounter information in an issue that you feel is incorrect, explain why you feel that way with additional supporting context, and be willing to be convinced that you may be wrong. By doing so, we can often reach the correct outcome much faster. ### Resolving a Bug Report In the majority of cases, issues are resolved by opening a Pull Request. The process for opening and reviewing a Pull Request is similar to that of opening and triaging issues, but carries with it a necessary review and approval workflow that ensures that the proposed changes meet the minimal quality and functional guidelines of the Tokio project. ## Pull Requests Pull Requests are the way concrete changes are made to the code, documentation, and dependencies in the `tonic` repository. Even tiny pull requests (e.g., one character pull request fixing a typo in API documentation) are greatly appreciated. Before making a large change, it is usually a good idea to first open an issue describing the change to solicit feedback and guidance. This will increase the likelihood of the PR getting merged. ### Tests If the change being proposed alters code (as opposed to only documentation for example), it is either adding new functionality to a crate or it is fixing existing, broken functionality. In both of these cases, the pull request should include one or more tests to ensure that the crate does not regress in the future. There are two ways to write tests: integration tests and documentation tests (Tokio avoids unit tests as much as possible). #### Integration tests Integration tests go in the same crate as the code they are testing. Each sub crate should have a `dev-dependency` on `tonic` itself. This makes all `tonic` utilities available to use in tests, no matter the crate being tested. The best strategy for writing a new integration test is to look at existing integration tests in the crate and follow the style. #### Documentation tests Ideally, every API has at least one [documentation test] that demonstrates how to use the API. Documentation tests are run with `cargo test --doc`. This ensures that the example is correct and provides additional test coverage. The trick to documentation tests is striking a balance between being succinct for a reader to understand and actually testing the API. The type level example for `tokio_timer::Timeout` provides a good example of a documentation test: ```rust /// // import the `timeout` function, usually this is done /// // with `use tokio::prelude::*` /// use tokio::prelude::FutureExt; /// use futures::Stream; /// use futures::sync::mpsc; /// use std::time::Duration; /// /// # fn main() { /// let (tx, rx) = mpsc::unbounded(); /// # tx.unbounded_send(()).unwrap(); /// # drop(tx); /// /// let process = rx.for_each(|item| { /// // do something with `item` /// # drop(item); /// # Ok(()) /// }); /// /// # tokio::runtime::current_thread::block_on_all( /// // Wrap the future with a `Timeout` set to expire in 10 milliseconds. /// process.timeout(Duration::from_millis(10)) /// # ).unwrap(); /// # } ``` Given that this is a *type* level documentation test and the primary way users of `tokio` will create an instance of `Timeout` is by using `FutureExt::timeout`, this is how the documentation test is structured. Lines that start with `/// #` are removed when the documentation is generated. They are only there to get the test to run. The `block_on_all` function is the easiest way to execute a future from a test. If this were a documentation test for the `Timeout::new` function, then the example would explicitly use `Timeout::new`. For example: ```rust /// use tokio::timer::Timeout; /// use futures::Future; /// use futures::sync::oneshot; /// use std::time::Duration; /// /// # fn main() { /// let (tx, rx) = oneshot::channel(); /// # tx.send(()).unwrap(); /// /// # tokio::runtime::current_thread::block_on_all( /// // Wrap the future with a `Timeout` set to expire in 10 milliseconds. /// Timeout::new(rx, Duration::from_millis(10)) /// # ).unwrap(); /// # } ``` #### Generated code When making changes to `tonic-build` that affects the generated code you will need to ensure that each of the sub crates gets updated as well. Each of the sub crates like, for example `tonic-health`, generate their gRPC code via `codegen` crate. ``` cargo run --package codegen ``` ### Commits It is a recommended best practice to keep your changes as logically grouped as possible within individual commits. There is no limit to the number of commits any single Pull Request may have, and many contributors find it easier to review changes that are split across multiple commits. That said, if you have a number of commits that are "checkpoints" and don't represent a single logical change, please squash those together. Note that multiple commits often get squashed when they are landed (see the notes about [commit squashing]). #### Commit message guidelines A good commit message should describe what changed and why. 1. The first line should: * contain a short description of the change (preferably 50 characters or less, and no more than 72 characters) * be entirely in lowercase with the exception of proper nouns, acronyms, and the words that refer to code, like function/variable names * be prefixed with the name of the crate being changed (without the `tonic` prefix) and start with an imperative verb. Examples: * build: add regex for parsing field filters * tonic: add `Clone` impl for `Service` and `MakeService` 2. Keep the second line blank. 3. Wrap all other lines at 72 columns (except for long URLs). 4. If your patch fixes an open issue, you can add a reference to it at the end of the log. Use the `Fixes: #` prefix and the issue number. For other references use `Refs: #`. `Refs` may include multiple issues, separated by a comma. Examples: - `Fixes: #1337` - `Refs: #1234` Sample complete commit message: ```txt subcrate: explain the commit in one line Body of commit message is a few lines of text, explaining things in more detail, possibly giving some background about the issue being fixed, etc. The body of the commit message can be several paragraphs, and please do proper word-wrap and keep columns shorter than about 72 characters or so. That way, `git log` will show things nicely even when it is indented. Fixes: #1337 Refs: #453, #154 ``` ### Opening the Pull Request From within GitHub, opening a new Pull Request will present you with a [template] that should be filled out. Please try to do your best at filling out the details, but feel free to skip parts if you're not sure what to put. [template]: .github/PULL_REQUEST_TEMPLATE.md ### Discuss and update You will probably get feedback or requests for changes to your Pull Request. This is a big part of the submission process so don't be discouraged! Some contributors may sign off on the Pull Request right away, others may have more detailed comments or feedback. This is a necessary part of the process in order to evaluate whether the changes are correct and necessary. **Any community member can review a PR and you might get conflicting feedback**. Keep an eye out for comments from code owners to provide guidance on conflicting feedback. **Once the PR is open, do not rebase the commits**. See [Commit Squashing] for more details. ### Commit Squashing In most cases, **do not squash commits that you add to your Pull Request during the review process**. When the commits in your Pull Request land, they may be squashed into one commit per logical change. Metadata will be added to the commit message (including links to the Pull Request, links to relevant issues, and the names of the reviewers). The commit history of your Pull Request, however, will stay intact on the Pull Request page. ## Reviewing Pull Requests **Any Tokio and Hyperium community member is welcome to review any pull request**. All Tokio contributors who choose to review and provide feedback on Pull Requests have a responsibility to both the project and the individual making the contribution. Reviews and feedback must be helpful, insightful, and geared towards improving the contribution as opposed to simply blocking it. If there are reasons why you feel the PR should not land, explain what those are. Do not expect to be able to block a Pull Request from advancing simply because you say "No" without giving an explanation. Be open to having your mind changed. Be open to working with the contributor to make the Pull Request better. Reviews that are dismissive or disrespectful of the contributor or any other reviewers are strictly counter to the Code of Conduct. When reviewing a Pull Request, the primary goals are for the codebase to improve and for the person submitting the request to succeed. **Even if a Pull Request does not land, the submitters should come away from the experience feeling like their effort was not wasted or unappreciated**. Every Pull Request from a new contributor is an opportunity to grow the community. ### Review a bit at a time. Do not overwhelm new contributors. It is tempting to micro-optimize and make everything about relative performance, perfect grammar, or exact style matches. Do not succumb to that temptation. Focus first on the most significant aspects of the change: 1. Does this change make sense for Tokio? 2. Does this change make Tokio better, even if only incrementally? 3. Are there clear bugs or larger scale issues that need attending to? 4. Is the commit message readable and correct? If it contains a breaking change is it clear enough? Note that only **incremental** improvement is needed to land a PR. This means that the PR does not need to be perfect, only better than the status quo. Follow up PRs may be opened to continue iterating. When changes are necessary, *request* them, do not *demand* them, and **do not assume that the submitter already knows how to add a test or run a benchmark**. Specific performance optimization techniques, coding styles and conventions change over time. The first impression you give to a new contributor never does. Nits (requests for small changes that are not essential) are fine, but try to avoid stalling the Pull Request. Most nits can typically be fixed by the Tokio Collaborator landing the Pull Request but they can also be an opportunity for the contributor to learn a bit more about the project. It is always good to clearly indicate nits when you comment: e.g. `Nit: change foo() to bar(). But this is not blocking.` If your comments were addressed but were not folded automatically after new commits or if they proved to be mistaken, please, [hide them][hiding-a-comment] with the appropriate reason to keep the conversation flow concise and relevant. ### Be aware of the person behind the code Be aware that *how* you communicate requests and reviews in your feedback can have a significant impact on the success of the Pull Request. Yes, we may land a particular change that makes `tonic` better, but the individual might just not want to have anything to do with `tonic` ever again. The goal is not just having good code. ### Abandoned or Stalled Pull Requests If a Pull Request appears to be abandoned or stalled, it is polite to first check with the contributor to see if they intend to continue the work before checking if they would mind if you took it over (especially if it just has nits left). When doing so, it is courteous to give the original contributor credit for the work they started (either by preserving their name and email address in the commit log, or by using an `Author: ` meta-data tag in the commit. _Adapted from the [Node.js contributing guide][node]_. [node]: https://github.com/nodejs/node/blob/master/CONTRIBUTING.md [hiding-a-comment]: https://help.github.com/articles/managing-disruptive-comments/#hiding-a-comment [documentation test]: https://doc.rust-lang.org/rustdoc/documentation-tests.html ## Releasing Since the Tonic project consists of a number of crates, many of which depend on each other, releasing new versions to crates.io can involve some complexities. When releasing a new version of a crate, follow these steps: 1. First you must pick the correct version to release, if there are breaking changes make sure to select a semver compatible version bump. 2. In general, tonic tries to keep all crates at the same version to make it easy to release and figure out what sub crates you need that will work with the core version of tonic. To prepare a release branch you can follow the commands below: ``` git checkout -b ./prepare-release.sh # where version is X.Y.Z ``` 3. Once all the crate versions have been updated its time to update the changelog. Tonic uses `conventional-changelog` and it's cli to generate the changelog. ``` conventional-changelog -p angular -i CHANGELOG.md -s ``` Once the entries have been generated, you must edit the `CHANGELOG.md` file to add the version and tag to the title and edit any changelog entries. You must also add any breaking changes here as sometimes they get lost. 4. Once the changelog has been updated you can now create a `chore: release vX.Y.Z` commit and push the release branch and open a release PR. 5. Once the release PR has been approved and merged into `master` the following command will release those changes. ``` ./publish-release.sh ``` 6. Once all the crates have been released you now must create a release on github using the text from the changelog. ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "tonic", "tonic-build", "tonic-health", "tonic-protobuf", "tonic-protobuf-build", "tonic-types", "tonic-reflection", "tonic-prost", "tonic-prost-build", "tonic-web", "examples", "codegen", "grpc", "xds-client", "tonic-xds", "interop", "tests/disable_comments", "tests/wellknown", "tests/wellknown-compiled", "tests/extern_path/uuid", "tests/extern_path/my_application", "tests/integration_tests", "tests/compression", "tests/compile", "tests/web", "tests/default_stubs", "tests/deprecated_methods", ] resolver = "2" [workspace.package] rust-version = "1.88" [workspace.lints.rust] missing_debug_implementations = "warn" missing_docs = "warn" rust_2018_idioms = "warn" unreachable_pub = "warn" [workspace.lints.clippy] uninlined_format_args = "deny" [workspace.lints.rustdoc] broken_intra_doc_links = "deny" ================================================ FILE: LICENSE ================================================ Copyright (c) 2025 Lucio Franco Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![](https://github.com/hyperium/tonic/raw/master/.github/assets/tonic-banner.svg?sanitize=true) A rust implementation of [gRPC], a high performance, open source, general RPC framework that puts mobile and HTTP/2 first. > **Note**: tonic's [master](https://github.com/hyperium/tonic) branch is > currently preparing breaking changes. For the most recently *released* code, > look to the [0.14.x branch](https://github.com/hyperium/tonic/tree/v0.14.x). [`tonic`] is a gRPC over HTTP/2 implementation focused on high performance, interoperability, and flexibility. This library was created to have first class support of async/await and to act as a core building block for production systems written in Rust. [![Crates.io](https://img.shields.io/crates/v/tonic)](https://crates.io/crates/tonic) [![Documentation](https://docs.rs/tonic/badge.svg)](https://docs.rs/tonic) [![Crates.io](https://img.shields.io/crates/l/tonic)](LICENSE) [Examples] | [Website] | [Docs] | [Chat][discord] ## Overview [`tonic`] is composed of three main components: the generic gRPC implementation, the high performance HTTP/2 implementation and the codegen powered by [`prost`]. The generic implementation can support any HTTP/2 implementation and any encoding via a set of generic traits. The HTTP/2 implementation is based on [`hyper`], a fast HTTP/1.1 and HTTP/2 client and server built on top of the robust [`tokio`] stack. The codegen contains the tools to build clients and servers from [`protobuf`] definitions. ## Features - Bi-directional streaming - High performance async io - Interoperability - TLS backed by [`rustls`] - Load balancing - Custom metadata - Authentication - Health Checking ## Getting Started - The [`helloworld`][helloworld-tutorial] tutorial provides a basic example of using `tonic`, perfect for first time users! - The [`routeguide`][routeguide-tutorial] tutorial provides a complete example of using `tonic` and all its features. Examples can be found in [`examples`] and for more complex scenarios [`interop`] may be a good resource as it shows examples of many of the gRPC features. ### Rust Version `tonic`'s MSRV is `1.88`. ### Dependencies [`tonic-build`] uses `protoc` [Protocol Buffers compiler] in some APIs which compile Protocol Buffers resource files such as [`tonic_build::compile_protos()`]. [Protocol Buffers compiler]: https://protobuf.dev/downloads/ [`tonic_build::compile_protos()`]: https://docs.rs/tonic-build/latest/tonic_build/fn.compile_protos.html ## Getting Help First, see if the answer to your question can be found in the API documentation. If the answer is not there, there is an active community in the [Tonic Discord channel][discord]. We would be happy to try to answer your question. If that doesn't work, try opening an [issue] with the question. [issue]: https://github.com/hyperium/tonic/issues/new/choose ## Project Layout - [`tonic`]: Generic gRPC and HTTP/2 client/server implementation. - [`tonic-build`]: [`prost`] based service codegen. - [`tonic-types`]: [`prost`] based grpc utility types including support for gRPC Well Known Types. - [`tonic-health`]: Implementation of the standard [gRPC health checking service][healthcheck]. Also serves as an example of both unary and response streaming. - [`tonic-reflection`]: A tonic based gRPC reflection implementation. - [`examples`]: Example gRPC implementations showing off tls, load balancing and bi-directional streaming. - [`interop`]: Interop tests implementation. ## Contributing :balloon: Thanks for your help improving the project! We are so happy to have you! We have a [contributing guide][guide] to help you get involved in the Tonic project. [guide]: CONTRIBUTING.md ## License This project is licensed under the [MIT license](LICENSE). ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Tonic by you, shall be licensed as MIT, without any additional terms or conditions. [gRPC]: https://grpc.io [`tonic`]: ./tonic [`tonic-build`]: ./tonic-build [`tonic-types`]: ./tonic-types [`tonic-health`]: ./tonic-health [`tonic-reflection`]: ./tonic-reflection [`examples`]: ./examples [`interop`]: ./interop [`tokio`]: https://github.com/tokio-rs/tokio [`hyper`]: https://github.com/hyperium/hyper [`prost`]: https://github.com/tokio-rs/prost [`protobuf`]: https://protobuf.dev/ [`rustls`]: https://github.com/rustls/rustls [`interop`]: https://github.com/hyperium/tonic/tree/master/interop [Examples]: https://github.com/hyperium/tonic/tree/master/examples [Website]: https://github.com/hyperium/tonic [Docs]: https://docs.rs/tonic [discord]: https://discord.gg/6yGkFeN [routeguide-tutorial]: https://github.com/hyperium/tonic/blob/master/examples/routeguide-tutorial.md [helloworld-tutorial]: https://github.com/hyperium/tonic/blob/master/examples/helloworld-tutorial.md [healthcheck]: https://grpc.io/docs/guides/health-checking/ ================================================ FILE: SECURITY.md ================================================ # Security Policy tonic (and related projects in hyperium) uses the same security policy as the [Tokio project][tokio-security]. ## Report a security issue The process for reporting an issue is the same as the [Tokio project][tokio-security]. This includes private reporting via security@tokio.rs. [tokio-security]: https://github.com/tokio-rs/tokio/security/policy ================================================ FILE: codegen/Cargo.toml ================================================ [package] name = "codegen" authors = ["Lucio Franco "] license = "MIT" edition = "2024" rust-version = { workspace = true } [dependencies] protox = "0.9" prettyplease = "0.2" quote = "1" syn = "2" tempfile = "3.8.0" tonic-prost-build = {path = "../tonic-prost-build", default-features = false, features = ["cleanup-markdown"]} ================================================ FILE: codegen/src/main.rs ================================================ use std::{ fs::File, io::{BufWriter, Write as _}, path::{Path, PathBuf}, time::Instant, }; use protox::prost::Message as _; use quote::quote; use tonic_prost_build::FileDescriptorSet; fn main() { println!("Running codegen..."); let start = Instant::now(); // tonic-health codegen( &PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("tonic-health"), &["proto/health.proto"], &["proto"], &PathBuf::from("src/generated"), &PathBuf::from("src/generated/grpc_health_v1_fds.rs"), true, true, ); // tonic-reflection codegen( &PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("tonic-reflection"), &["proto/reflection_v1.proto"], &["proto"], &PathBuf::from("src/generated"), &PathBuf::from("src/generated/reflection_v1_fds.rs"), true, true, ); codegen( &PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("tonic-reflection"), &["proto/reflection_v1alpha.proto"], &["proto"], &PathBuf::from("src/generated"), &PathBuf::from("src/generated/reflection_v1alpha1_fds.rs"), true, true, ); // tonic-types codegen( &PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("tonic-types"), &["proto/status.proto", "proto/error_details.proto"], &["proto"], &PathBuf::from("src/generated"), &PathBuf::from("src/generated/types_fds.rs"), false, false, ); // grpc codegen( &PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("grpc"), &["proto/echo/echo.proto"], &["proto"], &PathBuf::from("src/generated"), &PathBuf::from("src/generated/echo_fds.rs"), true, true, ); println!("Codgen completed: {}ms", start.elapsed().as_millis()); } fn codegen( root_dir: &Path, iface_files: &[&str], include_dirs: &[&str], out_dir: &Path, file_descriptor_set_path: &Path, build_client: bool, build_server: bool, ) { let tempdir = tempfile::Builder::new() .prefix("tonic-codegen-") .tempdir() .unwrap(); let iface_files = iface_files.iter().map(|&path| root_dir.join(path)); let include_dirs = include_dirs.iter().map(|&path| root_dir.join(path)); let out_dir = root_dir.join(out_dir); let file_descriptor_set_path = root_dir.join(file_descriptor_set_path); let fds = protox::compile(iface_files, include_dirs).unwrap(); write_fds(&fds, &file_descriptor_set_path); tonic_prost_build::configure() .build_client(build_client) .build_server(build_server) .build_transport(false) .out_dir(&tempdir) .compile_fds(fds) .unwrap(); for path in std::fs::read_dir(tempdir.path()).unwrap() { let path = path.unwrap().path(); let to = out_dir.join( path.file_name() .unwrap() .to_str() .unwrap() .strip_suffix(".rs") .unwrap() .replace('.', "_") + ".rs", ); std::fs::copy(&path, &to).unwrap(); } } fn write_fds(fds: &FileDescriptorSet, path: &Path) { const GENERATED_COMMENT: &str = "// This file is @generated by codegen."; let mut file_header = String::new(); let mut fds = fds.clone(); for fd in fds.file.iter() { let Some(source_code_info) = &fd.source_code_info else { continue; }; for location in &source_code_info.location { for comment in &location.leading_detached_comments { file_header += comment; } } } for fd in fds.file.iter_mut() { fd.source_code_info = None; } let fds_raw = fds.encode_to_vec(); let tokens = quote! { /// Byte encoded FILE_DESCRIPTOR_SET. pub const FILE_DESCRIPTOR_SET: &[u8] = &[#(#fds_raw),*]; }; let ast = syn::parse2(tokens).unwrap(); let formatted = prettyplease::unparse(&ast); let mut writer = BufWriter::new(File::create(path).unwrap()); writer.write_all(GENERATED_COMMENT.as_bytes()).unwrap(); writer.write_all(b"\n").unwrap(); if !file_header.is_empty() { let file_header = comment_out(&file_header); writer.write_all(file_header.as_bytes()).unwrap(); writer.write_all(b"\n").unwrap(); } writer.write_all(formatted.as_bytes()).unwrap() } fn comment_out(s: &str) -> String { s.split('\n') .map(|line| format!("// {line}")) .collect::>() .join("\n") } ================================================ FILE: examples/Cargo.toml ================================================ [package] authors = ["Lucio Franco "] edition = "2024" license = "MIT" name = "examples" [[bin]] name = "helloworld-server" path = "src/helloworld/server.rs" [[bin]] name = "helloworld-client" path = "src/helloworld/client.rs" [[bin]] name = "blocking-server" path = "src/blocking/server.rs" [[bin]] name = "blocking-client" path = "src/blocking/client.rs" [[bin]] name = "routeguide-server" path = "src/routeguide/server.rs" required-features = ["routeguide"] [[bin]] name = "routeguide-client" path = "src/routeguide/client.rs" required-features = ["routeguide"] [[bin]] name = "authentication-client" path = "src/authentication/client.rs" [[bin]] name = "authentication-server" path = "src/authentication/server.rs" [[bin]] name = "load-balance-client" path = "src/load_balance/client.rs" [[bin]] name = "load-balance-server" path = "src/load_balance/server.rs" [[bin]] name = "dynamic-load-balance-client" path = "src/dynamic_load_balance/client.rs" [[bin]] name = "dynamic-load-balance-server" path = "src/dynamic_load_balance/server.rs" [[bin]] name = "tls-client" path = "src/tls/client.rs" required-features = ["tls"] [[bin]] name = "tls-server" path = "src/tls/server.rs" required-features = ["tls"] [[bin]] name = "tls-rustls-client" path = "src/tls_rustls/client.rs" required-features = ["tls-rustls"] [[bin]] name = "tls-rustls-server" path = "src/tls_rustls/server.rs" required-features = ["tls-rustls"] [[bin]] name = "tls-client-auth-server" path = "src/tls_client_auth/server.rs" required-features = ["tls-client-auth"] [[bin]] name = "tls-client-auth-client" path = "src/tls_client_auth/client.rs" required-features = ["tls-client-auth"] [[bin]] name = "tower-server" path = "src/tower/server.rs" required-features = ["tower"] [[bin]] name = "tower-client" path = "src/tower/client.rs" required-features = ["tower"] [[bin]] name = "multiplex-server" path = "src/multiplex/server.rs" [[bin]] name = "multiplex-client" path = "src/multiplex/client.rs" [[bin]] name = "gcp-client" path = "src/gcp/client.rs" required-features = ["gcp"] [[bin]] name = "tracing-client" path = "src/tracing/client.rs" required-features = ["tracing"] [[bin]] name = "tracing-server" path = "src/tracing/server.rs" required-features = ["tracing"] [[bin]] name = "uds-client-standard" path = "src/uds/client_standard.rs" required-features = ["uds"] [[bin]] name = "uds-client-with-connector" path = "src/uds/client_with_connector.rs" required-features = ["uds"] [[bin]] name = "uds-server" path = "src/uds/server.rs" required-features = ["uds"] [[bin]] name = "interceptor-client" path = "src/interceptor/client.rs" [[bin]] name = "interceptor-server" path = "src/interceptor/server.rs" [[bin]] name = "health-server" path = "src/health/server.rs" required-features = ["health"] [[bin]] name = "reflection-server" path = "src/reflection/server.rs" required-features = ["reflection"] [[bin]] name = "autoreload-server" path = "src/autoreload/server.rs" required-features = ["autoreload"] [[bin]] name = "compression-server" path = "src/compression/server.rs" required-features = ["compression"] [[bin]] name = "compression-client" path = "src/compression/client.rs" required-features = ["compression"] [[bin]] name = "mock" path = "src/mock/mock.rs" required-features = ["mock"] [[bin]] name = "grpc-web-server" path = "src/grpc-web/server.rs" required-features = ["grpc-web"] [[bin]] name = "grpc-web-client" path = "src/grpc-web/client.rs" required-features = ["grpc-web"] [[bin]] name = "streaming-client" path = "src/streaming/client.rs" required-features = ["streaming"] [[bin]] name = "streaming-server" path = "src/streaming/server.rs" required-features = ["streaming"] [[bin]] name = "json-codec-client" path = "src/json-codec/client.rs" required-features = ["json-codec"] [[bin]] name = "json-codec-server" path = "src/json-codec/server.rs" required-features = ["json-codec"] [[bin]] name = "richer-error-client" path = "src/richer-error/client.rs" required-features = ["types"] [[bin]] name = "richer-error-server" path = "src/richer-error/server.rs" required-features = ["types"] [[bin]] name = "richer-error-client-vec" path = "src/richer-error/client_vec.rs" required-features = ["types"] [[bin]] name = "richer-error-server-vec" path = "src/richer-error/server_vec.rs" required-features = ["types"] [[bin]] name = "dynamic-server" path = "src/dynamic/server.rs" [[bin]] name = "h2c-server" path = "src/h2c/server.rs" required-features = ["h2c"] [[bin]] name = "h2c-client" path = "src/h2c/client.rs" required-features = ["h2c"] [[bin]] name = "cancellation-server" path = "src/cancellation/server.rs" required-features = ["cancellation"] [[bin]] name = "cancellation-client" path = "src/cancellation/client.rs" [[bin]] name = "codec-buffers-server" path = "src/codec_buffers/server.rs" [[bin]] name = "codec-buffers-client" path = "src/codec_buffers/client.rs" [features] gcp = ["dep:prost-types", "tonic/tls-ring"] routeguide = ["dep:async-stream", "dep:tokio-stream", "dep:rand", "dep:serde", "dep:serde_json"] reflection = ["dep:tonic-reflection"] autoreload = ["dep:tokio-stream", "tokio-stream?/net", "dep:listenfd"] health = ["dep:tonic-health"] grpc-web = ["dep:tonic-web", "dep:bytes", "dep:http", "dep:hyper", "dep:hyper-util", "dep:tracing-subscriber", "dep:tower", "dep:tower-http", "tower-http?/cors"] tracing = ["dep:tracing", "dep:tracing-subscriber"] uds = ["dep:tokio-stream", "tokio-stream?/net", "dep:tower", "dep:hyper", "dep:hyper-util"] streaming = ["dep:tokio-stream", "dep:h2"] mock = ["dep:tokio-stream", "dep:tower", "dep:hyper-util"] json-codec = ["dep:serde", "dep:serde_json", "dep:bytes"] compression = ["tonic/gzip"] tls = ["tonic/tls-ring"] tls-rustls = ["dep:http", "dep:hyper", "dep:hyper-util", "dep:hyper-rustls", "dep:tower", "tower-http/util", "tower-http/add-extension", "dep:tokio-rustls"] tls-client-auth = ["tonic/tls-ring"] types = ["dep:tonic-types"] h2c = ["dep:hyper", "dep:tower", "dep:http", "dep:hyper-util"] cancellation = ["dep:tokio-util"] full = ["gcp", "routeguide", "reflection", "autoreload", "health", "grpc-web", "tracing", "uds", "streaming", "mock", "json-codec", "compression", "tls", "tls-rustls", "tls-client-auth", "types", "cancellation", "h2c"] tower = ["dep:tower", "dep:http"] default = ["full"] [dependencies] # Common dependencies tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } prost = "0.14" tonic = { path = "../tonic" } tonic-prost = { path = "../tonic-prost" } # Optional dependencies tonic-web = { path = "../tonic-web", optional = true } tonic-health = { path = "../tonic-health", optional = true } tonic-reflection = { path = "../tonic-reflection", optional = true } tonic-types = { path = "../tonic-types", optional = true } async-stream = { version = "0.3", optional = true } tokio-stream = { version = "0.1", optional = true } tokio-util = { version = "0.7.8", optional = true } tower = { version = "0.5", optional = true } rand = { version = "0.9", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } tracing = { version = "0.1.16", optional = true } tracing-subscriber = { version = "0.3", features = ["tracing-log", "fmt"], optional = true } prost-types = { version = "0.14", optional = true } http = { version = "1", optional = true } hyper = { version = "1", optional = true } hyper-util = { version = "0.1.4", optional = true } listenfd = { version = "1.0", optional = true } bytes = { version = "1", optional = true } h2 = { version = "0.4", optional = true } tokio-rustls = { version = "0.26.1", optional = true, features = ["ring", "tls12"], default-features = false } hyper-rustls = { version = "0.27.0", features = ["http2", "ring", "tls12"], optional = true, default-features = false } tower-http = { version = "0.6", optional = true } [build-dependencies] tonic-prost-build = { path = "../tonic-prost-build" } ================================================ FILE: examples/README.md ================================================ # Examples Set of examples that show off the features provided by `tonic`. In order to build these examples, you must have the `protoc` Protocol Buffers compiler installed, along with the Protocol Buffers resource files. Ubuntu: ```bash sudo apt update && sudo apt upgrade -y sudo apt install -y protobuf-compiler libprotobuf-dev ``` Alpine Linux: ```sh sudo apk add protoc protobuf-dev ``` macOS: Assuming [Homebrew](https://brew.sh/) is already installed. (If not, see instructions for installing Homebrew on [the Homebrew website](https://brew.sh/).) ```zsh brew install protobuf ``` ## Helloworld ### Client ```bash $ cargo run --bin helloworld-client ``` ### Server ```bash $ cargo run --bin helloworld-server ``` ## RouteGuide ### Client ```bash $ cargo run --bin routeguide-client ``` ### Server ```bash $ cargo run --bin routeguide-server ``` ## Authentication ### Client ```bash $ cargo run --bin authentication-client ``` ### Server ```bash $ cargo run --bin authentication-server ``` ## Load Balance ### Client ```bash $ cargo run --bin load-balance-client ``` ### Server ```bash $ cargo run --bin load-balance-server ``` ## Dynamic Load Balance ### Client ```bash $ cargo run --bin dynamic-load-balance-client ``` ### Server ```bash $ cargo run --bin dynamic-load-balance-server ``` ## TLS (rustls) ### Client ```bash $ cargo run --bin tls-client ``` ### Server ```bash $ cargo run --bin tls-server ``` ## Health Checking ### Server ```bash $ cargo run --bin health-server ``` ## Server Reflection ### Server ```bash $ cargo run --bin reflection-server ``` ## Tower Middleware ### Server ```bash $ cargo run --bin tower-server ``` ## Autoreloading Server ### Server ```bash systemfd --no-pid -s http::[::1]:50051 -- cargo watch -x 'run --bin autoreload-server' ``` ### Notes: If you are using the `codegen` feature, then the following dependencies are **required**: * [bytes](https://crates.io/crates/bytes) * [prost](https://crates.io/crates/prost) * [prost-derive](https://crates.io/crates/prost-derive) The autoload example requires the following crates installed globally: * [systemfd](https://crates.io/crates/systemfd) * [cargo-watch](https://crates.io/crates/cargo-watch) ## Richer Error Both clients and both servers do the same thing, but using the two different approaches. Run one of the servers in one terminal, and then run the clients in another. ### Client using the `ErrorDetails` struct ```bash $ cargo run --bin richer-error-client ``` ### Client using a vector of error message types ```bash $ cargo run --bin richer-error-client-vec ``` ### Server using the `ErrorDetails` struct ```bash $ cargo run --bin richer-error-server ``` ### Server using a vector of error message types ```bash $ cargo run --bin richer-error-server-vec ``` ================================================ FILE: examples/build.rs ================================================ use std::{env, path::PathBuf}; fn main() { tonic_prost_build::configure() .compile_protos(&["proto/routeguide/route_guide.proto"], &["proto"]) .unwrap(); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); tonic_prost_build::configure() .file_descriptor_set_path(out_dir.join("helloworld_descriptor.bin")) .compile_protos(&["proto/helloworld/helloworld.proto"], &["proto"]) .unwrap(); tonic_prost_build::compile_protos("proto/echo/echo.proto").unwrap(); tonic_prost_build::compile_protos("proto/unaryecho/echo.proto").unwrap(); tonic_prost_build::configure() .server_mod_attribute("attrs", "#[cfg(feature = \"server\")]") .server_attribute("Echo", "#[derive(PartialEq)]") .client_mod_attribute("attrs", "#[cfg(feature = \"client\")]") .client_attribute("Echo", "#[derive(PartialEq)]") .compile_protos(&["proto/attrs/attrs.proto"], &["proto"]) .unwrap(); tonic_prost_build::configure() .build_server(false) .compile_protos( &["proto/googleapis/google/pubsub/v1/pubsub.proto"], &["proto/googleapis"], ) .unwrap(); build_json_codec_service(); let smallbuff_copy = out_dir.join("smallbuf"); let _ = std::fs::create_dir(smallbuff_copy.clone()); // This will panic below if the directory failed to create tonic_prost_build::configure() .out_dir(smallbuff_copy) .codec_path("crate::common::SmallBufferCodec") .compile_protos(&["proto/helloworld/helloworld.proto"], &["proto"]) .unwrap(); } // Manually define the json.helloworld.Greeter service which used a custom JsonCodec to use json // serialization instead of protobuf for sending messages on the wire. // This will result in generated client and server code which relies on its request, response and // codec types being defined in a module `crate::common`. // // See the client/server examples defined in `src/json-codec` for more information. fn build_json_codec_service() { let greeter_service = tonic_prost_build::manual::Service::builder() .name("Greeter") .package("json.helloworld") .method( tonic_prost_build::manual::Method::builder() .name("say_hello") .route_name("SayHello") .input_type("crate::common::HelloRequest") .output_type("crate::common::HelloResponse") .codec_path("crate::common::JsonCodec") .build(), ) .build(); tonic_prost_build::manual::Builder::new().compile(&[greeter_service]); } ================================================ FILE: examples/data/gcp/roots.pem ================================================ # Operating CA: Comodo Group # Issuer: CN=AAA Certificate Services O=Comodo CA Limited # Subject: CN=AAA Certificate Services O=Comodo CA Limited # Label: "Comodo AAA Services root" # Serial: 1 # MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0 # SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49 # SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4 -----BEGIN CERTIFICATE----- MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe 3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== -----END CERTIFICATE----- # Operating CA: Comodo Group # Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network # Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network # Label: "AddTrust External Root" # Serial: 1 # MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f # SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68 # SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2 -----BEGIN CERTIFICATE----- MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= -----END CERTIFICATE----- # Operating CA: Comodo Group # Issuer: CN=COMODO Certification Authority O=COMODO CA Limited # Subject: CN=COMODO Certification Authority O=COMODO CA Limited # Label: "COMODO Certification Authority" # Serial: 104350513648249232941998508985834464573 # MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 # SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b # SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 -----BEGIN CERTIFICATE----- MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI 2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp +2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW /zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB ZQ== -----END CERTIFICATE----- # Operating CA: Comodo Group # Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited # Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited # Label: "COMODO ECC Certification Authority" # Serial: 41578283867086692638256921589707938090 # MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 # SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 # SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 -----BEGIN CERTIFICATE----- MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- # Operating CA: Comodo Group # Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited # Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited # Label: "COMODO RSA Certification Authority" # Serial: 101909084537582093308941363524873193117 # MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 # SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 # SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 -----BEGIN CERTIFICATE----- MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR 6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC 9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV /erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z +pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB /wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM 4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV 2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl 0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB NVOFBkpdn627G190 -----END CERTIFICATE----- # Operating CA: Comodo Group # Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network # Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network # Label: "USERTrust ECC Certification Authority" # Serial: 123013823720199481456569720443997572134 # MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 # SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 # SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a -----BEGIN CERTIFICATE----- MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= -----END CERTIFICATE----- # Operating CA: Comodo Group # Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network # Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network # Label: "USERTrust RSA Certification Authority" # Serial: 2645093764781058787591871645665788717 # MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 # SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e # SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 -----BEGIN CERTIFICATE----- MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B 3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT 79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs 8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG jjxDah2nGN59PRbxYvnKkKj9 -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust # Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust # Label: "Baltimore CyberTrust Root" # Serial: 33554617 # MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4 # SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74 # SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb -----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc # Subject: CN=Cybertrust Global Root O=Cybertrust, Inc # Label: "Cybertrust Global Root" # Serial: 4835703278459682877484360 # MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1 # SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6 # SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3 -----BEGIN CERTIFICATE----- MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW WL1WMRJOEcgh4LMRkWXbtKaIOM5V -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert Assured ID Root CA" # Serial: 17154717934120587862167794914071425081 # MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 # SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 # SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c -----BEGIN CERTIFICATE----- MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe +o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert Assured ID Root G2" # Serial: 15385348160840213938643033620894905419 # MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d # SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f # SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 -----BEGIN CERTIFICATE----- MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I 0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo IhNzbM8m9Yop5w== -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert Assured ID Root G3" # Serial: 15459312981008553731928384953135426796 # MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb # SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 # SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 -----BEGIN CERTIFICATE----- MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv 6pZjamVFkpUBtA== -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert Global Root CA" # Serial: 10944719598952040374951832963794454346 # MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e # SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 # SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 -----BEGIN CERTIFICATE----- MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert Global Root G2" # Serial: 4293743540046975378534879503202253541 # MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 # SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 # SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f -----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI 2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx 1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV 5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY 1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl MrY= -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert Global Root G3" # Serial: 7089244469030293291760083333884364146 # MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca # SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e # SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 -----BEGIN CERTIFICATE----- MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 sycX -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert High Assurance EV Root CA" # Serial: 3553400076410547919724730734378100087 # MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a # SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 # SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf -----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep +OkuE6N36B9K -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert Trusted Root G4" # Serial: 7451500558977370777930084869016614236 # MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 # SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 # SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 -----BEGIN CERTIFICATE----- MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t 9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd +SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N 0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie 4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 /YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ -----END CERTIFICATE----- # Operating CA: DigiCert # Issuer: CN=GeoTrust Global CA O=GeoTrust Inc. # Subject: CN=GeoTrust Global CA O=GeoTrust Inc. # Label: "GeoTrust Global CA" # Serial: 144470 # MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5 # SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12 # SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a -----BEGIN CERTIFICATE----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- # Operating CA: Entrust Datacard # Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. # Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. # Label: "Entrust Root Certification Authority" # Serial: 1164660820 # MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4 # SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9 # SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c -----BEGIN CERTIFICATE----- MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi 94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP 9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m 0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE----- # Operating CA: Entrust Datacard # Issuer: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only # Subject: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only # Label: "Entrust Root Certification Authority - EC1" # Serial: 51543124481930649114116133369 # MD5 Fingerprint: b6:7e:1d:f0:58:c5:49:6c:24:3b:3d:ed:98:18:ed:bc # SHA1 Fingerprint: 20:d8:06:40:df:9b:25:f5:12:25:3a:11:ea:f7:59:8a:eb:14:b5:47 # SHA256 Fingerprint: 02:ed:0e:b2:8c:14:da:45:16:5c:56:67:91:70:0d:64:51:d7:fb:56:f0:b2:ab:1d:3b:8e:b0:70:e5:6e:df:f5 -----BEGIN CERTIFICATE----- MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G -----END CERTIFICATE----- # Operating CA: Entrust Datacard # Issuer: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only # Subject: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only # Label: "Entrust Root Certification Authority - G2" # Serial: 1246989352 # MD5 Fingerprint: 4b:e2:c9:91:96:65:0c:f4:0e:5a:93:92:a0:0a:fe:b2 # SHA1 Fingerprint: 8c:f4:27:fd:79:0c:3a:d1:66:06:8d:e8:1e:57:ef:bb:93:22:72:d4 # SHA256 Fingerprint: 43:df:57:74:b0:3e:7f:ef:5f:e4:0d:93:1a:7b:ed:f1:bb:2e:6b:42:73:8c:4e:6d:38:41:10:3d:3a:a7:f3:39 -----BEGIN CERTIFICATE----- MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v 1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== -----END CERTIFICATE----- # Operating CA: Entrust Datacard # Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited # Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited # Label: "Entrust.net Premium 2048 Secure Server CA" # Serial: 946069240 # MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90 # SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31 # SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77 -----BEGIN CERTIFICATE----- MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH 4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er fF6adulZkMV8gzURZVE= -----END CERTIFICATE----- # Operating CA: Entrust Datacard # Issuer: CN=AffirmTrust Commercial O=AffirmTrust # Subject: CN=AffirmTrust Commercial O=AffirmTrust # Label: "AffirmTrust Commercial" # Serial: 8608355977964138876 # MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7 # SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7 # SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7 -----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= -----END CERTIFICATE----- # Operating CA: Entrust Datacard # Issuer: CN=AffirmTrust Networking O=AffirmTrust # Subject: CN=AffirmTrust Networking O=AffirmTrust # Label: "AffirmTrust Networking" # Serial: 8957382827206547757 # MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f # SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f # SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b -----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp 6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= -----END CERTIFICATE----- # Operating CA: Entrust Datacard # Issuer: CN=AffirmTrust Premium O=AffirmTrust # Subject: CN=AffirmTrust Premium O=AffirmTrust # Label: "AffirmTrust Premium" # Serial: 7893706540734352110 # MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57 # SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27 # SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a -----BEGIN CERTIFICATE----- MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ +jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S 5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B 8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc 0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e KeC2uAloGRwYQw== -----END CERTIFICATE----- # Operating CA: Entrust Datacard # Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust # Subject: CN=AffirmTrust Premium ECC O=AffirmTrust # Label: "AffirmTrust Premium ECC" # Serial: 8401224907861490260 # MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d # SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb # SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23 -----BEGIN CERTIFICATE----- MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D 0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== -----END CERTIFICATE----- # Operating CA: GlobalSign # Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA # Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA # Label: "GlobalSign Root CA" # Serial: 4835703278459707669005204 # MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a # SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c # SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99 -----BEGIN CERTIFICATE----- MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp 1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE 38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== -----END CERTIFICATE----- # Operating CA: GlobalSign # Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 # Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 # Label: "GlobalSign Root CA - R3" # Serial: 4835703278459759426209954 # MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 # SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad # SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b -----BEGIN CERTIFICATE----- MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH WD9f -----END CERTIFICATE----- # Operating CA: GlobalSign # Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 # Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 # Label: "GlobalSign ECC Root CA - R5" # Serial: 32785792099990507226680698011560947931244 # MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 # SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa # SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 -----BEGIN CERTIFICATE----- MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc 8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg 515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO xwy8p2Fp8fc74SrL+SvzZpA3 -----END CERTIFICATE----- # Operating CA: GlobalSign # Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 # Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 # Label: "GlobalSign Root CA - R6" # Serial: 1417766617973444989252670301619537 # MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae # SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 # SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 -----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw 1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R 8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= -----END CERTIFICATE----- # Note: "GlobalSign Root CA - R7" not added on purpose. It is P-521. # Note: "GlobalSign Root CA - R8" is not yet included in Mozilla. # Operating CA: GoDaddy # Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. # Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. # Label: "Go Daddy Root Certificate Authority - G2" # Serial: 0 # MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 # SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b # SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da -----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH /PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu 9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo 2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI 4uJEvlz36hz1 -----END CERTIFICATE----- # Operating CA: GoDaddy # Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. # Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. # Label: "Starfield Root Certificate Authority - G2" # Serial: 0 # MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 # SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e # SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 -----BEGIN CERTIFICATE----- MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg 8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 -----END CERTIFICATE----- # Operating CA: GoDaddy # Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority # Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority # Label: "Starfield Class 2 CA" # Serial: 0 # MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24 # SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a # SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58 -----BEGIN CERTIFICATE----- MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf 8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN +lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA 1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= -----END CERTIFICATE----- # Operating CA: GoDaddy # Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority # Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority # Label: "Go Daddy Class 2 CA" # Serial: 0 # MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67 # SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4 # SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4 -----BEGIN CERTIFICATE----- MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h /t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf ReYNnyicsbkqWletNw+vHX/bvZ8= -----END CERTIFICATE----- # Operating CA: Google Trust Services LLC # Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 # Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 # Label: "GlobalSign Root CA - R2" # Serial: 4835703278459682885658125 # MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30 # SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe # SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e -----BEGIN CERTIFICATE----- MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG 3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO 291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== -----END CERTIFICATE----- # Operating CA: Google Trust Services LLC # Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 # Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 # Label: "GlobalSign ECC Root CA - R4" # Serial: 14367148294922964480859022125800977897474 # MD5 Fingerprint: 20:f0:27:68:d1:7e:a0:9d:0e:e6:2a:ca:df:5c:89:8e # SHA1 Fingerprint: 69:69:56:2e:40:80:f4:24:a1:e7:19:9f:14:ba:f3:ee:58:ab:6a:bb # SHA256 Fingerprint: be:c9:49:11:c2:95:56:76:db:6c:0a:55:09:86:d7:6e:3b:a0:05:66:7c:44:2c:97:62:b4:fb:b7:73:de:22:8c -----BEGIN CERTIFICATE----- MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs ewv4n4Q= -----END CERTIFICATE----- # Operating CA: Google Trust Services LLC # Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R1 # Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R1 # Label: "GTS Root R1" # Serial: 6e:47:a9:c5:4b:47:0c:0d:ec:33:d0:89:b9:1c:f4:e1 # MD5 Fingerprint: 82:1A:EF:D4:D2:4A:F2:9F:E2:3D:97:06:14:70:72:85 # SHA1 Fingerprint: E1:C9:50:E6:EF:22:F8:4C:56:45:72:8B:92:20:60:D7:D5:A7:A3:E8 # SHA256 Fingerprint: 2A:57:54:71:E3:13:40:BC:21:58:1C:BD:2C:F1:3E:15:84:63:20:3E:CE:94:BC:F9:D3:CC:19:6B:F0:9A:54:72 -----BEGIN CERTIFICATE----- MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB AQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX mX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7 zUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P fyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc vfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4 Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp zBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO Rc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW k70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+ DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF lQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV HQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW Cu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1 d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z XPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR gyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3 d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv J4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg DdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM +SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy F62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9 SQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws E3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl -----END CERTIFICATE----- # Operating CA: Google Trust Services LLC # Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R2 # Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R2 # Label: "GTS Root R2" # Serial: 6e:47:a9:c6:5a:b3:e7:20:c5:30:9a:3f:68:52:f2:6f # MD5 Fingerprint: 44:ED:9A:0E:A4:09:3B:00:F2:AE:4C:A3:C6:61:B0:8B # SHA1 Fingerprint: D2:73:96:2A:2A:5E:39:9F:73:3F:E1:C7:1E:64:3F:03:38:34:FC:4D # SHA256 Fingerprint: C4:5D:7B:B0:8E:6D:67:E6:2E:42:35:11:0B:56:4E:5F:78:FD:92:EF:05:8C:84:0A:EA:4E:64:55:D7:58:5C:60 -----BEGIN CERTIFICATE----- MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBH MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEB AQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3Kg GjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9Bu XvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOd re7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXu PuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1 mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K 8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqj x5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsR nTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0 kzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9Ok twIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV HQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBALZp 8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiT z9D2PGcDFWEJ+YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiA pJiS4wGWAqoC7o87xdFtCjMwc3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvb pxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3DaWsYDQvTtN6LwG1BUSw7YhN4ZKJmB R64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5rn/WkhLx3+WuXrD5R RaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56GtmwfuNmsk 0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC 5AwiWVIQ7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiF izoHCBy69Y9Vmhh1fuXsgWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLn yOd/xCxgXS/Dr55FBcOEArf9LAhST4Ldo/DUhgkC -----END CERTIFICATE----- # Operating CA: Google Trust Services LLC # Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R3 # Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R3 # Label: "GTS Root R3" # Serial: 6e:47:a9:c7:6c:a9:73:24:40:89:0f:03:55:dd:8d:1d # MD5 Fingerprint: 1A:79:5B:6B:04:52:9C:5D:C7:74:33:1B:25:9A:F9:25 # SHA1 Fingerprint: 30:D4:24:6F:07:FF:DB:91:89:8A:0B:E9:49:66:11:EB:8C:5E:46:E5 # SHA256 Fingerprint: 15:D5:B8:77:46:19:EA:7D:54:CE:1C:A6:D0:B0:C4:03:E0:37:A9:17:F1:31:E8:A0:4E:1E:6B:7A:71:BA:BC:E5 -----BEGIN CERTIFICATE----- MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQsw CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU MBIGA1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQA IgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout 736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2A DDL24CejQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud DgQWBBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFuk fCPAlaUs3L6JbyO5o91lAFJekazInXJ0glMLfalAvWhgxeG4VDvBNhcl2MG9AjEA njWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOaKaqW04MjyaR7YbPMAuhd -----END CERTIFICATE----- # Operating CA: Google Trust Services LLC # Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R4 # Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R4 # Label: "GTS Root R4" # Serial: 6e:47:a9:c8:8b:94:b6:e8:bb:3b:2a:d8:a2:b2:c1:99 # MD5 Fingerprint: 5D:B6:6A:C4:60:17:24:6A:1A:99:A8:4B:EE:5E:B4:26 # SHA1 Fingerprint: 2A:1D:60:27:D9:4A:B1:0A:1C:4D:91:5C:CD:33:A0:CB:3E:2D:54:CB # SHA256 Fingerprint: 71:CC:A5:39:1F:9E:79:4B:04:80:25:30:B3:63:E1:21:DA:8A:30:43:BB:26:66:2F:EA:4D:CA:7F:C9:51:A4:BD -----BEGIN CERTIFICATE----- MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQsw CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQA IgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/l xKvRHYqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud DgQWBBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0 CMRw3J5QdCHojXohw0+WbhXRIjVhLfoIN+4Zba3bssx9BzT1YBkstTTZbyACMANx sbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11xzPKwTdb+mciUqXWi4w== -----END CERTIFICATE----- ================================================ FILE: examples/data/route_guide_db.json ================================================ [ { "location": { "latitude": 407838351, "longitude": -746143763 }, "name": "Patriots Path, Mendham, NJ 07945, USA" }, { "location": { "latitude": 408122808, "longitude": -743999179 }, "name": "101 New Jersey 10, Whippany, NJ 07981, USA" }, { "location": { "latitude": 413628156, "longitude": -749015468 }, "name": "U.S. 6, Shohola, PA 18458, USA" }, { "location": { "latitude": 419999544, "longitude": -740371136 }, "name": "5 Conners Road, Kingston, NY 12401, USA" }, { "location": { "latitude": 414008389, "longitude": -743951297 }, "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA" }, { "location": { "latitude": 419611318, "longitude": -746524769 }, "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA" }, { "location": { "latitude": 406109563, "longitude": -742186778 }, "name": "4001 Tremley Point Road, Linden, NJ 07036, USA" }, { "location": { "latitude": 416802456, "longitude": -742370183 }, "name": "352 South Mountain Road, Wallkill, NY 12589, USA" }, { "location": { "latitude": 412950425, "longitude": -741077389 }, "name": "Bailey Turn Road, Harriman, NY 10926, USA" }, { "location": { "latitude": 412144655, "longitude": -743949739 }, "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA" }, { "location": { "latitude": 415736605, "longitude": -742847522 }, "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA" }, { "location": { "latitude": 413843930, "longitude": -740501726 }, "name": "162 Merrill Road, Highland Mills, NY 10930, USA" }, { "location": { "latitude": 410873075, "longitude": -744459023 }, "name": "Clinton Road, West Milford, NJ 07480, USA" }, { "location": { "latitude": 412346009, "longitude": -744026814 }, "name": "16 Old Brook Lane, Warwick, NY 10990, USA" }, { "location": { "latitude": 402948455, "longitude": -747903913 }, "name": "3 Drake Lane, Pennington, NJ 08534, USA" }, { "location": { "latitude": 406337092, "longitude": -740122226 }, "name": "6324 8th Avenue, Brooklyn, NY 11220, USA" }, { "location": { "latitude": 406421967, "longitude": -747727624 }, "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA" }, { "location": { "latitude": 416318082, "longitude": -749677716 }, "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA" }, { "location": { "latitude": 415301720, "longitude": -748416257 }, "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA" }, { "location": { "latitude": 402647019, "longitude": -747071791 }, "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA" }, { "location": { "latitude": 412567807, "longitude": -741058078 }, "name": "New York State Reference Route 987E, Southfields, NY 10975, USA" }, { "location": { "latitude": 416855156, "longitude": -744420597 }, "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA" }, { "location": { "latitude": 404663628, "longitude": -744820157 }, "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA" }, { "location": { "latitude": 407113723, "longitude": -749746483 }, "name": "" }, { "location": { "latitude": 402133926, "longitude": -743613249 }, "name": "" }, { "location": { "latitude": 400273442, "longitude": -741220915 }, "name": "" }, { "location": { "latitude": 411236786, "longitude": -744070769 }, "name": "" }, { "location": { "latitude": 411633782, "longitude": -746784970 }, "name": "211-225 Plains Road, Augusta, NJ 07822, USA" }, { "location": { "latitude": 415830701, "longitude": -742952812 }, "name": "" }, { "location": { "latitude": 413447164, "longitude": -748712898 }, "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA" }, { "location": { "latitude": 405047245, "longitude": -749800722 }, "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA" }, { "location": { "latitude": 418858923, "longitude": -746156790 }, "name": "" }, { "location": { "latitude": 417951888, "longitude": -748484944 }, "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA" }, { "location": { "latitude": 407033786, "longitude": -743977337 }, "name": "26 East 3rd Street, New Providence, NJ 07974, USA" }, { "location": { "latitude": 417548014, "longitude": -740075041 }, "name": "" }, { "location": { "latitude": 410395868, "longitude": -744972325 }, "name": "" }, { "location": { "latitude": 404615353, "longitude": -745129803 }, "name": "" }, { "location": { "latitude": 406589790, "longitude": -743560121 }, "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA" }, { "location": { "latitude": 414653148, "longitude": -740477477 }, "name": "18 Lannis Avenue, New Windsor, NY 12553, USA" }, { "location": { "latitude": 405957808, "longitude": -743255336 }, "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA" }, { "location": { "latitude": 411733589, "longitude": -741648093 }, "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA" }, { "location": { "latitude": 412676291, "longitude": -742606606 }, "name": "1270 Lakes Road, Monroe, NY 10950, USA" }, { "location": { "latitude": 409224445, "longitude": -748286738 }, "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA" }, { "location": { "latitude": 406523420, "longitude": -742135517 }, "name": "652 Garden Street, Elizabeth, NJ 07202, USA" }, { "location": { "latitude": 401827388, "longitude": -740294537 }, "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA" }, { "location": { "latitude": 410564152, "longitude": -743685054 }, "name": "13-17 Stanley Street, West Milford, NJ 07480, USA" }, { "location": { "latitude": 408472324, "longitude": -740726046 }, "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA" }, { "location": { "latitude": 412452168, "longitude": -740214052 }, "name": "5 White Oak Lane, Stony Point, NY 10980, USA" }, { "location": { "latitude": 409146138, "longitude": -746188906 }, "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" }, { "location": { "latitude": 404701380, "longitude": -744781745 }, "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA" }, { "location": { "latitude": 409642566, "longitude": -746017679 }, "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA" }, { "location": { "latitude": 408031728, "longitude": -748645385 }, "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA" }, { "location": { "latitude": 413700272, "longitude": -742135189 }, "name": "367 Prospect Road, Chester, NY 10918, USA" }, { "location": { "latitude": 404310607, "longitude": -740282632 }, "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA" }, { "location": { "latitude": 409319800, "longitude": -746201391 }, "name": "11 Ward Street, Mount Arlington, NJ 07856, USA" }, { "location": { "latitude": 406685311, "longitude": -742108603 }, "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA" }, { "location": { "latitude": 419018117, "longitude": -749142781 }, "name": "43 Dreher Road, Roscoe, NY 12776, USA" }, { "location": { "latitude": 412856162, "longitude": -745148837 }, "name": "Swan Street, Pine Island, NY 10969, USA" }, { "location": { "latitude": 416560744, "longitude": -746721964 }, "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA" }, { "location": { "latitude": 405314270, "longitude": -749836354 }, "name": "" }, { "location": { "latitude": 414219548, "longitude": -743327440 }, "name": "" }, { "location": { "latitude": 415534177, "longitude": -742900616 }, "name": "565 Winding Hills Road, Montgomery, NY 12549, USA" }, { "location": { "latitude": 406898530, "longitude": -749127080 }, "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA" }, { "location": { "latitude": 407586880, "longitude": -741670168 }, "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA" }, { "location": { "latitude": 400106455, "longitude": -742870190 }, "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA" }, { "location": { "latitude": 400066188, "longitude": -746793294 }, "name": "" }, { "location": { "latitude": 418803880, "longitude": -744102673 }, "name": "40 Mountain Road, Napanoch, NY 12458, USA" }, { "location": { "latitude": 414204288, "longitude": -747895140 }, "name": "" }, { "location": { "latitude": 414777405, "longitude": -740615601 }, "name": "" }, { "location": { "latitude": 415464475, "longitude": -747175374 }, "name": "48 North Road, Forestburgh, NY 12777, USA" }, { "location": { "latitude": 404062378, "longitude": -746376177 }, "name": "" }, { "location": { "latitude": 405688272, "longitude": -749285130 }, "name": "" }, { "location": { "latitude": 400342070, "longitude": -748788996 }, "name": "" }, { "location": { "latitude": 401809022, "longitude": -744157964 }, "name": "" }, { "location": { "latitude": 404226644, "longitude": -740517141 }, "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA" }, { "location": { "latitude": 410322033, "longitude": -747871659 }, "name": "" }, { "location": { "latitude": 407100674, "longitude": -747742727 }, "name": "" }, { "location": { "latitude": 418811433, "longitude": -741718005 }, "name": "213 Bush Road, Stone Ridge, NY 12484, USA" }, { "location": { "latitude": 415034302, "longitude": -743850945 }, "name": "" }, { "location": { "latitude": 411349992, "longitude": -743694161 }, "name": "" }, { "location": { "latitude": 404839914, "longitude": -744759616 }, "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA" }, { "location": { "latitude": 414638017, "longitude": -745957854 }, "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA" }, { "location": { "latitude": 412127800, "longitude": -740173578 }, "name": "" }, { "location": { "latitude": 401263460, "longitude": -747964303 }, "name": "" }, { "location": { "latitude": 412843391, "longitude": -749086026 }, "name": "" }, { "location": { "latitude": 418512773, "longitude": -743067823 }, "name": "" }, { "location": { "latitude": 404318328, "longitude": -740835638 }, "name": "42-102 Main Street, Belford, NJ 07718, USA" }, { "location": { "latitude": 419020746, "longitude": -741172328 }, "name": "" }, { "location": { "latitude": 404080723, "longitude": -746119569 }, "name": "" }, { "location": { "latitude": 401012643, "longitude": -744035134 }, "name": "" }, { "location": { "latitude": 404306372, "longitude": -741079661 }, "name": "" }, { "location": { "latitude": 403966326, "longitude": -748519297 }, "name": "" }, { "location": { "latitude": 405002031, "longitude": -748407866 }, "name": "" }, { "location": { "latitude": 409532885, "longitude": -742200683 }, "name": "" }, { "location": { "latitude": 416851321, "longitude": -742674555 }, "name": "" }, { "location": { "latitude": 406411633, "longitude": -741722051 }, "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA" }, { "location": { "latitude": 413069058, "longitude": -744597778 }, "name": "261 Van Sickle Road, Goshen, NY 10924, USA" }, { "location": { "latitude": 418465462, "longitude": -746859398 }, "name": "" }, { "location": { "latitude": 411733222, "longitude": -744228360 }, "name": "" }, { "location": { "latitude": 410248224, "longitude": -747127767 }, "name": "3 Hasta Way, Newton, NJ 07860, USA" } ] ================================================ FILE: examples/data/tls/ca.key ================================================ -----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC6XFF7KXhQGBXf 0OAcPt7sg8xlsHlYzW2A9N7FzhS8sBoVzTC/A+j6n4OkF+8tTYdi0hF/Bd+x/b3k h4knocY12ZVzMc2wHIVkWPdxQszRK7l0tzFvQm+JKJaquw5DuIS6oWKYATCINMbw +YNoF57Qh+z2Buc8rQ+fiLDR4Cv2etKl0VBwlJ0nO6yiutHTOtHl3SXcFzg9tb4K yBMaNVru/SMvkbL6CfKDdHymLEo9nfGFMb/hkUggTS5mJL4kAcQf2nYvPGhWXn5F DizFiOPLFD5ajCA7vXMz4ogxDkIhGMDObP3khdOfWs22PqOXwnNCe0LzxMnzkCpF +1iChaYJjGZvqKTxLb8xKkTwI9sbhFFCun1FcpSQ9N3fr51oDzrPwsB4Itoe1M7S sm+x5nqa6PZrmt+ZsK+RTxCQ1og1/pGOnct1HGauexD51aapMn1KXuHbGeoXS5DI kf3nW8lrXXDAw2a6Sm/CFlzvGnna9AZ4ldwfLfl2pIpPjQ0kIUSdti3M5mvPFc+k 3frEBlypt0sWXec2BBAviOH0skEkqXqf5paGsuXgcAgihjGUIuxViHhdFK5hQ1dN LzLdBGUu/wpphhEPbjvkywge1xqjndLXAd808A+XHiYFtyWMFAsJeTAYJjERfewl j6Ge2gLGpZVb9MV2ZTVeYsvBkqtjXwIDAQABAoICAD2kWqlH6s1nYhjhoLeHDjqi T8IVENZQQNfGZ0d7Zn2RLFeowuZz1yTLDYKCDjFocw87V+Exoq/fs+d682GCD6tx OI9dWmFV1cN+7/3tMA1CDrpt+/KGwZjXLZr0e3/n8TNAPXn07sYm2uULSy1rnrLw Ou+YEfWOctv8nSwWn8QMFVAWv6o1ZhP7l5tN+yiIzLPhJew6W/aBfoZXboYdPuJN Jc3OvioZjzdvGOnoPXhLHX/GmGb2pKpWjTHpFmGXmfVFUBFIxGRJJjAWQ7XlFR/v pr5RmjnYbNotJIpBYptK8j91arejkn+jy+ZrqrYAchp75gX2wiwHtAvo2vp1VZlD GOYe767tIBHRYgzXJxAH5DrdndmWlaXsVuE06hy0IE0rPXspQzr6+2+G+bld9MxG X/SNU6NT8xW31WM08A9BXFTFUgqBeXDKhltQ9TmWrOOEtAYXNFrMc6AP17sza4h0 zSKY8BZukDV/qjeBSU7+U2IvKFY3mtv/87QPSePaP6uV7Df3yeLVwn11f3spqhhJ UgiCSHo3uQprjO6rypYmXKMXM+VOWgHesF2bCJQE8La7Z7g24xgY0Oqj7EC9QNqI vMjFmwMZ19HSY64JXvUxgYwgsDR/+YNVTV3JR7+Vip6huUopc60ZZTCfTsZ6s66h N+gWhRi4CyfvPAE32hUdAoIBAQDqHdVZneJpohGG5jhLs6IkfQJ8sVbazqIJp9I/ ozC/H8UPcc0HuCQeKZYad5lE2E8wexIuQAPpB6KkzJfcUzgI+Eytra5LBx+FyTIk KKycX4kcaeIF/bzV5bAp6X9QtFh2O6I8ZBAzuXSvc0iKLtGTfjNmfdJ7XSFYrnwJ 7LA4DViS3/At/fF67gE6z4z27htu30AXIW/lbU3eeDEP5XNROPKelQAP/bTnaBXE RJclBjCVRpMkKGBMmUbd3+wIMro66k4cMWDxLnb9zJKAivpIt8TbLFi4MeTG74sF s07jLDsTxrm4WAYPhn1Rmt2woErshBvDK39YLbnteZlMi/vjAoIBAQDLx7wyrOHS SoUYDhwiS5XY97k8uzCMsW6S0cudIhJYr2abccj4IvlxAYWSRKEhDmos3I3TVwmh PyaT+tL9A7b2My5HfzNz/Y3oUVd51ufVGLE4pghJJjO6Edr5Wb7JvNpCzP2H5NjV swM+8ONJt0jwpRRRIaRFho4mPjwGtWXuzrL3CGBiXQefFOzZsyVtUG4uCPtcQpOU IbInshzDfJOymgGmbEQLsdN8XTpzGGSz0j/NpxjNT1H10KkZ9+NqfZozeql9Izey 2fqdjsbAzKCL7y9vlrjPsEMOZKjBGWvpZBwOn+mMPc71v39jLiLBZnuYHNOKoAC1 mxJ9Do30igtVAoIBAQC4FUsrkwxzOL8FTkJXq+BDRpRNDXgYxj78zptv9FYhAc8G DNpFRpIHsXVYTFAUpOznVu39tdIdSial5EVINZsq2moYaidQ0UIFBSVK7zyCHFCI Ke1R/qibm2YAHpxADf48wTkYuSlQMnPAfSo9lQCvM50g6rA01g6hV1kqyJPrDvtl SXXmA/X7TedjoczaYHDrpdkUFvOP93kyA1m4gRdCdz+2V7xb1oaHKf1rfO9Ham2L Apox5RmLQT5KuYYzEAgEyTUvz9fE7F8dwtwy/JQ911mPaHg+JOUZU0MB8XKHB8FQ FIL1oyjozjv9jYLhHbir7liSBsKzyAiY5HMYkD03AoIBAHFwoCjJqvCJAWxxtmG1 GBbvWJQhVJaN05Mx7RptNC9gfUs9XXYc6iVphnT1dYlUX/DXWrByvG6iHBS2xauJ 3NlThojQm9EPLmdMmNi/tNEg7M8vRl+KP7NuayryNc5SLmKPgPecgsT74WuxZ6XK vXURQK0lgDAgBpPtgzbs1nDJakEwzY8UYMDDQlKycrxW0O8ZmuwyN7t3wphsg6yj dgkvyIlfrcWg2a1arMYTp0OfYFtYkOsCJAsmfGxzXYsTnrrXpvB9oW0UAXqiV6xO fXVI0mxZSEp9weaKTJMqVrNXQnM1vCqQ4dxWHVEWBs0JAvab3XtHNP3j9LffWVDv Y/0CggEAF5+Rx6eqFwVgbxgM1nDAzym2VCP64JqryM5PpYpQ7tcxhNSzK7/6yB4K /pZ9C41e7j+GRTiubuopxjBLTXuHU3i0/CcYlmPQNF/Cm72EHFvuVW6LlMefL4YV 8fRpRthE3GbFdK2g0KHWZU2i08mToMgJWOuCrFMlpoweDI2Wa2kTvp7PfxNsY371 f95nwXKSQHdNhbqujf56WoemYMUP2UYzrAShyXXlyvLJnJOl4rtCtTZ5z2z55lM6 cfDWFCmj9eIjJ593bEQ7mku+2ID0OnSsRNMBEicNjXvgFMPA0o8aj6eZJcBzrPQO t9QUZ0RmRfLe8MaKDwmFDUIWy+Dk1A== -----END PRIVATE KEY----- ================================================ FILE: examples/data/tls/ca.pem ================================================ -----BEGIN CERTIFICATE----- MIIFijCCA3KgAwIBAgIUao+2CjQxejCTMhfk0SNhJJ9gZK4wDQYJKoZIhvcNAQEL BQAwKTEOMAwGA1UECgwFVG9uaWMxFzAVBgNVBAMMDnRlc3Qtc2VydmVyX2NhMB4X DTI1MTExNTE4MTI0OFoXDTM1MTExMzE4MTI0OFowKTEOMAwGA1UECgwFVG9uaWMx FzAVBgNVBAMMDnRlc3Qtc2VydmVyX2NhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A MIICCgKCAgEAulxReyl4UBgV39DgHD7e7IPMZbB5WM1tgPTexc4UvLAaFc0wvwPo +p+DpBfvLU2HYtIRfwXfsf295IeJJ6HGNdmVczHNsByFZFj3cULM0Su5dLcxb0Jv iSiWqrsOQ7iEuqFimAEwiDTG8PmDaBee0Ifs9gbnPK0Pn4iw0eAr9nrSpdFQcJSd JzusorrR0zrR5d0l3Bc4PbW+CsgTGjVa7v0jL5Gy+gnyg3R8pixKPZ3xhTG/4ZFI IE0uZiS+JAHEH9p2LzxoVl5+RQ4sxYjjyxQ+WowgO71zM+KIMQ5CIRjAzmz95IXT n1rNtj6jl8JzQntC88TJ85AqRftYgoWmCYxmb6ik8S2/MSpE8CPbG4RRQrp9RXKU kPTd36+daA86z8LAeCLaHtTO0rJvseZ6muj2a5rfmbCvkU8QkNaINf6Rjp3LdRxm rnsQ+dWmqTJ9Sl7h2xnqF0uQyJH951vJa11wwMNmukpvwhZc7xp52vQGeJXcHy35 dqSKT40NJCFEnbYtzOZrzxXPpN36xAZcqbdLFl3nNgQQL4jh9LJBJKl6n+aWhrLl 4HAIIoYxlCLsVYh4XRSuYUNXTS8y3QRlLv8KaYYRD2475MsIHtcao53S1wHfNPAP lx4mBbcljBQLCXkwGCYxEX3sJY+hntoCxqWVW/TFdmU1XmLLwZKrY18CAwEAAaOB qTCBpjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSc0TW892b8ugXApahDGkrR djIj7TBkBgNVHSMEXTBbgBSc0TW892b8ugXApahDGkrRdjIj7aEtpCswKTEOMAwG A1UECgwFVG9uaWMxFzAVBgNVBAMMDnRlc3Qtc2VydmVyX2NhghRqj7YKNDF6MJMy F+TRI2Ekn2BkrjAOBgNVHQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggIBALib vntll0DzlAIQWisIILM29xxohcypxDpnvvLn5RKhulW4FOiSNCzipSq7KMVj0aPY 2oLD6WZaS3AgzOKvviAo9M63EyLRieYMWSysDAxMKYAPNTFnaT4lwGCeypzvw9Nu BfGV/66TBefaWgvlE854SdvLN6xQQLc64OstjburCXmM8YiNdjv+a86V1afL45Fn TU2r2hB3DXaqfFXp+vnrXKQRKGplabXmUnnp4HR/vUqlDZ4/0c9q5aZktWFfd8ne 4eSNfYibsNdWnaAxVWQjaCrNS90Vehsoe+/weYQkZv7scot9tAYqfdjxl5pjWeP7 AypJhmYeWHwtMm32EPZAiX/UhVXMkkKWs8M4ruwlKbMceSrR5YQNTLG1wXaX+DkU 6og0VVY9H233ep+K6r7JaG+HTc3kzvfXA0XAfGxOTC9wvclQsNMn+P60ua7O5NA1 BbzPm2HTlY/exxYSo4ot2OelUACvObbxoVtsLfp10i8p2CpkZRwfpptcJayUEJu5 F2dNjSAYt9X0OTLjMIz1IuUVZLzdcdcSaL09RaAB1gWkmuS/Zy9WPA7t1AyTPDEz GYXBGIUxEPcSVzuqGq2Ba/nQYdU+GDKi8JjT2I7V2w2pbkf/sljvM7+zgZ8/Ag3T gpG2yPYc30FQ2n6euD2+nA6gwGP8wzT4irXk2GaL -----END CERTIFICATE----- ================================================ FILE: examples/data/tls/client1.key ================================================ -----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC3dUytzpnqjxW6 Dh2Ci0b4fB0iA8D251L0fVLIZE99XCXHOmv4YoFUIwTzbMEiTymgXgvqvyLgM9+v uXRNQi45yt111BKWSd5hTjsIr+mYuTdgRcCEDtNMwmEg+3dibHqSmXcxrg54tL5c RLPkozS1N5/i7tBgNpbQTdAtjFPRcGPfl2Xsc2o9IX7Jd0JdZscyQXzxmbaT2rjx s+QTAl7J74/HffjRVVwOhIVM18VuPlqiFmhzhrKjj3/58m+Q3xvgy2snSXC9JDsB GqfpFXLQg11pWsQh2f7k1ercd4RhJqJHUIa+fViSt5SAN384KynAZ/MJjDyTvMwZ 9Wn/5DTZ57p+OgxeVnsBz/oe7EUpPoIMOSjbHPK1mHOaNqOCWsqe27SI4JLoLwsT ealMsj8ZHz5CGKY90eQ53nVNKaX9BBmo5EqeiMjdQaNu9qlOIhAfnX7EWXUUWEvg twuzOIihacGf2XDGClBJwY7ogcBfrytC9btDJBQ6a/5gwK9DpRmVhOVR64vHTUYx NJOOAbPaVAZCYdD6zzl9nn+oc7C41KnV/IG+cPFutb9EBfV3peodbv1zvunzGbMC LwX3Vve4dPukle8bXOfkjafrn+eyH0QqzMN5r9tRSsOGG6z60cs7ugOQMWJijcT7 lKTsAveZ9vBApX4xWtgVrEEckt7z2wIDAQABAoICAAnkERZDJUPizaZnob+qsqYt oC8in1dbHBCCZowsnFHYk1DXyCpuunpGyRA2tNLyDFR7vY4ScAGWdQuMUXDjjd/K ctYqG4vEFRvkk9o2txQSKEzQdeiow/ZHeeUaqafCIXmgkdnj1ck8NWcpbRQj7cY3 ZtoH9hlkgrey+kj6X9UoMg+ZTK11gNd65Chd0v4mJGcukLCV5k9iY3Dkj09XDrU5 wo6xg/talslRlmjmLobFuy0x+BUCa28rYBhMXHzQX+MRad2HCsT9HGaa1qKtErDw jtYMnAvB17Q5XvLenrco1hGc4ybcqyx90/Z1si4yZYsyD4WxlzJ/40vNp2wHFFzz qmgtgyFa2TiMPcsbgHyM3C8k0dYwFo74uGrjFJyRGpKKZUVNihjcG5uqLxi4ay0V aU2Q8LyUbP6gm5TH3xy2jX3/RDJAnmk1/apmfC73NwpoQVDXFgrBu3P2r+HuECDC jFnPUsrCtPGVWmX4+dJi+ZgUJ4DRVwI2PQYIPzp4e9SUk0n4P3OZg8lXRWXOmQEy VBYHcoICq9sLgxjyzBsYctqFxrLswDSX4myj8IcW+XfZVuNUoxuj5MLIS9Vc4HbM qLSQIlhTyhmTfKAIV7owL8X9rR/MMlpdiUDjU8kXpSmMomb0ASESCIXhvmC3AkUL j6R2PwSZmuCESM7OXPtBAoIBAQDyldSeFdW7yLUWjp4ovMnxWgATjTOQvOU5Hcxj 53nrPIj8oa/ZfgrWIKMfRfKaX/s+SH/+j3Vv3UFJeCPhWWayNJAUbqEd8Kvl+K77 ea+rlr3mI+GOv/iRq3V3WGnkPXrhIuDo0ORm+eWOZOWAFPL7jA0NP2He6h3PWujA brscor6ZXxMypi7yIgyBx/REHGkuJL+1aad+omF5rW3zG7xKDirTfsPQQOhf0BlT 5obbSn0KruSL4GqiRavMBrOxU7Pjhb67JuN0QPPcrVvLcx/YvDKGCPmgbiUxspal RSGkJAlQ8LTgMRs6xQ6PHmhO5N/bgp4zKKWvkr62C2i3RwxvAoIBAQDBmm8SvTup MZITkyhfxTqXS4AxISLeLDjBuNnvXZR7yhol/pUzkA3HfkJWIXNwRYElFgBYbL8f EDeMU+hOx4V8oJCkbANASnBpRd4DmLcj7P7YUSVrWArKO+y8bUz0sy50WXR7Zop9 TAilcpwrOWG6SfpewdjmstatT06ou/1AU+Ny+Owo2rChUet7DLrgQwZy+XE405v4 1d/orM87PZ27o1/080dGPB3Ny0nrpWxJhT4J+iz82+p+vWvaTnSrmCi2l57i+dbc Td6C7rbiYjn8Y/jY9EJU00o/Dx4Uf0rsFsjJ+IOF7qQHJZuEWO68jR67c23am3lE 6KO51ULkNN1VAoIBAFjF6ePcG7Y3kcVqdYh05fXTuLlu79LkvYG0XOqmd+BU69B/ numZjX+ku+0i3NAPldLKF0Th3NkN/+lR9NdvrvxB7gP8JCvfuhhTdD2E33uMk3vX 36Asslskgr0k9sNWmFQxPlsUrrDcfFwqoi3H/M9/Bfu2GSvJQxVxsEFThFfLWrKn r0/WrtFfEnKf6MzQFNGVEy7hNjFKXR95DwZrPPFg091Hw4K/bgo6Djq83tb7IF07 eVmSy5MMqfzk6vdWqTr248B7T7toVZWJP1FplNrsrBSOzkMea7APKb/bV59IrLwZ CigM8GkGWfiX6RYN/bnHx/rywgdJTU8zR6PidTMCggEActpKMITAgwQcU66GUiJw OtcYiozM4Z68YPhnmaAbeUCROJ8KJle8RO/7LJuVnzIshjLCK7L/ws7dFUul0i59 W0zp4hEN8LL4cwt2xQ1xAEgVe4DQQRku9YCNVc9FyxkNYwq6loZjfCeCLZyLVv0o o9pFRLedFGdeAdy9nk4/1Eyv70IK38W06U7u8sW/i1FX3xdp+rtWmU1QEvmJyuwn yewG/grg6qK5T5/dD4XIcukvv72BuNRCDcQT2qOWhUG0TXYvVRnARFuRuH0jU7PQ EJHCS/rD6wyZzEUMpD5L1TlDDsZ2SBslhfPiiaY0ovjZFX1J21lGnQGiN5lzoGxY LQKCAQEAjawytF2oj8Sm8woMzHxuL+Fx2alPQy8Cn+O9CItdPA6mQCUbIh1QaXHy l9ECxysGMxU13TZ2mlK13jr2EfBPH1EkGkBhwiMYhcixZkQoBVR7qRUIGPdT3B/K aYGUDQB8n/u66MXwP4xkuq3FWkN5EFbOiQHnXdK3A6RyOlnB2WPq57J3JAkM6QAj m144rNSrdAhVSjxtUzzDTxr4UmNsnSqhBq84OFbaoy16YHO2DRW/I5pwko0USGBo Cq3PAcfHquktqColM9gS6sQv6dsKIE5zF0rU4ilh8nNkblc056xXMPr+jU28a9Y4 8Y87Hf3mj7ies/4Fweb+7nY2QUVjwQ== -----END PRIVATE KEY----- ================================================ FILE: examples/data/tls/client1.pem ================================================ -----BEGIN CERTIFICATE----- MIIFRDCCAyygAwIBAgICA+gwDQYJKoZIhvcNAQELBQAwKTEOMAwGA1UECgwFVG9u aWMxFzAVBgNVBAMMDnRlc3QtY2xpZW50X2NhMB4XDTI1MTExNTE4MTI0OVoXDTM1 MTExMzE4MTI0OVowJzEOMAwGA1UECgwFVG9uaWMxFTATBgNVBAMMDHRlc3QtY2xp ZW50MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALd1TK3OmeqPFboO HYKLRvh8HSIDwPbnUvR9UshkT31cJcc6a/higVQjBPNswSJPKaBeC+q/IuAz36+5 dE1CLjnK3XXUEpZJ3mFOOwiv6Zi5N2BFwIQO00zCYSD7d2JsepKZdzGuDni0vlxE s+SjNLU3n+Lu0GA2ltBN0C2MU9FwY9+XZexzaj0hfsl3Ql1mxzJBfPGZtpPauPGz 5BMCXsnvj8d9+NFVXA6EhUzXxW4+WqIWaHOGsqOPf/nyb5DfG+DLaydJcL0kOwEa p+kVctCDXWlaxCHZ/uTV6tx3hGEmokdQhr59WJK3lIA3fzgrKcBn8wmMPJO8zBn1 af/kNNnnun46DF5WewHP+h7sRSk+ggw5KNsc8rWYc5o2o4Jayp7btIjgkugvCxN5 qUyyPxkfPkIYpj3R5DnedU0ppf0EGajkSp6IyN1Bo272qU4iEB+dfsRZdRRYS+C3 C7M4iKFpwZ/ZcMYKUEnBjuiBwF+vK0L1u0MkFDpr/mDAr0OlGZWE5VHri8dNRjE0 k44Bs9pUBkJh0PrPOX2ef6hzsLjUqdX8gb5w8W61v0QF9Xel6h1u/XO+6fMZswIv BfdW97h0+6SV7xtc5+SNp+uf57IfRCrMw3mv21FKw4YbrPrRyzu6A5AxYmKNxPuU pOwC95n28EClfjFa2BWsQRyS3vPbAgMBAAGjeDB2MAwGA1UdEwEB/wQCMAAwHQYD VR0OBBYEFKKvs39cg9gXBOMojbExGCVSMdpxMA4GA1UdDwEB/wQEAwIF4DAWBgNV HSUBAf8EDDAKBggrBgEFBQcDAjAfBgNVHSMEGDAWgBQTvTgqEYFsfGGqd7S0QS1N /eV5eTANBgkqhkiG9w0BAQsFAAOCAgEAAYFFzp47Pbg18uVUYyBiEoJmp9RwIZUm yrywuLiOvRc6g7jgtgBdUmMhykIfeUlbDfiYlLy2+prRUi7RwCHJN+87itEh5yU/ qpaBgS/oexi3LJRYcTw0SlS5jk03xA5WH2jXJ+Pov0aD2tVtZvvGS5G//RzC12Jz 0Rd8v3RcK/azhf6UjIZcnEV1soBlu1YDaSDWcBYDuZZKzO+W4zqjSyKVLt8k0rpE Fvzyph6YSpN5ag3f0aJjuG6qD3Aq/60SbHMnNofLbp1KmLwaLwHdqFmyJApEfmRA e1OMryM8QiuoSKTn0JoD1AbxBG+RXAC5YGlyTcjU7ZUC0pOM/UPvtQA1a8btRyrg mlsXEWFmM9NGT0sHdQd4BIFjBRuT2LK7Zni25ReaVMCk9Q/KT9+DyyKkjWx7oV0B YPxrNrW3wBb86ZeT+40/MwTi0q2a8EzRvCL6iMHX6LaJE/+7K28S6vk9ei6ALzUe bdr4a+UcZhNFK17sdGypOVln+N5TvWNXYYXtoMqAlqGEWYlVtQtLHtTaFmq2UzQV +OIbMWGVa1aZLn9VPDyAX231xtKnM/+mO60e5lEtjyNuv2jwxpAJw4kkRk2fQSl9 o10OWChTysmQda/rBgh3u2srOEYExBWid2RzqNmaSN8WPc3iX0Scw1d4iaC1cKF4 vlnCk6W7k5w= -----END CERTIFICATE----- ================================================ FILE: examples/data/tls/client2.key ================================================ -----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCS5QyVoX2R4cXC UU93PnibomqTYDHwPBTcBUaSQ3CR6ALw1XzK2R3Ze/4itXmLPPrs5aE5fm7U1YgN nmznCFTHorr9FsU7HT3cuh5VELRE2k5qqlJex8Tx+0Xm4y1R+JjzX9ZoJjCXJ2A1 mxmMygUZcQWrwQyGTq4xKl+CtKxZxPVU/98fFknoBkOsBaux50+Kgw4gdvpOBjm+ Q855Wa18WysSttbY2NBL5aogWL5bCzYJjhsWx6OU8LKgm18OzHVf/ZHBKyftfXsC xQzPWONVFUIkyvM4vMkPPnM0CFOn8NclYqL6qor0oaMUY6i4NYB0i5XtEhJB7XCU hmNeQ1DHaXFoK5rG6o23CyTnQgR1FmqJqv4YqZyunM7YSD223Y+3Ue/YtunVjwSy L4QZ7pedRaWwVaFhz8oBEwJu81sEvE+D9EFXnNOO3bvXyGt0kmLyTb/TxEqZ2kPe koLny81coY8U+sOJV54rWRYMPD3AzNNPyw5ZnpPDLN0UX6P8uMqOB1BPOBOvIZtH IdOR2H/RkJ4LFtRPCHYdZ2SSlezowjqYE7tCWskjldz64YWkamgNvYLFPSA08BMQ 6YiQzKEl/9+92OdvRDEsELzaGZZL2sID1nyLWHengxhH//BAAiYI1qGhsYxzdyhI YObe/QwfX/axopi2jpkK48RZ/4OFTwIDAQABAoICAEXNPaukr2yw4i4yQRaiwuEB zq71WQWVJzxd7Wj3TvimTS9v4vrpLPv5pYuMvJLjiUEk5PXTFItiZGJEU+98z/yF YWF+fn5WqNzIPD2j0GWfbv/Uq4uKVhW+lzrd3N1Eyjlr4xBehVxuQ5N1fjCelsm+ ITm6ZF816CRIxFXx0KxfoUygwb+d8cSlpWaK00mYZv2kxQO8Rbjp0GqPAgA8Euoy 0Eb2RyWH8YL/QVZZlSriKu1jb4/lkd/meJtE+Wio02Wnsqkd/r0lkVyBklK9JFl7 3pBpOCZP3yK1RD0Hb+kv0qxbMKL4g0s3u6ymprfSBJZc9gNPQQm+8G5DZdiXGDzR Za+RxPoUtJlvcOSUm2AlCRlTqhnfCp2qLiGLNrUWji6qgsuANDheIM8R7bX0TPjN 1Y3GGMDMKyPC7hp8CCg4jscAIOJ1Ss0pAvf9vVPCBFQM8No0gDGc7GHNTTqb2lln x7JS9KPYz2NL7HLVGxRnyOrhSNhCB5Q0KJhSNF+Srw1naDyuDRMld4+7sDs5C51C TGQI51BeCcBF9PZzF3+qX5F5O1n9E3wsTU7sg5dyau/ryRXo/qbPSTF/nZFc60sG MNnkS5GpNC2PxtHocXviiGPc7NO1TgoEWalhmila4b4i3VHRZdl6BgERKIUyqyot gGv4HOHZ7N41bGlpwVoxAoIBAQDLcuzuwuCTOEy5JJEhhNkEr+2tPACg11lPM6F4 hE6LBI5qjU+l2TK++H4jC6dvhF9KlWszzEQ1Yspprgrh8ZmLYrbiVimd2Hv27PYJ GxBxnaQl96vrhmPW/qNGSkyEEX08F9AVmGgn/2mX/YHsQSzG+U1cgDrnGl8kGQ1d +4AYGiBrMowIZXtcb4qyJinx5guJb3mkg4AdswDEhXj96TieNhKVrPQRG02RUSE1 kgYa6rgwHC7jJ6BZ0WsS2yNj2D6nO2UXQ2V6FYINauCmyPaMIpqjUzby5UCgNgqe D2kqNNj5l/cAWC4x399xy7zLkZd15VhqWxNfuKPSXFsveuPFAoIBAQC41nkSa+ru RW8Fpb9RxZ/6ke6IyVTnvLqWp5A9CYVpjOBDOk38kMmX/cIoN1eyoD715fArO3gF IzLMGmR6WSgbUEWKdrVFBlUC/JQ9Tj3Th/KWBs1cnljK2Hg64k83UWynoZIL7ZGQ Q/x4WOqTvW7cCB7bOzI9v2kIzvkXSd8qT2TLNKnuR+n+6++Sx1/3P5OjPbUYlkbX eeHDmFGj3mCqtQgGo7GJmvOhIgbvSft7BY9eZ5OF4TxuiQ9uat3tIMOPD+bgvBuj Suii7K9je/cE1YOxDz/UBfyHA90LkVcKW2tO9e6i3tp0pzLd9ivzCI5i88JnVjNH vuDw/wv2wxIDAoIBAHAB8MGulpdWyp6vrBtJGXXEKdVTEIF8rhW1tjM0nE1bD9FH xU7omlCbXE6NDvyNYy0bwC5/ShoeLpQqFqG4MrGTgl5v02+sjOswIHB47v+uK34f sg30KilmfZuoMiIPwuP/tDb/dnB25Lqh/hKE+1L8VAQWMNelJDYqeLqCSU57q9d4 t7GztUv5uOFFs5gS780Vi2HwZ+tx7n1Dgo5ABUTNPkQbOS/l2Tmk8eSdZTESgp8y FPpIibaJXiq+bOh+WFgXkhtRpp+lEbmzWsVYJKyYLIy4tqrZXlAWEJheoaZz+/TG Hl2ZLq2UtF7hLSriGAH3Z0r+o1byv3aEFEu6m4UCggEAXO+5wIFyhoty3ywPnlX3 sk1d2nkgr8Q9LTLjW01GP2QN/r96JtvGAR4eWYo6Fh5siccrkxE7r5mbGPTMQJhD ijg6Pvyg+CvO4smM36ZPf+SDHNwetcsIajWdfj38BpxyPXcHr+eroRYOA5TxnYdK Dmgm26RQBqwPa9ZleEg0ZVm2HFZGewC9rueCdhK5NeBJo3KLc+lbhUxLL8WOhw5x HGQZvPzhb4bxqLsrMXXJdHm/NIBvtIkjkZBoqeQh4fDvYydjtuveGaS4g/Lt3N+f vFZ++K/qL/kyl9BhfEd/tD1zHyiY7FuRC8Zl1STJxFExBp/5x2uYLSLUh1g188Tx cQKCAQEAjx8JRviB6JCimQfXdNbJxaXwHUs7ByFrGpYbNfz4vznMumIKal8Uik7w ioCRZdKIdQ6WECNFIaH+xmQHJSAaRGBATSVwXmegQPpCoS0zmAKY+YL+Q/zfkP/6 58bEkurf4AC+GzBIO1/g1LoW9lAFN/MJrd5p5X2VM1Cw2haB8bD8aHim3KRNpLik f0khbIGLDqRlabyhUMBwBq4aDzYHlUw5YgKBs5baX88jJGzXlpVlTG0jke+FXnWt v7WX0iNidzkVQSOkG9qpEphw794CwSQf4XURuXkMxcxSLAuVYWyD+rjMdKkhqk0Z GRmcc1cjoz7e2eepBp+mnRIotyhjbQ== -----END PRIVATE KEY----- ================================================ FILE: examples/data/tls/client2.pem ================================================ -----BEGIN CERTIFICATE----- MIIFRDCCAyygAwIBAgICA+gwDQYJKoZIhvcNAQELBQAwKTEOMAwGA1UECgwFVG9u aWMxFzAVBgNVBAMMDnRlc3QtY2xpZW50X2NhMB4XDTI1MTExNTE4MTI0OVoXDTM1 MTExMzE4MTI0OVowJzEOMAwGA1UECgwFVG9uaWMxFTATBgNVBAMMDHRlc3QtY2xp ZW50MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJLlDJWhfZHhxcJR T3c+eJuiapNgMfA8FNwFRpJDcJHoAvDVfMrZHdl7/iK1eYs8+uzloTl+btTViA2e bOcIVMeiuv0WxTsdPdy6HlUQtETaTmqqUl7HxPH7RebjLVH4mPNf1mgmMJcnYDWb GYzKBRlxBavBDIZOrjEqX4K0rFnE9VT/3x8WSegGQ6wFq7HnT4qDDiB2+k4GOb5D znlZrXxbKxK21tjY0EvlqiBYvlsLNgmOGxbHo5TwsqCbXw7MdV/9kcErJ+19ewLF DM9Y41UVQiTK8zi8yQ8+czQIU6fw1yViovqqivShoxRjqLg1gHSLle0SEkHtcJSG Y15DUMdpcWgrmsbqjbcLJOdCBHUWaomq/hipnK6czthIPbbdj7dR79i26dWPBLIv hBnul51FpbBVoWHPygETAm7zWwS8T4P0QVec047du9fIa3SSYvJNv9PESpnaQ96S gufLzVyhjxT6w4lXnitZFgw8PcDM00/LDlmek8Ms3RRfo/y4yo4HUE84E68hm0ch 05HYf9GQngsW1E8Idh1nZJKV7OjCOpgTu0JaySOV3PrhhaRqaA29gsU9IDTwExDp iJDMoSX/373Y529EMSwQvNoZlkvawgPWfItYd6eDGEf/8EACJgjWoaGxjHN3KEhg 5t79DB9f9rGimLaOmQrjxFn/g4VPAgMBAAGjeDB2MAwGA1UdEwEB/wQCMAAwHQYD VR0OBBYEFOJW70lD3qk02xQbPYugfe8r+RJtMA4GA1UdDwEB/wQEAwIF4DAWBgNV HSUBAf8EDDAKBggrBgEFBQcDAjAfBgNVHSMEGDAWgBQTvTgqEYFsfGGqd7S0QS1N /eV5eTANBgkqhkiG9w0BAQsFAAOCAgEARvyLPHAioCTNYvv88yJoDWnbfDMpOYFJ CDcxGgat2KSuF9LB/Rua58VnkbvtkaymNo1SfGNLIXL83bRdmFA1CgW/sxqnYUpG oh8UfQbRV9AuJu0RrRubqFZ+MOW70jzs8LvUZIFQtBjz9w2zpmRuxKZOAgmvqXLw 8FKZoi2P8lktRw2ie361Z1yFSV/UCKWrIBYFpb8W5/+Elb75B/iRl7jZwGO4WuCV /8gYgP2ScWuHP4LfkTcifOYcxRUhZ856N2uEGL3ywcKuvjz/EaK9NyTQb6PTyy10 z77fjtCqu6uAW7y2cXkw9paLnp71GF3pglvgXzngpYbJN6wgZn/BNPKP9V8/pgR5 hJkezxr2e7cCrERp+a3eq2CDsrN7SDKgAJh0ZjyDg8Iuc/rP6OwbTqPyvU00bWi5 kFCp8qkOBun2v7eRpRfwjXB55O0iZID69xmy3UwX3E9as3ftzHnIcTGeiD3OoX8E hw4sXLlIH9Cgo4lIpihSWGPkJC76qikXu80IVUquutJFEnuOkAF/oXrhl14nSfAI hDQZwebh5YYC3TFQqjyQbzBcG8QFVb53YWsywEW3EpRp9Dm7qY4dmlG5lGmY5pKh PYGfB6tvLLU/iNoOgOVw15enRrPcnYHWKrUPQNexYzfvBywR8tVd3QcXBa+XZtu8 NbP1BZHBSuE= -----END CERTIFICATE----- ================================================ FILE: examples/data/tls/client_ca.key ================================================ -----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC0Yc8MUU0XaWXh I5IpE5BD7JYWmhDGZnZY/YdCgkwo/EZ+MPi6OuCWfS/9U7Z3q0AeBxZ037Wpu5Dy rqkgNMOsWZZjnHQ6QDjI9ieanZfCsg4SN45hO8JDUtOxSyi4BpdYuLo+N+G4x37n af5VBg5vqzcgYnaubjasvBApWNgVInyzMuifU25nxzGN66zZEd/dJZYFNvTxm9O3 CWj2ZMWvQ+3jX0Z0XnWqkcoekrmAzPFaBLkBm7pGKFfOA+RBB4yxSh10K0TXocp3 hzqNqvxBMxO3w5e6eK9yQIAyEChqo3I2cbE81H5k6M2zPWawCuegwWniHRVCdW23 5kNETkNYEfrJOpE8TQkbEnri5oh9rrGREMqiN8UKc2mBbkyq1rOSwVecSyuDskaT mBEcx4WuBKbocbG7bJityvCJkhFnzXLiMrTzbtoaq62g40QF5Fg/rF1u7oAyEr17 AP7Mf5u9itkTkYphGJMnOcJ7EM8h+32+wR9GJljAeNz4gDDc0wg7S6c8KPL/lXLH g4ekNqu9PGtHqoq/qeKPzA9df2gss+dMVFm2j8YQ6Ohay4u4v6VFsiHGUTwBr8bL Fp2HAK/hKZDYyZZcKqOwVXuU6ur+g5Et1w1iIJe8MSQ2egPfjoFziL1wbPj9s6IL qqIo3AF08h8zSU4I+PqMi5IAukkVmwIDAQABAoICAA728lNmR0wC7XXRGqYXvmc0 MHttiqi7BWR6nAcTgjgAPeToMSpaChnSBvIwSLoC1g3nrqBBkvOi4PKRNOy8E+Ov hv96klXm0A80BLMaEeYQGYS/YSuF5qbpLzDNOHKT8Whl4uWYgzxuKIQQUg5Bs17m SGuCX7A26ohIDdswSLTwKIJBXfUh2c34Q48vlyZnCjhxjtGd7pJAyD6uJCwUziWt MAYl+FVdJMvizm7c41DhL8C1FZghkEGTxT0NK5mNg8mNiYYCtPJitMZXzK7GGalb EG61ZlgM/RkwarI8ju3R437KzdZrFpVOjwootOUz8mVuxlcA7KuXzGNuUfRPvdxj yurNVZkGynCC7g+FYhUMZDIJU7YCFEgXmBr7bTleMEccGQIroW7zTXy8dhweaI0/ 7ehhy6qQObPxCehGYEXY8YThoPtpQbrKP2Y9PNQg92tZym+Jifb602MrYeSzxp71 SWXy6LSrODo+FpdKW6YHclRJx74Kkbp7Ux6oKYWqYvIlzAPOtY1FtMRBM/9p9AVI BrouacRZpTP35Mt6Mxg9JpAxBOGviebS3SoJvGD2Bmq1ALb8zwYhmT9M1whgrJiG H3qOWyS9Koh66m8Pny7V/XMh5KcLky+u+DN79dkdnzf8MNAzslr5vnBg67byvvVX dnH2x1DXkTXI6bxLIjhxAoIBAQDrn/E7YVJ7R1dSd9Tn/irhL6mkRJrNroMX8G37 otdhvkBiuLiTOcjoukINew1/GmzhrPnsWXx9zWQmpbtTOTfTRFIQ+UGw8LwAHBNS jhK+J2u9tFFWJ6lonrLD+HxhdkmauvcNs07Ol2jNMNpqBc2gQ+CdzljGZcSekTM9 asmkXut65bEk3tVhV+MylTEyGlGuMaR02mSLcfmIApOUFCNAi+kvfAs7qRUKjCBu Np4SE6k6mY2/cGj2vjyDSTYdhgnVv/evyZU/5YWAclTpKS92PuJyLBQXwqcyU/FS jhEUFz8PCA2DfYPLjSUPNSh8yO5oR3kXF89MB5SvTWYVH0vZAoIBAQDD+vN7hO52 Y/Uf5Y7wStU0kIb4PGtusEyyDXXIcTFTEuyN+34gZaACK6bQd52YhpRoLvtLeai8 xwrLtZRFUP8X0/rpwXvdl9EWWRNNnJnWx6wuzNZncrxTj6UHoKclYdP+ypJmneIf 8QipdROKcbvMEkgq8UJlL7iE2xj23VfKAyvm4NNyATzWQ2L5x/8xCu9X+6oHyJ2h W0aYA2bCSl0n465/1Ix33riAVR9v0S/Y3ndYJ142c0Sxt6uHTpvEFrcieJA3hklf 1HR/rOr4F9u1rueO482fok/8OrUdOiWEDS8WptGVmRujXYv/wlNEPuC7qx4xliel yi0bhoNYJ8iTAoIBAByaqhpfUjgNDRjB22LehC4aLyn1+iUGDnwVgASQXD1Nb1uM uTuGRFGcavBgA99uQdGTwjNjGa3cBVB7xiXwSEqpfJz65XKphuksf1wCS1wyRO4e udEPt40v1tvbip36Ui0qjVtobSS/VMW0LI+6bKAMHXSK3FQZfqkRT3shP6FotIWS 5NJpCtZqaPZ3DiXa9BeFh0V4TcRv7a8JQQk0+KKZWZGeKW7ws9E3+afnkYD4Sg8H HSIkb1mk9oupk5w57W+5gkQg4LGFF5PkTVKQ4WYldDAQEdBgnROLBTzUalZaDBmz DJDt59YKKHmUJnofmnGmW5jmFxBPy0kMyFvAFOECggEBAKHP7v86Z6WqrHaUlGuE bfsDpd0KDe4LdJfCk1BXXVFpG5WzY5UeM3n3TrlrOdz4qUpIGEZeAds0QI6nsu90 i9rBtLcSgNIaipF4JH79YOu8W0cc653oVuRrgugEVl2AI4iI+03s6ApVekBoISU6 +MLrCVkjcB2ZxDE1sawKX3S1H6d2VD8aFIAYjX0NC4ATtkCf0uiwVK4obeYPUOX0 fmv0Cl0TQpOqeg3DurwZLPdT35gSkTBGBh7yNpb2aZgC5Vx5zSF7J9QO182fGTaU hFzDN/97yYgXfREV6/pgyx73v7xlKkDpdK9zCqe2bAe6HtX02G46uyug/ZNvgbzk xC8CggEATGFt5dV6TZUJPC2k7BNev/VwP6CK41fTRVGkaMdiM+HAcHYAOaPoHvln MwxfQTof3KkgttA3/94S9l0IWO6kda0nKaNwcW4IHfmqQ0moqpOeHgz5XaxSMhX/ FwdzAkYUeB95wCjI/nY5jAA++0sejs1E0Ao1nmLRkAg9Whe4uRrcqp1u02vZQ5kk Gq+xG2VvQCGOyTocynVzXXsXfoohDKDszKNxQX99UZ24bAI38oundxqFnYTgifnN NSwNyDXlxK20CphSkAzt3Odf/Lr243Bxm4d0C0H29ZKceuReMDFBeUNStt+oluXy UKS7m+M+WmCsNX9UkKWh09Ks6KVO4Q== -----END PRIVATE KEY----- ================================================ FILE: examples/data/tls/client_ca.pem ================================================ -----BEGIN CERTIFICATE----- MIIFijCCA3KgAwIBAgIUP0tYMykcdcDwKamtdfqbDg//4O0wDQYJKoZIhvcNAQEL BQAwKTEOMAwGA1UECgwFVG9uaWMxFzAVBgNVBAMMDnRlc3QtY2xpZW50X2NhMB4X DTI1MTExNTE4MTI0OFoXDTM1MTExMzE4MTI0OFowKTEOMAwGA1UECgwFVG9uaWMx FzAVBgNVBAMMDnRlc3QtY2xpZW50X2NhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A MIICCgKCAgEAtGHPDFFNF2ll4SOSKROQQ+yWFpoQxmZ2WP2HQoJMKPxGfjD4ujrg ln0v/VO2d6tAHgcWdN+1qbuQ8q6pIDTDrFmWY5x0OkA4yPYnmp2XwrIOEjeOYTvC Q1LTsUsouAaXWLi6PjfhuMd+52n+VQYOb6s3IGJ2rm42rLwQKVjYFSJ8szLon1Nu Z8cxjeus2RHf3SWWBTb08ZvTtwlo9mTFr0Pt419GdF51qpHKHpK5gMzxWgS5AZu6 RihXzgPkQQeMsUoddCtE16HKd4c6jar8QTMTt8OXunivckCAMhAoaqNyNnGxPNR+ ZOjNsz1msArnoMFp4h0VQnVtt+ZDRE5DWBH6yTqRPE0JGxJ64uaIfa6xkRDKojfF CnNpgW5MqtazksFXnEsrg7JGk5gRHMeFrgSm6HGxu2yYrcrwiZIRZ81y4jK0827a GqutoONEBeRYP6xdbu6AMhK9ewD+zH+bvYrZE5GKYRiTJznCexDPIft9vsEfRiZY wHjc+IAw3NMIO0unPCjy/5Vyx4OHpDarvTxrR6qKv6nij8wPXX9oLLPnTFRZto/G EOjoWsuLuL+lRbIhxlE8Aa/GyxadhwCv4SmQ2MmWXCqjsFV7lOrq/oORLdcNYiCX vDEkNnoD346Bc4i9cGz4/bOiC6qiKNwBdPIfM0lOCPj6jIuSALpJFZsCAwEAAaOB qTCBpjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQTvTgqEYFsfGGqd7S0QS1N /eV5eTBkBgNVHSMEXTBbgBQTvTgqEYFsfGGqd7S0QS1N/eV5eaEtpCswKTEOMAwG A1UECgwFVG9uaWMxFzAVBgNVBAMMDnRlc3QtY2xpZW50X2NhghQ/S1gzKRx1wPAp qa11+psOD//g7TAOBgNVHQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggIBAGTV HtsZUI+XI9LB96T4xV4eHdk1hOt930QOS1pzdKEEFJhZYD8PlII94MlPYhpFbpTX U68XDA2zXMfKnNWp1WsOWudEGxy0MPVVOER6vc/w6qfdGi7vbxLB/pqGwifKAiPm +I3XcRXbLF1W7KbbsT61u9gecSWK4WoZUPY0Zs3hrDvut4jRVZr/egXQVvYp2gGs 8oTiVLuT2rYIGrDYinsQcY8DQJUnJQZwZM+hErNpylzVXRfM1nv8NzB2QQGzT3Pc 8GdNXD1iGUywjo8/LcEvp6HzSrFeqli9ZkT62yaMZncv3iw/8yxmrCClW53xTj6C AuXhPsBbLKxODM21BBkxxWFpOQc3U2vzgcXpo60DWfMBAnayDwXiCuM8tdRp38kH 0dEaAMMZHrL9xm4Lfyzqg7siIb5xn+EzZ8EXPc4WFXT257lraJp9mLHWHSWozJX3 6+mUXe/9qwfiyL4vYpSTXLEcuYim7Nwo5CL0jkERKNosp07USSDFWwZKHI91Hd/n HIlusaYwAVuZpcC6Xzj+0/kbl+ImfSTtrEATNFf3pEa37BW7nx4piB3j+xXyW9Gs qnZwBTq2q8JEUEhO5jGd+v6zrPWMx1ndHrfr14wP7yzgpIv991Qgr+4YMMAgx5u3 w9/u28aPz0wbD3HuDWwxmUoX+5uEPw4rxFc5k3CO -----END CERTIFICATE----- ================================================ FILE: examples/data/tls/create.sh ================================================ #!/bin/bash # Create the server CA certs. openssl req -x509 \ -newkey rsa:4096 \ -nodes \ -days 3650 \ -keyout ca.key \ -out ca.pem \ -subj /O=Tonic/CN=test-server_ca/ \ -config ./openssl.cnf \ -extensions test_ca \ -sha256 # Create the client CA certs. openssl req -x509 \ -newkey rsa:4096 \ -nodes \ -days 3650 \ -keyout client_ca.key \ -out client_ca.pem \ -subj /O=Tonic/CN=test-client_ca/ \ -config ./openssl.cnf \ -extensions test_ca \ -sha256 # Generate two server certs. openssl genrsa -out server.key 4096 openssl req -new \ -key server.key \ -out server_csr.pem \ -subj /O=Tonic/CN=test-server/ \ -config ./openssl.cnf \ -reqexts test_server openssl x509 -req \ -in server_csr.pem \ -CAkey ca.key \ -CA ca.pem \ -days 3650 \ -set_serial 1000 \ -out server.pem \ -extfile ./openssl.cnf \ -extensions test_server \ -sha256 openssl verify -verbose -CAfile ca.pem server.pem openssl genrsa -out server2.key 4096 openssl req -new \ -key server2.key \ -out server2_csr.pem \ -subj /O=Tonic/CN=test-server/ \ -config ./openssl.cnf \ -reqexts test_server2 openssl x509 -req \ -in server2_csr.pem \ -CAkey ca.key \ -CA ca.pem \ -days 3650 \ -set_serial 1000 \ -out server2.pem \ -extfile ./openssl.cnf \ -extensions test_server2 \ -sha256 openssl verify -verbose -CAfile ca.pem server2.pem # Generate two client certs. openssl genrsa -out client1.key 4096 openssl req -new \ -key client1.key \ -out client1_csr.pem \ -subj /O=Tonic/CN=test-client1/ \ -config ./openssl.cnf \ -reqexts test_client openssl x509 -req \ -in client1_csr.pem \ -CAkey client_ca.key \ -CA client_ca.pem \ -days 3650 \ -set_serial 1000 \ -out client1.pem \ -extfile ./openssl.cnf \ -extensions test_client \ -sha256 openssl verify -verbose -CAfile client_ca.pem client1.pem openssl genrsa -out client2.key 4096 openssl req -new \ -key client2.key \ -out client2_csr.pem \ -subj /O=Tonic/CN=test-client2/ \ -config ./openssl.cnf \ -reqexts test_client openssl x509 -req \ -in client2_csr.pem \ -CAkey client_ca.key \ -CA client_ca.pem \ -days 3650 \ -set_serial 1000 \ -out client2.pem \ -extfile ./openssl.cnf \ -extensions test_client \ -sha256 openssl verify -verbose -CAfile client_ca.pem client2.pem # Cleanup the CSRs. rm *_csr.pem ================================================ FILE: examples/data/tls/openssl.cnf ================================================ [req] distinguished_name = req_distinguished_name attributes = req_attributes [req_distinguished_name] [req_attributes] [test_ca] basicConstraints = critical,CA:TRUE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer:always keyUsage = critical,keyCertSign [test_server] basicConstraints = critical,CA:FALSE subjectKeyIdentifier = hash keyUsage = critical,digitalSignature,keyEncipherment,keyAgreement subjectAltName = @server_alt_names [server_alt_names] DNS.1 = example.com DNS.2 = *.example.com DNS.3 = example.test DNS.4 = localhost IP.1 = 127.0.0.1 IP.2 = 0:0:0:0:0:0:0:1 [test_server2] basicConstraints = critical,CA:FALSE subjectKeyIdentifier = hash keyUsage = critical,digitalSignature,keyEncipherment,keyAgreement subjectAltName = @server2_alt_names [server2_alt_names] DNS.1 = *.test.com [test_client] basicConstraints = critical,CA:FALSE subjectKeyIdentifier = hash keyUsage = critical,nonRepudiation,digitalSignature,keyEncipherment extendedKeyUsage = critical,clientAuth ================================================ FILE: examples/data/tls/server.key ================================================ -----BEGIN PRIVATE KEY----- MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCckNy1wG9TFgHM 2LlY2YnuaiqqYAyvNtzOH7Dsbgodhj2pNdeiN4siNjmL5QwgXuVJ7V8EyYBNIvu0 YjKWOtmcaXGEAdIBIw+aqfBKkrLOBNhkgG55W6bIALtbNpmLNFJ2QNOslH/Hm/fA 4r2Y7TFNSp+dHjUMaq6jSwMuC/Z0bYDEGrnHpQ4Ml/jYivB1i2n23SJBcDL4y39x LJ5abnWDSZkO+sGe4Eu0UV3JkLTPOmIHtwZNzC8luzhdYPiFvwJqLJKOLRmWIQoH cV4/Lsgbuq3KoSmeu3WrrVbFHrZr0Mk8O1WwfOI6ufpIIi89FqKU7+MwC/SiGirm FoBDdgIeI5sATmUnVvv6yv6Dc1xVRIwTCkY683VRRdtrt4YGWcd7PBm9wKPbIYJS SurE9skeTNYhBpA9wuosOFPO+V8wpUsPl2Xb+B9YQAj0/oeNZSbJx5qhQftK5PvJ EB3sfRqOiiIJmCCrxL30orvx3xHrOL91yNav+QLhra4tDMDJ9kq5vhLtikCS3yyB DFPldAGWqRle8j+I0bxWq3josocrWGyRxFgNsmVoxHlr0gRRA+sv7Brw283qh0ub QfIJWNco+Yb+e1ZiV99vaNHtIzEVxO0NIqL+kLPtN9PrNmxGAkO3HZtQZec8p9wy Itbq59JNYbvJEe1VrMHkOrQmAinLCwIDAQABAoICACvU5G1fs+rrWNKsA+vLbzGX eF1daX+uGM/+EE6Imf1dBSs3nyhTa3jG/IJdO/FeD8wMD9NrxCcUOaps6WOkkyKW ZIjD57KYybgPhm2iNzu1II22ZPdFwOHyToGBPig6HDHuATSqGEHeONS/xbun/+r3 DstyVwCkZ0Lg78F26Ob8dxWvs74gR9gfvbOF5j7KKn7JttriGesMMaASET0Y+n4o XxjR+93KRndzZhHVmMEBQ8uMIlAqbzKdgPCngrwS1w65bsgt+SFOG5ws0WlX4/7b WudFCj1dltpau1WRPRnfCE97/wfbVoyBX3/NTjNIgHi41f4wAqtVLxheSUdZpMqy Go08I+/TEd01pp15JjlMZVA8cqLbRpqXK1JddNZFGItWoLOlYBIg5+L4Hw/RyIRe bpBP6XXbTSaYMLyEwhrKuaX2kOfPqPMGBMRIil7r0uxHbhofX/1EbSUzemgxf7Nf XZTlxvCF6uAbDlZPoaenMv7TIueUZGUyUSjigzFDV/eKgoY2LGNiO0m3jobTYHlN Y8phN/WYYHXNtzjFQGsOXtxVyCt/Ana2N7WSc6qg2HoZI/1NFHOtqMRUN2i+WTH9 7+1sqi8lGQHbCqimb0o4Fm8vVlAonSsBOwt2J7PJdi0O+yntAPwncc2lsMzKTzbe 8WNok6VKQS87n1TjQytZAoIBAQDPjcMWvbCnpojY8EPzSuCJUpVP2pUxSEAJwk3U cEJKIIqz5FIU13hAnyMpr7qVVnNg8qZrghaxpYSYiDo1YqYeUI4h+c67c8QjEge/ 03NNBqZjZ614zSZwhMHztt82VqS7h4Nb6dV/OaeM/+HeBQTuQId5tAle4yYoXhpd Rp0WT16NYDR5hQnbquCT41f+TLWtmEKktfgbF9DW3hpT4FjLnNC1gEcliGd9mUMK u5m5fvf6EGHg/vXDZpTPJaC95x/zAvId9vuLUPTIFUCJZ8vvhWFmHFUi4mBmPirX +oht+tBAVwsGx2vsq23GEPerQAoGCn/1OJCxgZtKPni6H0spAoIBAQDBHFox3kjt KchgU9fZCSsw76+Hfo5sb7GuimgOeb+1cxIdO0iwsVwI8Ft6lZMQoqDj2kmeF9wr /qlnyKtFh7E2w7B0tJD/9vaZjXIn0ZW4hdNBVuh2o/x5ZgR43dneUpqd37hJPAe+ Tt5LEC+mTS7yD4DIy0gvmCFlAcfcYiiZDmkS2iExWJEazFohGveoBDD7qBH06wWH brJ2lUKTkntckhm/T8CXAfEsOER8kCyl46KEB1tuD/yctXP+FiSdyO15Q+Ue/Ok4 LDzUeyDFWRIH57nxkV0dZ1iktj61kEqHWBF7KwSCZ8LUA2JUaMQqRajQbOsul8qO l/b/MzD+u18TAoIBACu15UIizNNh5Swa3ZSdTlBdTgi1NfpdBu+HNjDpO2y1EcD0 8rxk7HRfj142HgtZW48tSxMVIIZlH1moRun7TpTPzj8lhv4/US05nNwvQfcU0XHZ 4dSxD8lejCIxfyzIboT38xgmVMoocDrnoL/LOtCaUm21Fswe9mhF+TNvraGHMZiA jEyTUhIrGITMujlaGmDm0hIyKIA8McUunUjQ1KJ88g9nZpRm97Sh1FasT6GbNco6 LQLfbw6pyekeOY4E4Nui4S6iVzNt5z9ECoVlkLNu2aZRjTR9jGO77/XwU08mJTmH m1DgKsB7EfFGnYI5SGMhTvVr5j0b2IJ4SaCY+pECggEAD2hgtevja7DSwQTPiwyV OqFVIv1xBavfxGpcpRMHvcWBo31wblCoZDoxQlWe10vyhFuNViTXN1dpUtOK/tA1 zoXMlXM2woWE5XmIqy6owIFE+sihYZ3x7gm6v54L3RZAKeqIvKcigwet6tVOx/kO jte30c2OY+XCfFmpBad7T8L7lTp8PYCwiy/U3SFWszwqYFnnnOAHn+ewK4/7MOUW HKu3jDEjz6ijAoE2za1/Mnk4JUqk++IgqKw9pf9ESqaWc/97z34kaYv2CooMclKK AsF1b5XGlSsCwpmb5Uau+5+GPYqQIIuA2wOuG8gEJs9KCd824I/R5JCxb2k5Noni qwKCAQAGZ1xHls7zmCwoqqjAHyvCxm+BUEEus2EQsePshsM4ALaxlOD+w9pLCwTZ Eyb6p9Nn5ZGfP2xawC+Zh43EDZHS2LmAnxx/dVkrUZWYFrvPq2I25c5LlN2aFmLN W5kDw9HT4vA7X+xztVdiha5rbCC7p2ipBK377R0ptfUvuF+0oEfa5irKkAtDgJ2P ALtzUIiXvGPDfNFdd8omAtmqMkfzPieCbQeHyeDTWdU3rlM1P+J1rGw9qQHgQv7K cGVD98d/pug5eEfrPGnATEmK9tGEpSgOQ/yJ4X7ij5HxGn7cLdKStSbaJntBzrBZ oX39ft84lbWc2E2utbbgdg3kIbRy -----END PRIVATE KEY----- ================================================ FILE: examples/data/tls/server.pem ================================================ -----BEGIN CERTIFICATE----- MIIFhTCCA22gAwIBAgICA+gwDQYJKoZIhvcNAQELBQAwKTEOMAwGA1UECgwFVG9u aWMxFzAVBgNVBAMMDnRlc3Qtc2VydmVyX2NhMB4XDTI1MTExNTE4MTI0OFoXDTM1 MTExMzE4MTI0OFowJjEOMAwGA1UECgwFVG9uaWMxFDASBgNVBAMMC3Rlc3Qtc2Vy dmVyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnJDctcBvUxYBzNi5 WNmJ7moqqmAMrzbczh+w7G4KHYY9qTXXojeLIjY5i+UMIF7lSe1fBMmATSL7tGIy ljrZnGlxhAHSASMPmqnwSpKyzgTYZIBueVumyAC7WzaZizRSdkDTrJR/x5v3wOK9 mO0xTUqfnR41DGquo0sDLgv2dG2AxBq5x6UODJf42IrwdYtp9t0iQXAy+Mt/cSye Wm51g0mZDvrBnuBLtFFdyZC0zzpiB7cGTcwvJbs4XWD4hb8CaiySji0ZliEKB3Fe Py7IG7qtyqEpnrt1q61WxR62a9DJPDtVsHziOrn6SCIvPRailO/jMAv0ohoq5haA Q3YCHiObAE5lJ1b7+sr+g3NcVUSMEwpGOvN1UUXba7eGBlnHezwZvcCj2yGCUkrq xPbJHkzWIQaQPcLqLDhTzvlfMKVLD5dl2/gfWEAI9P6HjWUmyceaoUH7SuT7yRAd 7H0ajooiCZggq8S99KK78d8R6zi/dcjWr/kC4a2uLQzAyfZKub4S7YpAkt8sgQxT 5XQBlqkZXvI/iNG8Vqt46LKHK1hskcRYDbJlaMR5a9IEUQPrL+wa8NvN6odLm0Hy CVjXKPmG/ntWYlffb2jR7SMxFcTtDSKi/pCz7TfT6zZsRgJDtx2bUGXnPKfcMiLW 6ufSTWG7yRHtVazB5Dq0JgIpywsCAwEAAaOBuTCBtjAMBgNVHRMBAf8EAjAAMB0G A1UdDgQWBBQEPLfmP4Eq7TBJNBTycSOpAm1EXTAOBgNVHQ8BAf8EBAMCA6gwVgYD VR0RBE8wTYILZXhhbXBsZS5jb22CDSouZXhhbXBsZS5jb22CDGV4YW1wbGUudGVz dIIJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMB8GA1UdIwQYMBaA FJzRNbz3Zvy6BcClqEMaStF2MiPtMA0GCSqGSIb3DQEBCwUAA4ICAQAMvZsUIQ6q 5Y2B1qrfOv/iXIoNE6/8XCvbtoTS6ULEotfhfVPdLVXczfEjc0lhwOlD9Hqwx5qN 7Lmp6IUodCQWLP2wRlF0hVii6EuWJzNxGwfwjwdQoNEgwNUQkyRM7HQZPQZAx0gi hNRZ9uJdqtkaKsyA5vr4PWaOLBKvcWGOV3HVMPMwH17JKTmNjevvQ7qhOSfc9sit OB5noLofNUxaIe1xfKqhBk6ngAyTfSd3jm9W8ceISlOtbjOecGw4P3TQXoEwwHDs /crMfc/DJPoi6cn3d+1D6m9YaNhujVdzDwjytU+YL2If8hWnJPlkXjmzNe4BjfS9 /P5IoBONAJ7w19TVN9wLTAxGOK7FJ7Z7/PlSWsJ6pzedZ5scOzJxmgmclWaHfGPb Y2YJayTJx5ddZ934IDT2ws86HMfPRfhbP7HsUp3jX0I+0DAi2H2cM9OSF0zhgvJu afbIPr7zpoIfbGpAK4oVZeZyiR9KMA+fnWBl0OrGWeXWYbMJQlLvrRVUrBJhNVl7 IdHrERmUaRg5nZa3CsgQ7R3JfCfxuLKh2iM2zqIgS//iu7a0fEFqkQ9DGiSO02CE LLzDOw7AoZ6FiDnG1LHXfOhzUU67IoXOCjXObSgBo3I6Sl3xKqpZQJ+eWYBrkPY2 DiWiew41BFDk7bADyeN5fffYkQjJCs54DQ== -----END CERTIFICATE----- ================================================ FILE: examples/data/tls/server2.key ================================================ -----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDDX6SRgoa4jTxv jbsx8+EXOXe0J5gdbdp1nsDH6tqsLTcLAdLvxEL5Sja/VGfm/6QbgYiiFf4EfvSN hAbe/2gg14yVTxoZoGTyrqnPfJCd6GK6v42MpmG0B/cxYsWZac+daPI08KgYEO5M pBHvWbZCPoqeFRofU7TBrD8GLbyBBZSi4GrCmFAf6wcdOddZi6+9a4K22PF6q5QV EaY/wQXhI+EBAWUDcfMhnSdHpHPLM2JD8DO10R+gLEs4Isq8xnAdebxZAfpphlER Ulho688ZduSyB+XZ4hxKty6SqI17+qt6RvRdGzTDOO+NAjBa1dSFiDHKGZW2OSu/ va4rD8ztf2hs2hCg++MGU56L8f4/GbWDnTXrpdqoC4P8sWBsNVfv7BVrL527O2Xg BFXJvCj5dBHwjHNM4kKzSYxaeJcD4HPvZl/143DvDWfdoI4WecihtaxYc7+t1NK/ SIj8FA+FMENRrWzacwY2tsc64zdBrzyStGdPEYretYKYSNvG9AytCT7jI+yCZkTv 1cLitWA4N27Si2ev1c8FKNWVgsEK9/WQTq58xgI2e+InGXNTYm75wGotPufK/Z/3 ch0h8JE81nbuBY137485cKVJLaevBsSfdcu2KihROwGHQs4AiRXxuf1gliwNBedq gddTIQRt5euFoFrGsr182wgOJoRmSwIDAQABAoICACmeL5fBhsI8b9HRjr1K1rmU R9AMGSa3tnAUw7syZ6eCsDV7i3wAwkWJThU+vS1jaoljdAAXEDfT9XNJ2w7YuE06 rVtG8Ofhl29szoaA4euJS7K9SEFcVfa8NaeDA1W1SQEekG2/kAckYCbaUmm6og0F dpDLLPfcZGZXM0u1JXEUh7+Ub5mJSZQrBCZxIlMUYvjGNJ+Pog0ADLjaPCBblU2K z4zfx61OsjyIVgw4V2RDS1zuh41lAUgk91b7blV9LffRbVJGjhm5f35hnKypAsJJ ZdkYvplFxcISuGyMVCtDUBDpmXdV6szlTX4rp7JJ144kX3rOLWqWTh/3n2jT4lKb R9nkQ6ldi5h5PMSSWemYij4uj+L+xT+KIh2OQce3pfnwTblsbCWVNd/6tN02chQl u4XjRUvbqLi1NbKKDa/dP5PF78jkoRnp+IppZ9yxhFKg8MZmHHJ4FO3wZ8sPHhuo V2pyX95iNbvjW3nUDciekfeDblhVOlpCcnhqenBImbmNsLrQgSslXUXrdeNAlCIe Xp9MErTnHpSQAUquA0IcRSMuJl97N/USJ2ALomWfWlYoNfS/2OhF+6k3UTlimYID EsSBytkCIBJSDb+fy7hB11IRQqdTSOrhi7BtsQpf7j+W7ELvNwhHFOD4oYqxF27v oFclVMcpYB/eX2ujXvP1AoIBAQD6yMZiRp1FpPMwPDEOKouZylH/tN7DGPAPSQ2k qjF6XU0e8ivxwEJMUTDG3+gyvl6fH1qdVUP+aMQp+7KldF9kxki/NXIO4d6UIUD+ u7H5/Qz6UAdHHWnZcplODKT2XbGEq7HROjgDIjhX1takJ+P+DZatG6NzCoymhiq9 Wv252hJh+ZOnvuXLE1tNH5cGlPKXVdUZ595Oc7BvMZ79X8aT7usgx8WuJueht95s kexrTe3S+bLSIy5AThQEqQSPI2bht5g2P8JcvKdz0Dpq8lrb2QvSkmPmKHApuyqE Iz0eCQb2Ywo+yt8iyE52F2+xMAHR2+sfZdLEtIlRvjUV45R9AoIBAQDHb9m8wVta GPWb21lbSV1JelBFFrznclbfTygSgm00nfKcJWzOus4rQ90CRSHi6osjcBzM9d+T mut8DT6sVppqw1pcGcRObf3xQjQu10vIVCfZMonEDIxG3RJlp5lX5Qd0x8FGW0+5 fF3s9xAhw45HpmDLb4GPlJC4pivJq3F9t0zeE4GBXjQG8i6zmyZgOFucp6lYHsSY SFR8NnLFH0AESBVVxmlQLmh1WyPvoT5xtN+wAb+NL43XpP11PSmyiZ1t6olpB3Gd jGmDuv8zT/L/Eu45Ho5niHZvlUTRDQhUseFhnpNqJgqFdcorQyIAdHiQqKugtaSX C8esU3c4WshnAoIBADQsBKl5S0qr0DPesc3ip/wsjPaHaDhqPbbQWdhB190+/8ZK Vz4J40EBOwaEmkfENucfnLhSfry0iuq5BCdLmHTck1HECqBx+N4UNPNJlPMO495H O9xdkCG+p4oHM2JVh5K0LV/7np94RQRxG2I3MkUuCoSk1VIfhbc2/jk6bsJ1TZG4 XDFvk1q2Ai5/PeoOnpFBN/4VsLfddX0Mc6j+fc9UyvGc3dEnItUP7WPkz6xbH/je HYd97idOlveJOfuVP9gBW9cdWI/2Z1e+oAiTtrc55i2+aq39B6iG2Yd31UUkV6B2 ZjE9/0G7Lid/JKtV82r3N2jmw/xMGq04xs5w8/kCggEBAKNW5SKlzhbFeGrUxKNy O7gROCrNi5uXVrUAFJbMQXqlnezq7emGr5JZu2m/ixhY9enpSX1IJnuylwj7pAfr yf6Ezr290LxXc3MjwK9YX1XMLvLBZHmvuTfUmdut33ZQOsIXVCE2ad5FkHuigZDV o/LkvkP/qEIa8Eh6uEQsaQAInykHGrsEl2HU+hDZkh2eKtHrs1Nbp7H9E0zEH014 4apgCzE8Fka4K49qPM1m50ijVczGMccFWotyMq/RjxQNh2VdgHsHwLLDLCvafGbf JOpp67fWYdVDu5R50WnR3a/YG/oisSNv0pApv5GdIvzyZ0g7DI3MFugqmrd1iKAc IiMCggEBAOqlCaNS5qU39gKQqmLsCF5ByXgJEt5SnnBM0ctvuaZpMYNR4C+spRxj XvOJkBpTP9msuVLGNaxf8XxyMIj4gnYWRclxketKEusQN08zlqOwKs+idp+Y+5Vf 5IVXlEH9h2yM4ZvTY4oiY9OT0dAjERqsydbH7yvOe2hX4nKY4E8c/J7Adda/plzG +dDOJeOGwnyFKXQ+X1efJo9GIOmsun9twIN77nz0j7MJoWDi0S6zccQtlsNhnXnP g3xeaEI7WAuOHyizGuD4vwJVYq7jEPXCG07pUX9O+IT8S4Dk8LJnqjN2fmsYa93Z VNZv2hCnQdKiJHAUT0UIVpzbI3qiKW0= -----END PRIVATE KEY----- ================================================ FILE: examples/data/tls/server2.pem ================================================ -----BEGIN CERTIFICATE----- MIIFQjCCAyqgAwIBAgICA+gwDQYJKoZIhvcNAQELBQAwKTEOMAwGA1UECgwFVG9u aWMxFzAVBgNVBAMMDnRlc3Qtc2VydmVyX2NhMB4XDTI2MDIxODE5MTI0MVoXDTM2 MDIxNjE5MTI0MVowJjEOMAwGA1UECgwFVG9uaWMxFDASBgNVBAMMC3Rlc3Qtc2Vy dmVyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw1+kkYKGuI08b427 MfPhFzl3tCeYHW3adZ7Ax+rarC03CwHS78RC+Uo2v1Rn5v+kG4GIohX+BH70jYQG 3v9oINeMlU8aGaBk8q6pz3yQnehiur+NjKZhtAf3MWLFmWnPnWjyNPCoGBDuTKQR 71m2Qj6KnhUaH1O0waw/Bi28gQWUouBqwphQH+sHHTnXWYuvvWuCttjxequUFRGm P8EF4SPhAQFlA3HzIZ0nR6RzyzNiQ/AztdEfoCxLOCLKvMZwHXm8WQH6aYZREVJY aOvPGXbksgfl2eIcSrcukqiNe/qrekb0XRs0wzjvjQIwWtXUhYgxyhmVtjkrv72u Kw/M7X9obNoQoPvjBlOei/H+Pxm1g50166XaqAuD/LFgbDVX7+wVay+duztl4ARV ybwo+XQR8IxzTOJCs0mMWniXA+Bz72Zf9eNw7w1n3aCOFnnIobWsWHO/rdTSv0iI /BQPhTBDUa1s2nMGNrbHOuM3Qa88krRnTxGK3rWCmEjbxvQMrQk+4yPsgmZE79XC 4rVgODdu0otnr9XPBSjVlYLBCvf1kE6ufMYCNnviJxlzU2Ju+cBqLT7nyv2f93Id IfCRPNZ27gWNd++POXClSS2nrwbEn3XLtiooUTsBh0LOAIkV8bn9YJYsDQXnaoHX UyEEbeXrhaBaxrK9fNsIDiaEZksCAwEAAaN3MHUwDAYDVR0TAQH/BAIwADAdBgNV HQ4EFgQUzCR56MKIPquyXh10IpIV3blZbNAwDgYDVR0PAQH/BAQDAgOoMBUGA1Ud EQQOMAyCCioudGVzdC5jb20wHwYDVR0jBBgwFoAUnNE1vPdm/LoFwKWoQxpK0XYy I+0wDQYJKoZIhvcNAQELBQADggIBAHi30l7AChiw6bhZkUOYmsnvyWOeuT7ghqwY 2IWizi4J1txRVlBcoG5Vrltaoe4mv5RDC90phKU/C3a0IxQYRVHJbIhGe++5ngAh AOOJzBta74nDzaQ/3e03Dr741F6LIsnlQj396X5fwqvhno5uhTeL/xtrgCPg8uck 5Pl4d1HXXE8YtzAvmkWrhE5Uc9DgO5z0pN96zugLheLB8jNKfdGrnfKCdOzb0mqO mPM3ThWQWqKAYEVsiRUNWfANfUJOppNV7Uj6WkHMxMeZAWVwTeEiU1ay7dCiiZMd sVx/VlE91p002Jhq1xGuB9W9+6ra7XJdp9aQaa1qnEMKk/apTV+u33UAgETs/SqU ZvOIoV3xR4YBbCraE8sIjZJSpPYz7nSCoosYhSVd5BhymhQsMlVPq4mVFhX+oSGt eO2nk/mKGD3Yd5H0F8K5FzqaX51PCX4sgTxoQLH2WDAhifTv5vncbTCNLnky78OF qW9qaBRTLnpgqtp5SYTsbGgUbSZEhNDmy2ONvmd6SVAEcrEJ8ahSUDTC98vHX6Wd SVVNTcWDFlqTr2qzDtXBzhtKJlGeuS/OH9H+dSoQ0h2zDpiH3FJTvbd8Y2S2QUID /NiTE6LvXSVyWY03Cq2nRtt1zcUIiIOCO9PeEOx23+zmfBZ17afAwDGLv2LAsYAX B/6jITez -----END CERTIFICATE----- ================================================ FILE: examples/helloworld-tutorial.md ================================================ # Getting Started This tutorial is meant to be an introduction to Tonic and assumes that you have basic [Rust] experience as well as an understanding of what [protocol buffers] are. If you don't, feel free to read up on the pages linked in this paragraph and come back to this tutorial once you feel you are ready! [rust]: https://www.rust-lang.org/ [protocol buffers]: https://developers.google.com/protocol-buffers/docs/overview ## Prerequisites To run the sample code and walk through the tutorial, the only prerequisite is Rust itself. [rustup] is a convenient tool to install it, if you haven't already. [rustup]: https://rustup.rs ## Project Setup For this tutorial, we will start by creating a new Rust project with Cargo: ```shell $ cargo new helloworld-tonic $ cd helloworld-tonic ``` `tonic` works on rust `1.39` and above as it requires support for the `async_await` feature. ```bash $ rustup update ``` ## Defining the HelloWorld service Our first step is to define the gRPC _service_ and the method _request_ and _response_ types using [protocol buffers]. We will keep our `.proto` files in a directory in our project's root. Note that Tonic does not really care where our `.proto` definitions live. ```shell $ mkdir proto $ touch proto/helloworld.proto ``` Then you define RPC methods inside your service definition, specifying their request and response types. gRPC lets you define four kinds of service methods, all of which are supported by Tonic. For this tutorial we will only use a simple RPC, if you would like to see a Tonic example which uses all four kinds please read the [routeguide tutorial]. [routeguide tutorial]: https://github.com/hyperium/tonic/blob/master/examples/routeguide-tutorial.md First we define our package name, which is what Tonic looks for when including your protos in the client and server applications. Lets give this one a name of `helloworld`. ```proto syntax = "proto3"; package helloworld; ``` Next we need to define our service. This service will contain the actual RPC calls we will be using in our application. An RPC contains an Identifier, a Request type, and returns a Response type. Here is our Greeter service, which provides the SayHello RPC method. ```proto service Greeter { // Our SayHello rpc accepts HelloRequests and returns HelloReplies rpc SayHello (HelloRequest) returns (HelloReply); } ``` Finally, we have to actually define those types we used above in our `SayHello` RPC method. RPC types are defined as messages which contain typed fields. Here is what that will look like for our HelloWorld application: ```proto message HelloRequest { // Request message contains the name to be greeted string name = 1; } message HelloReply { // Reply contains the greeting message string message = 1; } ``` Great! Now our `.proto` file should be complete and ready for use in our application. Here is what it should look like completed: ```proto syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } ``` ## Application Setup Now that have defined the protobuf for our application we can start writing our application with Tonic! Let's first add our required dependencies to the `Cargo.toml`. ```toml [package] name = "helloworld-tonic" version = "0.1.0" edition = "2021" [[bin]] # Bin to run the HelloWorld gRPC server name = "helloworld-server" path = "src/server.rs" [[bin]] # Bin to run the HelloWorld gRPC client name = "helloworld-client" path = "src/client.rs" [dependencies] tonic = "*" prost = "0.14" tonic-prost = "*" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } [build-dependencies] tonic-prost-build = "*" ``` We include `tonic-build` as a useful way to incorporate the generation of our client and server gRPC code into the build process of our application. We will setup this build process now: ## Generating Server and Client code At the root of your project (not /src), create a `build.rs` file and add the following code: ```rust fn main() -> Result<(), Box> { tonic_prost_build::compile_protos("proto/helloworld.proto")?; Ok(()) } ``` This tells `tonic-build` to compile your protobufs when you build your Rust project. While you can configure this build process in a number of ways, we will not get into the details in this introductory tutorial. Please see the [tonic-build] documentation for details on configuration. [tonic-build]: https://github.com/hyperium/tonic/blob/master/tonic-build/README.md ## Writing our Server Now that the build process is written and our dependencies are all setup, we can begin writing the fun stuff! We need to import the things we will be using in our server, including the protobuf. Start by making a file called `server.rs` in your `/src` directory and writing the following code: ```rust use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); // The string specified here must match the proto package name } ``` Next up, let's implement the Greeter service we previously defined in our `.proto` file. Here's what that might look like: ```rust #[derive(Debug, Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, // Accept request of type HelloRequest ) -> Result, Status> { // Return an instance of type HelloReply println!("Got a request: {:?}", request); let reply = HelloReply { message: format!("Hello {}!", request.into_inner().name), // We must use .into_inner() as the fields of gRPC requests and responses are private }; Ok(Response::new(reply)) // Send back our formatted greeting } } ``` Finally, let's define the Tokio runtime that our server will actually run on. This requires Tokio to be added as a dependency, so make sure you included that! ```rust #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse()?; let greeter = MyGreeter::default(); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ``` Altogether your server should look something like this once you are done: ```rust use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Debug, Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request: {:?}", request); let reply = HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse()?; let greeter = MyGreeter::default(); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ``` You should now be able to run your HelloWorld gRPC server using the command `cargo run --bin helloworld-server`. This uses the [[bin]] we defined earlier in our `Cargo.toml` to run specifically the server. If you have a gRPC GUI client such as [Postman] you should be able to send requests to the server and get back greetings! Or if you use [grpcurl] then you can simply try send requests like this: ``` $ grpcurl -plaintext -import-path ./proto -proto helloworld.proto -d '{"name": "Tonic"}' '[::1]:50051' helloworld.Greeter/SayHello ``` And receiving responses like this: ``` { "message": "Hello Tonic!" } ``` [postman]: https://www.postman.com/ [grpcurl]: https://github.com/fullstorydev/grpcurl ## Writing our Client So now we have a running gRPC server, and that's great but how can our application communicate with it? This is where our client would come in. Tonic supports both client and server implementations. Similar to the server, we will start by creating a file `client.rs` in our `/src` directory and importing everything we will need: ```rust use hello_world::greeter_client::GreeterClient; use hello_world::HelloRequest; pub mod hello_world { tonic::include_proto!("helloworld"); } ``` The client is much simpler than the server as we don't need to implement any service methods, just make requests. Here is a Tokio runtime which will make our request and print the response to your terminal: ```rust #[tokio::main] async fn main() -> Result<(), Box> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={:?}", response); Ok(()) } ``` That's it! Our complete client file should look something like below, if it doesn't please go back and make sure you followed along correctly: ```rust use hello_world::greeter_client::GreeterClient; use hello_world::HelloRequest; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={:?}", response); Ok(()) } ``` ## Putting it all together At this point we have written our protobuf file, a build file to compile our protobufs, a server which implements our SayHello service, and a client which makes requests to our server. You should have a `proto/helloworld.proto` file, a `build.rs` file at the root of your project, and `src/server.rs` as well as a `src/client.rs` files. To run the server, run `cargo run --bin helloworld-server`. To run the client, run `cargo run --bin helloworld-client` in another terminal window. You should see the request logged out by the server in its terminal window, as well as the response logged out by the client in its window. Congrats on making it through this introductory tutorial! We hope that this walkthrough tutorial has helped you understand the basics of Tonic, and how to get started writing high-performance, interoperable, and flexible gRPC servers in Rust. For a more in-depth tutorial which showcases an advanced gRPC server in Tonic, please see the [routeguide tutorial]. ================================================ FILE: examples/proto/attrs/attrs.proto ================================================ syntax = "proto3"; package attrs; // EchoRequest is the request for echo. message EchoRequest { string message = 1; } // EchoResponse is the response for echo. message EchoResponse { string message = 1; } // Echo is the echo service. service Echo { // UnaryEcho is unary echo. rpc UnaryEcho(EchoRequest) returns (EchoResponse) {} } ================================================ FILE: examples/proto/echo/echo.proto ================================================ /* * * Copyright 2018 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ syntax = "proto3"; package grpc.examples.echo; // EchoRequest is the request for echo. message EchoRequest { string message = 1; } // EchoResponse is the response for echo. message EchoResponse { string message = 1; } // Echo is the echo service. service Echo { // UnaryEcho is unary echo. rpc UnaryEcho(EchoRequest) returns (EchoResponse) {} // ServerStreamingEcho is server side streaming. rpc ServerStreamingEcho(EchoRequest) returns (stream EchoResponse) {} // ClientStreamingEcho is client side streaming. rpc ClientStreamingEcho(stream EchoRequest) returns (EchoResponse) {} // BidirectionalStreamingEcho is bidi streaming. rpc BidirectionalStreamingEcho(stream EchoRequest) returns (stream EchoResponse) {} } ================================================ FILE: examples/proto/googleapis/google/api/annotations.proto ================================================ // Copyright (c) 2015, Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.api; import "google/api/http.proto"; import "google/protobuf/descriptor.proto"; option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; option java_multiple_files = true; option java_outer_classname = "AnnotationsProto"; option java_package = "com.google.api"; option objc_class_prefix = "GAPI"; extend google.protobuf.MethodOptions { // See `HttpRule`. HttpRule http = 72295728; } ================================================ FILE: examples/proto/googleapis/google/api/client.proto ================================================ // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.api; import "google/protobuf/descriptor.proto"; option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; option java_multiple_files = true; option java_outer_classname = "ClientProto"; option java_package = "com.google.api"; option objc_class_prefix = "GAPI"; extend google.protobuf.MethodOptions { // A definition of a client library method signature. // // In client libraries, each proto RPC corresponds to one or more methods // which the end user is able to call, and calls the underlying RPC. // Normally, this method receives a single argument (a struct or instance // corresponding to the RPC request object). Defining this field will // add one or more overloads providing flattened or simpler method signatures // in some languages. // // The fields on the method signature are provided as a comma-separated // string. // // For example, the proto RPC and annotation: // // rpc CreateSubscription(CreateSubscriptionRequest) // returns (Subscription) { // option (google.api.method_signature) = "name,topic"; // } // // Would add the following Java overload (in addition to the method accepting // the request object): // // public final Subscription createSubscription(String name, String topic) // // The following backwards-compatibility guidelines apply: // // * Adding this annotation to an unannotated method is backwards // compatible. // * Adding this annotation to a method which already has existing // method signature annotations is backwards compatible if and only if // the new method signature annotation is last in the sequence. // * Modifying or removing an existing method signature annotation is // a breaking change. // * Re-ordering existing method signature annotations is a breaking // change. repeated string method_signature = 1051; } extend google.protobuf.ServiceOptions { // The hostname for this service. // This should be specified with no prefix or protocol. // // Example: // // service Foo { // option (google.api.default_host) = "foo.googleapi.com"; // ... // } string default_host = 1049; // OAuth scopes needed for the client. // // Example: // // service Foo { // option (google.api.oauth_scopes) = \ // "https://www.googleapis.com/auth/cloud-platform"; // ... // } // // If there is more than one scope, use a comma-separated string: // // Example: // // service Foo { // option (google.api.oauth_scopes) = \ // "https://www.googleapis.com/auth/cloud-platform," // "https://www.googleapis.com/auth/monitoring"; // ... // } string oauth_scopes = 1050; } ================================================ FILE: examples/proto/googleapis/google/api/field_behavior.proto ================================================ // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.api; import "google/protobuf/descriptor.proto"; option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; option java_multiple_files = true; option java_outer_classname = "FieldBehaviorProto"; option java_package = "com.google.api"; option objc_class_prefix = "GAPI"; extend google.protobuf.FieldOptions { // A designation of a specific field behavior (required, output only, etc.) // in protobuf messages. // // Examples: // // string name = 1 [(google.api.field_behavior) = REQUIRED]; // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; // google.protobuf.Duration ttl = 1 // [(google.api.field_behavior) = INPUT_ONLY]; // google.protobuf.Timestamp expire_time = 1 // [(google.api.field_behavior) = OUTPUT_ONLY, // (google.api.field_behavior) = IMMUTABLE]; repeated google.api.FieldBehavior field_behavior = 1052; } // An indicator of the behavior of a given field (for example, that a field // is required in requests, or given as output but ignored as input). // This **does not** change the behavior in protocol buffers itself; it only // denotes the behavior and may affect how API tooling handles the field. // // Note: This enum **may** receive new values in the future. enum FieldBehavior { // Conventional default for enums. Do not use this. FIELD_BEHAVIOR_UNSPECIFIED = 0; // Specifically denotes a field as optional. // While all fields in protocol buffers are optional, this may be specified // for emphasis if appropriate. OPTIONAL = 1; // Denotes a field as required. // This indicates that the field **must** be provided as part of the request, // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). REQUIRED = 2; // Denotes a field as output only. // This indicates that the field is provided in responses, but including the // field in a request does nothing (the server *must* ignore it and // *must not* throw an error as a result of the field's presence). OUTPUT_ONLY = 3; // Denotes a field as input only. // This indicates that the field is provided in requests, and the // corresponding field is not included in output. INPUT_ONLY = 4; // Denotes a field as immutable. // This indicates that the field may be set once in a request to create a // resource, but may not be changed thereafter. IMMUTABLE = 5; // Denotes that a (repeated) field is an unordered list. // This indicates that the service may provide the elements of the list // in any arbitrary order, rather than the order the user originally // provided. Additionally, the list's order may or may not be stable. UNORDERED_LIST = 6; } ================================================ FILE: examples/proto/googleapis/google/api/http.proto ================================================ // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.api; option cc_enable_arenas = true; option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; option java_multiple_files = true; option java_outer_classname = "HttpProto"; option java_package = "com.google.api"; option objc_class_prefix = "GAPI"; // Defines the HTTP configuration for an API service. It contains a list of // [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method // to one or more HTTP REST API methods. message Http { // A list of HTTP configuration rules that apply to individual API methods. // // **NOTE:** All service configuration rules follow "last one wins" order. repeated HttpRule rules = 1; // When set to true, URL path parameters will be fully URI-decoded except in // cases of single segment matches in reserved expansion, where "%2F" will be // left encoded. // // The default behavior is to not decode RFC 6570 reserved characters in multi // segment matches. bool fully_decode_reserved_expansion = 2; } // # gRPC Transcoding // // gRPC Transcoding is a feature for mapping between a gRPC method and one or // more HTTP REST endpoints. It allows developers to build a single API service // that supports both gRPC APIs and REST APIs. Many systems, including [Google // APIs](https://github.com/googleapis/googleapis), // [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC // Gateway](https://github.com/grpc-ecosystem/grpc-gateway), // and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature // and use it for large scale production services. // // `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies // how different portions of the gRPC request message are mapped to the URL // path, URL query parameters, and HTTP request body. It also controls how the // gRPC response message is mapped to the HTTP response body. `HttpRule` is // typically specified as an `google.api.http` annotation on the gRPC method. // // Each mapping specifies a URL path template and an HTTP method. The path // template may refer to one or more fields in the gRPC request message, as long // as each field is a non-repeated field with a primitive (non-message) type. // The path template controls how fields of the request message are mapped to // the URL path. // // Example: // // service Messaging { // rpc GetMessage(GetMessageRequest) returns (Message) { // option (google.api.http) = { // get: "/v1/{name=messages/*}" // }; // } // } // message GetMessageRequest { // string name = 1; // Mapped to URL path. // } // message Message { // string text = 1; // The resource content. // } // // This enables an HTTP REST to gRPC mapping as below: // // HTTP | gRPC // -----|----- // `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` // // Any fields in the request message which are not bound by the path template // automatically become HTTP query parameters if there is no HTTP request body. // For example: // // service Messaging { // rpc GetMessage(GetMessageRequest) returns (Message) { // option (google.api.http) = { // get:"/v1/messages/{message_id}" // }; // } // } // message GetMessageRequest { // message SubMessage { // string subfield = 1; // } // string message_id = 1; // Mapped to URL path. // int64 revision = 2; // Mapped to URL query parameter `revision`. // SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. // } // // This enables a HTTP JSON to RPC mapping as below: // // HTTP | gRPC // -----|----- // `GET /v1/messages/123456?revision=2&sub.subfield=foo` | // `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: // "foo"))` // // Note that fields which are mapped to URL query parameters must have a // primitive type or a repeated primitive type or a non-repeated message type. // In the case of a repeated type, the parameter can be repeated in the URL // as `...?param=A¶m=B`. In the case of a message type, each field of the // message is mapped to a separate parameter, such as // `...?foo.a=A&foo.b=B&foo.c=C`. // // For HTTP methods that allow a request body, the `body` field // specifies the mapping. Consider a REST update method on the // message resource collection: // // service Messaging { // rpc UpdateMessage(UpdateMessageRequest) returns (Message) { // option (google.api.http) = { // patch: "/v1/messages/{message_id}" // body: "message" // }; // } // } // message UpdateMessageRequest { // string message_id = 1; // mapped to the URL // Message message = 2; // mapped to the body // } // // The following HTTP JSON to RPC mapping is enabled, where the // representation of the JSON in the request body is determined by // protos JSON encoding: // // HTTP | gRPC // -----|----- // `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: // "123456" message { text: "Hi!" })` // // The special name `*` can be used in the body mapping to define that // every field not bound by the path template should be mapped to the // request body. This enables the following alternative definition of // the update method: // // service Messaging { // rpc UpdateMessage(Message) returns (Message) { // option (google.api.http) = { // patch: "/v1/messages/{message_id}" // body: "*" // }; // } // } // message Message { // string message_id = 1; // string text = 2; // } // // // The following HTTP JSON to RPC mapping is enabled: // // HTTP | gRPC // -----|----- // `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: // "123456" text: "Hi!")` // // Note that when using `*` in the body mapping, it is not possible to // have HTTP parameters, as all fields not bound by the path end in // the body. This makes this option more rarely used in practice when // defining REST APIs. The common usage of `*` is in custom methods // which don't use the URL at all for transferring data. // // It is possible to define multiple HTTP methods for one RPC by using // the `additional_bindings` option. Example: // // service Messaging { // rpc GetMessage(GetMessageRequest) returns (Message) { // option (google.api.http) = { // get: "/v1/messages/{message_id}" // additional_bindings { // get: "/v1/users/{user_id}/messages/{message_id}" // } // }; // } // } // message GetMessageRequest { // string message_id = 1; // string user_id = 2; // } // // This enables the following two alternative HTTP JSON to RPC mappings: // // HTTP | gRPC // -----|----- // `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` // `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: // "123456")` // // ## Rules for HTTP mapping // // 1. Leaf request fields (recursive expansion nested messages in the request // message) are classified into three categories: // - Fields referred by the path template. They are passed via the URL path. // - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP // request body. // - All other fields are passed via the URL query parameters, and the // parameter name is the field path in the request message. A repeated // field can be represented as multiple query parameters under the same // name. // 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields // are passed via URL path and HTTP request body. // 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all // fields are passed via URL path and URL query parameters. // // ### Path template syntax // // Template = "/" Segments [ Verb ] ; // Segments = Segment { "/" Segment } ; // Segment = "*" | "**" | LITERAL | Variable ; // Variable = "{" FieldPath [ "=" Segments ] "}" ; // FieldPath = IDENT { "." IDENT } ; // Verb = ":" LITERAL ; // // The syntax `*` matches a single URL path segment. The syntax `**` matches // zero or more URL path segments, which must be the last part of the URL path // except the `Verb`. // // The syntax `Variable` matches part of the URL path as specified by its // template. A variable template must not contain other variables. If a variable // matches a single path segment, its template may be omitted, e.g. `{var}` // is equivalent to `{var=*}`. // // The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` // contains any reserved character, such characters should be percent-encoded // before the matching. // // If a variable contains exactly one path segment, such as `"{var}"` or // `"{var=*}"`, when such a variable is expanded into a URL path on the client // side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The // server side does the reverse decoding. Such variables show up in the // [Discovery // Document](https://developers.google.com/discovery/v1/reference/apis) as // `{var}`. // // If a variable contains multiple path segments, such as `"{var=foo/*}"` // or `"{var=**}"`, when such a variable is expanded into a URL path on the // client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. // The server side does the reverse decoding, except "%2F" and "%2f" are left // unchanged. Such variables show up in the // [Discovery // Document](https://developers.google.com/discovery/v1/reference/apis) as // `{+var}`. // // ## Using gRPC API Service Configuration // // gRPC API Service Configuration (service config) is a configuration language // for configuring a gRPC service to become a user-facing product. The // service config is simply the YAML representation of the `google.api.Service` // proto message. // // As an alternative to annotating your proto file, you can configure gRPC // transcoding in your service config YAML files. You do this by specifying a // `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same // effect as the proto annotation. This can be particularly useful if you // have a proto that is reused in multiple services. Note that any transcoding // specified in the service config will override any matching transcoding // configuration in the proto. // // Example: // // http: // rules: // # Selects a gRPC method and applies HttpRule to it. // - selector: example.v1.Messaging.GetMessage // get: /v1/messages/{message_id}/{sub.subfield} // // ## Special notes // // When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the // proto to JSON conversion must follow the [proto3 // specification](https://developers.google.com/protocol-buffers/docs/proto3#json). // // While the single segment variable follows the semantics of // [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String // Expansion, the multi segment variable **does not** follow RFC 6570 Section // 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion // does not expand special characters like `?` and `#`, which would lead // to invalid URLs. As the result, gRPC Transcoding uses a custom encoding // for multi segment variables. // // The path variables **must not** refer to any repeated or mapped field, // because client libraries are not capable of handling such variable expansion. // // The path variables **must not** capture the leading "/" character. The reason // is that the most common use case "{var}" does not capture the leading "/" // character. For consistency, all path variables must share the same behavior. // // Repeated message fields must not be mapped to URL query parameters, because // no client library can support such complicated mapping. // // If an API needs to use a JSON array for request or response body, it can map // the request or response body to a repeated field. However, some gRPC // Transcoding implementations may not support this feature. message HttpRule { // Selects a method to which this rule applies. // // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. string selector = 1; // Determines the URL pattern is matched by this rules. This pattern can be // used with any of the {get|put|post|delete|patch} methods. A custom method // can be defined using the 'custom' field. oneof pattern { // Maps to HTTP GET. Used for listing and getting information about // resources. string get = 2; // Maps to HTTP PUT. Used for replacing a resource. string put = 3; // Maps to HTTP POST. Used for creating a resource or performing an action. string post = 4; // Maps to HTTP DELETE. Used for deleting a resource. string delete = 5; // Maps to HTTP PATCH. Used for updating a resource. string patch = 6; // The custom pattern is used for specifying an HTTP method that is not // included in the `pattern` field, such as HEAD, or "*" to leave the // HTTP method unspecified for this rule. The wild-card rule is useful // for services that provide content to Web (HTML) clients. CustomHttpPattern custom = 8; } // The name of the request field whose value is mapped to the HTTP request // body, or `*` for mapping all request fields not captured by the path // pattern to the HTTP body, or omitted for not having any HTTP request body. // // NOTE: the referred field must be present at the top-level of the request // message type. string body = 7; // Optional. The name of the response field whose value is mapped to the HTTP // response body. When omitted, the entire response message will be used // as the HTTP response body. // // NOTE: The referred field must be present at the top-level of the response // message type. string response_body = 12; // Additional HTTP bindings for the selector. Nested bindings must // not contain an `additional_bindings` field themselves (that is, // the nesting may only be one level deep). repeated HttpRule additional_bindings = 11; } // A custom pattern is used for defining custom HTTP verb. message CustomHttpPattern { // The name of this custom HTTP verb. string kind = 1; // The path matched by this custom verb. string path = 2; } ================================================ FILE: examples/proto/googleapis/google/api/resource.proto ================================================ // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.api; import "google/protobuf/descriptor.proto"; option cc_enable_arenas = true; option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; option java_multiple_files = true; option java_outer_classname = "ResourceProto"; option java_package = "com.google.api"; option objc_class_prefix = "GAPI"; extend google.protobuf.FieldOptions { // An annotation that describes a resource reference, see // [ResourceReference][]. google.api.ResourceReference resource_reference = 1055; } extend google.protobuf.FileOptions { // An annotation that describes a resource definition without a corresponding // message; see [ResourceDescriptor][]. repeated google.api.ResourceDescriptor resource_definition = 1053; } extend google.protobuf.MessageOptions { // An annotation that describes a resource definition, see // [ResourceDescriptor][]. google.api.ResourceDescriptor resource = 1053; } // A simple descriptor of a resource type. // // ResourceDescriptor annotates a resource message (either by means of a // protobuf annotation or use in the service config), and associates the // resource's schema, the resource type, and the pattern of the resource name. // // Example: // // message Topic { // // Indicates this message defines a resource schema. // // Declares the resource type in the format of {service}/{kind}. // // For Kubernetes resources, the format is {api group}/{kind}. // option (google.api.resource) = { // type: "pubsub.googleapis.com/Topic" // name_descriptor: { // pattern: "projects/{project}/topics/{topic}" // parent_type: "cloudresourcemanager.googleapis.com/Project" // parent_name_extractor: "projects/{project}" // } // }; // } // // The ResourceDescriptor Yaml config will look like: // // resources: // - type: "pubsub.googleapis.com/Topic" // name_descriptor: // - pattern: "projects/{project}/topics/{topic}" // parent_type: "cloudresourcemanager.googleapis.com/Project" // parent_name_extractor: "projects/{project}" // // Sometimes, resources have multiple patterns, typically because they can // live under multiple parents. // // Example: // // message LogEntry { // option (google.api.resource) = { // type: "logging.googleapis.com/LogEntry" // name_descriptor: { // pattern: "projects/{project}/logs/{log}" // parent_type: "cloudresourcemanager.googleapis.com/Project" // parent_name_extractor: "projects/{project}" // } // name_descriptor: { // pattern: "folders/{folder}/logs/{log}" // parent_type: "cloudresourcemanager.googleapis.com/Folder" // parent_name_extractor: "folders/{folder}" // } // name_descriptor: { // pattern: "organizations/{organization}/logs/{log}" // parent_type: "cloudresourcemanager.googleapis.com/Organization" // parent_name_extractor: "organizations/{organization}" // } // name_descriptor: { // pattern: "billingAccounts/{billing_account}/logs/{log}" // parent_type: "billing.googleapis.com/BillingAccount" // parent_name_extractor: "billingAccounts/{billing_account}" // } // }; // } // // The ResourceDescriptor Yaml config will look like: // // resources: // - type: 'logging.googleapis.com/LogEntry' // name_descriptor: // - pattern: "projects/{project}/logs/{log}" // parent_type: "cloudresourcemanager.googleapis.com/Project" // parent_name_extractor: "projects/{project}" // - pattern: "folders/{folder}/logs/{log}" // parent_type: "cloudresourcemanager.googleapis.com/Folder" // parent_name_extractor: "folders/{folder}" // - pattern: "organizations/{organization}/logs/{log}" // parent_type: "cloudresourcemanager.googleapis.com/Organization" // parent_name_extractor: "organizations/{organization}" // - pattern: "billingAccounts/{billing_account}/logs/{log}" // parent_type: "billing.googleapis.com/BillingAccount" // parent_name_extractor: "billingAccounts/{billing_account}" // // For flexible resources, the resource name doesn't contain parent names, but // the resource itself has parents for policy evaluation. // // Example: // // message Shelf { // option (google.api.resource) = { // type: "library.googleapis.com/Shelf" // name_descriptor: { // pattern: "shelves/{shelf}" // parent_type: "cloudresourcemanager.googleapis.com/Project" // } // name_descriptor: { // pattern: "shelves/{shelf}" // parent_type: "cloudresourcemanager.googleapis.com/Folder" // } // }; // } // // The ResourceDescriptor Yaml config will look like: // // resources: // - type: 'library.googleapis.com/Shelf' // name_descriptor: // - pattern: "shelves/{shelf}" // parent_type: "cloudresourcemanager.googleapis.com/Project" // - pattern: "shelves/{shelf}" // parent_type: "cloudresourcemanager.googleapis.com/Folder" message ResourceDescriptor { // A description of the historical or future-looking state of the // resource pattern. enum History { // The "unset" value. HISTORY_UNSPECIFIED = 0; // The resource originally had one pattern and launched as such, and // additional patterns were added later. ORIGINALLY_SINGLE_PATTERN = 1; // The resource has one pattern, but the API owner expects to add more // later. (This is the inverse of ORIGINALLY_SINGLE_PATTERN, and prevents // that from being necessary once there are multiple patterns.) FUTURE_MULTI_PATTERN = 2; } // A flag representing a specific style that a resource claims to conform to. enum Style { // The unspecified value. Do not use. STYLE_UNSPECIFIED = 0; // This resource is intended to be "declarative-friendly". // // Declarative-friendly resources must be more strictly consistent, and // setting this to true communicates to tools that this resource should // adhere to declarative-friendly expectations. // // Note: This is used by the API linter (linter.aip.dev) to enable // additional checks. DECLARATIVE_FRIENDLY = 1; } // The resource type. It must be in the format of // {service_name}/{resource_type_kind}. The `resource_type_kind` must be // singular and must not include version numbers. // // Example: `storage.googleapis.com/Bucket` // // The value of the resource_type_kind must follow the regular expression // /[A-Za-z][a-zA-Z0-9]+/. It should start with an upper case character and // should use PascalCase (UpperCamelCase). The maximum number of // characters allowed for the `resource_type_kind` is 100. string type = 1; // Optional. The relative resource name pattern associated with this resource // type. The DNS prefix of the full resource name shouldn't be specified here. // // The path pattern must follow the syntax, which aligns with HTTP binding // syntax: // // Template = Segment { "/" Segment } ; // Segment = LITERAL | Variable ; // Variable = "{" LITERAL "}" ; // // Examples: // // - "projects/{project}/topics/{topic}" // - "projects/{project}/knowledgeBases/{knowledge_base}" // // The components in braces correspond to the IDs for each resource in the // hierarchy. It is expected that, if multiple patterns are provided, // the same component name (e.g. "project") refers to IDs of the same // type of resource. repeated string pattern = 2; // Optional. The field on the resource that designates the resource name // field. If omitted, this is assumed to be "name". string name_field = 3; // Optional. The historical or future-looking state of the resource pattern. // // Example: // // // The InspectTemplate message originally only supported resource // // names with organization, and project was added later. // message InspectTemplate { // option (google.api.resource) = { // type: "dlp.googleapis.com/InspectTemplate" // pattern: // "organizations/{organization}/inspectTemplates/{inspect_template}" // pattern: "projects/{project}/inspectTemplates/{inspect_template}" // history: ORIGINALLY_SINGLE_PATTERN // }; // } History history = 4; // The plural name used in the resource name and permission names, such as // 'projects' for the resource name of 'projects/{project}' and the permission // name of 'cloudresourcemanager.googleapis.com/projects.get'. It is the same // concept of the `plural` field in k8s CRD spec // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ // // Note: The plural form is required even for singleton resources. See // https://aip.dev/156 string plural = 5; // The same concept of the `singular` field in k8s CRD spec // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ // Such as "project" for the `resourcemanager.googleapis.com/Project` type. string singular = 6; // Style flag(s) for this resource. // These indicate that a resource is expected to conform to a given // style. See the specific style flags for additional information. repeated Style style = 10; } // Defines a proto annotation that describes a string field that refers to // an API resource. message ResourceReference { // The resource type that the annotated field references. // // Example: // // message Subscription { // string topic = 2 [(google.api.resource_reference) = { // type: "pubsub.googleapis.com/Topic" // }]; // } // // Occasionally, a field may reference an arbitrary resource. In this case, // APIs use the special value * in their resource reference. // // Example: // // message GetIamPolicyRequest { // string resource = 2 [(google.api.resource_reference) = { // type: "*" // }]; // } string type = 1; // The resource type of a child collection that the annotated field // references. This is useful for annotating the `parent` field that // doesn't have a fixed resource type. // // Example: // // message ListLogEntriesRequest { // string parent = 1 [(google.api.resource_reference) = { // child_type: "logging.googleapis.com/LogEntry" // }; // } string child_type = 2; } ================================================ FILE: examples/proto/googleapis/google/pubsub/v1/pubsub.proto ================================================ // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.pubsub.v1; import "google/api/annotations.proto"; import "google/api/client.proto"; import "google/api/field_behavior.proto"; import "google/api/resource.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/field_mask.proto"; import "google/protobuf/timestamp.proto"; import "google/pubsub/v1/schema.proto"; option cc_enable_arenas = true; option csharp_namespace = "Google.Cloud.PubSub.V1"; option go_package = "google.golang.org/genproto/googleapis/pubsub/v1;pubsub"; option java_multiple_files = true; option java_outer_classname = "PubsubProto"; option java_package = "com.google.pubsub.v1"; option php_namespace = "Google\\Cloud\\PubSub\\V1"; option ruby_package = "Google::Cloud::PubSub::V1"; // The service that an application uses to manipulate topics, and to send // messages to a topic. service Publisher { option (google.api.default_host) = "pubsub.googleapis.com"; option (google.api.oauth_scopes) = "https://www.googleapis.com/auth/cloud-platform," "https://www.googleapis.com/auth/pubsub"; // Creates the given topic with the given name. See the [resource name rules] // (https://cloud.google.com/pubsub/docs/admin#resource_names). rpc CreateTopic(Topic) returns (Topic) { option (google.api.http) = { put: "/v1/{name=projects/*/topics/*}" body: "*" }; option (google.api.method_signature) = "name"; } // Updates an existing topic. Note that certain properties of a // topic are not modifiable. rpc UpdateTopic(UpdateTopicRequest) returns (Topic) { option (google.api.http) = { patch: "/v1/{topic.name=projects/*/topics/*}" body: "*" }; } // Adds one or more messages to the topic. Returns `NOT_FOUND` if the topic // does not exist. rpc Publish(PublishRequest) returns (PublishResponse) { option (google.api.http) = { post: "/v1/{topic=projects/*/topics/*}:publish" body: "*" }; option (google.api.method_signature) = "topic,messages"; } // Gets the configuration of a topic. rpc GetTopic(GetTopicRequest) returns (Topic) { option (google.api.http) = { get: "/v1/{topic=projects/*/topics/*}" }; option (google.api.method_signature) = "topic"; } // Lists matching topics. rpc ListTopics(ListTopicsRequest) returns (ListTopicsResponse) { option (google.api.http) = { get: "/v1/{project=projects/*}/topics" }; option (google.api.method_signature) = "project"; } // Lists the names of the attached subscriptions on this topic. rpc ListTopicSubscriptions(ListTopicSubscriptionsRequest) returns (ListTopicSubscriptionsResponse) { option (google.api.http) = { get: "/v1/{topic=projects/*/topics/*}/subscriptions" }; option (google.api.method_signature) = "topic"; } // Lists the names of the snapshots on this topic. Snapshots are used in // [Seek](https://cloud.google.com/pubsub/docs/replay-overview) operations, // which allow you to manage message acknowledgments in bulk. That is, you can // set the acknowledgment state of messages in an existing subscription to the // state captured by a snapshot. rpc ListTopicSnapshots(ListTopicSnapshotsRequest) returns (ListTopicSnapshotsResponse) { option (google.api.http) = { get: "/v1/{topic=projects/*/topics/*}/snapshots" }; option (google.api.method_signature) = "topic"; } // Deletes the topic with the given name. Returns `NOT_FOUND` if the topic // does not exist. After a topic is deleted, a new topic may be created with // the same name; this is an entirely new topic with none of the old // configuration or subscriptions. Existing subscriptions to this topic are // not deleted, but their `topic` field is set to `_deleted-topic_`. rpc DeleteTopic(DeleteTopicRequest) returns (google.protobuf.Empty) { option (google.api.http) = { delete: "/v1/{topic=projects/*/topics/*}" }; option (google.api.method_signature) = "topic"; } // Detaches a subscription from this topic. All messages retained in the // subscription are dropped. Subsequent `Pull` and `StreamingPull` requests // will return FAILED_PRECONDITION. If the subscription is a push // subscription, pushes to the endpoint will stop. rpc DetachSubscription(DetachSubscriptionRequest) returns (DetachSubscriptionResponse) { option (google.api.http) = { post: "/v1/{subscription=projects/*/subscriptions/*}:detach" }; } } // A policy constraining the storage of messages published to the topic. message MessageStoragePolicy { // A list of IDs of GCP regions where messages that are published to the topic // may be persisted in storage. Messages published by publishers running in // non-allowed GCP regions (or running outside of GCP altogether) will be // routed for storage in one of the allowed regions. An empty list means that // no regions are allowed, and is not a valid configuration. repeated string allowed_persistence_regions = 1; } // Settings for validating messages published against a schema. message SchemaSettings { // Required. The name of the schema that messages published should be // validated against. Format is `projects/{project}/schemas/{schema}`. The // value of this field will be `_deleted-schema_` if the schema has been // deleted. string schema = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Schema" } ]; // The encoding of messages validated against `schema`. Encoding encoding = 2; } // A topic resource. message Topic { option (google.api.resource) = { type: "pubsub.googleapis.com/Topic" pattern: "projects/{project}/topics/{topic}" pattern: "_deleted-topic_" }; // Required. The name of the topic. It must have the format // `"projects/{project}/topics/{topic}"`. `{topic}` must start with a letter, // and contain only letters (`[A-Za-z]`), numbers (`[0-9]`), dashes (`-`), // underscores (`_`), periods (`.`), tildes (`~`), plus (`+`) or percent // signs (`%`). It must be between 3 and 255 characters in length, and it // must not start with `"goog"`. string name = 1 [(google.api.field_behavior) = REQUIRED]; // See [Creating and managing labels] // (https://cloud.google.com/pubsub/docs/labels). map labels = 2; // Policy constraining the set of Google Cloud Platform regions where messages // published to the topic may be stored. If not present, then no constraints // are in effect. MessageStoragePolicy message_storage_policy = 3; // The resource name of the Cloud KMS CryptoKey to be used to protect access // to messages published on this topic. // // The expected format is `projects/*/locations/*/keyRings/*/cryptoKeys/*`. string kms_key_name = 5; // Settings for validating messages published against a schema. // // EXPERIMENTAL: Schema support is in development and may not work yet. SchemaSettings schema_settings = 6; // Reserved for future use. This field is set only in responses from the // server; it is ignored if it is set in any requests. bool satisfies_pzs = 7; } // A message that is published by publishers and consumed by subscribers. The // message must contain either a non-empty data field or at least one attribute. // Note that client libraries represent this object differently // depending on the language. See the corresponding [client library // documentation](https://cloud.google.com/pubsub/docs/reference/libraries) for // more information. See [quotas and limits] // (https://cloud.google.com/pubsub/quotas) for more information about message // limits. message PubsubMessage { // The message data field. If this field is empty, the message must contain // at least one attribute. bytes data = 1; // Attributes for this message. If this field is empty, the message must // contain non-empty data. This can be used to filter messages on the // subscription. map attributes = 2; // ID of this message, assigned by the server when the message is published. // Guaranteed to be unique within the topic. This value may be read by a // subscriber that receives a `PubsubMessage` via a `Pull` call or a push // delivery. It must not be populated by the publisher in a `Publish` call. string message_id = 3; // The time at which the message was published, populated by the server when // it receives the `Publish` call. It must not be populated by the // publisher in a `Publish` call. google.protobuf.Timestamp publish_time = 4; // If non-empty, identifies related messages for which publish order should be // respected. If a `Subscription` has `enable_message_ordering` set to `true`, // messages published with the same non-empty `ordering_key` value will be // delivered to subscribers in the order in which they are received by the // Pub/Sub system. All `PubsubMessage`s published in a given `PublishRequest` // must specify the same `ordering_key` value. string ordering_key = 5; } // Request for the GetTopic method. message GetTopicRequest { // Required. The name of the topic to get. // Format is `projects/{project}/topics/{topic}`. string topic = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Topic" } ]; } // Request for the UpdateTopic method. message UpdateTopicRequest { // Required. The updated topic object. Topic topic = 1 [(google.api.field_behavior) = REQUIRED]; // Required. Indicates which fields in the provided topic to update. Must be // specified and non-empty. Note that if `update_mask` contains // "message_storage_policy" but the `message_storage_policy` is not set in // the `topic` provided above, then the updated value is determined by the // policy configured at the project or organization level. google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; } // Request for the Publish method. message PublishRequest { // Required. The messages in the request will be published on this topic. // Format is `projects/{project}/topics/{topic}`. string topic = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Topic" } ]; // Required. The messages to publish. repeated PubsubMessage messages = 2 [(google.api.field_behavior) = REQUIRED]; } // Response for the `Publish` method. message PublishResponse { // The server-assigned ID of each published message, in the same order as // the messages in the request. IDs are guaranteed to be unique within // the topic. repeated string message_ids = 1; } // Request for the `ListTopics` method. message ListTopicsRequest { // Required. The name of the project in which to list topics. // Format is `projects/{project-id}`. string project = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "cloudresourcemanager.googleapis.com/Project" } ]; // Maximum number of topics to return. int32 page_size = 2; // The value returned by the last `ListTopicsResponse`; indicates that this is // a continuation of a prior `ListTopics` call, and that the system should // return the next page of data. string page_token = 3; } // Response for the `ListTopics` method. message ListTopicsResponse { // The resulting topics. repeated Topic topics = 1; // If not empty, indicates that there may be more topics that match the // request; this value should be passed in a new `ListTopicsRequest`. string next_page_token = 2; } // Request for the `ListTopicSubscriptions` method. message ListTopicSubscriptionsRequest { // Required. The name of the topic that subscriptions are attached to. // Format is `projects/{project}/topics/{topic}`. string topic = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Topic" } ]; // Maximum number of subscription names to return. int32 page_size = 2; // The value returned by the last `ListTopicSubscriptionsResponse`; indicates // that this is a continuation of a prior `ListTopicSubscriptions` call, and // that the system should return the next page of data. string page_token = 3; } // Response for the `ListTopicSubscriptions` method. message ListTopicSubscriptionsResponse { // The names of subscriptions attached to the topic specified in the request. repeated string subscriptions = 1 [(google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" }]; // If not empty, indicates that there may be more subscriptions that match // the request; this value should be passed in a new // `ListTopicSubscriptionsRequest` to get more subscriptions. string next_page_token = 2; } // Request for the `ListTopicSnapshots` method. message ListTopicSnapshotsRequest { // Required. The name of the topic that snapshots are attached to. // Format is `projects/{project}/topics/{topic}`. string topic = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Topic" } ]; // Maximum number of snapshot names to return. int32 page_size = 2; // The value returned by the last `ListTopicSnapshotsResponse`; indicates // that this is a continuation of a prior `ListTopicSnapshots` call, and // that the system should return the next page of data. string page_token = 3; } // Response for the `ListTopicSnapshots` method. message ListTopicSnapshotsResponse { // The names of the snapshots that match the request. repeated string snapshots = 1; // If not empty, indicates that there may be more snapshots that match // the request; this value should be passed in a new // `ListTopicSnapshotsRequest` to get more snapshots. string next_page_token = 2; } // Request for the `DeleteTopic` method. message DeleteTopicRequest { // Required. Name of the topic to delete. // Format is `projects/{project}/topics/{topic}`. string topic = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Topic" } ]; } // Request for the DetachSubscription method. message DetachSubscriptionRequest { // Required. The subscription to detach. // Format is `projects/{project}/subscriptions/{subscription}`. string subscription = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; } // Response for the DetachSubscription method. // Reserved for future use. message DetachSubscriptionResponse {} // The service that an application uses to manipulate subscriptions and to // consume messages from a subscription via the `Pull` method or by // establishing a bi-directional stream using the `StreamingPull` method. service Subscriber { option (google.api.default_host) = "pubsub.googleapis.com"; option (google.api.oauth_scopes) = "https://www.googleapis.com/auth/cloud-platform," "https://www.googleapis.com/auth/pubsub"; // Creates a subscription to a given topic. See the [resource name rules] // (https://cloud.google.com/pubsub/docs/admin#resource_names). // If the subscription already exists, returns `ALREADY_EXISTS`. // If the corresponding topic doesn't exist, returns `NOT_FOUND`. // // If the name is not provided in the request, the server will assign a random // name for this subscription on the same project as the topic, conforming // to the [resource name format] // (https://cloud.google.com/pubsub/docs/admin#resource_names). The generated // name is populated in the returned Subscription object. Note that for REST // API requests, you must specify a name in the request. rpc CreateSubscription(Subscription) returns (Subscription) { option (google.api.http) = { put: "/v1/{name=projects/*/subscriptions/*}" body: "*" }; option (google.api.method_signature) = "name,topic,push_config,ack_deadline_seconds"; } // Gets the configuration details of a subscription. rpc GetSubscription(GetSubscriptionRequest) returns (Subscription) { option (google.api.http) = { get: "/v1/{subscription=projects/*/subscriptions/*}" }; option (google.api.method_signature) = "subscription"; } // Updates an existing subscription. Note that certain properties of a // subscription, such as its topic, are not modifiable. rpc UpdateSubscription(UpdateSubscriptionRequest) returns (Subscription) { option (google.api.http) = { patch: "/v1/{subscription.name=projects/*/subscriptions/*}" body: "*" }; } // Lists matching subscriptions. rpc ListSubscriptions(ListSubscriptionsRequest) returns (ListSubscriptionsResponse) { option (google.api.http) = { get: "/v1/{project=projects/*}/subscriptions" }; option (google.api.method_signature) = "project"; } // Deletes an existing subscription. All messages retained in the subscription // are immediately dropped. Calls to `Pull` after deletion will return // `NOT_FOUND`. After a subscription is deleted, a new one may be created with // the same name, but the new one has no association with the old // subscription or its topic unless the same topic is specified. rpc DeleteSubscription(DeleteSubscriptionRequest) returns (google.protobuf.Empty) { option (google.api.http) = { delete: "/v1/{subscription=projects/*/subscriptions/*}" }; option (google.api.method_signature) = "subscription"; } // Modifies the ack deadline for a specific message. This method is useful // to indicate that more time is needed to process a message by the // subscriber, or to make the message available for redelivery if the // processing was interrupted. Note that this does not modify the // subscription-level `ackDeadlineSeconds` used for subsequent messages. rpc ModifyAckDeadline(ModifyAckDeadlineRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/v1/{subscription=projects/*/subscriptions/*}:modifyAckDeadline" body: "*" }; option (google.api.method_signature) = "subscription,ack_ids,ack_deadline_seconds"; } // Acknowledges the messages associated with the `ack_ids` in the // `AcknowledgeRequest`. The Pub/Sub system can remove the relevant messages // from the subscription. // // Acknowledging a message whose ack deadline has expired may succeed, // but such a message may be redelivered later. Acknowledging a message more // than once will not result in an error. rpc Acknowledge(AcknowledgeRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/v1/{subscription=projects/*/subscriptions/*}:acknowledge" body: "*" }; option (google.api.method_signature) = "subscription,ack_ids"; } // Pulls messages from the server. The server may return `UNAVAILABLE` if // there are too many concurrent pull requests pending for the given // subscription. rpc Pull(PullRequest) returns (PullResponse) { option (google.api.http) = { post: "/v1/{subscription=projects/*/subscriptions/*}:pull" body: "*" }; option (google.api.method_signature) = "subscription,return_immediately,max_messages"; } // Establishes a stream with the server, which sends messages down to the // client. The client streams acknowledgements and ack deadline modifications // back to the server. The server will close the stream and return the status // on any error. The server may close the stream with status `UNAVAILABLE` to // reassign server-side resources, in which case, the client should // re-establish the stream. Flow control can be achieved by configuring the // underlying RPC channel. rpc StreamingPull(stream StreamingPullRequest) returns (stream StreamingPullResponse) {} // Modifies the `PushConfig` for a specified subscription. // // This may be used to change a push subscription to a pull one (signified by // an empty `PushConfig`) or vice versa, or change the endpoint URL and other // attributes of a push subscription. Messages will accumulate for delivery // continuously through the call regardless of changes to the `PushConfig`. rpc ModifyPushConfig(ModifyPushConfigRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/v1/{subscription=projects/*/subscriptions/*}:modifyPushConfig" body: "*" }; option (google.api.method_signature) = "subscription,push_config"; } // Gets the configuration details of a snapshot. Snapshots are used in // Seek // operations, which allow you to manage message acknowledgments in bulk. That // is, you can set the acknowledgment state of messages in an existing // subscription to the state captured by a snapshot. rpc GetSnapshot(GetSnapshotRequest) returns (Snapshot) { option (google.api.http) = { get: "/v1/{snapshot=projects/*/snapshots/*}" }; option (google.api.method_signature) = "snapshot"; } // Lists the existing snapshots. Snapshots are used in [Seek]( // https://cloud.google.com/pubsub/docs/replay-overview) operations, which // allow you to manage message acknowledgments in bulk. That is, you can set // the acknowledgment state of messages in an existing subscription to the // state captured by a snapshot. rpc ListSnapshots(ListSnapshotsRequest) returns (ListSnapshotsResponse) { option (google.api.http) = { get: "/v1/{project=projects/*}/snapshots" }; option (google.api.method_signature) = "project"; } // Creates a snapshot from the requested subscription. Snapshots are used in // [Seek](https://cloud.google.com/pubsub/docs/replay-overview) operations, // which allow you to manage message acknowledgments in bulk. That is, you can // set the acknowledgment state of messages in an existing subscription to the // state captured by a snapshot. // If the snapshot already exists, returns `ALREADY_EXISTS`. // If the requested subscription doesn't exist, returns `NOT_FOUND`. // If the backlog in the subscription is too old -- and the resulting snapshot // would expire in less than 1 hour -- then `FAILED_PRECONDITION` is returned. // See also the `Snapshot.expire_time` field. If the name is not provided in // the request, the server will assign a random // name for this snapshot on the same project as the subscription, conforming // to the [resource name format] // (https://cloud.google.com/pubsub/docs/admin#resource_names). The // generated name is populated in the returned Snapshot object. Note that for // REST API requests, you must specify a name in the request. rpc CreateSnapshot(CreateSnapshotRequest) returns (Snapshot) { option (google.api.http) = { put: "/v1/{name=projects/*/snapshots/*}" body: "*" }; option (google.api.method_signature) = "name,subscription"; } // Updates an existing snapshot. Snapshots are used in // Seek // operations, which allow // you to manage message acknowledgments in bulk. That is, you can set the // acknowledgment state of messages in an existing subscription to the state // captured by a snapshot. rpc UpdateSnapshot(UpdateSnapshotRequest) returns (Snapshot) { option (google.api.http) = { patch: "/v1/{snapshot.name=projects/*/snapshots/*}" body: "*" }; } // Removes an existing snapshot. Snapshots are used in [Seek] // (https://cloud.google.com/pubsub/docs/replay-overview) operations, which // allow you to manage message acknowledgments in bulk. That is, you can set // the acknowledgment state of messages in an existing subscription to the // state captured by a snapshot. // When the snapshot is deleted, all messages retained in the snapshot // are immediately dropped. After a snapshot is deleted, a new one may be // created with the same name, but the new one has no association with the old // snapshot or its subscription, unless the same subscription is specified. rpc DeleteSnapshot(DeleteSnapshotRequest) returns (google.protobuf.Empty) { option (google.api.http) = { delete: "/v1/{snapshot=projects/*/snapshots/*}" }; option (google.api.method_signature) = "snapshot"; } // Seeks an existing subscription to a point in time or to a given snapshot, // whichever is provided in the request. Snapshots are used in [Seek] // (https://cloud.google.com/pubsub/docs/replay-overview) operations, which // allow you to manage message acknowledgments in bulk. That is, you can set // the acknowledgment state of messages in an existing subscription to the // state captured by a snapshot. Note that both the subscription and the // snapshot must be on the same topic. rpc Seek(SeekRequest) returns (SeekResponse) { option (google.api.http) = { post: "/v1/{subscription=projects/*/subscriptions/*}:seek" body: "*" }; } } // A subscription resource. message Subscription { option (google.api.resource) = { type: "pubsub.googleapis.com/Subscription" pattern: "projects/{project}/subscriptions/{subscription}" }; // Required. The name of the subscription. It must have the format // `"projects/{project}/subscriptions/{subscription}"`. `{subscription}` must // start with a letter, and contain only letters (`[A-Za-z]`), numbers // (`[0-9]`), dashes (`-`), underscores (`_`), periods (`.`), tildes (`~`), // plus (`+`) or percent signs (`%`). It must be between 3 and 255 characters // in length, and it must not start with `"goog"`. string name = 1 [(google.api.field_behavior) = REQUIRED]; // Required. The name of the topic from which this subscription is receiving // messages. Format is `projects/{project}/topics/{topic}`. The value of this // field will be `_deleted-topic_` if the topic has been deleted. string topic = 2 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Topic" } ]; // If push delivery is used with this subscription, this field is // used to configure it. An empty `pushConfig` signifies that the subscriber // will pull and ack messages using API methods. PushConfig push_config = 4; // The approximate amount of time (on a best-effort basis) Pub/Sub waits for // the subscriber to acknowledge receipt before resending the message. In the // interval after the message is delivered and before it is acknowledged, it // is considered to be outstanding. During that time period, the // message will not be redelivered (on a best-effort basis). // // For pull subscriptions, this value is used as the initial value for the ack // deadline. To override this value for a given message, call // `ModifyAckDeadline` with the corresponding `ack_id` if using // non-streaming pull or send the `ack_id` in a // `StreamingModifyAckDeadlineRequest` if using streaming pull. // The minimum custom deadline you can specify is 10 seconds. // The maximum custom deadline you can specify is 600 seconds (10 minutes). // If this parameter is 0, a default value of 10 seconds is used. // // For push delivery, this value is also used to set the request timeout for // the call to the push endpoint. // // If the subscriber never acknowledges the message, the Pub/Sub // system will eventually redeliver the message. int32 ack_deadline_seconds = 5; // Indicates whether to retain acknowledged messages. If true, then // messages are not expunged from the subscription's backlog, even if they are // acknowledged, until they fall out of the `message_retention_duration` // window. This must be true if you would like to [Seek to a timestamp] // (https://cloud.google.com/pubsub/docs/replay-overview#seek_to_a_time). bool retain_acked_messages = 7; // How long to retain unacknowledged messages in the subscription's backlog, // from the moment a message is published. // If `retain_acked_messages` is true, then this also configures the retention // of acknowledged messages, and thus configures how far back in time a `Seek` // can be done. Defaults to 7 days. Cannot be more than 7 days or less than 10 // minutes. google.protobuf.Duration message_retention_duration = 8; // See Creating and // managing labels. map labels = 9; // If true, messages published with the same `ordering_key` in `PubsubMessage` // will be delivered to the subscribers in the order in which they // are received by the Pub/Sub system. Otherwise, they may be delivered in // any order. bool enable_message_ordering = 10; // A policy that specifies the conditions for this subscription's expiration. // A subscription is considered active as long as any connected subscriber is // successfully consuming messages from the subscription or is issuing // operations on the subscription. If `expiration_policy` is not set, a // *default policy* with `ttl` of 31 days will be used. The minimum allowed // value for `expiration_policy.ttl` is 1 day. ExpirationPolicy expiration_policy = 11; // An expression written in the Pub/Sub [filter // language](https://cloud.google.com/pubsub/docs/filtering). If non-empty, // then only `PubsubMessage`s whose `attributes` field matches the filter are // delivered on this subscription. If empty, then no messages are filtered // out. string filter = 12; // A policy that specifies the conditions for dead lettering messages in // this subscription. If dead_letter_policy is not set, dead lettering // is disabled. // // The Cloud Pub/Sub service account associated with this subscriptions's // parent project (i.e., // service-{project_number}@gcp-sa-pubsub.iam.gserviceaccount.com) must have // permission to Acknowledge() messages on this subscription. DeadLetterPolicy dead_letter_policy = 13; // A policy that specifies how Pub/Sub retries message delivery for this // subscription. // // If not set, the default retry policy is applied. This generally implies // that messages will be retried as soon as possible for healthy subscribers. // RetryPolicy will be triggered on NACKs or acknowledgement deadline // exceeded events for a given message. RetryPolicy retry_policy = 14; // Indicates whether the subscription is detached from its topic. Detached // subscriptions don't receive messages from their topic and don't retain any // backlog. `Pull` and `StreamingPull` requests will return // FAILED_PRECONDITION. If the subscription is a push subscription, pushes to // the endpoint will not be made. bool detached = 15; } // A policy that specifies how Cloud Pub/Sub retries message delivery. // // Retry delay will be exponential based on provided minimum and maximum // backoffs. https://en.wikipedia.org/wiki/Exponential_backoff. // // RetryPolicy will be triggered on NACKs or acknowledgement deadline exceeded // events for a given message. // // Retry Policy is implemented on a best effort basis. At times, the delay // between consecutive deliveries may not match the configuration. That is, // delay can be more or less than configured backoff. message RetryPolicy { // The minimum delay between consecutive deliveries of a given message. // Value should be between 0 and 600 seconds. Defaults to 10 seconds. google.protobuf.Duration minimum_backoff = 1; // The maximum delay between consecutive deliveries of a given message. // Value should be between 0 and 600 seconds. Defaults to 600 seconds. google.protobuf.Duration maximum_backoff = 2; } // Dead lettering is done on a best effort basis. The same message might be // dead lettered multiple times. // // If validation on any of the fields fails at subscription creation/updation, // the create/update subscription request will fail. message DeadLetterPolicy { // The name of the topic to which dead letter messages should be published. // Format is `projects/{project}/topics/{topic}`.The Cloud Pub/Sub service // account associated with the enclosing subscription's parent project (i.e., // service-{project_number}@gcp-sa-pubsub.iam.gserviceaccount.com) must have // permission to Publish() to this topic. // // The operation will fail if the topic does not exist. // Users should ensure that there is a subscription attached to this topic // since messages published to a topic with no subscriptions are lost. string dead_letter_topic = 1; // The maximum number of delivery attempts for any message. The value must be // between 5 and 100. // // The number of delivery attempts is defined as 1 + (the sum of number of // NACKs and number of times the acknowledgement deadline has been exceeded // for the message). // // A NACK is any call to ModifyAckDeadline with a 0 deadline. Note that // client libraries may automatically extend ack_deadlines. // // This field will be honored on a best effort basis. // // If this parameter is 0, a default value of 5 is used. int32 max_delivery_attempts = 2; } // A policy that specifies the conditions for resource expiration (i.e., // automatic resource deletion). message ExpirationPolicy { // Specifies the "time-to-live" duration for an associated resource. The // resource expires if it is not active for a period of `ttl`. The definition // of "activity" depends on the type of the associated resource. The minimum // and maximum allowed values for `ttl` depend on the type of the associated // resource, as well. If `ttl` is not set, the associated resource never // expires. google.protobuf.Duration ttl = 1; } // Configuration for a push delivery endpoint. message PushConfig { // Contains information needed for generating an // [OpenID Connect // token](https://developers.google.com/identity/protocols/OpenIDConnect). message OidcToken { // [Service account // email](https://cloud.google.com/iam/docs/service-accounts) // to be used for generating the OIDC token. The caller (for // CreateSubscription, UpdateSubscription, and ModifyPushConfig RPCs) must // have the iam.serviceAccounts.actAs permission for the service account. string service_account_email = 1; // Audience to be used when generating OIDC token. The audience claim // identifies the recipients that the JWT is intended for. The audience // value is a single case-sensitive string. Having multiple values (array) // for the audience field is not supported. More info about the OIDC JWT // token audience here: https://tools.ietf.org/html/rfc7519#section-4.1.3 // Note: if not specified, the Push endpoint URL will be used. string audience = 2; } // A URL locating the endpoint to which messages should be pushed. // For example, a Webhook endpoint might use `https://example.com/push`. string push_endpoint = 1; // Endpoint configuration attributes that can be used to control different // aspects of the message delivery. // // The only currently supported attribute is `x-goog-version`, which you can // use to change the format of the pushed message. This attribute // indicates the version of the data expected by the endpoint. This // controls the shape of the pushed message (i.e., its fields and metadata). // // If not present during the `CreateSubscription` call, it will default to // the version of the Pub/Sub API used to make such call. If not present in a // `ModifyPushConfig` call, its value will not be changed. `GetSubscription` // calls will always return a valid version, even if the subscription was // created without this attribute. // // The only supported values for the `x-goog-version` attribute are: // // * `v1beta1`: uses the push format defined in the v1beta1 Pub/Sub API. // * `v1` or `v1beta2`: uses the push format defined in the v1 Pub/Sub API. // // For example: //
attributes { "x-goog-version": "v1" } 
map attributes = 2; // An authentication method used by push endpoints to verify the source of // push requests. This can be used with push endpoints that are private by // default to allow requests only from the Cloud Pub/Sub system, for example. // This field is optional and should be set only by users interested in // authenticated push. oneof authentication_method { // If specified, Pub/Sub will generate and attach an OIDC JWT token as an // `Authorization` header in the HTTP request for every pushed message. OidcToken oidc_token = 3; } } // A message and its corresponding acknowledgment ID. message ReceivedMessage { // This ID can be used to acknowledge the received message. string ack_id = 1; // The message. PubsubMessage message = 2; // The approximate number of times that Cloud Pub/Sub has attempted to deliver // the associated message to a subscriber. // // More precisely, this is 1 + (number of NACKs) + // (number of ack_deadline exceeds) for this message. // // A NACK is any call to ModifyAckDeadline with a 0 deadline. An ack_deadline // exceeds event is whenever a message is not acknowledged within // ack_deadline. Note that ack_deadline is initially // Subscription.ackDeadlineSeconds, but may get extended automatically by // the client library. // // Upon the first delivery of a given message, `delivery_attempt` will have a // value of 1. The value is calculated at best effort and is approximate. // // If a DeadLetterPolicy is not set on the subscription, this will be 0. int32 delivery_attempt = 3; } // Request for the GetSubscription method. message GetSubscriptionRequest { // Required. The name of the subscription to get. // Format is `projects/{project}/subscriptions/{sub}`. string subscription = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; } // Request for the UpdateSubscription method. message UpdateSubscriptionRequest { // Required. The updated subscription object. Subscription subscription = 1 [(google.api.field_behavior) = REQUIRED]; // Required. Indicates which fields in the provided subscription to update. // Must be specified and non-empty. google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; } // Request for the `ListSubscriptions` method. message ListSubscriptionsRequest { // Required. The name of the project in which to list subscriptions. // Format is `projects/{project-id}`. string project = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "cloudresourcemanager.googleapis.com/Project" } ]; // Maximum number of subscriptions to return. int32 page_size = 2; // The value returned by the last `ListSubscriptionsResponse`; indicates that // this is a continuation of a prior `ListSubscriptions` call, and that the // system should return the next page of data. string page_token = 3; } // Response for the `ListSubscriptions` method. message ListSubscriptionsResponse { // The subscriptions that match the request. repeated Subscription subscriptions = 1; // If not empty, indicates that there may be more subscriptions that match // the request; this value should be passed in a new // `ListSubscriptionsRequest` to get more subscriptions. string next_page_token = 2; } // Request for the DeleteSubscription method. message DeleteSubscriptionRequest { // Required. The subscription to delete. // Format is `projects/{project}/subscriptions/{sub}`. string subscription = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; } // Request for the ModifyPushConfig method. message ModifyPushConfigRequest { // Required. The name of the subscription. // Format is `projects/{project}/subscriptions/{sub}`. string subscription = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; // Required. The push configuration for future deliveries. // // An empty `pushConfig` indicates that the Pub/Sub system should // stop pushing messages from the given subscription and allow // messages to be pulled and acknowledged - effectively pausing // the subscription if `Pull` or `StreamingPull` is not called. PushConfig push_config = 2 [(google.api.field_behavior) = REQUIRED]; } // Request for the `Pull` method. message PullRequest { // Required. The subscription from which messages should be pulled. // Format is `projects/{project}/subscriptions/{sub}`. string subscription = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; // Optional. If this field set to true, the system will respond immediately // even if it there are no messages available to return in the `Pull` // response. Otherwise, the system may wait (for a bounded amount of time) // until at least one message is available, rather than returning no messages. // Warning: setting this field to `true` is discouraged because it adversely // impacts the performance of `Pull` operations. We recommend that users do // not set this field. bool return_immediately = 2 [deprecated = true, (google.api.field_behavior) = OPTIONAL]; // Required. The maximum number of messages to return for this request. Must // be a positive integer. The Pub/Sub system may return fewer than the number // specified. int32 max_messages = 3 [(google.api.field_behavior) = REQUIRED]; } // Response for the `Pull` method. message PullResponse { // Received Pub/Sub messages. The list will be empty if there are no more // messages available in the backlog. For JSON, the response can be entirely // empty. The Pub/Sub system may return fewer than the `maxMessages` requested // even if there are more messages available in the backlog. repeated ReceivedMessage received_messages = 1; } // Request for the ModifyAckDeadline method. message ModifyAckDeadlineRequest { // Required. The name of the subscription. // Format is `projects/{project}/subscriptions/{sub}`. string subscription = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; // Required. List of acknowledgment IDs. repeated string ack_ids = 4 [(google.api.field_behavior) = REQUIRED]; // Required. The new ack deadline with respect to the time this request was // sent to the Pub/Sub system. For example, if the value is 10, the new ack // deadline will expire 10 seconds after the `ModifyAckDeadline` call was // made. Specifying zero might immediately make the message available for // delivery to another subscriber client. This typically results in an // increase in the rate of message redeliveries (that is, duplicates). // The minimum deadline you can specify is 0 seconds. // The maximum deadline you can specify is 600 seconds (10 minutes). int32 ack_deadline_seconds = 3 [(google.api.field_behavior) = REQUIRED]; } // Request for the Acknowledge method. message AcknowledgeRequest { // Required. The subscription whose message is being acknowledged. // Format is `projects/{project}/subscriptions/{sub}`. string subscription = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; // Required. The acknowledgment ID for the messages being acknowledged that // was returned by the Pub/Sub system in the `Pull` response. Must not be // empty. repeated string ack_ids = 2 [(google.api.field_behavior) = REQUIRED]; } // Request for the `StreamingPull` streaming RPC method. This request is used to // establish the initial stream as well as to stream acknowledgements and ack // deadline modifications from the client to the server. message StreamingPullRequest { // Required. The subscription for which to initialize the new stream. This // must be provided in the first request on the stream, and must not be set in // subsequent requests from client to server. // Format is `projects/{project}/subscriptions/{sub}`. string subscription = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; // List of acknowledgement IDs for acknowledging previously received messages // (received on this stream or a different stream). If an ack ID has expired, // the corresponding message may be redelivered later. Acknowledging a message // more than once will not result in an error. If the acknowledgement ID is // malformed, the stream will be aborted with status `INVALID_ARGUMENT`. repeated string ack_ids = 2; // The list of new ack deadlines for the IDs listed in // `modify_deadline_ack_ids`. The size of this list must be the same as the // size of `modify_deadline_ack_ids`. If it differs the stream will be aborted // with `INVALID_ARGUMENT`. Each element in this list is applied to the // element in the same position in `modify_deadline_ack_ids`. The new ack // deadline is with respect to the time this request was sent to the Pub/Sub // system. Must be >= 0. For example, if the value is 10, the new ack deadline // will expire 10 seconds after this request is received. If the value is 0, // the message is immediately made available for another streaming or // non-streaming pull request. If the value is < 0 (an error), the stream will // be aborted with status `INVALID_ARGUMENT`. repeated int32 modify_deadline_seconds = 3; // List of acknowledgement IDs whose deadline will be modified based on the // corresponding element in `modify_deadline_seconds`. This field can be used // to indicate that more time is needed to process a message by the // subscriber, or to make the message available for redelivery if the // processing was interrupted. repeated string modify_deadline_ack_ids = 4; // Required. The ack deadline to use for the stream. This must be provided in // the first request on the stream, but it can also be updated on subsequent // requests from client to server. The minimum deadline you can specify is 10 // seconds. The maximum deadline you can specify is 600 seconds (10 minutes). int32 stream_ack_deadline_seconds = 5 [(google.api.field_behavior) = REQUIRED]; // A unique identifier that is used to distinguish client instances from each // other. Only needs to be provided on the initial request. When a stream // disconnects and reconnects for the same stream, the client_id should be set // to the same value so that state associated with the old stream can be // transferred to the new stream. The same client_id should not be used for // different client instances. string client_id = 6; // Flow control settings for the maximum number of outstanding messages. When // there are `max_outstanding_messages` or more currently sent to the // streaming pull client that have not yet been acked or nacked, the server // stops sending more messages. The sending of messages resumes once the // number of outstanding messages is less than this value. If the value is // <= 0, there is no limit to the number of outstanding messages. This // property can only be set on the initial StreamingPullRequest. If it is set // on a subsequent request, the stream will be aborted with status // `INVALID_ARGUMENT`. int64 max_outstanding_messages = 7; // Flow control settings for the maximum number of outstanding bytes. When // there are `max_outstanding_bytes` or more worth of messages currently sent // to the streaming pull client that have not yet been acked or nacked, the // server will stop sending more messages. The sending of messages resumes // once the number of outstanding bytes is less than this value. If the value // is <= 0, there is no limit to the number of outstanding bytes. This // property can only be set on the initial StreamingPullRequest. If it is set // on a subsequent request, the stream will be aborted with status // `INVALID_ARGUMENT`. int64 max_outstanding_bytes = 8; } // Response for the `StreamingPull` method. This response is used to stream // messages from the server to the client. message StreamingPullResponse { // Received Pub/Sub messages. This will not be empty. repeated ReceivedMessage received_messages = 1; } // Request for the `CreateSnapshot` method. message CreateSnapshotRequest { // Required. User-provided name for this snapshot. If the name is not provided // in the request, the server will assign a random name for this snapshot on // the same project as the subscription. Note that for REST API requests, you // must specify a name. See the resource // name rules. Format is `projects/{project}/snapshots/{snap}`. string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Snapshot" } ]; // Required. The subscription whose backlog the snapshot retains. // Specifically, the created snapshot is guaranteed to retain: // (a) The existing backlog on the subscription. More precisely, this is // defined as the messages in the subscription's backlog that are // unacknowledged upon the successful completion of the // `CreateSnapshot` request; as well as: // (b) Any messages published to the subscription's topic following the // successful completion of the CreateSnapshot request. // Format is `projects/{project}/subscriptions/{sub}`. string subscription = 2 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; // See Creating and // managing labels. map labels = 3; } // Request for the UpdateSnapshot method. message UpdateSnapshotRequest { // Required. The updated snapshot object. Snapshot snapshot = 1 [(google.api.field_behavior) = REQUIRED]; // Required. Indicates which fields in the provided snapshot to update. // Must be specified and non-empty. google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; } // A snapshot resource. Snapshots are used in // [Seek](https://cloud.google.com/pubsub/docs/replay-overview) // operations, which allow you to manage message acknowledgments in bulk. That // is, you can set the acknowledgment state of messages in an existing // subscription to the state captured by a snapshot. message Snapshot { option (google.api.resource) = { type: "pubsub.googleapis.com/Snapshot" pattern: "projects/{project}/snapshots/{snapshot}" }; // The name of the snapshot. string name = 1; // The name of the topic from which this snapshot is retaining messages. string topic = 2 [ (google.api.resource_reference) = { type: "pubsub.googleapis.com/Topic" } ]; // The snapshot is guaranteed to exist up until this time. // A newly-created snapshot expires no later than 7 days from the time of its // creation. Its exact lifetime is determined at creation by the existing // backlog in the source subscription. Specifically, the lifetime of the // snapshot is `7 days - (age of oldest unacked message in the subscription)`. // For example, consider a subscription whose oldest unacked message is 3 days // old. If a snapshot is created from this subscription, the snapshot -- which // will always capture this 3-day-old backlog as long as the snapshot // exists -- will expire in 4 days. The service will refuse to create a // snapshot that would expire in less than 1 hour after creation. google.protobuf.Timestamp expire_time = 3; // See [Creating and managing labels] // (https://cloud.google.com/pubsub/docs/labels). map labels = 4; } // Request for the GetSnapshot method. message GetSnapshotRequest { // Required. The name of the snapshot to get. // Format is `projects/{project}/snapshots/{snap}`. string snapshot = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Snapshot" } ]; } // Request for the `ListSnapshots` method. message ListSnapshotsRequest { // Required. The name of the project in which to list snapshots. // Format is `projects/{project-id}`. string project = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "cloudresourcemanager.googleapis.com/Project" } ]; // Maximum number of snapshots to return. int32 page_size = 2; // The value returned by the last `ListSnapshotsResponse`; indicates that this // is a continuation of a prior `ListSnapshots` call, and that the system // should return the next page of data. string page_token = 3; } // Response for the `ListSnapshots` method. message ListSnapshotsResponse { // The resulting snapshots. repeated Snapshot snapshots = 1; // If not empty, indicates that there may be more snapshot that match the // request; this value should be passed in a new `ListSnapshotsRequest`. string next_page_token = 2; } // Request for the `DeleteSnapshot` method. message DeleteSnapshotRequest { // Required. The name of the snapshot to delete. // Format is `projects/{project}/snapshots/{snap}`. string snapshot = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Snapshot" } ]; } // Request for the `Seek` method. message SeekRequest { // Required. The subscription to affect. string subscription = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Subscription" } ]; oneof target { // The time to seek to. // Messages retained in the subscription that were published before this // time are marked as acknowledged, and messages retained in the // subscription that were published after this time are marked as // unacknowledged. Note that this operation affects only those messages // retained in the subscription (configured by the combination of // `message_retention_duration` and `retain_acked_messages`). For example, // if `time` corresponds to a point before the message retention // window (or to a point before the system's notion of the subscription // creation time), only retained messages will be marked as unacknowledged, // and already-expunged messages will not be restored. google.protobuf.Timestamp time = 2; // The snapshot to seek to. The snapshot's topic must be the same as that of // the provided subscription. // Format is `projects/{project}/snapshots/{snap}`. string snapshot = 3 [(google.api.resource_reference) = { type: "pubsub.googleapis.com/Snapshot" }]; } } // Response for the `Seek` method (this response is empty). message SeekResponse {} ================================================ FILE: examples/proto/googleapis/google/pubsub/v1/schema.proto ================================================ // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.pubsub.v1; import "google/api/annotations.proto"; import "google/api/client.proto"; import "google/api/field_behavior.proto"; import "google/api/resource.proto"; import "google/protobuf/empty.proto"; option cc_enable_arenas = true; option csharp_namespace = "Google.Cloud.PubSub.V1"; option go_package = "google.golang.org/genproto/googleapis/pubsub/v1;pubsub"; option java_multiple_files = true; option java_outer_classname = "SchemaProto"; option java_package = "com.google.pubsub.v1"; option php_namespace = "Google\\Cloud\\PubSub\\V1"; option ruby_package = "Google::Cloud::PubSub::V1"; // Service for doing schema-related operations. // // EXPERIMENTAL: The Schema service is in development and may not work yet. service SchemaService { option (google.api.default_host) = "pubsub.googleapis.com"; option (google.api.oauth_scopes) = "https://www.googleapis.com/auth/cloud-platform," "https://www.googleapis.com/auth/pubsub"; // Creates a schema. rpc CreateSchema(CreateSchemaRequest) returns (Schema) { option (google.api.http) = { post: "/v1/{parent=projects/*}/schemas" body: "schema" }; option (google.api.method_signature) = "parent,schema,schema_id"; } // Gets a schema. rpc GetSchema(GetSchemaRequest) returns (Schema) { option (google.api.http) = { get: "/v1/{name=projects/*/schemas/*}" }; option (google.api.method_signature) = "name"; } // Lists schemas in a project. rpc ListSchemas(ListSchemasRequest) returns (ListSchemasResponse) { option (google.api.http) = { get: "/v1/{parent=projects/*}/schemas" }; option (google.api.method_signature) = "parent"; } // Deletes a schema. rpc DeleteSchema(DeleteSchemaRequest) returns (google.protobuf.Empty) { option (google.api.http) = { delete: "/v1/{name=projects/*/schemas/*}" }; option (google.api.method_signature) = "name"; } // Validates a schema. rpc ValidateSchema(ValidateSchemaRequest) returns (ValidateSchemaResponse) { option (google.api.http) = { post: "/v1/{parent=projects/*}/schemas:validate" body: "*" }; option (google.api.method_signature) = "parent,schema"; } // Validates a message against a schema. rpc ValidateMessage(ValidateMessageRequest) returns (ValidateMessageResponse) { option (google.api.http) = { post: "/v1/{parent=projects/*}/schemas:validateMessage" body: "*" }; } } // A schema resource. message Schema { option (google.api.resource) = { type: "pubsub.googleapis.com/Schema" pattern: "projects/{project}/schemas/{schema}" }; // Possible schema definition types. enum Type { // Default value. This value is unused. TYPE_UNSPECIFIED = 0; // A Protocol Buffer schema definition. PROTOCOL_BUFFER = 1; // An Avro schema definition. AVRO = 2; } // Required. Name of the schema. // Format is `projects/{project}/schemas/{schema}`. string name = 1 [(google.api.field_behavior) = REQUIRED]; // The type of the schema definition. Type type = 2; // The definition of the schema. This should contain a string representing // the full definition of the schema that is a valid schema definition of // the type specified in `type`. string definition = 3; } // Request for the CreateSchema method. message CreateSchemaRequest { // Required. The name of the project in which to create the schema. // Format is `projects/{project-id}`. string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { child_type: "pubsub.googleapis.com/Schema" } ]; // Required. The schema object to create. // // This schema's `name` parameter is ignored. The schema object returned // by CreateSchema will have a `name` made using the given `parent` and // `schema_id`. Schema schema = 2 [(google.api.field_behavior) = REQUIRED]; // The ID to use for the schema, which will become the final component of // the schema's resource name. // // See https://cloud.google.com/pubsub/docs/admin#resource_names for resource // name constraints. string schema_id = 3; } // View of Schema object fields to be returned by GetSchema and ListSchemas. enum SchemaView { // The default / unset value. // The API will default to the BASIC view. SCHEMA_VIEW_UNSPECIFIED = 0; // Include the name and type of the schema, but not the definition. BASIC = 1; // Include all Schema object fields. FULL = 2; } // Request for the GetSchema method. message GetSchemaRequest { // Required. The name of the schema to get. // Format is `projects/{project}/schemas/{schema}`. string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Schema" } ]; // The set of fields to return in the response. If not set, returns a Schema // with `name` and `type`, but not `definition`. Set to `FULL` to retrieve all // fields. SchemaView view = 2; } // Request for the `ListSchemas` method. message ListSchemasRequest { // Required. The name of the project in which to list schemas. // Format is `projects/{project-id}`. string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "cloudresourcemanager.googleapis.com/Project" } ]; // The set of Schema fields to return in the response. If not set, returns // Schemas with `name` and `type`, but not `definition`. Set to `FULL` to // retrieve all fields. SchemaView view = 2; // Maximum number of schemas to return. int32 page_size = 3; // The value returned by the last `ListSchemasResponse`; indicates that // this is a continuation of a prior `ListSchemas` call, and that the // system should return the next page of data. string page_token = 4; } // Response for the `ListSchemas` method. message ListSchemasResponse { // The resulting schemas. repeated Schema schemas = 1; // If not empty, indicates that there may be more schemas that match the // request; this value should be passed in a new `ListSchemasRequest`. string next_page_token = 2; } // Request for the `DeleteSchema` method. message DeleteSchemaRequest { // Required. Name of the schema to delete. // Format is `projects/{project}/schemas/{schema}`. string name = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "pubsub.googleapis.com/Schema" } ]; } // Request for the `ValidateSchema` method. message ValidateSchemaRequest { // Required. The name of the project in which to validate schemas. // Format is `projects/{project-id}`. string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "cloudresourcemanager.googleapis.com/Project" } ]; // Required. The schema object to validate. Schema schema = 2 [(google.api.field_behavior) = REQUIRED]; } // Response for the `ValidateSchema` method. message ValidateSchemaResponse {} // Request for the `ValidateMessage` method. message ValidateMessageRequest { // Required. The name of the project in which to validate schemas. // Format is `projects/{project-id}`. string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { type: "cloudresourcemanager.googleapis.com/Project" } ]; oneof schema_spec { // Name of the schema against which to validate. // // Format is `projects/{project}/schemas/{schema}`. string name = 2 [ (google.api.resource_reference) = { type: "pubsub.googleapis.com/Schema" } ]; // Ad-hoc schema against which to validate Schema schema = 3; } // Message to validate against the provided `schema_spec`. bytes message = 4; // The encoding expected for messages Encoding encoding = 5; } // Response for the `ValidateMessage` method. message ValidateMessageResponse {} // Possible encoding types for messages. enum Encoding { // Unspecified ENCODING_UNSPECIFIED = 0; // JSON encoding JSON = 1; // Binary encoding, as defined by the schema type. For some schema types, // binary encoding may not be available. BINARY = 2; } ================================================ FILE: examples/proto/helloworld/helloworld.proto ================================================ // Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; package helloworld; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } ================================================ FILE: examples/proto/routeguide/route_guide.proto ================================================ // Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.routeguide"; option java_outer_classname = "RouteGuideProto"; package routeguide; // Interface exported by the server. service RouteGuide { // A simple RPC. // // Obtains the feature at a given position. // // A feature with an empty name is returned if there's no feature at the given // position. rpc GetFeature(Point) returns (Feature) {} // A server-to-client streaming RPC. // // Obtains the Features available within the given Rectangle. Results are // streamed rather than returned at once (e.g. in a response message with a // repeated field), as the rectangle may cover a large area and contain a // huge number of features. rpc ListFeatures(Rectangle) returns (stream Feature) {} // A client-to-server streaming RPC. // // Accepts a stream of Points on a route being traversed, returning a // RouteSummary when traversal is completed. rpc RecordRoute(stream Point) returns (RouteSummary) {} // A Bidirectional streaming RPC. // // Accepts a stream of RouteNotes sent while a route is being traversed, // while receiving other RouteNotes (e.g. from other users). rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} } // Points are represented as latitude-longitude pairs in the E7 representation // (degrees multiplied by 10**7 and rounded to the nearest integer). // Latitudes should be in the range +/- 90 degrees and longitude should be in // the range +/- 180 degrees (inclusive). message Point { int32 latitude = 1; int32 longitude = 2; } // A latitude-longitude rectangle, represented as two diagonally opposite // points "lo" and "hi". message Rectangle { // One corner of the rectangle. Point lo = 1; // The other corner of the rectangle. Point hi = 2; } // A feature names something at a given point. // // If a feature could not be named, the name is empty. message Feature { // The name of the feature. string name = 1; // The point where the feature is detected. Point location = 2; } // A RouteNote is a message sent while at a given point. message RouteNote { // The location from which the message is sent. Point location = 1; // The message to be sent. string message = 2; } // A RouteSummary is received in response to a RecordRoute rpc. // // It contains the number of individual points received, the number of // detected features, and the total distance covered as the cumulative sum of // the distance between each point. message RouteSummary { // The number of points received. int32 point_count = 1; // The number of known features passed while traversing the route. int32 feature_count = 2; // The distance covered in metres. int32 distance = 3; // The duration of the traversal in seconds. int32 elapsed_time = 4; } ================================================ FILE: examples/proto/unaryecho/echo.proto ================================================ /* * * Copyright 2018 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ syntax = "proto3"; package grpc.examples.unaryecho; // EchoRequest is the request for echo. message EchoRequest { string message = 1; } // EchoResponse is the response for echo. message EchoResponse { string message = 1; } // Echo is the echo service. service Echo { // UnaryEcho is unary echo. rpc UnaryEcho(EchoRequest) returns (EchoResponse) {} } ================================================ FILE: examples/routeguide-tutorial.md ================================================ # gRPC Basics: Tonic This tutorial, adapted from [grpc-go], provides a basic introduction to working with gRPC and Tonic. By walking through this example you'll learn how to: - Define a service in a `.proto` file. - Generate server and client code. - Write a simple client and server for your service. It assumes you are familiar with [protocol buffers] and basic Rust. Note that the example in this tutorial uses the proto3 version of the protocol buffers language, you can find out more in the [proto3 language guide][proto3]. [grpc-go]: https://github.com/grpc/grpc-go/blob/master/examples/gotutorial.md [protocol buffers]: https://developers.google.com/protocol-buffers/docs/overview [proto3]: https://developers.google.com/protocol-buffers/docs/proto3 ## Why use gRPC? Our example is a simple route mapping application that lets clients get information about features on their route, create a summary of their route, and exchange route information such as traffic updates with the server and other clients. With gRPC we can define our service once in a `.proto` file and implement clients and servers in any of gRPC's supported languages, which in turn can be run in environments ranging from servers inside Google to your own tablet - all the complexity of communication between different languages and environments is handled for you by gRPC. We also get all the advantages of working with protocol buffers, including efficient serialization, a simple IDL, and easy interface updating. ## Prerequisites To run the sample code and walk through the tutorial, the only prerequisite is Rust itself. [rustup] is a convenient tool to install it, if you haven't already. [rustup]: https://rustup.rs ## Running the example Clone or download Tonic's repository: ```shell $ git clone https://github.com/hyperium/tonic.git ``` Change your current directory to Tonic's repository root: ```shell $ cd tonic ``` Run the server ```shell $ cargo run --bin routeguide-server ``` In a separate shell, run the client ```shell $ cargo run --bin routeguide-client ``` You should see some logging output flying past really quickly on both terminal windows. On the shell where you ran the client binary, you should see the output of the bidirectional streaming rpc, printing 1 line per second: ``` NOTE = RouteNote { location: Some(Point { latitude: 409146139, longitude: -746188906 }), message: "at 1.000319208s" } ``` If you scroll up you should see the output of the other 3 request types: simple rpc, server-side streaming and client-side streaming. ## Project setup We will develop our example from scratch in a new crate: ```shell $ cargo new routeguide $ cd routeguide ``` ## Defining the service Our first step is to define the gRPC *service* and the method *request* and *response* types using [protocol buffers]. We will keep our `.proto` files in a directory in our crate's root. Note that Tonic does not really care where our `.proto` definitions live. We will see how to use different [code generation configuration](#tonic-build) later in the tutorial. ```shell $ mkdir proto && touch proto/route_guide.proto ``` You can see the complete `.proto` file in [examples/proto/routeguide/route_guide.proto][routeguide-proto]. To define a service, you specify a named `service` in your `.proto` file: ```proto service RouteGuide { ... } ``` Then you define `rpc` methods inside your service definition, specifying their request and response types. gRPC lets you define four kinds of service method, all of which are used in the `RouteGuide` service: - A *simple RPC* where the client sends a request to the server and waits for a response to come back, just like a normal function call. ```proto // Obtains the feature at a given position. rpc GetFeature(Point) returns (Feature) {} ``` - A *server-side streaming RPC* where the client sends a request to the server and gets a stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages. As you can see in our example, you specify a server-side streaming method by placing the `stream` keyword before the *response* type. ```proto // Obtains the Features available within the given Rectangle. Results are // streamed rather than returned at once (e.g. in a response message with a // repeated field), as the rectangle may cover a large area and contain a // huge number of features. rpc ListFeatures(Rectangle) returns (stream Feature) {} ``` - A *client-side streaming RPC* where the client writes a sequence of messages and sends them to the server. Once the client has finished writing the messages, it waits for the server to read them all and return its response. You specify a client-side streaming method by placing the `stream` keyword before the *request* type. ```proto // Accepts a stream of Points on a route being traversed, returning a // RouteSummary when traversal is completed. rpc RecordRoute(stream Point) returns (RouteSummary) {} ``` - A *bidirectional streaming RPC* where both sides send a sequence of messages. The two streams operate independently, so clients and servers can read and write in whatever order they like: for example, the server could wait to receive all the client messages before writing its responses, or it could alternately read a message then write a message, or some other combination of reads and writes. The order of messages in each stream is preserved. You specify this type of method by placing the `stream` keyword before both the request and the response. ```proto // Accepts a stream of RouteNotes sent while a route is being traversed, // while receiving other RouteNotes (e.g. from other users). rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} ``` Our `.proto` file also contains protocol buffer message type definitions for all the request and response types used in our service methods - for example, here's the `Point` message type: ```proto // Points are represented as latitude-longitude pairs in the E7 representation // (degrees multiplied by 10**7 and rounded to the nearest integer). // Latitudes should be in the range +/- 90 degrees and longitude should be in // the range +/- 180 degrees (inclusive). message Point { int32 latitude = 1; int32 longitude = 2; } ``` [routeguide-proto]: https://github.com/hyperium/tonic/blob/master/examples/proto/routeguide/route_guide.proto ## Generating client and server code Tonic can be configured to generate code as part cargo's normal build process. This is very convenient because once we've set everything up, there is no extra step to keep the generated code and our `.proto` definitions in sync. Behind the scenes, Tonic uses [PROST!] to handle protocol buffer serialization and code generation. Edit `Cargo.toml` and add all the dependencies we'll need for this example: ```toml [dependencies] tonic = "*" prost = "0.14" tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "sync", "time"] } tokio-stream = "0.1" async-stream = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" rand = "0.8" [build-dependencies] tonic-prost-build = "*" ``` Create a `build.rs` file at the root of your crate: ```rust fn main() { tonic_prost_build::compile_protos("proto/route_guide.proto") .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); } ``` ```shell $ cargo build ``` That's it. The generated code contains: - Struct definitions for message types `Point`, `Rectangle`, `Feature`, `RouteNote`, `RouteSummary`. - A service trait we'll need to implement: `route_guide_server::RouteGuide`. - A client type we'll use to call the server: `route_guide_client::RouteGuideClient`. If your are curious as to where the generated files are, keep reading. The mystery will be revealed soon! We can now move on to the fun part. [PROST!]: https://github.com/danburkert/prost ## Creating the server First let's look at how we create a `RouteGuide` server. If you're only interested in creating gRPC clients, you can skip this section and go straight to [Creating the client](#client) (though you might find it interesting anyway!). There are two parts to making our `RouteGuide` service do its job: - Implementing the service trait generated from our service definition. - Running a gRPC server to listen for requests from clients. You can find our example `RouteGuide` server in [examples/src/routeguide/server.rs][routeguide-server]. [routeguide-server]: https://github.com/hyperium/tonic/blob/master/examples/src/routeguide/server.rs ### Implementing the RouteGuide server trait We can start by defining a struct to represent our service, we can do this on `main.rs` for now: ```rust #[derive(Debug)] struct RouteGuideService; ``` Next, we need to implement the `route_guide_server::RouteGuide` trait that is generated in our build step. The generated code is placed inside our target directory, in a location defined by the `OUT_DIR` environment variable that is set by cargo. For our example, this means you can find the generated code in a path similar to `target/debug/build/routeguide/out/routeguide.rs`. You can learn more about `build.rs` and the `OUT_DIR` environment variable in the [cargo book]. We can use Tonic's `include_proto` macro to bring the generated code into scope: ```rust pub mod routeguide { tonic::include_proto!("routeguide"); } use routeguide::route_guide_server::{RouteGuide, RouteGuideServer}; use routeguide::{Feature, Point, Rectangle, RouteNote, RouteSummary}; ``` **Note**: The token passed to the `include_proto` macro (in our case "routeguide") is the name of the package declared in our `.proto` file, not a filename, e.g "routeguide.rs". With this in place, we can stub out our service implementation: ```rust use std::pin::Pin; use std::sync::Arc; use tokio::sync::mpsc; use tonic::{Request, Response, Status}; use tokio_stream::{wrappers::ReceiverStream, Stream}; ``` ```rust #[tonic::async_trait] impl RouteGuide for RouteGuideService { async fn get_feature(&self, _request: Request) -> Result, Status> { unimplemented!() } type ListFeaturesStream = ReceiverStream>; async fn list_features( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn record_route( &self, _request: Request>, ) -> Result, Status> { unimplemented!() } type RouteChatStream = Pin> + Send + 'static>>; async fn route_chat( &self, _request: Request>, ) -> Result, Status> { unimplemented!() } } ``` **Note**: The `tonic::async_trait` attribute macro adds support for async functions in traits. It uses [async-trait] internally. You can learn more about `async fn` in traits in the [async book]. [cargo book]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts [async-trait]: https://github.com/dtolnay/async-trait [async book]: https://rust-lang.github.io/async-book/07_workarounds/05_async_in_traits.html ### Server state Our service needs access to an immutable list of features. When the server starts, we are going to deserialize them from a json file and keep them around as our only piece of shared state: ```rust #[derive(Debug)] pub struct RouteGuideService { features: Arc>, } ``` Create the json data file and a helper module to read and deserialize our features. ```shell $ mkdir data && touch data/route_guide_db.json $ touch src/data.rs ``` You can find our example json data in [examples/data/route_guide_db.json][route-guide-db] and the corresponding `data` module to load and deserialize it in [examples/routeguide/data.rs][data-module]. **Note:** If you are following along, you'll need to change the data file's path from `examples/data/route_guide_db.json` to `data/route_guide_db.json`. Lastly, we need implement two helper functions: `in_range` and `calc_distance`. We'll use them when performing feature lookups. You can find them in [examples/src/routeguide/server.rs][in-range-fn]. [route-guide-db]: https://github.com/hyperium/tonic/blob/master/examples/data/route_guide_db.json [data-module]: https://github.com/hyperium/tonic/blob/master/examples/src/routeguide/data.rs [in-range-fn]: https://github.com/hyperium/tonic/blob/master/examples/src/routeguide/server.rs#L174 #### Request and Response types All our service methods receive a `tonic::Request` and return a `Result, tonic::Status>`. The concrete type of `T` depends on how our methods are declared in our *service* `.proto` definition. It can be either: - A single value, e.g `Point`, `Rectangle`, or even a message type that includes a repeated field. - A stream of values, e.g. `impl Stream>`. #### Simple RPC Let's look at the simplest method first, `get_feature`, which just gets a `tonic::Request` from the client and tries to find a feature at the given `Point`. If no feature is found, it returns an empty one. ```rust async fn get_feature(&self, request: Request) -> Result, Status> { for feature in &self.features[..] { if feature.location.as_ref() == Some(request.get_ref()) { return Ok(Response::new(feature.clone())); } } Ok(Response::new(Feature::default())) } ``` #### Server-side streaming RPC Now let's look at one of our streaming RPCs. `list_features` is a server-side streaming RPC, so we need to send back multiple `Feature`s to our client. ```rust type ListFeaturesStream = ReceiverStream>; async fn list_features( &self, request: Request, ) -> Result, Status> { let (tx, rx) = mpsc::channel(4); let features = self.features.clone(); tokio::spawn(async move { for feature in &features[..] { if in_range(feature.location.as_ref().unwrap(), request.get_ref()) { tx.send(Ok(feature.clone())).await.unwrap(); } } }); Ok(Response::new(ReceiverStream::new(rx))) } ``` Like `get_feature`, `list_features`'s input is a single message, a `Rectangle` in this case. This time, however, we need to return a stream of values, rather than a single one. We create a channel and spawn a new asynchronous task where we perform a lookup, sending the features that satisfy our constraints into the channel. The `Stream` half of the channel is returned to the caller, wrapped in a `tonic::Response`. #### Client-side streaming RPC Now let's look at something a little more complicated: the client-side streaming method `record_route`, where we get a stream of `Point`s from the client and return a single `RouteSummary` with information about their trip. As you can see, this time the method receives a `tonic::Request>`. ```rust use std::time::Instant; use tokio_stream::StreamExt; ``` ```rust async fn record_route( &self, request: Request>, ) -> Result, Status> { let mut stream = request.into_inner(); let mut summary = RouteSummary::default(); let mut last_point = None; let now = Instant::now(); while let Some(point) = stream.next().await { let point = point?; summary.point_count += 1; for feature in &self.features[..] { if feature.location.as_ref() == Some(&point) { summary.feature_count += 1; } } if let Some(ref last_point) = last_point { summary.distance += calc_distance(last_point, &point); } last_point = Some(point); } summary.elapsed_time = now.elapsed().as_secs() as i32; Ok(Response::new(summary)) } ``` `record_route` is conceptually simple: we get a stream of `Points` and fold it into a `RouteSummary`. In other words, we build a summary value as we process each `Point` in our stream, one by one. When there are no more `Points` in our stream, we return the `RouteSummary` wrapped in a `tonic::Response`. #### Bidirectional streaming RPC Finally, let's look at our bidirectional streaming RPC `route_chat`, which receives a stream of `RouteNote`s and returns a stream of `RouteNote`s. ```rust use std::collections::HashMap; ``` ```rust type RouteChatStream = Pin> + Send + 'static>>; async fn route_chat( &self, request: Request>, ) -> Result, Status> { let mut notes = HashMap::new(); let mut stream = request.into_inner(); let output = async_stream::try_stream! { while let Some(note) = stream.next().await { let note = note?; let location = note.location.unwrap(); let location_notes = notes.entry(location).or_insert(vec![]); location_notes.push(note); for note in location_notes { yield note.clone(); } } }; Ok(Response::new(Box::pin(output) as Self::RouteChatStream)) } ``` `route_chat` uses the [async-stream] crate to perform an asynchronous transformation from one (input) stream to another (output) stream. As the input is processed, each value is inserted into the notes map, yielding a clone of the original `RouteNote`. The resulting stream is then returned to the caller. Neat. **Note**: The funky `as` cast is needed due to a limitation in the rust compiler. This is expected to be fixed soon. [async-stream]: https://github.com/tokio-rs/async-stream ### Starting the server Once we've implemented all our methods, we also need to start up a gRPC server so that clients can actually use our service. This is how our `main` function looks like: ```rust mod data; use tonic::transport::Server; ``` ```rust #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:10000".parse().unwrap(); let route_guide = RouteGuideService { features: Arc::new(data::load()), }; let svc = RouteGuideServer::new(route_guide); Server::builder().add_service(svc).serve(addr).await?; Ok(()) } ``` To handle requests, `Tonic` uses [Tower] and [hyper] internally. What this means, among other things, is that we have a flexible and composable stack we can build on top of. We can, for example, add an [interceptor][authentication-example] to process requests before they reach our service methods. [Tower]: https://github.com/tower-rs [hyper]: https://github.com/hyperium/hyper [authentication-example]: https://github.com/hyperium/tonic/blob/master/examples/src/authentication/server.rs#L56 ## Creating the client In this section, we'll look at creating a Tonic client for our `RouteGuide` service. You can see our complete example client code in [examples/src/routeguide/client.rs][routeguide-client]. Our crate will have two binary targets: `routeguide-client` and `routeguide-server`. We need to edit our `Cargo.toml` accordingly: ```toml [[bin]] name = "routeguide-server" path = "src/server.rs" [[bin]] name = "routeguide-client" path = "src/client.rs" ``` Rename `main.rs` to `server.rs` and create a new file `client.rs`. ```shell $ mv src/main.rs src/server.rs $ touch src/client.rs ``` To call service methods, we first need to create a gRPC *client* to communicate with the server. Like in the server case, we'll start by bringing the generated code into scope: ```rust pub mod routeguide { tonic::include_proto!("routeguide"); } use routeguide::route_guide_client::RouteGuideClient; use routeguide::{Point, Rectangle, RouteNote}; #[tokio::main] async fn main() -> Result<(), Box> { let mut client = RouteGuideClient::connect("http://[::1]:10000").await?; Ok(()) } ``` Same as in the server implementation, we start by bringing our generated code into scope. We then create a client in our main function, passing the server's full URL to `RouteGuideClient::connect`. Our client is now ready to make service calls. Note that `client` is mutable, this is because it needs to manage internal state. [routeguide-client]: https://github.com/hyperium/tonic/blob/master/examples/src/routeguide/client.rs ### Calling service methods Now let's look at how we call our service methods. Note that in Tonic, RPCs are asynchronous, which means that RPC calls need to be `.await`ed. #### Simple RPC Calling the simple RPC `get_feature` is as straightforward as calling a local method: ```rust use tonic::Request; ``` ```rust let response = client .get_feature(Request::new(Point { latitude: 409146138, longitude: -746188906, })) .await?; println!("RESPONSE = {:?}", response); ``` We call the `get_feature` client method, passing a single `Point` value wrapped in a `tonic::Request`. We get a `Result, tonic::Status>` back. #### Server-side streaming RPC Here's where we call the server-side streaming method `list_features`, which returns a stream of geographical `Feature`s. ```rust use tonic::transport::Channel; use std::error::Error; ``` ```rust async fn print_features(client: &mut RouteGuideClient) -> Result<(), Box> { let rectangle = Rectangle { lo: Some(Point { latitude: 400000000, longitude: -750000000, }), hi: Some(Point { latitude: 420000000, longitude: -730000000, }), }; let mut stream = client .list_features(Request::new(rectangle)) .await? .into_inner(); while let Some(feature) = stream.message().await? { println!("FEATURE = {:?}", feature); } Ok(()) } ``` As in the simple RPC, we pass a single value request. However, instead of getting a single value back, we get a stream of `Features`. We use the `message()` method from the `tonic::Streaming` struct to repeatedly read in the server's responses to a response protocol buffer object (in this case a `Feature`) until there are no more messages left in the stream. #### Client-side streaming RPC The client-side streaming method `record_route` takes a stream of `Point`s and returns a single `RouteSummary` value. ```rust use rand::rngs::ThreadRng; use rand::Rng; ``` ```rust async fn run_record_route(client: &mut RouteGuideClient) -> Result<(), Box> { let mut rng = rand::rng(); let point_count: i32 = rng.random_range(2..100); let mut points = vec![]; for _ in 0..=point_count { points.push(random_point(&mut rng)) } println!("Traversing {} points", points.len()); let request = Request::new(tokio_stream::iter(points)); match client.record_route(request).await { Ok(response) => println!("SUMMARY: {:?}", response.into_inner()), Err(e) => println!("something went wrong: {:?}", e), } Ok(()) } ``` ```rust fn random_point(rng: &mut ThreadRng) -> Point { let latitude = (rng.random_range(0..180) - 90) * 10_000_000; let longitude = (rng.random_range(0..360) - 180) * 10_000_000; Point { latitude, longitude, } } ``` We build a vector of a random number of `Point` values (between 2 and 100) and then convert it into a `Stream` using the `tokio_stream::iter` function. This is a cheap an easy way to get a stream suitable for passing into our service method. The resulting stream is then wrapped in a `tonic::Request`. #### Bidirectional streaming RPC Finally, let's look at our bidirectional streaming RPC. The `route_chat` method takes a stream of `RouteNotes` and returns either another stream of `RouteNotes` or an error. ```rust use std::time::Duration; use tokio::time; ``` ```rust async fn run_route_chat(client: &mut RouteGuideClient) -> Result<(), Box> { let start = time::Instant::now(); let outbound = async_stream::stream! { let mut interval = time::interval(Duration::from_secs(1)); while let time = interval.tick().await { let elapsed = time.duration_since(start); let note = RouteNote { location: Some(Point { latitude: 409146138 + elapsed.as_secs() as i32, longitude: -746188906, }), message: format!("at {:?}", elapsed), }; yield note; } }; let response = client.route_chat(Request::new(outbound)).await?; let mut inbound = response.into_inner(); while let Some(note) = inbound.message().await? { println!("NOTE = {:?}", note); } Ok(()) } ``` In this case, we use the [async-stream] crate to generate our outbound stream, yielding `RouteNote` values in one second intervals. We then iterate over the stream returned by the server, printing each value in the stream. ## Try it out! ### Run the server ```shell $ cargo run --bin routeguide-server ``` ### Run the client ```shell $ cargo run --bin routeguide-client ``` ## Appendix ### tonic_prost_build configuration Tonic's default code generation configuration is convenient for self contained examples and small projects. However, there are some cases when we need a slightly different workflow. For example: - When building rust clients and servers in different crates. - When building a rust client or server (or both) as part of a larger, multi-language project. - When we want editor support for the generate code and our editor does not index the generated files in the default location. More generally, whenever we want to keep our `.proto` definitions in a central place and generate code for different crates or different languages, the default configuration is not enough. Luckily, `tonic_prost_build` can be configured to fit whatever workflow we need. Here are just two possibilities: 1) We can keep our `.proto` definitions in a separate crate and generate our code on demand, as opposed to at build time, placing the resulting modules wherever we need them. `main.rs` ```rust fn main() { tonic_prost_build::configure() .build_client(false) .out_dir("another_crate/src/pb") .compile_protos(&["path/my_proto.proto"], &["path"]) .expect("failed to compile protos"); } ``` On `cargo run`, this will generate code for the server only, and place the resulting file in `another_crate/src/pb`. 2) Similarly, we could also keep the `.proto` definitions in a separate crate and then use that crate as a direct dependency wherever we need it. ================================================ FILE: examples/src/authentication/client.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.unaryecho"); } use pb::{EchoRequest, echo_client::EchoClient}; use tonic::{Request, metadata::MetadataValue, transport::Channel}; #[tokio::main] async fn main() -> Result<(), Box> { let channel = Channel::from_static("http://[::1]:50051").connect().await?; let token: MetadataValue<_> = "Bearer some-auth-token".parse()?; let mut client = EchoClient::with_interceptor(channel, move |mut req: Request<()>| { req.metadata_mut().insert("authorization", token.clone()); Ok(req) }); let request = tonic::Request::new(EchoRequest { message: "hello".into(), }); let response = client.unary_echo(request).await?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/authentication/server.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.unaryecho"); } use pb::{EchoRequest, EchoResponse}; use tonic::{Request, Response, Status, metadata::MetadataValue, transport::Server}; type EchoResult = Result, Status>; #[derive(Default)] pub struct EchoServer {} #[tonic::async_trait] impl pb::echo_server::Echo for EchoServer { async fn unary_echo(&self, request: Request) -> EchoResult { let message = request.into_inner().message; Ok(Response::new(EchoResponse { message })) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let server = EchoServer::default(); let svc = pb::echo_server::EchoServer::with_interceptor(server, check_auth); Server::builder().add_service(svc).serve(addr).await?; Ok(()) } fn check_auth(req: Request<()>) -> Result, Status> { let token: MetadataValue<_> = "Bearer some-secret-token".parse().unwrap(); match req.metadata().get("authorization") { Some(t) if token == t => Ok(req), _ => Err(Status::unauthenticated("No valid auth token")), } } ================================================ FILE: examples/src/autoreload/server.rs ================================================ use tonic::{Request, Response, Status, transport::Server}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); let server = Server::builder().add_service(GreeterServer::new(greeter)); match listenfd::ListenFd::from_env().take_tcp_listener(0)? { Some(listener) => { listener.set_nonblocking(true)?; let listener = tokio_stream::wrappers::TcpListenerStream::new( tokio::net::TcpListener::from_std(listener)?, ); server.serve_with_incoming(listener).await?; } None => { server.serve(addr).await?; } } Ok(()) } ================================================ FILE: examples/src/blocking/client.rs ================================================ use tokio::runtime::{Builder, Runtime}; pub mod hello_world { tonic::include_proto!("helloworld"); } use hello_world::{HelloReply, HelloRequest, greeter_client::GreeterClient}; type StdError = Box; type Result = ::std::result::Result; // The order of the fields in this struct is important. They must be ordered // such that when `BlockingClient` is dropped the client is dropped // before the runtime. Not doing this will result in a deadlock when dropped. // Rust drops struct fields in declaration order. struct BlockingClient { client: GreeterClient, rt: Runtime, } impl BlockingClient { pub fn connect(dst: D) -> Result where D: TryInto, D::Error: Into, { let rt = Builder::new_multi_thread().enable_all().build().unwrap(); let client = rt.block_on(GreeterClient::connect(dst))?; Ok(Self { client, rt }) } pub fn say_hello( &mut self, request: impl tonic::IntoRequest, ) -> Result, tonic::Status> { self.rt.block_on(self.client.say_hello(request)) } } fn main() -> Result<()> { let mut client = BlockingClient::connect("http://[::1]:50051")?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request)?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/blocking/server.rs ================================================ use tonic::{Request, Response, Status, transport::Server}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; use tokio::runtime::Runtime; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Debug, Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request: {:?}", request); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } fn main() { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); let rt = Runtime::new().expect("failed to obtain a new RunTime object"); let server_future = Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr); rt.block_on(server_future) .expect("failed to successfully run the future on RunTime"); } ================================================ FILE: examples/src/cancellation/client.rs ================================================ use hello_world::HelloRequest; use hello_world::greeter_client::GreeterClient; use tokio::time::{Duration, timeout}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); // Cancelling the request by dropping the request future after 1 second let response = match timeout(Duration::from_secs(1), client.say_hello(request)).await { Ok(response) => response?, Err(_) => { println!("Cancelled request after 1s"); return Ok(()); } }; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/cancellation/server.rs ================================================ use std::future::Future; use tokio_util::sync::CancellationToken; use tonic::{Request, Response, Status, transport::Server}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; use tokio::select; use tokio::time::Duration; use tokio::time::sleep; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { let remote_addr = request.remote_addr(); let request_future = async move { println!("Got a request from {:?}", request.remote_addr()); // Take a long time to complete request for the client to cancel early sleep(Duration::from_secs(10)).await; let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) }; let cancellation_future = async move { println!("Request from {remote_addr:?} cancelled by client"); // If this future is executed it means the request future was dropped, // so it doesn't actually matter what is returned here Err(Status::cancelled("Request cancelled by client")) }; with_cancellation_handler(request_future, cancellation_future).await } } async fn with_cancellation_handler( request_future: FRequest, cancellation_future: FCancellation, ) -> Result, Status> where FRequest: Future, Status>> + Send + 'static, FCancellation: Future, Status>> + Send + 'static, { let token = CancellationToken::new(); // Will call token.cancel() when the future is dropped, such as when the client cancels the request let _drop_guard = token.clone().drop_guard(); let select_task = tokio::spawn(async move { // Can select on token cancellation on any cancellable future while handling the request, // allowing for custom cleanup code or monitoring select! { res = request_future => res, _ = token.cancelled() => cancellation_future.await, } }); select_task.await.unwrap() } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/codec_buffers/client.rs ================================================ //! A HelloWorld example that uses a custom codec instead of the default Prost codec. //! //! Generated code is the output of codegen as defined in the `examples/build.rs` file. //! The generation is the one with .codec_path("crate::common::SmallBufferCodec") //! The generated code assumes that a module `crate::common` exists which defines //! `SmallBufferCodec`, and `SmallBufferCodec` must have a Default implementation. pub mod common; pub mod small_buf { include!(concat!(env!("OUT_DIR"), "/smallbuf/helloworld.rs")); } use small_buf::greeter_client::GreeterClient; use crate::small_buf::HelloRequest; #[tokio::main] async fn main() -> Result<(), Box> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/codec_buffers/common.rs ================================================ //! This module defines a common encoder with small buffers. This is useful //! when you have many concurrent RPC's, and not a huge volume of data per //! rpc normally. //! //! Note that you can customize your codecs per call to the code generator's //! compile function. This lets you group services by their codec needs. //! //! While this codec demonstrates customizing the built-in Prost codec, you //! can use this to implement other codecs as well, as long as they have a //! Default implementation. use std::marker::PhantomData; use prost::Message; use tonic::codec::{BufferSettings, Codec}; use tonic_prost::ProstCodec; #[derive(Debug, Clone, Copy, Default)] pub struct SmallBufferCodec(PhantomData<(T, U)>); impl Codec for SmallBufferCodec where T: Message + Send + 'static, U: Message + Default + Send + 'static, { type Encode = T; type Decode = U; type Encoder = as Codec>::Encoder; type Decoder = as Codec>::Decoder; fn encoder(&mut self) -> Self::Encoder { // Here, we will just customize the prost codec's internal buffer settings. // You can of course implement a complete Codec, Encoder, and Decoder if // you wish! ProstCodec::::raw_encoder(BufferSettings::new(512, 4096)) } fn decoder(&mut self) -> Self::Decoder { ProstCodec::::raw_decoder(BufferSettings::new(512, 4096)) } } ================================================ FILE: examples/src/codec_buffers/server.rs ================================================ //! A HelloWorld example that uses a custom codec instead of the default Prost codec. //! //! Generated code is the output of codegen as defined in the `examples/build.rs` file. //! The generation is the one with .codec_path("crate::common::SmallBufferCodec") //! The generated code assumes that a module `crate::common` exists which defines //! `SmallBufferCodec`, and `SmallBufferCodec` must have a Default implementation. use tonic::{Request, Response, Status, transport::Server}; pub mod common; pub mod small_buf { include!(concat!(env!("OUT_DIR"), "/smallbuf/helloworld.rs")); } use small_buf::{ HelloReply, HelloRequest, greeter_server::{Greeter, GreeterServer}, }; #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/compression/client.rs ================================================ use hello_world::HelloRequest; use hello_world::greeter_client::GreeterClient; use tonic::codec::CompressionEncoding; use tonic::transport::Channel; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { let channel = Channel::builder("http://[::1]:50051".parse().unwrap()) .connect() .await .unwrap(); let mut client = GreeterClient::new(channel) .send_compressed(CompressionEncoding::Gzip) .accept_compressed(CompressionEncoding::Gzip); let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; dbg!(response); Ok(()) } ================================================ FILE: examples/src/compression/server.rs ================================================ use tonic::{Request, Response, Status, transport::Server}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; use tonic::codec::CompressionEncoding; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); let service = GreeterServer::new(greeter) .send_compressed(CompressionEncoding::Gzip) .accept_compressed(CompressionEncoding::Gzip); Server::builder().add_service(service).serve(addr).await?; Ok(()) } ================================================ FILE: examples/src/dynamic/server.rs ================================================ use std::env; use tonic::{Request, Response, Status, service::RoutesBuilder, transport::Server}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; use echo::echo_server::{Echo, EchoServer}; use echo::{EchoRequest, EchoResponse}; pub mod hello_world { tonic::include_proto!("helloworld"); } pub mod echo { tonic::include_proto!("grpc.examples.unaryecho"); } type EchoResult = Result, Status>; #[derive(Default)] pub struct MyEcho {} #[tonic::async_trait] impl Echo for MyEcho { async fn unary_echo(&self, request: Request) -> EchoResult { println!("Got an echo request from {:?}", request.remote_addr()); let message = format!("you said: {}", request.into_inner().message); Ok(Response::new(EchoResponse { message })) } } fn init_echo(args: &[String], builder: &mut RoutesBuilder) { let enabled = args.iter().any(|arg| arg.as_str() == "echo"); if enabled { println!("Adding Echo service..."); let svc = EchoServer::new(MyEcho::default()); builder.add_service(svc); } } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a greet request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } fn init_greeter(args: &[String], builder: &mut RoutesBuilder) { let enabled = args.iter().any(|arg| arg.as_str() == "greeter"); if enabled { println!("Adding Greeter service..."); let svc = GreeterServer::new(MyGreeter::default()); builder.add_service(svc); } } #[tokio::main] async fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); let mut routes_builder = RoutesBuilder::default(); init_greeter(&args, &mut routes_builder); init_echo(&args, &mut routes_builder); let addr = "[::1]:50051".parse().unwrap(); println!("Grpc server listening on {addr}"); Server::builder() .add_routes(routes_builder.routes()) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/dynamic_load_balance/client.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.unaryecho"); } use pb::{EchoRequest, echo_client::EchoClient}; use tonic::transport::Channel; use tonic::transport::Endpoint; use tonic::transport::channel::Change; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering::SeqCst}; use tokio::time::timeout; #[tokio::main] async fn main() -> Result<(), Box> { let e1 = Endpoint::from_static("http://[::1]:50051"); let e2 = Endpoint::from_static("http://[::1]:50052"); let (channel, rx) = Channel::balance_channel(10); let mut client = EchoClient::new(channel); let done = Arc::new(AtomicBool::new(false)); let demo_done = done.clone(); tokio::spawn(async move { tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; println!("Added first endpoint"); let change = Change::Insert("1", e1); let res = rx.send(change).await; println!("{res:?}"); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; println!("Added second endpoint"); let change = Change::Insert("2", e2); let res = rx.send(change).await; println!("{res:?}"); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; println!("Removed first endpoint"); let change = Change::Remove("1"); let res = rx.send(change).await; println!("{res:?}"); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; println!("Removed second endpoint"); let change = Change::Remove("2"); let res = rx.send(change).await; println!("{res:?}"); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; println!("Added third endpoint"); let e3 = Endpoint::from_static("http://[::1]:50051"); let change = Change::Insert("3", e3); let res = rx.send(change).await; println!("{res:?}"); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; println!("Removed third endpoint"); let change = Change::Remove("3"); let res = rx.send(change).await; println!("{res:?}"); demo_done.swap(true, SeqCst); }); while !done.load(SeqCst) { tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; let request = tonic::Request::new(EchoRequest { message: "hello".into(), }); let rx = client.unary_echo(request); if let Ok(resp) = timeout(tokio::time::Duration::from_secs(10), rx).await { println!("RESPONSE={resp:?}"); } else { println!("did not receive value within 10 secs"); } } println!("... Bye"); Ok(()) } ================================================ FILE: examples/src/dynamic_load_balance/server.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.unaryecho"); } use std::net::SocketAddr; use tokio::sync::mpsc; use tonic::{Request, Response, Status, transport::Server}; use pb::{EchoRequest, EchoResponse}; type EchoResult = Result, Status>; #[derive(Debug)] pub struct EchoServer { addr: SocketAddr, } #[tonic::async_trait] impl pb::echo_server::Echo for EchoServer { async fn unary_echo(&self, request: Request) -> EchoResult { let message = format!("{} (from {})", request.into_inner().message, self.addr); Ok(Response::new(EchoResponse { message })) } } #[tokio::main] async fn main() -> Result<(), Box> { let addrs = ["[::1]:50051", "[::1]:50052"]; let (tx, mut rx) = mpsc::unbounded_channel(); for addr in &addrs { let addr = addr.parse()?; let tx = tx.clone(); let server = EchoServer { addr }; let serve = Server::builder() .add_service(pb::echo_server::EchoServer::new(server)) .serve(addr); tokio::spawn(async move { if let Err(e) = serve.await { eprintln!("Error = {e:?}"); } tx.send(()).unwrap(); }); } rx.recv().await; Ok(()) } ================================================ FILE: examples/src/gcp/README.md ================================================ # Google Cloud Pubsub example This example will attempt to fetch a list of topics using the google gRPC protobuf specification. This will use an OAuth token and TLS to fetch the list of topics. First, you must generate a access token via the [OAuth playground]. From here select the `Cloud Pub/Sub API v1` and its urls as the scope. This will start the OAuth flow. Then you must hit the `Exchange authorization code for tokens` button to generate an `access_token` which will show up in the HTTP response to the right under the `access_token` field in the response json. Once, you have this token you must fetch your GCP project id which can be found from the main page on the dashboard. When you have both of these items you can run the example like so: ```shell GCP_AUTH_TOKEN="" cargo run --bin gcp-client -- ``` [OAuth playground]: https://developers.google.com/oauthplayground/ ================================================ FILE: examples/src/gcp/client.rs ================================================ pub mod api { tonic::include_proto!("google.pubsub.v1"); } use api::{ListTopicsRequest, publisher_client::PublisherClient}; use tonic::{ Request, metadata::MetadataValue, transport::{Certificate, Channel, ClientTlsConfig}, }; const ENDPOINT: &str = "https://pubsub.googleapis.com"; #[tokio::main] async fn main() -> Result<(), Box> { let token = std::env::var("GCP_AUTH_TOKEN").map_err(|_| { "Pass a valid 0Auth bearer token via `GCP_AUTH_TOKEN` environment variable.".to_string() })?; let project = std::env::args() .nth(1) .ok_or_else(|| "Expected a project name as the first argument.".to_string())?; let bearer_token = format!("Bearer {token}"); let header_value: MetadataValue<_> = bearer_token.parse()?; let data_dir = std::path::PathBuf::from_iter([std::env!("CARGO_MANIFEST_DIR"), "data"]); let certs = std::fs::read_to_string(data_dir.join("gcp/roots.pem"))?; let tls_config = ClientTlsConfig::new() .ca_certificate(Certificate::from_pem(certs)) .domain_name("pubsub.googleapis.com"); let channel = Channel::from_static(ENDPOINT) .tls_config(tls_config)? .connect() .await?; let mut service = PublisherClient::with_interceptor(channel, move |mut req: Request<()>| { req.metadata_mut() .insert("authorization", header_value.clone()); Ok(req) }); let response = service .list_topics(Request::new(ListTopicsRequest { project: format!("projects/{project}"), page_size: 10, ..Default::default() })) .await?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/grpc-web/client.rs ================================================ use hello_world::{HelloRequest, greeter_client::GreeterClient}; use hyper_util::rt::TokioExecutor; use tonic_web::GrpcWebClientLayer; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { // Must use hyper directly... let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build_http(); let svc = tower::ServiceBuilder::new() .layer(GrpcWebClientLayer::new()) .service(client); let mut client = GreeterClient::with_origin(svc, "http://127.0.0.1:3000".try_into()?); let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/grpc-web/server.rs ================================================ use tonic::{Request, Response, Status, service::LayerExt as _, transport::Server}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); let addr = "127.0.0.1:3000".parse().unwrap(); let greeter = MyGreeter::default(); let greeter = tower::ServiceBuilder::new() .layer(tower_http::cors::CorsLayer::new()) .layer(tonic_web::GrpcWebLayer::new()) .into_inner() .named_layer(GreeterServer::new(greeter)); println!("GreeterServer listening on {addr}"); Server::builder() // GrpcWeb is over http1 so we must enable it. .accept_http1(true) .add_service(greeter) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/h2c/client.rs ================================================ use hello_world::HelloRequest; use hello_world::greeter_client::GreeterClient; use http::Uri; use hyper_util::client::legacy::Client; use hyper_util::rt::TokioExecutor; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { let origin = Uri::from_static("http://[::1]:50051"); let h2c_client = h2c::H2cChannel { client: Client::builder(TokioExecutor::new()).build_http(), }; let mut client = GreeterClient::with_origin(h2c_client, origin); let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } mod h2c { use std::{ pin::Pin, task::{Context, Poll}, }; use hyper::body::Incoming; use hyper_util::{ client::legacy::{Client, connect::HttpConnector}, rt::TokioExecutor, }; use tonic::body::Body; use tower::Service; pub struct H2cChannel { pub client: Client, } impl Service> for H2cChannel { type Response = http::Response; type Error = hyper::Error; type Future = Pin> + Send>>; fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, request: http::Request) -> Self::Future { let client = self.client.clone(); Box::pin(async move { let origin = request.uri(); let h2c_req = hyper::Request::builder() .uri(origin) .header(http::header::UPGRADE, "h2c") .body(Body::default()) .unwrap(); let res = client.request(h2c_req).await.unwrap(); if res.status() != http::StatusCode::SWITCHING_PROTOCOLS { panic!("Our server didn't upgrade: {}", res.status()); } let upgraded_io = hyper::upgrade::on(res).await.unwrap(); // In an ideal world you would somehow cache this connection let (mut h2_client, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor::new()) .handshake(upgraded_io) .await .unwrap(); tokio::spawn(conn); h2_client.send_request(request).await }) } } } ================================================ FILE: examples/src/h2c/server.rs ================================================ use std::net::SocketAddr; use hyper_util::rt::{TokioExecutor, TokioIo}; use hyper_util::server::conn::auto::Builder; use hyper_util::service::TowerToHyperService; use tokio::net::TcpListener; use tonic::{Request, Response, Status, service::Routes}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr: SocketAddr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); let incoming = TcpListener::bind(addr).await?; let svc = Routes::new(GreeterServer::new(greeter)).prepare(); let h2c = h2c::H2c { s: svc }; loop { match incoming.accept().await { Ok((io, _)) => { let router = h2c.clone(); tokio::spawn(async move { let builder = Builder::new(TokioExecutor::new()); let conn = builder.serve_connection_with_upgrades( TokioIo::new(io), TowerToHyperService::new(router), ); let _ = conn.await; }); } Err(e) => { eprintln!("Error accepting connection: {e}"); } } } } mod h2c { use std::pin::Pin; use http::{Request, Response}; use hyper::body::Incoming; use hyper_util::{rt::TokioExecutor, service::TowerToHyperService}; use tonic::body::Body; use tower::{Service, ServiceExt}; #[derive(Clone)] pub struct H2c { pub s: S, } type BoxError = Box; impl Service> for H2c where S: Service, Response = Response> + Clone + Send + 'static, S::Future: Send, S::Error: Into + 'static, { type Response = hyper::Response; type Error = hyper::Error; type Future = Pin> + Send>>; fn poll_ready( &mut self, _: &mut std::task::Context<'_>, ) -> std::task::Poll> { std::task::Poll::Ready(Ok(())) } fn call(&mut self, req: hyper::Request) -> Self::Future { let mut req = req.map(Body::new); let svc = self .s .clone() .map_request(|req: Request<_>| req.map(Body::new)); Box::pin(async move { tokio::spawn(async move { let upgraded_io = hyper::upgrade::on(&mut req).await.unwrap(); hyper::server::conn::http2::Builder::new(TokioExecutor::new()) .serve_connection(upgraded_io, TowerToHyperService::new(svc)) .await .unwrap(); }); let mut res = hyper::Response::new(Body::default()); *res.status_mut() = http::StatusCode::SWITCHING_PROTOCOLS; res.headers_mut().insert( hyper::header::UPGRADE, http::header::HeaderValue::from_static("h2c"), ); Ok(res) }) } } } ================================================ FILE: examples/src/health/README.md ================================================ # Health checks gRPC has a [health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) that defines how health checks for services should be carried out. Tonic supports this protocol with the optional [tonic health crate](https://docs.rs/tonic-health). This example uses the crate to set up a HealthServer that will run alongside the application service. In order to test it, you may use community tools like [grpc_health_probe](https://github.com/grpc-ecosystem/grpc-health-probe). For example, running the following bash script: ```bash while [ true ]; do ./grpc_health_probe -addr=[::1]:50051 -service=helloworld.Greeter sleep 1 done ``` will show the change in health status of the service over time. ================================================ FILE: examples/src/health/server.rs ================================================ use tonic::{Request, Response, Status, transport::Server}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; use std::time::Duration; use tonic_health::server::HealthReporter; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } /// This function (somewhat improbably) flips the status of a service every second, in order /// that the effect of `tonic_health::HealthReporter::watch` can be easily observed. async fn twiddle_service_status(reporter: HealthReporter) { let mut iter = 0u64; loop { iter += 1; tokio::time::sleep(Duration::from_secs(1)).await; if iter % 2 == 0 { reporter.set_serving::>().await; } else { reporter.set_not_serving::>().await; }; } } #[tokio::main] async fn main() -> Result<(), Box> { let (health_reporter, health_service) = tonic_health::server::health_reporter(); health_reporter .set_serving::>() .await; tokio::spawn(twiddle_service_status(health_reporter.clone())); let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("HealthServer + GreeterServer listening on {addr}"); Server::builder() .add_service(health_service) .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/helloworld/client.rs ================================================ use hello_world::HelloRequest; use hello_world::greeter_client::GreeterClient; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/helloworld/server.rs ================================================ use tonic::{Request, Response, Status, transport::Server}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/interceptor/client.rs ================================================ use hello_world::HelloRequest; use hello_world::greeter_client::GreeterClient; use tonic::{ Request, Status, codegen::InterceptedService, service::Interceptor, transport::{Channel, Endpoint}, }; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { let channel = Endpoint::from_static("http://[::1]:50051") .connect() .await?; let mut client = GreeterClient::with_interceptor(channel, intercept); let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } /// This function will get called on each outbound request. Returning a /// `Status` here will cancel the request and have that status returned to /// the caller. fn intercept(req: Request<()>) -> Result, Status> { println!("Intercepting request: {req:?}"); Ok(req) } // You can also use the `Interceptor` trait to create an interceptor type // that is easy to name struct MyInterceptor; impl Interceptor for MyInterceptor { fn call(&mut self, request: tonic::Request<()>) -> Result, Status> { Ok(request) } } #[allow(dead_code, unused_variables)] async fn using_named_interceptor() -> Result<(), Box> { let channel = Endpoint::from_static("http://[::1]:50051") .connect() .await?; let client: GreeterClient> = GreeterClient::with_interceptor(channel, MyInterceptor); Ok(()) } // Using a function pointer type might also be possible if your interceptor is a // bare function that doesn't capture any variables #[allow(dead_code, unused_variables, clippy::type_complexity)] async fn using_function_pointer_interceptro() -> Result<(), Box> { let channel = Endpoint::from_static("http://[::1]:50051") .connect() .await?; let client: GreeterClient< InterceptedService) -> Result, Status>>, > = GreeterClient::with_interceptor(channel, intercept); Ok(()) } ================================================ FILE: examples/src/interceptor/server.rs ================================================ use tonic::{Request, Response, Status, transport::Server}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { let extension = request.extensions().get::().unwrap(); println!("extension data = {}", extension.some_piece_of_data); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); // See examples/src/interceptor/client.rs for an example of how to create a // named interceptor that can be returned from functions or stored in // structs. let svc = GreeterServer::with_interceptor(greeter, intercept); println!("GreeterServer listening on {addr}"); Server::builder().add_service(svc).serve(addr).await?; Ok(()) } /// This function will get called on each inbound request, if a `Status` /// is returned, it will cancel the request and return that status to the /// client. fn intercept(mut req: Request<()>) -> Result, Status> { println!("Intercepting request: {req:?}"); // Set an extension that can be retrieved by `say_hello` req.extensions_mut().insert(MyExtension { some_piece_of_data: "foo".to_string(), }); Ok(req) } #[derive(Clone)] struct MyExtension { some_piece_of_data: String, } ================================================ FILE: examples/src/json-codec/client.rs ================================================ //! A HelloWorld example that uses JSON instead of protobuf as the message serialization format. //! //! Generated code is the output of codegen as defined in the `build_json_codec_service` function //! in the `examples/build.rs` file. As defined there, the generated code assumes that a module //! `crate::common` exists which defines `HelloRequest`, `HelloResponse`, and `JsonCodec`. pub mod common; use common::HelloRequest; pub mod hello_world { include!(concat!(env!("OUT_DIR"), "/json.helloworld.Greeter.rs")); } use hello_world::greeter_client::GreeterClient; #[tokio::main] async fn main() -> Result<(), Box> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/json-codec/common.rs ================================================ //! This module defines common request/response types as well as the JsonCodec that is used by the //! json.helloworld.Greeter service which is defined manually (instead of via proto files) by the //! `build_json_codec_service` function in the `examples/build.rs` file. use bytes::{Buf, BufMut}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; use tonic::{ Status, codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}, }; #[derive(Debug, Deserialize, Serialize)] pub struct HelloRequest { pub name: String, } #[derive(Debug, Deserialize, Serialize)] pub struct HelloResponse { pub message: String, } #[derive(Debug)] pub struct JsonEncoder(PhantomData); impl Encoder for JsonEncoder { type Item = T; type Error = Status; fn encode(&mut self, item: Self::Item, buf: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { serde_json::to_writer(buf.writer(), &item).map_err(|e| Status::internal(e.to_string())) } } #[derive(Debug)] pub struct JsonDecoder(PhantomData); impl Decoder for JsonDecoder { type Item = U; type Error = Status; fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error> { if !buf.has_remaining() { return Ok(None); } let item: Self::Item = serde_json::from_reader(buf.reader()).map_err(|e| Status::internal(e.to_string()))?; Ok(Some(item)) } } /// A [`Codec`] that implements `application/grpc+json` via the serde library. #[derive(Debug, Clone)] pub struct JsonCodec(PhantomData<(T, U)>); impl Default for JsonCodec { fn default() -> Self { Self(PhantomData) } } impl Codec for JsonCodec where T: serde::Serialize + Send + 'static, U: serde::de::DeserializeOwned + Send + 'static, { type Encode = T; type Decode = U; type Encoder = JsonEncoder; type Decoder = JsonDecoder; fn encoder(&mut self) -> Self::Encoder { JsonEncoder(PhantomData) } fn decoder(&mut self) -> Self::Decoder { JsonDecoder(PhantomData) } } ================================================ FILE: examples/src/json-codec/server.rs ================================================ //! A HelloWorld example that uses JSON instead of protobuf as the message serialization format. //! //! Generated code is the output of codegen as defined in the `build_json_codec_service` function //! in the `examples/build.rs` file. As defined there, the generated code assumes that a module //! `crate::common` exists which defines `HelloRequest`, `HelloResponse`, and `JsonCodec`. use tonic::{Request, Response, Status, transport::Server}; pub mod common; use common::{HelloRequest, HelloResponse}; pub mod hello_world { include!(concat!(env!("OUT_DIR"), "/json.helloworld.Greeter.rs")); } use hello_world::greeter_server::{Greeter, GreeterServer}; #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = HelloResponse { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/load_balance/client.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.unaryecho"); } use pb::{EchoRequest, echo_client::EchoClient}; use tonic::transport::Channel; #[tokio::main] async fn main() -> Result<(), Box> { let endpoints = ["http://[::1]:50051", "http://[::1]:50052"] .iter() .map(|a| Channel::from_static(a)); let channel = Channel::balance_list(endpoints); let mut client = EchoClient::new(channel); for _ in 0..12usize { let request = tonic::Request::new(EchoRequest { message: "hello".into(), }); let response = client.unary_echo(request).await?; println!("RESPONSE={response:?}"); } Ok(()) } ================================================ FILE: examples/src/load_balance/server.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.unaryecho"); } use std::net::SocketAddr; use tokio::sync::mpsc; use tonic::{Request, Response, Status, transport::Server}; use pb::{EchoRequest, EchoResponse}; type EchoResult = Result, Status>; #[derive(Debug)] pub struct EchoServer { addr: SocketAddr, } #[tonic::async_trait] impl pb::echo_server::Echo for EchoServer { async fn unary_echo(&self, request: Request) -> EchoResult { let message = format!("{} (from {})", request.into_inner().message, self.addr); Ok(Response::new(EchoResponse { message })) } } #[tokio::main] async fn main() -> Result<(), Box> { let addrs = ["[::1]:50051", "[::1]:50052"]; let (tx, mut rx) = mpsc::unbounded_channel(); for addr in &addrs { let addr = addr.parse()?; let tx = tx.clone(); let server = EchoServer { addr }; let serve = Server::builder() .add_service(pb::echo_server::EchoServer::new(server)) .serve(addr); tokio::spawn(async move { if let Err(e) = serve.await { eprintln!("Error = {e:?}"); } tx.send(()).unwrap(); }); } rx.recv().await; Ok(()) } ================================================ FILE: examples/src/mock/mock.rs ================================================ use hyper_util::rt::TokioIo; use tonic::{ Request, Response, Status, transport::{Endpoint, Server, Uri}, }; use tower::service_fn; pub mod hello_world { tonic::include_proto!("helloworld"); } use hello_world::{ HelloReply, HelloRequest, greeter_client::GreeterClient, greeter_server::{Greeter, GreeterServer}, }; #[tokio::main] async fn main() -> Result<(), Box> { let (client, server) = tokio::io::duplex(1024); let greeter = MyGreeter::default(); tokio::spawn(async move { Server::builder() .add_service(GreeterServer::new(greeter)) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await }); // Move client to an option so we can _move_ the inner value // on the first attempt to connect. All other attempts will fail. let mut client = Some(client); let channel = Endpoint::try_from("http://[::]:50051")? .connect_with_connector(service_fn(move |_: Uri| { let client = client.take(); async move { if let Some(client) = client { Ok(TokioIo::new(client)) } else { Err(std::io::Error::other("Client already taken")) } } })) .await?; let mut client = GreeterClient::new(channel); let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request: {:?}", request); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } ================================================ FILE: examples/src/multiplex/client.rs ================================================ pub mod hello_world { tonic::include_proto!("helloworld"); } pub mod echo { tonic::include_proto!("grpc.examples.unaryecho"); } use echo::{EchoRequest, echo_client::EchoClient}; use hello_world::{HelloRequest, greeter_client::GreeterClient}; use tonic::transport::Endpoint; #[tokio::main] async fn main() -> Result<(), Box> { let channel = Endpoint::from_static("http://[::1]:50051") .connect() .await?; let mut greeter_client = GreeterClient::new(channel.clone()); let mut echo_client = EchoClient::new(channel); let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = greeter_client.say_hello(request).await?; println!("GREETER RESPONSE={response:?}"); let request = tonic::Request::new(EchoRequest { message: "hello".into(), }); let response = echo_client.unary_echo(request).await?; println!("ECHO RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/multiplex/server.rs ================================================ use tonic::{Request, Response, Status, transport::Server}; pub mod hello_world { tonic::include_proto!("helloworld"); } pub mod echo { tonic::include_proto!("grpc.examples.unaryecho"); } use hello_world::{ HelloReply, HelloRequest, greeter_server::{Greeter, GreeterServer}, }; use echo::{ EchoRequest, EchoResponse, echo_server::{Echo, EchoServer}, }; #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = GreeterServer::new(MyGreeter::default()); let echo = EchoServer::new(MyEcho::default()); Server::builder() .add_service(greeter) .add_service(echo) .serve(addr) .await?; Ok(()) } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[derive(Default)] pub struct MyEcho {} #[tonic::async_trait] impl Echo for MyEcho { async fn unary_echo( &self, request: Request, ) -> Result, Status> { let message = request.into_inner().message; Ok(Response::new(EchoResponse { message })) } } ================================================ FILE: examples/src/reflection/server.rs ================================================ use tonic::transport::Server; use tonic::{Request, Response, Status}; mod proto { tonic::include_proto!("helloworld"); pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("helloworld_descriptor"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl proto::greeter_server::Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = proto::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let service = tonic_reflection::server::Builder::configure() .register_encoded_file_descriptor_set(proto::FILE_DESCRIPTOR_SET) .build_v1() .unwrap(); let addr = "[::1]:50052".parse().unwrap(); let greeter = MyGreeter::default(); Server::builder() .add_service(service) .add_service(proto::greeter_server::GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/richer-error/client.rs ================================================ use tonic_types::StatusExt; use hello_world::HelloRequest; use hello_world::greeter_client::GreeterClient; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { // Valid request // name: "Tonic".into(), // Name cannot be empty name: "".into(), // Name is too long // name: "some excessively long name".into(), }); let response = match client.say_hello(request).await { Ok(response) => response, Err(status) => { println!(" Error status received. Extracting error details...\n"); let err_details = status.get_error_details(); if let Some(bad_request) = err_details.bad_request() { // Handle bad_request details println!(" {bad_request:?}"); } if let Some(help) = err_details.help() { // Handle help details println!(" {help:?}"); } if let Some(localized_message) = err_details.localized_message() { // Handle localized_message details println!(" {localized_message:?}"); } println!(); return Ok(()); } }; println!(" Successful response received.\n\n {response:?}\n"); Ok(()) } ================================================ FILE: examples/src/richer-error/client_vec.rs ================================================ use tonic_types::{ErrorDetail, StatusExt}; use hello_world::HelloRequest; use hello_world::greeter_client::GreeterClient; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { // Valid request // name: "Tonic".into(), // Name cannot be empty name: "".into(), // Name is too long // name: "some excessively long name".into(), }); let response = match client.say_hello(request).await { Ok(response) => response, Err(status) => { println!(" Error status received. Extracting error details...\n"); let err_details = status.get_error_details_vec(); for (i, err_detail) in err_details.iter().enumerate() { println!("err_detail[{i}]"); match err_detail { ErrorDetail::BadRequest(bad_request) => { // Handle bad_request details println!(" {bad_request:?}"); } ErrorDetail::Help(help) => { // Handle help details println!(" {help:?}"); } ErrorDetail::LocalizedMessage(localized_message) => { // Handle localized_message details println!(" {localized_message:?}"); } _ => {} } } println!(); return Ok(()); } }; println!(" Successful response received.\n\n {response:?}\n"); Ok(()) } ================================================ FILE: examples/src/richer-error/server.rs ================================================ use tonic::{Code, Request, Response, Status, transport::Server}; use tonic_types::{ErrorDetails, StatusExt}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); // Extract request data let name = request.into_inner().name; // Create empty ErrorDetails struct let mut err_details = ErrorDetails::new(); // Add error details conditionally if name.is_empty() { err_details.add_bad_request_violation("name", "name cannot be empty"); } else if name.len() > 20 { err_details.add_bad_request_violation("name", "name is too long"); } if err_details.has_bad_request_violations() { // Add additional error details if necessary err_details .add_help_link("description of link", "https://resource.example.local") .set_localized_message("en-US", "message for the user"); // Generate error status let status = Status::with_error_details( Code::InvalidArgument, "request contains invalid arguments", err_details, ); return Err(status); } let reply = hello_world::HelloReply { message: format!("Hello {name}!"), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/richer-error/server_vec.rs ================================================ use tonic::{Code, Request, Response, Status, transport::Server}; use tonic_types::{BadRequest, Help, LocalizedMessage, StatusExt}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); // Extract request data let name = request.into_inner().name; // Create empty BadRequest struct let mut bad_request = BadRequest::new(vec![]); // Add violations conditionally if name.is_empty() { bad_request.add_violation("name", "name cannot be empty"); } else if name.len() > 20 { bad_request.add_violation("name", "name is too long"); } if !bad_request.is_empty() { // Add additional error details if necessary let help = Help::with_link("description of link", "https://resource.example.local"); let localized_message = LocalizedMessage::new("en-US", "message for the user"); // Generate error status let status = Status::with_error_details_vec( Code::InvalidArgument, "request contains invalid arguments", vec![bad_request.into(), help.into(), localized_message.into()], ); return Err(status); } let reply = hello_world::HelloReply { message: format!("Hello {name}!"), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/routeguide/client.rs ================================================ use std::error::Error; use std::time::Duration; use rand::Rng; use rand::rngs::ThreadRng; use tokio::time; use tonic::Request; use tonic::transport::Channel; use routeguide::route_guide_client::RouteGuideClient; use routeguide::{Point, Rectangle, RouteNote}; pub mod routeguide { tonic::include_proto!("routeguide"); } async fn print_features(client: &mut RouteGuideClient) -> Result<(), Box> { let rectangle = Rectangle { lo: Some(Point { latitude: 400_000_000, longitude: -750_000_000, }), hi: Some(Point { latitude: 420_000_000, longitude: -730_000_000, }), }; let mut stream = client .list_features(Request::new(rectangle)) .await? .into_inner(); while let Some(feature) = stream.message().await? { println!("FEATURE = {feature:?}"); } Ok(()) } async fn run_record_route(client: &mut RouteGuideClient) -> Result<(), Box> { let mut rng = rand::rng(); let point_count: i32 = rng.random_range(2..100); let mut points = vec![]; for _ in 0..=point_count { points.push(random_point(&mut rng)) } println!("Traversing {} points", points.len()); let request = Request::new(tokio_stream::iter(points)); match client.record_route(request).await { Ok(response) => println!("SUMMARY: {:?}", response.into_inner()), Err(e) => println!("something went wrong: {e:?}"), } Ok(()) } async fn run_route_chat(client: &mut RouteGuideClient) -> Result<(), Box> { let start = time::Instant::now(); let outbound = async_stream::stream! { let mut interval = time::interval(Duration::from_secs(1)); loop { let time = interval.tick().await; let elapsed = time.duration_since(start); let note = RouteNote { location: Some(Point { latitude: 409146138 + elapsed.as_secs() as i32, longitude: -746188906, }), message: format!("at {elapsed:?}"), }; yield note; } }; let response = client.route_chat(Request::new(outbound)).await?; let mut inbound = response.into_inner(); while let Some(note) = inbound.message().await? { println!("NOTE = {note:?}"); } Ok(()) } #[tokio::main] async fn main() -> Result<(), Box> { let mut client = RouteGuideClient::connect("http://[::1]:10000").await?; println!("*** SIMPLE RPC ***"); let response = client .get_feature(Request::new(Point { latitude: 409_146_138, longitude: -746_188_906, })) .await?; println!("RESPONSE = {response:?}"); println!("\n*** SERVER STREAMING ***"); print_features(&mut client).await?; println!("\n*** CLIENT STREAMING ***"); run_record_route(&mut client).await?; println!("\n*** BIDIRECTIONAL STREAMING ***"); run_route_chat(&mut client).await?; Ok(()) } fn random_point(rng: &mut ThreadRng) -> Point { let latitude = (rng.random_range(0..180) - 90) * 10_000_000; let longitude = (rng.random_range(0..360) - 180) * 10_000_000; Point { latitude, longitude, } } ================================================ FILE: examples/src/routeguide/data.rs ================================================ use serde::Deserialize; use std::fs::File; #[derive(Debug, Deserialize)] struct Feature { location: Location, name: String, } #[derive(Debug, Deserialize)] struct Location { latitude: i32, longitude: i32, } #[allow(dead_code)] pub fn load() -> Vec { let data_dir = std::path::PathBuf::from_iter([std::env!("CARGO_MANIFEST_DIR"), "data"]); let file = File::open(data_dir.join("route_guide_db.json")).expect("failed to open data file"); let decoded: Vec = serde_json::from_reader(&file).expect("failed to deserialize features"); decoded .into_iter() .map(|feature| crate::routeguide::Feature { name: feature.name, location: Some(crate::routeguide::Point { longitude: feature.location.longitude, latitude: feature.location.latitude, }), }) .collect() } ================================================ FILE: examples/src/routeguide/server.rs ================================================ use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; use std::time::Instant; use tokio::sync::mpsc; use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream}; use tonic::transport::Server; use tonic::{Request, Response, Status}; use routeguide::route_guide_server::{RouteGuide, RouteGuideServer}; use routeguide::{Feature, Point, Rectangle, RouteNote, RouteSummary}; pub mod routeguide { tonic::include_proto!("routeguide"); } mod data; #[derive(Debug)] pub struct RouteGuideService { features: Arc>, } #[tonic::async_trait] impl RouteGuide for RouteGuideService { async fn get_feature(&self, request: Request) -> Result, Status> { println!("GetFeature = {:?}", request); for feature in &self.features[..] { if feature.location.as_ref() == Some(request.get_ref()) { return Ok(Response::new(feature.clone())); } } Ok(Response::new(Feature::default())) } type ListFeaturesStream = ReceiverStream>; async fn list_features( &self, request: Request, ) -> Result, Status> { println!("ListFeatures = {:?}", request); let (tx, rx) = mpsc::channel(4); let features = self.features.clone(); tokio::spawn(async move { for feature in &features[..] { if in_range(feature.location.as_ref().unwrap(), request.get_ref()) { println!(" => send {feature:?}"); tx.send(Ok(feature.clone())).await.unwrap(); } } println!(" /// done sending"); }); Ok(Response::new(ReceiverStream::new(rx))) } async fn record_route( &self, request: Request>, ) -> Result, Status> { println!("RecordRoute"); let mut stream = request.into_inner(); let mut summary = RouteSummary::default(); let mut last_point = None; let now = Instant::now(); while let Some(point) = stream.next().await { let point = point?; println!(" ==> Point = {point:?}"); // Increment the point count summary.point_count += 1; // Find features for feature in &self.features[..] { if feature.location.as_ref() == Some(&point) { summary.feature_count += 1; } } // Calculate the distance if let Some(ref last_point) = last_point { summary.distance += calc_distance(last_point, &point); } last_point = Some(point); } summary.elapsed_time = now.elapsed().as_secs() as i32; Ok(Response::new(summary)) } type RouteChatStream = Pin> + Send + 'static>>; async fn route_chat( &self, request: Request>, ) -> Result, Status> { println!("RouteChat"); let mut notes = HashMap::new(); let mut stream = request.into_inner(); let output = async_stream::try_stream! { while let Some(note) = stream.next().await { let note = note?; let location = note.location.unwrap(); let location_notes = notes.entry(location).or_insert(vec![]); location_notes.push(note); for note in location_notes { yield note.clone(); } } }; Ok(Response::new(Box::pin(output) as Self::RouteChatStream)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:10000".parse().unwrap(); println!("RouteGuideServer listening on: {addr}"); let route_guide = RouteGuideService { features: Arc::new(data::load()), }; let svc = RouteGuideServer::new(route_guide); Server::builder().add_service(svc).serve(addr).await?; Ok(()) } fn in_range(point: &Point, rect: &Rectangle) -> bool { use std::cmp; let lo = rect.lo.as_ref().unwrap(); let hi = rect.hi.as_ref().unwrap(); let left = cmp::min(lo.longitude, hi.longitude); let right = cmp::max(lo.longitude, hi.longitude); let top = cmp::max(lo.latitude, hi.latitude); let bottom = cmp::min(lo.latitude, hi.latitude); point.longitude >= left && point.longitude <= right && point.latitude >= bottom && point.latitude <= top } /// Calculates the distance between two points using the "haversine" formula. /// This code was taken from http://www.movable-type.co.uk/scripts/latlong.html. fn calc_distance(p1: &Point, p2: &Point) -> i32 { const CORD_FACTOR: f64 = 1e7; const R: f64 = 6_371_000.0; // meters let lat1 = p1.latitude as f64 / CORD_FACTOR; let lat2 = p2.latitude as f64 / CORD_FACTOR; let lng1 = p1.longitude as f64 / CORD_FACTOR; let lng2 = p2.longitude as f64 / CORD_FACTOR; let lat_rad1 = lat1.to_radians(); let lat_rad2 = lat2.to_radians(); let delta_lat = (lat2 - lat1).to_radians(); let delta_lng = (lng2 - lng1).to_radians(); let a = (delta_lat / 2f64).sin() * (delta_lat / 2f64).sin() + (lat_rad1).cos() * (lat_rad2).cos() * (delta_lng / 2f64).sin() * (delta_lng / 2f64).sin(); let c = 2f64 * a.sqrt().atan2((1f64 - a).sqrt()); (R * c) as i32 } ================================================ FILE: examples/src/streaming/client.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.echo"); } use std::time::Duration; use tokio_stream::{Stream, StreamExt}; use tonic::transport::Channel; use pb::{EchoRequest, echo_client::EchoClient}; fn echo_requests_iter() -> impl Stream { tokio_stream::iter(1..usize::MAX).map(|i| EchoRequest { message: format!("msg {i:02}"), }) } async fn streaming_echo(client: &mut EchoClient, num: usize) { let stream = client .server_streaming_echo(EchoRequest { message: "foo".into(), }) .await .unwrap() .into_inner(); // stream is infinite - take just 5 elements and then disconnect let mut stream = stream.take(num); while let Some(item) = stream.next().await { println!("\treceived: {}", item.unwrap().message); } // stream is dropped here and the disconnect info is sent to server } async fn bidirectional_streaming_echo(client: &mut EchoClient, num: usize) { let in_stream = echo_requests_iter().take(num); let response = client .bidirectional_streaming_echo(in_stream) .await .unwrap(); let mut resp_stream = response.into_inner(); while let Some(received) = resp_stream.next().await { let received = received.unwrap(); println!("\treceived message: `{}`", received.message); } } async fn bidirectional_streaming_echo_throttle(client: &mut EchoClient, dur: Duration) { let in_stream = echo_requests_iter().throttle(dur); let response = client .bidirectional_streaming_echo(in_stream) .await .unwrap(); let mut resp_stream = response.into_inner(); while let Some(received) = resp_stream.next().await { let received = received.unwrap(); println!("\treceived message: `{}`", received.message); } } #[tokio::main] async fn main() -> Result<(), Box> { let mut client = EchoClient::connect("http://[::1]:50051").await.unwrap(); println!("Streaming echo:"); streaming_echo(&mut client, 5).await; tokio::time::sleep(Duration::from_secs(1)).await; //do not mess server println functions // Echo stream that sends 17 requests then graceful end that connection println!("\r\nBidirectional stream echo:"); bidirectional_streaming_echo(&mut client, 17).await; // Echo stream that sends up to `usize::MAX` requests. One request each 2s. // Exiting client with CTRL+C demonstrate how to distinguish broken pipe from // graceful client disconnection (above example) on the server side. println!("\r\nBidirectional stream echo (kill client with CTLR+C):"); bidirectional_streaming_echo_throttle(&mut client, Duration::from_secs(2)).await; Ok(()) } ================================================ FILE: examples/src/streaming/server.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.echo"); } use std::{error::Error, io::ErrorKind, net::ToSocketAddrs, pin::Pin, time::Duration}; use tokio::sync::mpsc; use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream}; use tonic::{Request, Response, Status, Streaming, transport::Server}; use pb::{EchoRequest, EchoResponse}; type EchoResult = Result, Status>; type ResponseStream = Pin> + Send>>; fn match_for_io_error(err_status: &Status) -> Option<&std::io::Error> { let mut err: &(dyn Error + 'static) = err_status; loop { if let Some(io_err) = err.downcast_ref::() { return Some(io_err); } // h2::Error do not expose std::io::Error with `source()` // https://github.com/hyperium/h2/pull/462 if let Some(h2_err) = err.downcast_ref::() && let Some(io_err) = h2_err.get_io() { return Some(io_err); } err = err.source()?; } } #[derive(Debug)] pub struct EchoServer {} #[tonic::async_trait] impl pb::echo_server::Echo for EchoServer { async fn unary_echo(&self, _: Request) -> EchoResult { Err(Status::unimplemented("not implemented")) } type ServerStreamingEchoStream = ResponseStream; async fn server_streaming_echo( &self, req: Request, ) -> EchoResult { println!("EchoServer::server_streaming_echo"); println!("\tclient connected from: {:?}", req.remote_addr()); // creating infinite stream with requested message let repeat = std::iter::repeat(EchoResponse { message: req.into_inner().message, }); let mut stream = Box::pin(tokio_stream::iter(repeat).throttle(Duration::from_millis(200))); // spawn and channel are required if you want handle "disconnect" functionality // the `out_stream` will not be polled after client disconnect let (tx, rx) = mpsc::channel(128); tokio::spawn(async move { while let Some(item) = stream.next().await { match tx.send(Result::<_, Status>::Ok(item)).await { Ok(_) => { // item (server response) was queued to be send to client } Err(_item) => { // output_stream was build from rx and both are dropped break; } } } println!("\tclient disconnected"); }); let output_stream = ReceiverStream::new(rx); Ok(Response::new( Box::pin(output_stream) as Self::ServerStreamingEchoStream )) } async fn client_streaming_echo( &self, _: Request>, ) -> EchoResult { Err(Status::unimplemented("not implemented")) } type BidirectionalStreamingEchoStream = ResponseStream; async fn bidirectional_streaming_echo( &self, req: Request>, ) -> EchoResult { println!("EchoServer::bidirectional_streaming_echo"); let mut in_stream = req.into_inner(); let (tx, rx) = mpsc::channel(128); // this spawn here is required if you want to handle connection error. // If we just map `in_stream` and write it back as `out_stream` the `out_stream` // will be dropped when connection error occurs and error will never be propagated // to mapped version of `in_stream`. tokio::spawn(async move { while let Some(result) = in_stream.next().await { match result { Ok(v) => tx .send(Ok(EchoResponse { message: v.message })) .await .expect("working rx"), Err(err) => { if let Some(io_err) = match_for_io_error(&err) && io_err.kind() == ErrorKind::BrokenPipe { // here you can handle special case when client // disconnected in unexpected way eprintln!("\tclient disconnected: broken pipe"); break; } match tx.send(Err(err)).await { Ok(_) => (), Err(_err) => break, // response was dropped } } } } println!("\tstream ended"); }); // echo just write the same data that was received let out_stream = ReceiverStream::new(rx); Ok(Response::new( Box::pin(out_stream) as Self::BidirectionalStreamingEchoStream )) } } #[tokio::main] async fn main() -> Result<(), Box> { let server = EchoServer {}; Server::builder() .add_service(pb::echo_server::EchoServer::new(server)) .serve("[::1]:50051".to_socket_addrs().unwrap().next().unwrap()) .await .unwrap(); Ok(()) } ================================================ FILE: examples/src/tls/client.rs ================================================ pub mod pb { tonic::include_proto!("/grpc.examples.unaryecho"); } use pb::{EchoRequest, echo_client::EchoClient}; use tonic::transport::{Certificate, Channel, ClientTlsConfig}; #[tokio::main] async fn main() -> Result<(), Box> { let data_dir = std::path::PathBuf::from_iter([std::env!("CARGO_MANIFEST_DIR"), "data"]); let pem = std::fs::read_to_string(data_dir.join("tls/ca.pem"))?; let ca = Certificate::from_pem(pem); let tls = ClientTlsConfig::new() .ca_certificate(ca) .domain_name("example.com"); let channel = Channel::from_static("https://[::1]:50051") .tls_config(tls)? .connect() .await?; let mut client = EchoClient::new(channel); let request = tonic::Request::new(EchoRequest { message: "hello".into(), }); let response = client.unary_echo(request).await?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/tls/server.rs ================================================ pub mod pb { tonic::include_proto!("/grpc.examples.unaryecho"); } use pb::{EchoRequest, EchoResponse}; use tonic::{ Request, Response, Status, transport::{ Identity, Server, ServerTlsConfig, server::{TcpConnectInfo, TlsConnectInfo}, }, }; type EchoResult = Result, Status>; #[derive(Default)] pub struct EchoServer {} #[tonic::async_trait] impl pb::echo_server::Echo for EchoServer { async fn unary_echo(&self, request: Request) -> EchoResult { let conn_info = request .extensions() .get::>() .unwrap(); println!( "Got a request from {:?} with info {:?}", request.remote_addr(), conn_info ); let message = request.into_inner().message; Ok(Response::new(EchoResponse { message })) } } #[tokio::main] async fn main() -> Result<(), Box> { let data_dir = std::path::PathBuf::from_iter([std::env!("CARGO_MANIFEST_DIR"), "data"]); let cert = std::fs::read_to_string(data_dir.join("tls/server.pem"))?; let key = std::fs::read_to_string(data_dir.join("tls/server.key"))?; let identity = Identity::from_pem(cert, key); let addr = "[::1]:50051".parse().unwrap(); let server = EchoServer::default(); Server::builder() .tls_config(ServerTlsConfig::new().identity(identity))? .add_service(pb::echo_server::EchoServer::new(server)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/tls_client_auth/client.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.unaryecho"); } use pb::{EchoRequest, echo_client::EchoClient}; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; #[tokio::main] async fn main() -> Result<(), Box> { let data_dir = std::path::PathBuf::from_iter([std::env!("CARGO_MANIFEST_DIR"), "data"]); let server_root_ca_cert = std::fs::read_to_string(data_dir.join("tls/ca.pem"))?; let server_root_ca_cert = Certificate::from_pem(server_root_ca_cert); let client_cert = std::fs::read_to_string(data_dir.join("tls/client1.pem"))?; let client_key = std::fs::read_to_string(data_dir.join("tls/client1.key"))?; let client_identity = Identity::from_pem(client_cert, client_key); let tls = ClientTlsConfig::new() .domain_name("localhost") .ca_certificate(server_root_ca_cert) .identity(client_identity); let channel = Channel::from_static("https://[::1]:50051") .tls_config(tls)? .connect() .await?; let mut client = EchoClient::new(channel); let request = tonic::Request::new(EchoRequest { message: "hello".into(), }); let response = client.unary_echo(request).await?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/tls_client_auth/server.rs ================================================ pub mod pb { tonic::include_proto!("grpc.examples.unaryecho"); } use pb::{EchoRequest, EchoResponse}; use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig}; use tonic::{Request, Response, Status}; type EchoResult = Result, Status>; #[derive(Default)] pub struct EchoServer {} #[tonic::async_trait] impl pb::echo_server::Echo for EchoServer { async fn unary_echo(&self, request: Request) -> EchoResult { let certs = request .peer_certs() .expect("Client did not send its certs!"); println!("Got {} peer certs!", certs.len()); let message = request.into_inner().message; Ok(Response::new(EchoResponse { message })) } } #[tokio::main] async fn main() -> Result<(), Box> { let data_dir = std::path::PathBuf::from_iter([std::env!("CARGO_MANIFEST_DIR"), "data"]); let cert = std::fs::read_to_string(data_dir.join("tls/server.pem"))?; let key = std::fs::read_to_string(data_dir.join("tls/server.key"))?; let server_identity = Identity::from_pem(cert, key); let client_ca_cert = std::fs::read_to_string(data_dir.join("tls/client_ca.pem"))?; let client_ca_cert = Certificate::from_pem(client_ca_cert); let addr = "[::1]:50051".parse().unwrap(); let server = EchoServer::default(); let tls = ServerTlsConfig::new() .identity(server_identity) .client_ca_root(client_ca_cert); Server::builder() .tls_config(tls)? .add_service(pb::echo_server::EchoServer::new(server)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/tls_rustls/client.rs ================================================ //! This examples shows how you can combine `hyper-rustls` and `tonic` to //! provide a custom `ClientConfig` for the tls configuration. pub mod pb { tonic::include_proto!("/grpc.examples.unaryecho"); } use hyper::Uri; use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; use pb::{EchoRequest, echo_client::EchoClient}; use tokio_rustls::rustls::{ pki_types::{CertificateDer, pem::PemObject as _}, {ClientConfig, RootCertStore}, }; #[tokio::main] async fn main() -> Result<(), Box> { let data_dir = std::path::PathBuf::from_iter([std::env!("CARGO_MANIFEST_DIR"), "data"]); let fd = std::fs::File::open(data_dir.join("tls/ca.pem"))?; let mut roots = RootCertStore::empty(); let mut buf = std::io::BufReader::new(&fd); let certs = CertificateDer::pem_reader_iter(&mut buf).collect::, _>>()?; roots.add_parsable_certificates(certs.into_iter()); let tls = ClientConfig::builder() .with_root_certificates(roots) .with_no_client_auth(); let mut http = HttpConnector::new(); http.enforce_http(false); // We have to do some wrapping here to map the request type from // `https://example.com` -> `https://[::1]:50051` because `rustls` // doesn't accept ip's as `ServerName`. let connector = tower::ServiceBuilder::new() .layer_fn(move |s| { let tls = tls.clone(); hyper_rustls::HttpsConnectorBuilder::new() .with_tls_config(tls) .https_or_http() .enable_http2() .wrap_connector(s) }) // Since our cert is signed with `example.com` but we actually want to connect // to a local server we will override the Uri passed from the `HttpsConnector` // and map it to the correct `Uri` that will connect us directly to the local server. .map_request(|_| Uri::from_static("https://[::1]:50051")) .service(http); let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(connector); // Using `with_origin` will let the codegenerated client set the `scheme` and // `authority` from the provided `Uri`. let uri = Uri::from_static("https://example.com"); let mut client = EchoClient::with_origin(client, uri); let request = tonic::Request::new(EchoRequest { message: "hello".into(), }); let response = client.unary_echo(request).await?; println!("RESPONSE={response:?}"); Ok(()) } ================================================ FILE: examples/src/tls_rustls/server.rs ================================================ pub mod pb { tonic::include_proto!("/grpc.examples.unaryecho"); } use hyper::server::conn::http2::Builder; use hyper_util::{ rt::{TokioExecutor, TokioIo}, service::TowerToHyperService, }; use pb::{EchoRequest, EchoResponse}; use std::sync::Arc; use tokio::net::TcpListener; use tokio_rustls::{ TlsAcceptor, rustls::{ ServerConfig, pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject as _}, }, }; use tonic::{Request, Response, Status, body::Body, service::Routes}; use tower::ServiceExt; use tower_http::ServiceBuilderExt; #[tokio::main] async fn main() -> Result<(), Box> { let data_dir = std::path::PathBuf::from_iter([std::env!("CARGO_MANIFEST_DIR"), "data"]); let certs = { let fd = std::fs::File::open(data_dir.join("tls/server.pem"))?; let mut buf = std::io::BufReader::new(&fd); CertificateDer::pem_reader_iter(&mut buf).collect::, _>>()? }; let key = { let fd = std::fs::File::open(data_dir.join("tls/server.key"))?; let mut buf = std::io::BufReader::new(&fd); PrivateKeyDer::from_pem_reader(&mut buf)? }; let mut tls = ServerConfig::builder() .with_no_client_auth() .with_single_cert(certs, key)?; tls.alpn_protocols = vec![b"h2".to_vec()]; let server = EchoServer::default(); let svc = Routes::new(pb::echo_server::EchoServer::new(server)).prepare(); let http = Builder::new(TokioExecutor::new()); let listener = TcpListener::bind("[::1]:50051").await?; let tls_acceptor = TlsAcceptor::from(Arc::new(tls)); loop { let (conn, addr) = match listener.accept().await { Ok(incoming) => incoming, Err(e) => { eprintln!("Error accepting connection: {e}"); continue; } }; let http = http.clone(); let tls_acceptor = tls_acceptor.clone(); let svc = svc.clone(); tokio::spawn(async move { let mut certificates = Vec::new(); let conn = tls_acceptor .accept_with(conn, |info| { if let Some(certs) = info.peer_certificates() { for cert in certs { certificates.push(cert.clone()); } } }) .await .unwrap(); let svc = tower::ServiceBuilder::new() .add_extension(Arc::new(ConnInfo { addr, certificates })) .service(svc); http.serve_connection( TokioIo::new(conn), TowerToHyperService::new( svc.map_request(|req: http::Request<_>| req.map(Body::new)), ), ) .await .unwrap(); }); } } #[derive(Debug)] struct ConnInfo { addr: std::net::SocketAddr, certificates: Vec>, } type EchoResult = Result, Status>; #[derive(Default)] pub struct EchoServer {} #[tonic::async_trait] impl pb::echo_server::Echo for EchoServer { async fn unary_echo(&self, request: Request) -> EchoResult { let conn_info = request.extensions().get::>().unwrap(); println!( "Got a request from: {:?} with certs: {:?}", conn_info.addr, conn_info.certificates ); let message = request.into_inner().message; Ok(Response::new(EchoResponse { message })) } } ================================================ FILE: examples/src/tower/client.rs ================================================ use hello_world::HelloRequest; use hello_world::greeter_client::GreeterClient; use service::AuthSvc; use tower::ServiceBuilder; use tonic::{Request, Status, transport::Channel}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[tokio::main] async fn main() -> Result<(), Box> { let channel = Channel::from_static("http://[::1]:50051").connect().await?; let channel = ServiceBuilder::new() // Interceptors can be also be applied as middleware .layer(tonic::service::InterceptorLayer::new(intercept)) .layer_fn(AuthSvc::new) .service(channel); let mut client = GreeterClient::new(channel); let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } // An interceptor function. fn intercept(req: Request<()>) -> Result, Status> { println!("received {req:?}"); Ok(req) } mod service { use http::{Request, Response}; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tonic::body::Body; use tonic::transport::Channel; use tower::Service; pub struct AuthSvc { inner: Channel, } impl AuthSvc { pub fn new(inner: Channel) -> Self { AuthSvc { inner } } } impl Service> for AuthSvc { type Response = Response; type Error = Box; #[allow(clippy::type_complexity)] type Future = Pin> + Send>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, req: Request) -> Self::Future { // See: https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services let clone = self.inner.clone(); let mut inner = std::mem::replace(&mut self.inner, clone); Box::pin(async move { // Do extra async work here... let response = inner.call(req).await?; Ok(response) }) } } } ================================================ FILE: examples/src/tower/server.rs ================================================ use std::{ pin::Pin, task::{Context, Poll}, }; use tonic::{Request, Response, Status, transport::Server}; use tower::{Layer, Service}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {addr}"); let svc = GreeterServer::new(greeter); // The stack of middleware that our service will be wrapped in let layer = tower::ServiceBuilder::new() // Apply our own middleware .layer(MyMiddlewareLayer::default()) // Interceptors can be also be applied as middleware .layer(tonic::service::InterceptorLayer::new(intercept)) .into_inner(); Server::builder() // Wrap all services in the middleware stack .layer(layer) .add_service(svc) .serve(addr) .await?; Ok(()) } // An interceptor function. fn intercept(req: Request<()>) -> Result, Status> { Ok(req) } #[derive(Debug, Clone, Default)] struct MyMiddlewareLayer {} impl Layer for MyMiddlewareLayer { type Service = MyMiddleware; fn layer(&self, service: S) -> Self::Service { MyMiddleware { inner: service } } } #[derive(Debug, Clone)] struct MyMiddleware { inner: S, } type BoxFuture<'a, T> = Pin + Send + 'a>>; impl Service> for MyMiddleware where S: Service, Response = http::Response> + Clone + Send + 'static, S::Future: Send + 'static, ReqBody: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, req: http::Request) -> Self::Future { // See: https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services let clone = self.inner.clone(); let mut inner = std::mem::replace(&mut self.inner, clone); Box::pin(async move { // Do extra async work here... let response = inner.call(req).await?; Ok(response) }) } } ================================================ FILE: examples/src/tracing/client.rs ================================================ pub mod hello_world { tonic::include_proto!("helloworld"); } use hello_world::{HelloRequest, greeter_client::GreeterClient}; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::FmtSubscriber::builder() .with_max_level(tracing::Level::DEBUG) .init(); say_hi("Bob".into()).await?; Ok(()) } #[tracing::instrument] async fn say_hi(name: String) -> Result<(), Box> { let mut client = GreeterClient::connect("http://[::1]:50051").await?; let request = tonic::Request::new(HelloRequest { name }); tracing::info!( message = "Sending request.", request = %request.get_ref().name ); let response = client.say_hello(request).await?; tracing::info!( message = "Got a response.", response = %response.get_ref().message ); Ok(()) } ================================================ FILE: examples/src/tracing/server.rs ================================================ use tonic::{Request, Response, Status, transport::Server}; pub mod hello_world { tonic::include_proto!("helloworld"); } use hello_world::{ HelloReply, HelloRequest, greeter_server::{Greeter, GreeterServer}, }; #[derive(Debug, Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { #[tracing::instrument] async fn say_hello( &self, request: Request, ) -> Result, Status> { tracing::info!("received request"); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; tracing::debug!("sending response"); Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .init(); let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); tracing::info!(message = "Starting server.", %addr); Server::builder() .trace_fn(|_| tracing::info_span!("helloworld_server")) .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } ================================================ FILE: examples/src/uds/client_standard.rs ================================================ #![cfg_attr(not(unix), allow(unused_imports))] pub mod hello_world { tonic::include_proto!("helloworld"); } use hello_world::{HelloRequest, greeter_client::GreeterClient}; #[cfg(unix)] #[tokio::main] async fn main() -> Result<(), Box> { // Unix socket URI follows [RFC-3986](https://datatracker.ietf.org/doc/html/rfc3986) // which is aligned with [the gRPC naming convention](https://github.com/grpc/grpc/blob/master/doc/naming.md). // - unix:relative_path // - unix:///absolute_path let path = "unix:///tmp/tonic/helloworld"; let mut client = GreeterClient::connect(path).await?; let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } #[cfg(not(unix))] fn main() { panic!("The `uds` example only works on unix systems!"); } ================================================ FILE: examples/src/uds/client_with_connector.rs ================================================ #![cfg_attr(not(unix), allow(unused_imports))] pub mod hello_world { tonic::include_proto!("helloworld"); } use hello_world::{HelloRequest, greeter_client::GreeterClient}; use hyper_util::rt::TokioIo; #[cfg(unix)] use tokio::net::UnixStream; use tonic::transport::{Endpoint, Uri}; use tower::service_fn; #[cfg(unix)] #[tokio::main] async fn main() -> Result<(), Box> { // We will ignore this uri because uds do not use it // if your connector does use the uri it will be provided // as the request to the `MakeConnection`. let channel = Endpoint::try_from("http://[::]:50051")? .connect_with_connector(service_fn(|_: Uri| async { let path = "/tmp/tonic/helloworld"; // Connect to a Uds socket Ok::<_, std::io::Error>(TokioIo::new(UnixStream::connect(path).await?)) })) .await?; let mut client = GreeterClient::new(channel); let request = tonic::Request::new(HelloRequest { name: "Tonic".into(), }); let response = client.say_hello(request).await?; println!("RESPONSE={response:?}"); Ok(()) } #[cfg(not(unix))] fn main() { panic!("The `uds` example only works on unix systems!"); } ================================================ FILE: examples/src/uds/server.rs ================================================ #![cfg_attr(not(unix), allow(unused_imports))] use std::path::Path; #[cfg(unix)] use tokio::net::UnixListener; #[cfg(unix)] use tokio_stream::wrappers::UnixListenerStream; #[cfg(unix)] use tonic::transport::server::UdsConnectInfo; use tonic::{Request, Response, Status, transport::Server}; pub mod hello_world { tonic::include_proto!("helloworld"); } use hello_world::{ HelloReply, HelloRequest, greeter_server::{Greeter, GreeterServer}, }; #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request, ) -> Result, Status> { #[cfg(unix)] { let conn_info = request.extensions().get::().unwrap(); println!("Got a request {request:?} with info {conn_info:?}"); } let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[cfg(unix)] #[tokio::main] async fn main() -> Result<(), Box> { let path = "/tmp/tonic/helloworld"; std::fs::create_dir_all(Path::new(path).parent().unwrap())?; let greeter = MyGreeter::default(); let uds = UnixListener::bind(path)?; let uds_stream = UnixListenerStream::new(uds); Server::builder() .add_service(GreeterServer::new(greeter)) .serve_with_incoming(uds_stream) .await?; Ok(()) } #[cfg(not(unix))] fn main() { panic!("The `uds` example only works on unix systems!"); } ================================================ FILE: flake.nix ================================================ { description = "Description for the project"; inputs = { flake-parts.url = "github:hercules-ci/flake-parts"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; fenix = { url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; git-hooks = { url = "github:cachix/git-hooks.nix"; inputs = { nixpkgs.follows = "nixpkgs"; }; }; }; outputs = inputs@{ self, flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { imports = [ inputs.git-hooks.flakeModule ]; systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; perSystem = { config, pkgs, system, ... }: let rustToolchain = pkgs.fenix.stable; in { _module.args.pkgs = import inputs.nixpkgs { inherit system; overlays = [ inputs.fenix.overlays.default ]; config = { }; }; formatter = config.treefmt.build.wrapper; checks.formatting = config.treefmt.build.check self; pre-commit = { check.enable = true; settings.hooks = { actionlint.enable = true; shellcheck.enable = true; clippy = { enable = true; packageOverrides = { cargo = rustToolchain.cargo; clippy = rustToolchain.clippy; }; }; cargo-check = { enable = true; package = rustToolchain.cargo; entry = "${rustToolchain.cargo}/bin/cargo check --workspace --all-features"; files = "\\.rs$"; pass_filenames = false; }; rustfmt = { enable = true; packageOverrides = { rustfmt = rustToolchain.rustfmt; cargo = rustToolchain.cargo; }; }; }; }; devShells.default = pkgs.mkShell { packages = with pkgs; [ cargo-nextest pre-commit cmake (rustToolchain.withComponents [ "cargo" "clippy" "rust-src" "rustc" "rustfmt" "rust-analyzer" ]) ]; hardeningDisable = [ "fortify" ]; shellHook = '' ${config.pre-commit.installationScript} ''; }; # App for running the full CI udeps check # `nix run .#udeps-ci` apps.udeps-ci = { type = "app"; program = "${pkgs.writeShellScript "udeps-ci" '' set -e export PATH="${pkgs.rustup}/bin:${pkgs.cargo-udeps}/bin:${pkgs.cargo-hack}/bin:$PATH" # Ensure nightly toolchain is installed if ! rustup toolchain list | grep -q "nightly-2025-03-27"; then echo "Installing nightly-2025-03-27 toolchain..." rustup toolchain install nightly-2025-03-27 fi # Set the toolchain for this run export RUSTUP_TOOLCHAIN=nightly-2025-03-27 echo "Running cargo hack udeps..." cargo hack udeps --workspace --exclude-features=_tls-any,tls,tls-aws-lc,tls-ring,tls-connect-info --each-feature echo "Running tonic TLS feature checks..." cargo udeps --package tonic --features tls-ring,transport cargo udeps --package tonic --features tls-ring,server cargo udeps --package tonic --features tls-ring,channel cargo udeps --package tonic --features tls-aws-lc,transport cargo udeps --package tonic --features tls-aws-lc,server cargo udeps --package tonic --features tls-aws-lc,channel cargo udeps --package tonic --features tls-connect-info echo "✓ All udeps checks passed!" ''}"; }; }; }; } ================================================ FILE: grpc/Cargo.toml ================================================ [package] name = "grpc" version = "0.9.0-alpha.1" edition = "2024" authors = ["gRPC Authors"] license = "MIT" rust-version = { workspace = true } [package.metadata.cargo_check_external_types] allowed_external_types = [ "bytes::*", "tonic::*", "futures_core::stream::Stream", "tokio::sync::oneshot::Sender", ] [features] default = ["dns", "_runtime-tokio", "tls-rustls"] dns = ["dep:hickory-resolver", "_runtime-tokio"] # The following feature is used to ensure all modules use the runtime # abstraction instead of using tokio directly. # Using tower/buffer enables tokio's rt feature even though it's possible to # create Buffers with a user provided executor. _runtime-tokio = [ "tokio/rt", "tokio/net", "tokio/time", "dep:socket2", "dep:tower", ] # Used for testing with udeps as it wants this feature to exist # to be able to do its checks. tower = ["_runtime-tokio"] tls-rustls = [ "dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types", "dep:tokio-rustls", "dep:rustls-platform-verifier", "dep:rustls-webpki", ] [dependencies] bytes = "1.10.1" hickory-resolver = { version = "0.25.1", optional = true } http = "1.1.0" http-body = "1.0.1" hyper = { version = "1.6.0", features = ["client", "http2"] } parking_lot = "0.12.4" pin-project-lite = "0.2.16" rand = "0.9" rustls = { version = "0.23", optional = true, default-features = false, features = [ "tls12", "logging", "std", ] } rustls-pemfile = { version = "2.1", optional = true, default-features = false, features = [ "std", ] } rustls-pki-types = { version = "1.8", optional = true, default-features = false } rustls-platform-verifier = { version = "0.6", optional = true, default-features = false } rustls-webpki = { version = "0.102", optional = true, default-features = false } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" socket2 = { version = "0.6", optional = true } tokio = { version = "1.37.0", features = ["sync", "macros"] } tokio-rustls = { version = "0.26", optional = true, default-features = false } tokio-stream = { version = "0.1.17", default-features = false } tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = [ "codegen", ] } tower = { version = "0.5.2", features = [ "limit", "util", "buffer", ], optional = true } tower-service = "0.3.3" trait-variant = "0.1.2" url = "2.5.0" [dev-dependencies] async-stream = "0.3.6" hickory-server = "0.25.2" prost = "0.14.0" rustls = { version = "0.23", default-features = false, features = ["ring"] } tempfile = "3.26" tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = [ "server", "router", ] } tonic-prost = { version = "0.14.0", path = "../tonic-prost" } ================================================ FILE: grpc/examples/inmemory.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use bytes::Buf; use bytes::Bytes; use grpc::client; use grpc::client::CallOptions; use grpc::client::Channel; use grpc::client::ChannelOptions; use grpc::client::Invoke; use grpc::client::RecvStream as _; use grpc::client::SendStream as _; use grpc::core::ClientResponseStreamItem; use grpc::core::RecvMessage; use grpc::core::RequestHeaders; use grpc::core::SendMessage; use grpc::core::ServerResponseStreamItem; use grpc::credentials::InsecureChannelCredentials; use grpc::inmemory; use grpc::server; use grpc::server::Handle; struct Handler { id: String, } #[derive(Debug, Default)] struct MyReqMessage(String); impl SendMessage for MyReqMessage { fn encode(&self) -> Result, String> { Ok(Box::new(Bytes::from(self.0.clone()))) } } impl RecvMessage for MyReqMessage { fn decode(&mut self, data: &mut dyn Buf) -> Result<(), String> { let b = data.copy_to_bytes(data.remaining()); self.0 = String::from_utf8(b.to_vec()).map_err(|e| e.to_string())?; Ok(()) } } #[derive(Debug, Default)] struct MyResMessage(String); impl SendMessage for MyResMessage { fn encode(&self) -> Result, String> { Ok(Box::new(Bytes::from(self.0.clone()))) } } impl RecvMessage for MyResMessage { fn decode(&mut self, data: &mut dyn Buf) -> Result<(), String> { let b = data.copy_to_bytes(data.remaining()); self.0 = String::from_utf8(b.to_vec()).map_err(|e| e.to_string())?; Ok(()) } } impl Handle for Handler { async fn handle( &self, headers: RequestHeaders, tx: &mut impl server::SendStream, mut rx: impl server::RecvStream + 'static, ) { let method = headers.method_name().clone(); let id = self.id.clone(); // Send headers let _ = tx .send( ServerResponseStreamItem::Headers(grpc::core::ResponseHeaders::default()), server::SendOptions::default(), ) .await; let mut req_msg = MyReqMessage::default(); while rx.next(&mut req_msg).await.is_ok() { let res_msg = MyResMessage(format!( "Server {}: responding to: {}; msg: {}", id, method, req_msg.0, )); let _ = tx .send( ServerResponseStreamItem::Message(&res_msg), server::SendOptions::default(), ) .await; } // Send trailers let _ = tx .send( ServerResponseStreamItem::Trailers(grpc::core::Trailers::new(grpc::Status::new( grpc::StatusCode::Ok, "OK", ))), server::SendOptions::default(), ) .await; } } #[tokio::main] async fn main() { inmemory::reg(); let mut listeners = Vec::new(); for _ in 0..3 { let lis = inmemory::InMemoryListener::new(); let mut srv = grpc::server::Server::new(); srv.set_handler(Handler { id: lis.id() }); let lis_clone = lis.clone(); tokio::task::spawn(async move { srv.serve(&lis_clone).await; println!("serve returned for listener {}!", lis_clone.id()); }); listeners.push(lis); } let ids: Vec = listeners.iter().map(|lis| lis.id()).collect(); let target = format!("inmemory:///{}", ids.join(",")); println!("Creating channel for {target}"); let chan_opts = ChannelOptions::default(); let chan = Channel::new( target.as_str(), InsecureChannelCredentials::new(), chan_opts, ); let expected_servers: std::collections::HashSet<_> = ids.into_iter().collect(); let mut responding_servers = std::collections::HashSet::new(); let start = std::time::Instant::now(); while responding_servers != expected_servers && start.elapsed() < std::time::Duration::from_secs(3) { let server_id = run_rpc(&chan).await; if !server_id.is_empty() { responding_servers.insert(server_id); } tokio::time::sleep(std::time::Duration::from_millis(10)).await; } println!("Responding servers: {:?}", responding_servers); assert_eq!(responding_servers, expected_servers); drop(chan); for lis in listeners { lis.close().await; } } async fn run_rpc(chan: &Channel) -> String { let (mut tx, mut rx) = chan .invoke( RequestHeaders::new().with_method_name("/some/method"), CallOptions::default(), ) .await; tokio::spawn(async move { let reqs = vec![ MyReqMessage("My Request 1".to_string()), MyReqMessage("My Request 2".to_string()), MyReqMessage("My Request 3".to_string()), ]; for req in reqs { tx.send(&req, client::SendOptions::default()).await.unwrap(); } }); let mut server_id = String::new(); loop { let mut res = MyResMessage::default(); match rx.next(&mut res).await { ClientResponseStreamItem::Headers(_) => continue, ClientResponseStreamItem::Message(_) => { println!("CALL RESPONSE: {}", res.0); if let Some(id) = res .0 .strip_prefix("Server ") .and_then(|s| s.split(':').next()) { server_id = id.to_string(); } } ClientResponseStreamItem::Trailers(_) => break, ClientResponseStreamItem::StreamClosed => break, } } server_id } ================================================ FILE: grpc/proto/echo/echo.proto ================================================ /* * * Copyright 2018 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ syntax = "proto3"; package grpc.examples.echo; // EchoRequest is the request for echo. message EchoRequest { string message = 1; } // EchoResponse is the response for echo. message EchoResponse { string message = 1; } // Echo is the echo service. service Echo { // UnaryEcho is unary echo. rpc UnaryEcho(EchoRequest) returns (EchoResponse) {} // ServerStreamingEcho is server side streaming. rpc ServerStreamingEcho(EchoRequest) returns (stream EchoResponse) {} // ClientStreamingEcho is client side streaming. rpc ClientStreamingEcho(stream EchoRequest) returns (EchoResponse) {} // BidirectionalStreamingEcho is bidi streaming. rpc BidirectionalStreamingEcho(stream EchoRequest) returns (stream EchoResponse) {} } ================================================ FILE: grpc/src/attributes/linked_list.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::collections::BTreeSet; use std::sync::Arc; /// A node in the persistent linked list. /// /// Each node points to the previous state of the list. #[derive(Clone, Debug)] struct Node { key: K, value: V, parent: Option>>, } /// A persistent linked list that behaves like a map. /// /// This list is persistent, meaning that modifying it returns a new version of /// the list, while preserving the old version. It uses structural sharing to /// minimize memory usage. /// /// The list supports shadowing: adding a key that already exists will effectively /// update the value for that key in the new version of the list. /// /// # Warning /// /// This list is intended to store a small number of values (few hundreds) and /// is optimized for memory usage. It is **not** optimized for query speed. #[derive(Debug)] pub(crate) struct LinkedList { head: Option>>, } impl Clone for LinkedList { fn clone(&self) -> Self { Self { head: self.head.clone(), } } } impl Default for LinkedList { fn default() -> Self { Self { head: None } } } impl LinkedList { /// Creates a new, empty list. pub(crate) fn new() -> Self { Self::default() } /// Adds a key-value pair to the front of the list. /// /// If the key already exists in the list, this new entry will shadow the /// old one, effectively updating the value. pub(crate) fn add(&self, key: K, value: V) -> Self { LinkedList { head: Some(Arc::new(Node { key, value, parent: self.head.clone(), })), } } } impl LinkedList { /// Gets the value associated with the given key. /// /// This method iterates through the list from the front to find the most recent /// entry for the key. If a deletion marker is encountered for the key, `None` /// is returned. /// /// # Arguments /// /// * `key` - The key to look up. /// /// # Returns /// /// The value associated with the key, or `None` if the key is not present or /// has been removed. pub(crate) fn get(&self, key: &K) -> Option<&V> { let mut current = self.head.as_ref(); while let Some(node) = current { if &node.key == key { return Some(&node.value); } current = node.parent.as_ref(); } None } } impl LinkedList { /// Returns an iterator over the key-value pairs in the list. /// /// The iterator yields unique keys. If a key has been added multiple times, /// only the most recent value is returned. Keys that have been removed are /// skipped. pub(crate) fn iter(&self) -> Iter<'_, K, V> { Iter { current: self.head.as_ref(), seen: BTreeSet::new(), } } } /// An iterator over the items of a `LinkedList`. pub struct Iter<'a, K, V> { current: Option<&'a Arc>>, seen: BTreeSet<&'a K>, } impl<'a, K: Ord, V> Iterator for Iter<'a, K, V> { type Item = (&'a K, &'a V); fn next(&mut self) -> Option { loop { let node = self.current?; self.current = node.parent.as_ref(); if self.seen.insert(&node.key) { return Some((&node.key, &node.value)); } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_and_iter() { let l = LinkedList::new().add(1, "a").add(2, "b").add(3, "c"); let v: Vec<_> = l.iter().map(|(k, v)| (*k, *v)).collect(); assert_eq!(v, vec![(3, "c"), (2, "b"), (1, "a")]); } #[test] fn test_persistence() { let l1 = LinkedList::new().add(1, "a"); let l2 = l1.add(2, "b"); // l1 should be unchanged let v1: Vec<_> = l1.iter().map(|(k, v)| (*k, *v)).collect(); assert_eq!(v1, vec![(1, "a")]); // l2 should have both let v2: Vec<_> = l2.iter().map(|(k, v)| (*k, *v)).collect(); assert_eq!(v2, vec![(2, "b"), (1, "a")]); } #[test] fn test_shadowing() { let l = LinkedList::new().add(1, "a").add(1, "b"); let v: Vec<_> = l.iter().map(|(k, v)| (*k, *v)).collect(); // Should return the most recently added value for key 1 assert_eq!(v, vec![(1, "b")]); } #[test] fn test_send_sync() { fn assert_send_sync() {} assert_send_sync::>(); } } ================================================ FILE: grpc/src/attributes/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::any::Any; use std::any::TypeId; use std::cmp::Ordering; use std::fmt::Debug; use crate::attributes::linked_list::LinkedList; mod linked_list; /// Ensures only types that support comparison can be inserted into the /// Attributes struct. This allows the use of value-based equality rather than /// relying on pointer comparisons. trait AttributeTrait: Any + Send + Sync + Debug { fn any_ref(&self) -> &dyn Any; fn dyn_eq(&self, other: &dyn AttributeTrait) -> bool; fn dyn_cmp(&self, other: &dyn AttributeTrait) -> Ordering; } impl AttributeTrait for T { fn any_ref(&self) -> &dyn Any { self } fn dyn_eq(&self, other: &dyn AttributeTrait) -> bool { if let Some(other) = other.any_ref().downcast_ref::() { self == other } else { false } } fn dyn_cmp(&self, other: &dyn AttributeTrait) -> Ordering { if let Some(other) = other.any_ref().downcast_ref::() { self.cmp(other) } else { // Fallback for safety, though map structure guarantees same-type // comparison. TypeId::of::().cmp(&other.any_ref().type_id()) } } } #[derive(Debug)] struct AttributeValue { inner: Box, } impl PartialEq for AttributeValue { fn eq(&self, other: &Self) -> bool { self.inner.dyn_eq(other.inner.as_ref()) } } impl Eq for AttributeValue {} impl PartialOrd for AttributeValue { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for AttributeValue { fn cmp(&self, other: &Self) -> Ordering { self.inner.dyn_cmp(other.inner.as_ref()) } } /// A collection of attributes indexed by their type. /// /// `Attributes` provides a map-like interface where values are keyed by their /// TypeId. /// /// Equality and ordering of `Attributes` are structural. /// This means two `Attributes` maps are equal if they contain the same set of /// values, compared by value (via `Eq` trait). /// Stored types must implement `Any + Send + Sync + Eq + Ord + Debug`. /// /// # Warning /// /// This collection is intended to store a small number of values (few hundreds) /// and is optimized for memory usage. It is **not** optimized for query speed. #[derive(Clone, Default, Debug)] pub struct Attributes { elements: LinkedList, } impl Attributes { pub fn new() -> Self { Self::default() } /// Adds a value to the attributes. /// Returns a new Attributes object with the value added. /// If a value of the same type already exists, it is replaced. pub fn add(&self, value: T) -> Self { let id = TypeId::of::(); Attributes { elements: self.elements.add( id, AttributeValue { inner: Box::new(value), }, ), } } /// Gets a reference to a value of type T. pub fn get(&self) -> Option<&T> { let id = TypeId::of::(); self.elements .get(&id) .and_then(|v| v.inner.any_ref().downcast_ref()) } } impl PartialEq for Attributes { fn eq(&self, other: &Self) -> bool { let mut v1: Vec<_> = self.elements.iter().collect(); let mut v2: Vec<_> = other.elements.iter().collect(); if v1.len() != v2.len() { return false; } v1.sort(); v2.sort(); v1 == v2 } } impl Eq for Attributes {} impl PartialOrd for Attributes { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Attributes { fn cmp(&self, other: &Self) -> Ordering { let mut v1: Vec<_> = self.elements.iter().collect(); let mut v2: Vec<_> = other.elements.iter().collect(); v1.sort(); v2.sort(); v1.cmp(&v2) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_eq() { let a1 = Attributes::new().add(10i32); let a2 = a1.clone(); let a3 = Attributes::new().add(10i32); // Structural equality assert_eq!(a1, a2); assert_eq!(a1, a3); // Now equal because 10 == 10 let a4 = Attributes::new().add(10i32).add("foo".to_string()); assert_ne!(a1, a4); } #[test] fn test_attributes() { let attrs = Attributes::new(); let attrs = attrs.add(42i32); let attrs = attrs.add("hello".to_string()); assert_eq!(attrs.get::(), Some(&42)); assert_eq!(attrs.get::(), Some(&"hello".to_string())); assert_eq!(attrs.get::(), None); } #[test] fn test_persistence() { let a1 = Attributes::new().add(10i32); let a2 = a1.add(20u32); assert_eq!(a1.get::(), Some(&10)); assert_eq!(a1.get::(), None); assert_eq!(a2.get::(), Some(&10)); assert_eq!(a2.get::(), Some(&20)); } #[test] fn test_overwrite() { let a1 = Attributes::new().add(10i32); let a2 = a1.add(20i32); assert_eq!(a1.get::(), Some(&10)); assert_eq!(a2.get::(), Some(&20)); } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct Priority { weight: u64, name: String, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct Config { retries: u32, timeout_ms: u64, } #[test] fn test_custom_structs() { let p = Priority { weight: 123, name: "alice".into(), }; let config = Config { retries: 3, timeout_ms: 1000, }; let attrs = Attributes::new().add(p.clone()).add(config.clone()); assert_eq!(attrs.get::(), Some(&p)); assert_eq!(attrs.get::(), Some(&config)); // Test overwrite let p2 = Priority { weight: 456, name: "bob".into(), }; let attrs2 = attrs.add(p2.clone()); assert_eq!(attrs2.get::(), Some(&p2)); assert_eq!(attrs2.get::(), Some(&config)); // original should be unchanged assert_eq!(attrs.get::(), Some(&p)); } } ================================================ FILE: grpc/src/byte_str.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use core::str; use std::ops::Deref; use bytes::Bytes; /// A cheaply cloneable and sliceable chunk of contiguous memory. #[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct ByteStr { // Invariant: bytes contains valid UTF-8 bytes: Bytes, } impl Deref for ByteStr { type Target = str; #[inline] fn deref(&self) -> &str { let b: &[u8] = self.bytes.as_ref(); // The invariant of `bytes` is that it contains valid UTF-8 allows us // to unwrap. str::from_utf8(b).unwrap() } } impl From for ByteStr { #[inline] fn from(src: String) -> ByteStr { ByteStr { // Invariant: src is a String so contains valid UTF-8. bytes: Bytes::from(src), } } } ================================================ FILE: grpc/src/client/channel.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use core::panic; use std::any::Any; use std::error::Error; use std::mem; use std::str::FromStr; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; use std::time::Instant; use std::vec; use serde_json::json; use tokio::sync::Notify; use tokio::sync::mpsc; use tokio::sync::watch; use url::Url; // NOTE: http::Uri requires non-empty authority portion of URI use crate::attributes::Attributes; use crate::client::CallOptions; use crate::client::ConnectivityState; use crate::client::DynInvoke; use crate::client::DynRecvStream; use crate::client::DynSendStream; use crate::client::Invoke; use crate::client::load_balancing::ExternalSubchannel; use crate::client::load_balancing::GLOBAL_LB_REGISTRY; use crate::client::load_balancing::LbPolicy; use crate::client::load_balancing::LbPolicyBuilder; use crate::client::load_balancing::LbPolicyOptions; use crate::client::load_balancing::LbState; use crate::client::load_balancing::ParsedJsonLbConfig; use crate::client::load_balancing::PickResult; use crate::client::load_balancing::Picker; use crate::client::load_balancing::Subchannel; use crate::client::load_balancing::SubchannelState; use crate::client::load_balancing::WorkScheduler; use crate::client::load_balancing::pick_first; use crate::client::load_balancing::round_robin; use crate::client::load_balancing::{self}; use crate::client::name_resolution::Address; use crate::client::name_resolution::ResolverUpdate; use crate::client::name_resolution::global_registry; use crate::client::name_resolution::{self}; use crate::client::service_config::LbPolicyType; use crate::client::service_config::ServiceConfig; use crate::client::subchannel::InternalSubchannel; use crate::client::subchannel::InternalSubchannelPool; use crate::client::subchannel::NopBackoff; use crate::client::subchannel::SubchannelKey; use crate::client::subchannel::SubchannelStateWatcher; use crate::client::transport::GLOBAL_TRANSPORT_REGISTRY; use crate::client::transport::TransportRegistry; use crate::core::RequestHeaders; use crate::credentials::ChannelCredentials; use crate::credentials::dyn_wrapper::DynChannelCredentials; use crate::rt; use crate::rt::GrpcEndpoint; use crate::rt::GrpcRuntime; use crate::rt::default_runtime; #[non_exhaustive] pub struct ChannelOptions { pub transport_options: Attributes, // ? pub override_authority: Option, pub connection_backoff: Option, pub default_service_config: Option, pub disable_proxy: bool, pub disable_service_config_lookup: bool, pub disable_health_checks: bool, pub max_retry_memory: u32, // ? pub idle_timeout: Duration, // TODO: pub transport_registry: Option, // TODO: pub name_resolver_registry: Option, // TODO: pub lb_policy_registry: Option, // Typically we allow settings at the channel level that impact all RPCs, // but can also be set per-RPC. E.g.s: // // - interceptors // - user-agent string override // - max message sizes // - max retry/hedged attempts // - disable retry // // In gRPC-Go, we can express CallOptions as DialOptions, which is a nice // pattern: https://pkg.go.dev/google.golang.org/grpc#WithDefaultCallOptions // // To do this in rust, all optional behavior for a request would need to be // expressed through a trait that applies a mutation to a request. We'd // apply all those mutations before the user's options so the user's options // would override the defaults, or so the defaults would occur first. pub default_request_extensions: Vec>, // ?? } impl Default for ChannelOptions { fn default() -> Self { Self { transport_options: Attributes::default(), override_authority: None, connection_backoff: None, default_service_config: None, disable_proxy: false, disable_service_config_lookup: false, disable_health_checks: false, max_retry_memory: 8 * 1024 * 1024, // 8MB -- ??? idle_timeout: Duration::from_secs(30 * 60), default_request_extensions: vec![], } } } impl ChannelOptions { pub fn transport_options(self, transport_options: TODO) -> Self { todo!(); // add to existing options. } pub fn override_authority(self, authority: String) -> Self { Self { override_authority: Some(authority), ..self } } // etc } // All of Channel needs to be thread-safe. Arc? Or give out // Arc from constructor? #[derive(Clone)] pub struct Channel { inner: Arc, } impl Channel { /// Constructs a new gRPC channel. A gRPC channel is a virtual, persistent /// connection to a service. Channel creation cannot fail, but if the /// target string is invalid, the returned channel will never connect, and /// will fail all RPCs. // TODO: should this return a Result instead? pub fn new(target: &str, credentials: C, options: ChannelOptions) -> Self where C: ChannelCredentials, C::Output>: GrpcEndpoint + 'static, { pick_first::reg(); round_robin::reg(); Self { inner: Arc::new(PersistentChannel::new( target, Box::new(credentials) as Box, default_runtime(), options, )), } } // TODO: enter_idle(&self) and graceful_stop()? /// Returns the current state of the channel. If there is no underlying active channel, /// returns Idle. If `connect` is true, will create a new active channel. pub fn state(&mut self, connect: bool) -> ConnectivityState { self.inner.state(connect) } /// Waits for the state of the channel to change from source. Times out and /// returns an error after the deadline. pub async fn wait_for_state_change( &self, source: ConnectivityState, deadline: Instant, ) -> Result<(), Box> { todo!() } } impl Invoke for Channel { type SendStream = Box; type RecvStream = Box; async fn invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { let ac = self.inner.get_active_channel(); ac.invoke(headers, options).await } } // A PersistentChannel represents the static configuration of a channel and an // optional Arc of an ActiveChannel. An ActiveChannel exists whenever the // PersistentChannel is not IDLE. Every channel is IDLE at creation, or after // some configurable timeout elapses without any any RPC activity. struct PersistentChannel { target: Url, options: ChannelOptions, active_channel: Mutex>>, runtime: GrpcRuntime, } impl PersistentChannel { // Channels begin idle so `new()` does not automatically connect. // ChannelOption contain only optional parameters. fn new( target: &str, _credentials: Box, runtime: GrpcRuntime, options: ChannelOptions, ) -> Self { Self { target: Url::from_str(target).unwrap(), // TODO handle err active_channel: Mutex::default(), options, runtime, } } /// Returns the current state of the channel. If there is no underlying active channel, /// returns Idle. If `connect` is true, will create a new active channel iff none exists. fn state(&self, connect: bool) -> ConnectivityState { // Done this away to avoid potentially locking twice. let active_channel = if connect { self.get_active_channel() } else { match self.active_channel.lock().unwrap().clone() { Some(x) => x, None => { return ConnectivityState::Idle; } } }; active_channel .connectivity_state .cur() .unwrap_or(ConnectivityState::Idle) } /// Gets the underlying active channel. If there is no current connection, it will create one. /// This cannot fail and will always return a valid active channel. fn get_active_channel(&self) -> Arc { let mut active_channel = self.active_channel.lock().unwrap(); if active_channel.is_none() { *active_channel = Some(ActiveChannel::new( self.target.clone(), &self.options, self.runtime.clone(), )); } active_channel.clone().unwrap() // We have ensured this is not None. } } struct ActiveChannel { abort_handle: Box, picker: Arc>>, connectivity_state: Arc>, runtime: GrpcRuntime, } impl ActiveChannel { fn new(target: Url, options: &ChannelOptions, runtime: GrpcRuntime) -> Arc { let (tx, mut rx) = mpsc::unbounded_channel::(); let transport_registry = GLOBAL_TRANSPORT_REGISTRY.clone(); let resolve_now = Arc::new(Notify::new()); let connectivity_state = Arc::new(Watcher::new()); let picker = Arc::new(Watcher::new()); let mut channel_controller = InternalChannelController::new( transport_registry, resolve_now.clone(), tx.clone(), picker.clone(), connectivity_state.clone(), runtime.clone(), ); // TODO(arjan-bal): Return error here instead of panicking. let rb = global_registry().get(target.scheme()).unwrap(); let target = name_resolution::Target::from(target); let authority = target.authority_host_port(); let authority = if authority.is_empty() { rb.default_authority(&target).to_owned() } else { authority }; let work_scheduler = Arc::new(ResolverWorkScheduler { wqtx: tx }); let resolver_opts = name_resolution::ResolverOptions { authority, work_scheduler, runtime: runtime.clone(), }; let resolver = rb.build(&target, resolver_opts); let jh = runtime.spawn(Box::pin(async move { let mut resolver = resolver; while let Some(w) = rx.recv().await { match w { WorkQueueItem::Closure(func) => func(&mut channel_controller), WorkQueueItem::ScheduleResolver => resolver.work(&mut channel_controller), } } })); Arc::new(Self { abort_handle: jh, picker: picker.clone(), connectivity_state: connectivity_state.clone(), runtime, }) } } impl Invoke for Arc { type SendStream = Box; type RecvStream = Box; async fn invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { let mut i = self.picker.iter(); loop { if let Some(p) = i.next().await { let result = &p.pick(&headers); match result { PickResult::Pick(pr) => { if let Some(sc) = (pr.subchannel.as_ref() as &dyn Any) .downcast_ref::() { return sc .isc .as_ref() .unwrap() .dyn_invoke(headers, options.clone()) .await; } else { panic!( "picked subchannel is not an implementation provided by the channel" ); } } PickResult::Queue => { // Continue and retry the RPC with the next picker. } PickResult::Fail(status) => { todo!("failed pick: {:?}", status); } PickResult::Drop(status) => { todo!("dropped pick: {:?}", status); } } } } } } impl Drop for ActiveChannel { fn drop(&mut self) { self.abort_handle.abort(); } } struct ResolverWorkScheduler { wqtx: WorkQueueTx, } pub(super) type WorkQueueTx = mpsc::UnboundedSender; impl name_resolution::WorkScheduler for ResolverWorkScheduler { fn schedule_work(&self) { let _ = self.wqtx.send(WorkQueueItem::ScheduleResolver); } } pub(crate) struct InternalChannelController { pub(super) lb: Arc, // called and passes mutable parent to it, so must be Arc. transport_registry: TransportRegistry, pub(super) subchannel_pool: Arc, resolve_now: Arc, wqtx: WorkQueueTx, lb_work_scheduler: Arc, picker: Arc>>, connectivity_state: Arc>, runtime: GrpcRuntime, } impl InternalChannelController { fn new( transport_registry: TransportRegistry, resolve_now: Arc, wqtx: WorkQueueTx, picker: Arc>>, connectivity_state: Arc>, runtime: GrpcRuntime, ) -> Self { let lb_work_scheduler = Arc::new(LbWorkScheduler { wqtx: wqtx.clone(), pending: Mutex::default(), }); let lb = Arc::new(LbController::new( lb_work_scheduler.clone(), runtime.clone(), )); Self { lb, transport_registry, subchannel_pool: Arc::new(InternalSubchannelPool::new()), resolve_now, wqtx, picker, connectivity_state, runtime, lb_work_scheduler, } } fn new_esc_for_isc(&self, isc: Arc) -> Arc { let sc = Arc::new(ExternalSubchannel::new(isc.clone(), self.wqtx.clone())); let watcher = Arc::new(SubchannelStateWatcher::new(sc.clone(), self.wqtx.clone())); sc.set_watcher(watcher.clone()); isc.register_connectivity_state_watcher(watcher.clone()); sc } } impl name_resolution::ChannelController for InternalChannelController { fn update(&mut self, update: ResolverUpdate) -> Result<(), String> { let lb = self.lb.clone(); lb.handle_resolver_update(update, self) .map_err(|err| err.to_string()) } fn parse_service_config(&self, config: &str) -> Result { Err("service configs not supported".to_string()) } } impl load_balancing::ChannelController for InternalChannelController { fn new_subchannel(&mut self, address: &Address) -> Arc { let key = SubchannelKey::new(address.clone()); if let Some(isc) = self.subchannel_pool.lookup_subchannel(&key) { return self.new_esc_for_isc(isc); } // If we get here, it means one of two things: // 1. provided key is not found in the map // 2. provided key points to an unpromotable value, which can occur if // its internal subchannel has been dropped but hasn't been // unregistered yet. let transport = self .transport_registry .get_transport(address.network_type) .unwrap(); let scp = self.subchannel_pool.clone(); let isc = InternalSubchannel::new( key.clone(), transport, Arc::new(NopBackoff {}), Box::new(move |k: SubchannelKey| { scp.unregister_subchannel(&k); }), self.runtime.clone(), ); let _ = self.subchannel_pool.register_subchannel(&key, isc.clone()); self.new_esc_for_isc(isc) } fn update_picker(&mut self, update: LbState) { println!( "update picker called with state: {:?}", update.connectivity_state ); self.picker.update(update.picker); self.connectivity_state.update(update.connectivity_state); } fn request_resolution(&mut self) { self.resolve_now.notify_one(); } } // A channel that is not idle (connecting, ready, or erroring). #[derive(Debug)] pub(super) struct LbController { pub(super) policy: Mutex>>, policy_builder: Mutex>>, runtime: GrpcRuntime, work_scheduler: Arc, } #[derive(Debug)] struct LbWorkScheduler { pending: Mutex, wqtx: WorkQueueTx, } impl WorkScheduler for LbWorkScheduler { fn schedule_work(&self) { if mem::replace(&mut *self.pending.lock().unwrap(), true) { // Already had a pending call scheduled. return; } let _ = self.wqtx.send(WorkQueueItem::Closure(Box::new( |c: &mut InternalChannelController| { *c.lb_work_scheduler.pending.lock().unwrap() = false; c.lb.clone() .policy .lock() .unwrap() .as_mut() .unwrap() .work(c); }, ))); } } impl LbController { fn new(work_scheduler: Arc, runtime: GrpcRuntime) -> Self { Self { policy_builder: Mutex::default(), policy: Mutex::default(), // new(None::>), work_scheduler, runtime, } } fn handle_resolver_update( self: &Arc, update: ResolverUpdate, controller: &mut InternalChannelController, ) -> Result<(), Box> { let mut policy_name = pick_first::POLICY_NAME; if let Ok(Some(service_config)) = update.service_config.as_ref() && service_config .load_balancing_policy .as_ref() .is_some_and(|p| *p == LbPolicyType::RoundRobin) { policy_name = round_robin::POLICY_NAME; } let mut p = self.policy.lock().unwrap(); if p.is_none() { let builder = GLOBAL_LB_REGISTRY.get_policy(policy_name).unwrap(); let newpol = builder.build(LbPolicyOptions { work_scheduler: self.work_scheduler.clone(), runtime: self.runtime.clone(), }); *self.policy_builder.lock().unwrap() = Some(builder); *p = Some(newpol); } // TODO: config should come from ServiceConfig. let builder = self.policy_builder.lock().unwrap(); let config = match builder .as_ref() .unwrap() .parse_config(&ParsedJsonLbConfig::from_value( json!({"shuffleAddressList": true, "unknown_field": false}), )) { Ok(cfg) => cfg, Err(e) => { return Err(e); } }; p.as_mut() .unwrap() .resolver_update(update, config.as_ref(), controller) // TODO: close old LB policy gracefully vs. drop? } pub(super) fn subchannel_update( &self, subchannel: Arc, state: &SubchannelState, channel_controller: &mut dyn load_balancing::ChannelController, ) { let mut p = self.policy.lock().unwrap(); p.as_mut() .unwrap() .subchannel_update(subchannel, state, channel_controller); } } pub(super) enum WorkQueueItem { // Execute the closure. Closure(Box), // Call the resolver to do work. ScheduleResolver, } pub struct TODO; // Enables multiple receivers to view data output from a single producer. // Producer calls update. Consumers call iter() and call next() until they find // a good value or encounter None. pub(crate) struct Watcher { tx: watch::Sender>, rx: watch::Receiver>, } impl Watcher { fn new() -> Self { let (tx, rx) = watch::channel(None); Self { tx, rx } } pub(crate) fn iter(&self) -> WatcherIter { let mut rx = self.rx.clone(); rx.mark_changed(); WatcherIter { rx } } pub(crate) fn cur(&self) -> Option { let mut rx = self.rx.clone(); rx.mark_changed(); let c = rx.borrow(); c.clone() } fn update(&self, item: T) { self.tx.send(Some(item)).unwrap(); } } pub(crate) struct WatcherIter { rx: watch::Receiver>, } // TODO: Use an arc_swap::ArcSwap instead that contains T and a channel closed // when T is updated. Even if the channel needs a lock, the fast path becomes // lock-free. impl WatcherIter { /// Returns the next unseen value pub(crate) async fn next(&mut self) -> Option { loop { self.rx.changed().await.ok()?; let x = self.rx.borrow_and_update(); if x.is_some() { return x.clone(); } } } } ================================================ FILE: grpc/src/client/interceptor.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use crate::client::CallOptions; use crate::client::Invoke; use crate::client::InvokeOnce; use crate::client::RecvStream; use crate::client::SendStream; use crate::core::RequestHeaders; /// A trait which allows intercepting an RPC invoke operation. The trait is /// generic on I which should either be implemented as InvokeOnce (for /// interceptors that only need to call next once) or Invoke[+Clone+'static] /// (for interceptors that need to call next multiple times). #[trait_variant::make(Send)] pub trait Intercept: Sync { type SendStream: SendStream + 'static; type RecvStream: RecvStream + 'static; /// Intercepts the start of a call. Implementations should generally use /// next to create and start a call whose streams are optionally wrapped /// before being returned. async fn intercept( &self, headers: RequestHeaders, options: CallOptions, next: I, ) -> (Self::SendStream, Self::RecvStream); } /// Like Intercept, but not reusable. #[trait_variant::make(Send)] pub trait InterceptOnce: Sync { type SendStream: SendStream + 'static; type RecvStream: RecvStream + 'static; /// Intercepts the start of a call. Implementations should generally use /// next to create and start a call whose streams are optionally wrapped /// before being returned. async fn intercept_once( self, headers: RequestHeaders, options: CallOptions, next: I, ) -> (Self::SendStream, Self::RecvStream); } /// Wraps an interceptor that implements Intercept, and implements InterceptOnce /// instead, allowing it to be paired with a non-reusable Invoke implementation /// inside an `Intercepted` struct. #[derive(Clone)] pub struct IntoOnce(pub T); impl InterceptOnce for IntoOnce where I: InvokeOnce, SS: SendStream + 'static, RS: RecvStream + 'static, T: Intercept, { type SendStream = SS; type RecvStream = RS; async fn intercept_once( self, headers: RequestHeaders, options: CallOptions, next: I, ) -> (Self::SendStream, Self::RecvStream) { self.0.intercept(headers, options, next).await } } /// Wraps `Invoke` and `Intercept` impls and implements `Invoke` for the /// combination. Or wraps `InvokeOnce` and `InterceptOnce` impls /// and implements `InvokeOnce` for the combination. #[derive(Clone, Copy)] pub struct Intercepted { invoke: Inv, intercept: Int, } impl Intercepted { pub fn new(invoke: Inv, intercept: Int) -> Self { Self { invoke, intercept } } } impl InvokeOnce for Intercepted where Inv: InvokeOnce, Int: InterceptOnce, { type SendStream = Int::SendStream; type RecvStream = Int::RecvStream; async fn invoke_once( self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { self.intercept .intercept_once(headers, options, self.invoke) .await } } impl Invoke for Intercepted where Inv: Send + Sync, for<'a> Int: Send + Sync + Intercept<&'a Inv, SendStream = SS, RecvStream = RS>, SS: SendStream + 'static, RS: RecvStream + 'static, { type SendStream = SS; type RecvStream = RS; async fn invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { self.intercept .intercept(headers, options, &self.invoke) .await } } /// Combines an `Invoke` with an `InterceptOnce` to implement `InvokeOnce`. pub struct InterceptedOnce { invoke: Inv, intercept: Int, } impl InterceptedOnce { pub fn new(invoke: Inv, intercept: Int) -> Self { Self { invoke, intercept } } } impl InvokeOnce for InterceptedOnce where Inv: Send + Sync, for<'a> Int: InterceptOnce<&'a Inv, SendStream = SS, RecvStream = RS>, SS: SendStream + 'static, RS: RecvStream + 'static, { type SendStream = SS; type RecvStream = RS; async fn invoke_once( self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { self.intercept .intercept_once(headers, options, &self.invoke) .await } } /// Implements methods for combining `Invoke` implementations with either /// `Intercept` or `InterceptOnce` interceptors. Blanket implemented on any /// `Invoke`. pub trait InvokeExt: Invoke + Sized { fn with_interceptor(self, interceptor: Int) -> Intercepted where for<'a> Int: Intercept<&'a Self>, { Intercepted::new(self, interceptor) } fn with_once_interceptor(self, interceptor: Int) -> InterceptedOnce where for<'a> Int: InterceptOnce<&'a Self>, { InterceptedOnce::new(self, interceptor) } } /// Implements methods for combining `InvokeOnce` implementations with /// `Intercept` or `InterceptOnce` interceptors. /// Blanket implemented on any `InvokeOnce`. pub trait InvokeOnceExt: InvokeOnce + Sized { fn with_interceptor(self, interceptor: Int) -> Intercepted> where for<'a> Int: Intercept, { Intercepted::new(self, IntoOnce(interceptor)) } fn with_once_interceptor(self, interceptor: Int) -> Intercepted where Int: InterceptOnce, { Intercepted::new(self, interceptor) } } impl InvokeExt for T {} impl InvokeOnceExt for T {} // Tests for Intercepted and InterceptedOnce and examples of the four types of // interceptors, how they are implemented, and how they are combined with // Invoke/InvokeOnce. // // The four different kinds of interceptors are: // // 1. Reusable: uses `next` once. // impl Intercept // // 2. ResuableFanOut: uses `next` multiple times. // impl Intercept<&Invoke [+ Clone + 'static if needed]> // // 3. Oneshot: uses `next` once. // impl InterceptOnce // // 4. OneshotFanOut: uses `next` multiple times. // impl InterceptOnce<&Invoke [+ Clone + 'static if needed]> // // Examples of each of these are defined below. #[cfg(test)] mod test { use std::future::Future; use std::sync::Arc; use bytes::Buf; use bytes::Bytes; use tokio::pin; use tokio::select; use tokio::sync::Mutex; use tokio::sync::Notify; use tokio::sync::broadcast; use tokio::sync::mpsc; use tokio::task; use super::*; use crate::Status; use crate::StatusCode; use crate::client::CallOptions; use crate::client::Invoke; use crate::client::RecvStream; use crate::client::SendOptions; use crate::client::SendStream; use crate::core::ClientResponseStreamItem; use crate::core::RecvMessage; use crate::core::ResponseHeaders; use crate::core::SendMessage; use crate::core::Trailers; #[derive(Clone)] struct Reusable; impl Intercept for Reusable { type SendStream = NopStream; type RecvStream = I::RecvStream; async fn intercept( &self, headers: RequestHeaders, args: CallOptions, next: I, ) -> (Self::SendStream, Self::RecvStream) { let (_, rx) = next.invoke_once(headers, args).await; (NopStream, rx) } } #[derive(Clone)] struct ReusableFanOut; impl Intercept<&I> for ReusableFanOut { type SendStream = RetrySendStream; type RecvStream = RetryRecvStream; async fn intercept( &self, headers: RequestHeaders, args: CallOptions, next: &I, ) -> (Self::SendStream, Self::RecvStream) { start_retry_streams(next, headers, args).await } } struct Oneshot; impl InterceptOnce for Oneshot { type SendStream = I::SendStream; type RecvStream = NopStream; async fn intercept_once( self, headers: RequestHeaders, args: CallOptions, next: I, ) -> (Self::SendStream, Self::RecvStream) { let (tx, _) = next.invoke_once(headers, args).await; (tx, NopStream) } } struct OneshotFanOut; impl InterceptOnce<&I> for OneshotFanOut { type SendStream = I::SendStream; type RecvStream = I::RecvStream; async fn intercept_once( self, headers: RequestHeaders, args: CallOptions, next: &I, ) -> (Self::SendStream, Self::RecvStream) { let (_, _) = next.invoke(headers.clone(), args.clone()).await; next.invoke(headers, args).await } } // Tests all 6 valid scenarios combining an invoker with an interceptor. #[tokio::test] async fn test_interceptor_creation() { // Reusable Invoke with resuable Intercept. { let i = NopInvoker.with_interceptor(Reusable); i.invoke(RequestHeaders::default(), CallOptions::default()) .await; // Since Invoke is implemented on &Intercepted, it is reusable. i.invoke(RequestHeaders::default(), CallOptions::default()) .await; } // One-shot Invoke with resuable Intercept. { let i = NopOnceInvoker.with_interceptor(Reusable); i.invoke_once(RequestHeaders::default(), CallOptions::default()) .await; } // Reusable Invoke with resuable fan-out Intercept. { let i = NopInvoker.with_interceptor(ReusableFanOut); i.invoke(RequestHeaders::default(), CallOptions::default()) .await; // Since Invoke is implemented on &Intercepted, it is reusable. i.invoke(RequestHeaders::default(), CallOptions::default()) .await; } // One-shot Invoke with fan-out Intercept is illegal. // Reusable Invoke with one-shot Intercept. { let i = NopInvoker.with_once_interceptor(Oneshot); i.invoke_once(RequestHeaders::default(), CallOptions::default()) .await; } // One-shot Invoke with one-shot Intercept. { let i = NopOnceInvoker.with_once_interceptor(Oneshot); i.invoke_once(RequestHeaders::default(), CallOptions::default()) .await; } // Reusable Invoke with one-shot fan-out Intercept. { let i = NopInvoker.with_once_interceptor(OneshotFanOut); i.invoke_once(RequestHeaders::default(), CallOptions::default()) .await; } // One-shot Invoke with one-shot fan-out Intercept is illegal. } // Tests that a a retry interceptor retries cached send operations and // ultimately completes with success on an OK response. #[tokio::test] async fn test_retry_interceptor_succeeds() { let (invoker, mut controller) = MockInvoker::new(); let chan = invoker.with_interceptor(ReusableFanOut); let (mut tx, mut rx) = chan .invoke(RequestHeaders::default(), CallOptions::default()) .await; let one = Bytes::from(vec![1]); let two = Bytes::from(vec![2]); tx.send(&ByteSendMsg::new(&one), SendOptions::default()) .await .unwrap(); assert_eq!(controller.recv_req().await.0, one); controller .send_resp(ClientResponseStreamItem::Trailers(Trailers::new( Status::new(StatusCode::Internal, ""), ))) .await; let handle = task::spawn(async move { rx.next(&mut ByteRecvMsg::new()).await }); assert_eq!(controller.recv_req().await.0, one); tx.send(&ByteSendMsg::new(&two), SendOptions::default()) .await .unwrap(); assert_eq!(controller.recv_req().await.0, two); controller .send_resp(ClientResponseStreamItem::Trailers(Trailers::new( Status::new(StatusCode::Internal, ""), ))) .await; assert_eq!(controller.recv_req().await.0, one); assert_eq!(controller.recv_req().await.0, two); controller .send_resp(ClientResponseStreamItem::Trailers(Trailers::new( Status::new(StatusCode::Ok, ""), ))) .await; let resp = handle.await.unwrap(); let ClientResponseStreamItem::Trailers(trailers) = resp else { panic!("unexpected resp: {resp:?}"); }; assert_eq!(trailers.status().code(), StatusCode::Ok); } // Tests that a a retry interceptor retries cached send operations but fails // after the backing streams fail three times. #[tokio::test] async fn test_retry_interceptor_fails() { let (invoker, mut controller) = MockInvoker::new(); let chan = invoker.with_interceptor(ReusableFanOut); let (mut tx, mut rx) = chan .invoke(RequestHeaders::default(), CallOptions::default()) .await; let one = Bytes::from(vec![1]); let two = Bytes::from(vec![2]); tx.send(&ByteSendMsg::new(&one), SendOptions::default()) .await .unwrap(); assert_eq!(controller.recv_req().await.0, one); controller .send_resp(ClientResponseStreamItem::Trailers(Trailers::new( Status::new(crate::StatusCode::Internal, ""), ))) .await; let handle = task::spawn(async move { rx.next(&mut ByteRecvMsg::new()).await }); assert_eq!(controller.recv_req().await.0, one); tx.send(&ByteSendMsg::new(&two), SendOptions::default()) .await .unwrap(); assert_eq!(controller.recv_req().await.0, two); controller .send_resp(ClientResponseStreamItem::Trailers(Trailers::new( Status::new(crate::StatusCode::Internal, ""), ))) .await; assert_eq!(controller.recv_req().await.0, one); assert_eq!(controller.recv_req().await.0, two); controller .send_resp(ClientResponseStreamItem::Trailers(Trailers::new( Status::new(crate::StatusCode::Internal, ""), ))) .await; assert_eq!(controller.recv_req().await.0, one); assert_eq!(controller.recv_req().await.0, two); controller .send_resp(ClientResponseStreamItem::Trailers(Trailers::new( Status::new(crate::StatusCode::Internal, ""), ))) .await; let resp = handle.await.unwrap(); let ClientResponseStreamItem::Trailers(trailers) = resp else { panic!("unexpected resp: {resp:?}"); }; assert_eq!(trailers.status().code(), crate::StatusCode::Internal); } // Tests that a a retry interceptor doesn't retry cached operations after // receiving the headers from the server. #[tokio::test] async fn test_retry_interceptor_commit_on_headers() { let (invoker, mut controller) = MockInvoker::new(); let chan = invoker.with_interceptor(ReusableFanOut); let (mut tx, mut rx) = chan .invoke(RequestHeaders::default(), CallOptions::default()) .await; let one = Bytes::from(vec![1]); tx.send(&ByteSendMsg::new(&one), SendOptions::default()) .await .unwrap(); assert_eq!(controller.recv_req().await.0, one); controller .send_resp(ClientResponseStreamItem::Headers(ResponseHeaders::default())) .await; let resp = rx.next(&mut ByteRecvMsg::new()).await; assert!(matches!(resp, ClientResponseStreamItem::Headers(_))); controller .send_resp(ClientResponseStreamItem::Trailers(Trailers::new( Status::new(crate::StatusCode::Internal, ""), ))) .await; let resp = rx.next(&mut ByteRecvMsg::new()).await; let ClientResponseStreamItem::Trailers(trailers) = resp else { panic!("unexpected resp: {resp:?}"); }; assert_eq!(trailers.status().code(), crate::StatusCode::Internal); } /// An Invoke impl that can be controlled via its paired /// MockInvokerController. #[derive(Clone)] struct MockInvoker { resp_tx: broadcast::Sender, req_tx: mpsc::Sender<(Bytes, SendOptions)>, } /// A controller used to control the behavior of its paired MockInvoker's /// SendStream and RecvStream. struct MockInvokerController { resp_tx: broadcast::Sender, req_rx: mpsc::Receiver<(Bytes, SendOptions)>, } impl MockInvoker { fn new() -> (Self, MockInvokerController) { // We create receivers as needed in invoke(). let (resp_tx, _) = broadcast::channel(1); let (req_tx, req_rx) = mpsc::channel(1); ( MockInvoker { resp_tx: resp_tx.clone(), req_tx, }, MockInvokerController { req_rx, resp_tx }, ) } } impl MockInvokerController { async fn recv_req(&mut self) -> (Bytes, SendOptions) { self.req_rx.recv().await.unwrap() } async fn send_resp(&mut self, item: ClientResponseStreamItem) { self.resp_tx.send(item).unwrap(); } } impl Invoke for MockInvoker { type SendStream = MockSendStream; type RecvStream = MockRecvStream; async fn invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { ( MockSendStream(self.req_tx.clone()), MockRecvStream(self.resp_tx.subscribe()), ) } } struct MockSendStream(mpsc::Sender<(Bytes, SendOptions)>); impl SendStream for MockSendStream { async fn send(&mut self, item: &dyn SendMessage, options: SendOptions) -> Result<(), ()> { let mut data = item.encode().unwrap(); self.0 .send((data.copy_to_bytes(data.remaining()), options)) .await .map_err(|_| ()) } } struct MockRecvStream(broadcast::Receiver); impl RecvStream for MockRecvStream { async fn next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem { self.0.recv().await.unwrap() } } async fn start_retry_streams( invoker: &I, headers: RequestHeaders, options: CallOptions, ) -> (RetrySendStream, RetryRecvStream) { let invoker = invoker.clone(); // Get an owned Invoker. let (send_stream, recv_stream) = invoker.invoke(headers.clone(), options.clone()).await; let cache = Cache::new(); ( RetrySendStream { send_stream, cache: cache.clone(), }, RetryRecvStream { invoker, headers, options, recv_stream, cache, committed: false, }, ) } /// Stores information shared between a retry SendStream/RecvStream pair. struct Cache { send_stream: Option, // the most recent backing SendStream, if available // Set when the stream is committed; SendStream will not wait for a new // stream after a send failure when set. committed: bool, data: Vec<(Bytes, SendOptions)>, // cached send operations // Allows the sender to wait for a new stream. notify: Arc, } impl Cache { fn new() -> Arc> { Arc::new(Mutex::new(Cache { send_stream: None, committed: false, data: Default::default(), notify: Default::default(), })) } } struct RetrySendStream { send_stream: S, // locally cached send stream cache: Arc>>, } impl SendStream for RetrySendStream { async fn send(&mut self, msg: &dyn SendMessage, options: SendOptions) -> Result<(), ()> { loop { let res = self.send_stream.send(msg, options.clone()).await; let mut cache = self.cache.lock().await; if cache.committed { return res; } if res.is_ok() { // Success; cache this message. let mut data = msg.encode().unwrap(); cache .data .push((data.copy_to_bytes(data.remaining()), options)); return res; } if cache.send_stream.is_none() { // No new stream and not committed; wait for a new stream. let notify = cache.notify.clone(); drop(cache); notify.notified().await; cache = self.cache.lock().await; } let Some(send_stream) = cache.send_stream.take() else { // We were notified but no new stream was present; return error. return Err(()); }; // Retry on the new stream. self.send_stream = send_stream; } } } pub struct RetryRecvStream { invoker: I, // the invoker to use to retry calls headers: RequestHeaders, options: CallOptions, recv_stream: I::RecvStream, // the most recent attempt's recv_stream cache: Arc>>, // local copy of committed to avoid taking lock held by send operation // if we know we have committed. committed: bool, } // Returns true if we can retry a stream based on the response item -- i.e. // if it is any error status. Any other response will commit the RPC. fn should_retry(i: &ClientResponseStreamItem) -> bool { if let ClientResponseStreamItem::Trailers(t) = &i { t.status().code() != StatusCode::Ok } else { false } } const MAX_ATTEMPTS: usize = 3; impl RecvStream for RetryRecvStream { async fn next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem { let mut recv_resp = self.recv_stream.next(msg).await; if self.committed { return recv_resp; } let mut cache = self.cache.lock().await; let mut attempt = 0; loop { attempt += 1; if !should_retry(&recv_resp) || attempt > MAX_ATTEMPTS { self.committed = true; cache.committed = true; cache.data.clear(); // Notify the sender in case it is blocked and waiting for a // new stream. cache.notify.notify_waiters(); return recv_resp; } // Retry the whole stream. let (mut send_stream, recv_stream) = self .invoker .invoke(self.headers.clone(), self.options.clone()) .await; self.recv_stream = recv_stream; // Run the current recv operation in parallel with replaying // the stream. let recv_fut = self.recv_stream.next(msg); pin!(recv_fut); let mut recv_state = RecvStreamState::Pending(recv_fut); if replay_sends(&mut send_stream, &cache.data, &mut recv_state).await { // Replay completed successfully. Update the send stream // and release the lock while resolving the recv operation. cache.send_stream = Some(send_stream); cache.notify.notify_waiters(); drop(cache); recv_resp = recv_state.resolve().await; cache = self.cache.lock().await; } else { // Errors occurred while sending. Update recv_resp and // re-check while still holding the lock. recv_resp = recv_state.resolve().await; } } } } async fn replay_sends( send_stream: &mut S, cached_sends: &Vec<(Bytes, SendOptions)>, recv_state: &mut RecvStreamState, ) -> bool where S: SendStream, F: Future + Unpin, { for (data, options) in cached_sends { let send_msg = ByteSendMsg::new(data); let send_fut = send_stream.send(&send_msg, options.clone()); pin!(send_fut); // Poll both the recv and send until the send completes or the recv // indicates we should retry. loop { match recv_state.race_with(&mut send_fut).await { Some(res) => { if res.is_err() { return false; } break; } None => { let RecvStreamState::Done(resp) = &recv_state else { unreachable!() }; // Abort sending now if we know we need to retry. // Otherwise we will be committing, so we need to finish // replaying the sends. if should_retry(resp) { return false; } } } } } true } // Holds either a Pending() future to a recv call or its Done() result. enum RecvStreamState { Pending(F), Done(ClientResponseStreamItem), } impl + Unpin> RecvStreamState { /// Runs `fut` alongside `self` if it is Pending. Returns None if `self` /// starts Pending and resolves before fut -- `self` then changes to Done /// with the result. Otherwise, returns Some(fut's output) and keeps /// `self` in Pending. async fn race_with(&mut self, fut: &mut F2) -> Option { match self { RecvStreamState::Pending(recv_fut) => { select! { res = recv_fut => { *self = RecvStreamState::Done(res); None } res = fut => { Some(res) } } } RecvStreamState::Done(_) => Some(fut.await), } } /// Resolves `self`: either returns the already-Done() result of the /// recv operation or awaits the future and returns the result. async fn resolve(self) -> ClientResponseStreamItem { match self { RecvStreamState::Pending(fut) => fut.await, RecvStreamState::Done(resp) => resp, } } } struct ByteRecvMsg { data: Option, } impl ByteRecvMsg { fn new() -> Self { Self { data: None } } } impl RecvMessage for ByteRecvMsg { fn decode(&mut self, data: &mut dyn Buf) -> Result<(), String> { self.data = Some(data.copy_to_bytes(data.remaining())); Ok(()) } } struct ByteSendMsg<'a> { data: &'a Bytes, } impl<'a> ByteSendMsg<'a> { fn new(data: &'a Bytes) -> Self { Self { data } } } impl<'a> SendMessage for ByteSendMsg<'a> { fn encode(&self) -> Result, String> { Ok(Box::new(self.data.clone())) } } #[derive(Clone)] struct NopInvoker; impl Invoke for NopInvoker { type SendStream = NopStream; type RecvStream = NopStream; async fn invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { (NopStream, NopStream) } } struct NopOnceInvoker; impl InvokeOnce for NopOnceInvoker { type SendStream = NopStream; type RecvStream = NopStream; async fn invoke_once( self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { (NopStream, NopStream) } } struct NopStream; impl SendStream for NopStream { async fn send(&mut self, _item: &dyn SendMessage, _options: SendOptions) -> Result<(), ()> { Ok(()) } } impl RecvStream for NopStream { async fn next( &mut self, _msg: &mut dyn RecvMessage, ) -> crate::core::ClientResponseStreamItem { ClientResponseStreamItem::StreamClosed } } } ================================================ FILE: grpc/src/client/load_balancing/child_manager.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ //! A utility which helps parent LB policies manage multiple children for the //! purposes of forwarding channel updates. // TODO: This is mainly provided as a fairly complex example of the current LB // policy in use. Complete tests must be written before it can be used in // production. use std::collections::HashMap; use std::collections::HashSet; use std::error::Error; use std::fmt::Debug; use std::hash::Hash; use std::mem; use std::sync::Arc; use std::sync::Mutex; use crate::client::ConnectivityState; use crate::client::load_balancing::ChannelController; use crate::client::load_balancing::LbConfig; use crate::client::load_balancing::LbPolicy; use crate::client::load_balancing::LbPolicyBuilder; use crate::client::load_balancing::LbPolicyOptions; use crate::client::load_balancing::LbState; use crate::client::load_balancing::Subchannel; use crate::client::load_balancing::SubchannelState; use crate::client::load_balancing::WeakSubchannel; use crate::client::load_balancing::WorkScheduler; use crate::client::name_resolution::Address; use crate::client::name_resolution::ResolverUpdate; use crate::rt::GrpcRuntime; // An LbPolicy implementation that manages multiple children. #[derive(Debug)] pub(crate) struct ChildManager { subchannel_to_child_idx: HashMap, children: Vec>, pending_work: Arc>>, runtime: GrpcRuntime, updated: bool, // Set when any child updates its picker; cleared when accessed. work_scheduler: Arc, } #[non_exhaustive] #[derive(Debug)] pub(crate) struct Child { pub identifier: T, pub builder: Arc, pub state: LbState, policy: Box, work_scheduler: Arc, } /// A collection of data sent to a child of the ChildManager. pub(crate) struct ChildUpdate { /// The identifier the ChildManager should use for this child. pub child_identifier: T, /// The builder the ChildManager should use to create this child if it does /// not exist. The child_policy_builder's name is effectively a part of the /// child_identifier. If two identifiers are identical but have different /// builder names, they are treated as different children. pub child_policy_builder: Arc, /// The relevant ResolverUpdate and LbConfig to send to this child. If /// None, then resolver_update will not be called on the child. Should /// generally be Some for any new children, otherwise they will not be /// called. pub child_update: Option<(ResolverUpdate, Option)>, } impl ChildManager where T: Debug + PartialEq + Hash + Eq + Send + Sync + 'static, { /// Creates a new ChildManager LB policy. shard_update is called whenever a /// resolver_update operation occurs. pub fn new(runtime: GrpcRuntime, work_scheduler: Arc) -> Self { Self { subchannel_to_child_idx: Default::default(), children: Default::default(), pending_work: Default::default(), runtime, work_scheduler, updated: false, } } /// Returns data for all current children. pub fn children(&self) -> impl Iterator> { self.children.iter() } /// Aggregates states from child policies. /// /// If any child is READY then we consider the aggregate state to be READY. /// Otherwise, if any child is CONNECTING, then report CONNECTING. /// Otherwise, if any child is IDLE, then report IDLE. /// Report TRANSIENT FAILURE if no conditions above apply. pub fn aggregate_states(&self) -> ConnectivityState { let mut is_connecting = false; let mut is_idle = false; for child in &self.children { match child.state.connectivity_state { ConnectivityState::Ready => { return ConnectivityState::Ready; } ConnectivityState::Connecting => { is_connecting = true; } ConnectivityState::Idle => { is_idle = true; } ConnectivityState::TransientFailure => {} } } // Decide the new aggregate state if no child is READY. if is_connecting { ConnectivityState::Connecting } else if is_idle { ConnectivityState::Idle } else { ConnectivityState::TransientFailure } } // Called to update all accounting in the ChildManager from operations // performed by a child policy on the WrappedController that was created for // it. child_idx is an index into the children map for the relevant child. // // TODO: this post-processing step can be eliminated by capturing the right // state inside the WrappedController, however it is fairly complex. Decide // which way is better. fn resolve_child_controller( &mut self, channel_controller: WrappedController, child_idx: usize, ) { // Add all created subchannels into the subchannel_child_map. for csc in channel_controller.created_subchannels { self.subchannel_to_child_idx.insert(csc.into(), child_idx); } // Update the tracked state if the child produced an update. if let Some(state) = channel_controller.picker_update { self.children[child_idx].state = state; self.updated = true; }; } /// Returns true if any child has updated its picker since the last call to /// child_updated. pub fn child_updated(&mut self) -> bool { mem::take(&mut self.updated) } /// Retains only the child policies specified by the iterator. /// /// If an ID is provided that does not exist in the ChildManager, it will be /// ignored. pub fn retain_children( &mut self, ids_builders: impl IntoIterator)>, ) { self.reset_children(ids_builders, true); } /// Resets the children and all state related to tracking them in accordance /// with the iterator provided. When retain_only is true, any entry in /// ids_builders that is not in the current set of children will be ignored; /// otherwise a new child will be built for it. fn reset_children( &mut self, ids_builders: impl IntoIterator)>, retain_only: bool, ) { // Hold the lock to prevent new work requests during this operation and // rewrite the indices. let mut pending_work = self.pending_work.lock().unwrap(); // Reset pending work; we will re-add any entries it contains with the // right index later. let old_pending_work = mem::take(&mut *pending_work); // Replace self.children with an empty vec. let old_children = mem::take(&mut self.children); // Replace the subchannel map with an empty map. let old_subchannel_child_map = mem::take(&mut self.subchannel_to_child_idx); // Reverse the old subchannel map into a vector indexed by the old child ID. let mut old_child_subchannels: Vec> = Vec::new(); old_child_subchannels.resize_with(old_children.len(), Vec::new); for (subchannel, old_idx) in old_subchannel_child_map { old_child_subchannels[old_idx].push(subchannel); } // Build a map of the old children from their IDs for efficient lookups. // This leverages a Child to hold all the entries where the // identifier becomes the index within the old self.children vector. let mut old_children: HashMap<(&'static str, T), _> = old_children .into_iter() .enumerate() .map(|(old_idx, e)| { ( (e.builder.name(), e.identifier), Child { identifier: old_idx, policy: e.policy, builder: e.builder, state: e.state, work_scheduler: e.work_scheduler, }, ) }) .collect(); // Transfer children whose identifiers appear before and after the // update, and create new children. Add entries back into the // subchannel map. for (new_idx, (identifier, builder)) in ids_builders.into_iter().enumerate() { let k = (builder.name(), identifier); if let Some(old_child) = old_children.remove(&k) { let old_idx = old_child.identifier; for subchannel in mem::take(&mut old_child_subchannels[old_idx]) { self.subchannel_to_child_idx.insert(subchannel, new_idx); } if old_pending_work.contains(&old_idx) { pending_work.insert(new_idx); } *old_child.work_scheduler.idx.lock().unwrap() = Some(new_idx); self.children.push(Child { builder, identifier: k.1, state: old_child.state, policy: old_child.policy, work_scheduler: old_child.work_scheduler, }); } else if !retain_only { let work_scheduler = Arc::new(ChildWorkScheduler { pending_work: self.pending_work.clone(), idx: Mutex::new(Some(new_idx)), work_scheduler: self.work_scheduler.clone(), }); let policy = builder.build(LbPolicyOptions { work_scheduler: work_scheduler.clone(), runtime: self.runtime.clone(), }); self.children.push(Child { builder, identifier: k.1, state: LbState::initial(), policy, work_scheduler, }); }; } // Invalidate all deleted children's work_schedulers. for (_, old_child) in old_children { old_child.work_scheduler.invalidate(); } // Anything left in old_children will just be Dropped and cleaned up. } /// Updates the ChildManager's children. /// /// `child_updates` is used to determine which children should exist (one /// for each item), how to construct them if they don't already, and what to /// send to their `resolver_update` methods, if anything. Any existing /// children not present in child_updates will be removed. pub fn update( &mut self, child_updates: impl IntoIterator>, channel_controller: &mut dyn ChannelController, ) -> Result<(), Box> { // Split the child updates into the IDs and builders, and the // ResolverUpdates/LbConfigs. let mut errs = vec![]; let (ids_builders, updates): (Vec<_>, Vec<_>) = child_updates .into_iter() .map(|e| ((e.child_identifier, e.child_policy_builder), e.child_update)) .unzip(); self.reset_children(ids_builders, false); // Call resolver_update on all children. let mut updates = updates.into_iter(); for child_idx in 0..self.children.len() { let child = &mut self.children[child_idx]; let child_update = updates.next().unwrap(); let Some((resolver_update, config)) = child_update else { continue; }; let mut channel_controller = WrappedController::new(channel_controller); if let Err(err) = child.policy.resolver_update( resolver_update, config.as_ref(), &mut channel_controller, ) { errs.push(err); } self.resolve_child_controller(channel_controller, child_idx); } if errs.is_empty() { Ok(()) } else { let err = errs .into_iter() .map(|e| e.to_string()) .collect::>() .join("; "); Err(err.into()) } } /// Forwards the `resolver_update` and `config` to all current children. /// /// Returns the Result from calling into each child. pub fn resolver_update( &mut self, resolver_update: ResolverUpdate, config: Option<&LbConfig>, channel_controller: &mut dyn ChannelController, ) -> Result<(), Box> { let mut errs = Vec::with_capacity(self.children.len()); for child_idx in 0..self.children.len() { let child = &mut self.children[child_idx]; let mut channel_controller = WrappedController::new(channel_controller); if let Err(err) = child.policy.resolver_update( resolver_update.clone(), config, &mut channel_controller, ) { errs.push(err); } self.resolve_child_controller(channel_controller, child_idx); } if errs.is_empty() { Ok(()) } else { let err = errs .into_iter() .map(|e| e.to_string()) .collect::>() .join("; "); Err(err.into()) } } /// Forwards the incoming subchannel_update to the child that created the /// subchannel being updated. pub fn subchannel_update( &mut self, subchannel: Arc, state: &SubchannelState, channel_controller: &mut dyn ChannelController, ) { // Determine which child created this subchannel. let child_idx = *self .subchannel_to_child_idx .get(&WeakSubchannel::new(&subchannel)) .unwrap(); let policy = &mut self.children[child_idx].policy; // Wrap the channel_controller to track the child's operations. let mut channel_controller = WrappedController::new(channel_controller); // Call the proper child. policy.subchannel_update(subchannel, state, &mut channel_controller); self.resolve_child_controller(channel_controller, child_idx); } /// Calls work on any children that scheduled work via the work scheduler. pub fn work(&mut self, channel_controller: &mut dyn ChannelController) { let child_idxes = mem::take(&mut *self.pending_work.lock().unwrap()); for child_idx in child_idxes { let mut channel_controller = WrappedController::new(channel_controller); self.children[child_idx] .policy .work(&mut channel_controller); self.resolve_child_controller(channel_controller, child_idx); } } /// Calls exit_idle on all children. pub fn exit_idle(&mut self, channel_controller: &mut dyn ChannelController) { for child_idx in 0..self.children.len() { let child = &mut self.children[child_idx]; let mut channel_controller = WrappedController::new(channel_controller); child.policy.exit_idle(&mut channel_controller); self.resolve_child_controller(channel_controller, child_idx); } } } struct WrappedController<'a> { channel_controller: &'a mut dyn ChannelController, created_subchannels: Vec>, picker_update: Option, } impl<'a> WrappedController<'a> { fn new(channel_controller: &'a mut dyn ChannelController) -> Self { Self { channel_controller, created_subchannels: vec![], picker_update: None, } } } impl ChannelController for WrappedController<'_> { fn new_subchannel(&mut self, address: &Address) -> Arc { let subchannel = self.channel_controller.new_subchannel(address); self.created_subchannels.push(subchannel.clone()); subchannel } fn update_picker(&mut self, update: LbState) { self.picker_update = Some(update); } fn request_resolution(&mut self) { self.channel_controller.request_resolution(); } } #[derive(Debug)] struct ChildWorkScheduler { work_scheduler: Arc, // The real work scheduler of the channel. pending_work: Arc>>, // Must be taken first for correctness idx: Mutex>, // None if the child is deleted. } impl WorkScheduler for ChildWorkScheduler { fn schedule_work(&self) { let mut pending_work = self.pending_work.lock().unwrap(); // If self.idx is None then this WorkScheduler has been invalidated as // it is associated with a deleted child; do nothing in that case. if let Some(idx) = *self.idx.lock().unwrap() { pending_work.insert(idx); self.work_scheduler.schedule_work(); } } } impl ChildWorkScheduler { // Sets the ChildWorkScheduler so that it will not honor future // schedule_work requests. fn invalidate(&self) { *self.idx.lock().unwrap() = None; } } #[cfg(test)] mod test { use crate::client::ConnectivityState; use crate::client::load_balancing::ChannelController; use crate::client::load_balancing::GLOBAL_LB_REGISTRY; use crate::client::load_balancing::LbPolicyBuilder; use crate::client::load_balancing::LbState; use crate::client::load_balancing::QueuingPicker; use crate::client::load_balancing::Subchannel; use crate::client::load_balancing::SubchannelState; use crate::client::load_balancing::child_manager::ChildManager; use crate::client::load_balancing::child_manager::ChildUpdate; use crate::client::load_balancing::test_utils::StubPolicyFuncs; use crate::client::load_balancing::test_utils::TestChannelController; use crate::client::load_balancing::test_utils::TestEvent; use crate::client::load_balancing::test_utils::TestWorkScheduler; use crate::client::load_balancing::test_utils::{self}; use crate::client::name_resolution::Address; use crate::client::name_resolution::Endpoint; use crate::client::name_resolution::ResolverUpdate; use crate::client::service_config::LbConfig; use crate::rt::default_runtime; use std::collections::HashMap; use std::error::Error; use std::panic; use std::sync::Arc; use std::sync::Mutex; use tokio::sync::mpsc; // Sets up the test environment. // // Performs the following: // 1. Creates a work scheduler. // 2. Creates a fake channel that acts as a channel controller. // 3. Creates an StubPolicyBuilder with StubFuncs that each test will define // and name of the test. // 4. Creates an EndpointSharder with StubPolicyBuilder passed in as the // child policy. // 5. Creates a ChildManager with the EndpointSharder. // // Returns the following: // 1. A receiver for events initiated by the LB policy (like creating a new // subchannel, sending a new picker etc). // 2. The ChildManager to send resolver and subchannel updates from the // test. // 3. The controller to pass to the LB policy as part of the updates. fn setup( funcs: StubPolicyFuncs, test_name: &'static str, ) -> ( mpsc::UnboundedReceiver, ChildManager, Box, ) { test_utils::reg_stub_policy(test_name, funcs); let (tx_events, rx_events) = mpsc::unbounded_channel::(); let tcc = Box::new(TestChannelController { tx_events: tx_events.clone(), }); let child_manager = ChildManager::new(default_runtime(), Arc::new(TestWorkScheduler { tx_events })); (rx_events, child_manager, tcc) } fn create_n_endpoints_with_k_addresses(n: usize, k: usize) -> Vec { let mut endpoints = Vec::with_capacity(n); for i in 0..n { let mut addresses: Vec
= Vec::with_capacity(k); for j in 0..k { addresses.push(Address { address: format!("{}.{}.{}.{}:{}", i + 1, i + 1, i + 1, i + 1, j).into(), ..Default::default() }); } endpoints.push(Endpoint { addresses, ..Default::default() }) } endpoints } // Sends a resolver update to the LB policy with the specified endpoint. fn send_resolver_update_to_policy( child_manager: &mut ChildManager, endpoints: Vec, builder: Arc, tcc: &mut dyn ChannelController, ) -> Result<(), Box> { let updates = endpoints.iter().map(|e| ChildUpdate { child_identifier: e.clone(), child_policy_builder: builder.clone(), child_update: Some(( ResolverUpdate { attributes: crate::attributes::Attributes::default(), endpoints: Ok(vec![e.clone()]), service_config: Ok(None), resolution_note: None, }, None, )), }); child_manager.update(updates, tcc) } fn move_subchannel_to_state( child_manager: &mut ChildManager, subchannel: Arc, tcc: &mut dyn ChannelController, state: ConnectivityState, ) { child_manager.subchannel_update( subchannel, &SubchannelState { connectivity_state: state, ..Default::default() }, tcc, ); } // Verifies that the expected number of subchannels is created. Returns the // subchannels created. async fn verify_subchannel_creation_from_policy( rx_events: &mut mpsc::UnboundedReceiver, number_of_subchannels: usize, ) -> Vec> { let mut subchannels = Vec::new(); for _ in 0..number_of_subchannels { match rx_events.recv().await.unwrap() { TestEvent::NewSubchannel(sc) => { subchannels.push(sc); } other => panic!("unexpected event {:?}", other), }; } subchannels } // Defines the functions resolver_update and subchannel_update to test // aggregate_states. fn create_verifying_funcs_for_aggregate_tests() -> StubPolicyFuncs { StubPolicyFuncs { // Closure for resolver_update. resolver_update should only receive // one endpoint and create one subchannel for the endpoint it // receives. resolver_update: Some(Arc::new( move |data, update: ResolverUpdate, _, controller| { assert_eq!(update.endpoints.iter().len(), 1); let endpoint = update.endpoints.unwrap().pop().unwrap(); let subchannel = controller.new_subchannel(&endpoint.addresses[0]); Ok(()) }, )), // Closure for subchannel_update. Sends a picker of the same state // that was passed to it. subchannel_update: Some(Arc::new( move |data, updated_subchannel, state, controller| { controller.update_picker(LbState { connectivity_state: state.connectivity_state, picker: Arc::new(QueuingPicker {}), }); }, )), work: None, } } // Tests the scenario where one child is READY and the rest are in // CONNECTING, IDLE, or TRANSIENT FAILURE. The child manager's // aggregate_states function should report READY. #[tokio::test] async fn childmanager_aggregate_state_is_ready_if_any_child_is_ready() { let test_name = "stub-childmanager_aggregate_state_is_ready_if_any_child_is_ready"; let (mut rx_events, mut child_manager, mut tcc) = setup(create_verifying_funcs_for_aggregate_tests(), test_name); let builder: Arc = GLOBAL_LB_REGISTRY.get_policy(test_name).unwrap(); let endpoints = create_n_endpoints_with_k_addresses(4, 1); send_resolver_update_to_policy( &mut child_manager, endpoints.clone(), builder, tcc.as_mut(), ) .unwrap(); let mut subchannels = vec![]; for endpoint in endpoints { subchannels.push( verify_subchannel_creation_from_policy(&mut rx_events, endpoint.addresses.len()) .await .remove(0), ); } let mut subchannels = subchannels.into_iter(); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::TransientFailure, ); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::Idle, ); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::Connecting, ); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::Ready, ); assert_eq!(child_manager.aggregate_states(), ConnectivityState::Ready); } // Tests the scenario where no children are READY and the children are in // CONNECTING, IDLE, or TRANSIENT FAILURE. The child manager's // aggregate_states function should report CONNECTING. #[tokio::test] async fn childmanager_aggregate_state_is_connecting_if_no_child_is_ready() { let test_name = "stub-childmanager_aggregate_state_is_connecting_if_no_child_is_ready"; let (mut rx_events, mut child_manager, mut tcc) = setup(create_verifying_funcs_for_aggregate_tests(), test_name); let builder: Arc = GLOBAL_LB_REGISTRY.get_policy(test_name).unwrap(); let endpoints = create_n_endpoints_with_k_addresses(3, 1); send_resolver_update_to_policy( &mut child_manager, endpoints.clone(), builder, tcc.as_mut(), ) .unwrap(); let mut subchannels = vec![]; for endpoint in endpoints { subchannels.push( verify_subchannel_creation_from_policy(&mut rx_events, endpoint.addresses.len()) .await .remove(0), ); } let mut subchannels = subchannels.into_iter(); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::TransientFailure, ); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::Idle, ); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::Connecting, ); assert_eq!( child_manager.aggregate_states(), ConnectivityState::Connecting ); } // Tests the scenario where no children are READY or CONNECTING and the // children are in IDLE, or TRANSIENT FAILURE. The child manager's // aggregate_states function should report IDLE. #[tokio::test] async fn childmanager_aggregate_state_is_idle_if_only_idle_and_failure() { let test_name = "stub-childmanager_aggregate_state_is_idle_if_only_idle_and_failure"; let (mut rx_events, mut child_manager, mut tcc) = setup(create_verifying_funcs_for_aggregate_tests(), test_name); let builder: Arc = GLOBAL_LB_REGISTRY.get_policy(test_name).unwrap(); let endpoints = create_n_endpoints_with_k_addresses(2, 1); send_resolver_update_to_policy( &mut child_manager, endpoints.clone(), builder, tcc.as_mut(), ) .unwrap(); let mut subchannels = vec![]; for endpoint in endpoints { subchannels.push( verify_subchannel_creation_from_policy(&mut rx_events, endpoint.addresses.len()) .await .remove(0), ); } let mut subchannels = subchannels.into_iter(); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::TransientFailure, ); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::Idle, ); assert_eq!(child_manager.aggregate_states(), ConnectivityState::Idle); } // Tests the scenario where no children are READY, CONNECTING, or IDLE and // all children are in TRANSIENT FAILURE. The child manager's // aggregate_states function should report TRANSIENT FAILURE. #[tokio::test] async fn childmanager_aggregate_state_is_transient_failure_if_all_children_are() { let test_name = "stub-childmanager_aggregate_state_is_transient_failure_if_all_children_are"; let (mut rx_events, mut child_manager, mut tcc) = setup(create_verifying_funcs_for_aggregate_tests(), test_name); let builder: Arc = GLOBAL_LB_REGISTRY.get_policy(test_name).unwrap(); let endpoints = create_n_endpoints_with_k_addresses(2, 1); send_resolver_update_to_policy( &mut child_manager, endpoints.clone(), builder, tcc.as_mut(), ) .unwrap(); let mut subchannels = vec![]; for endpoint in endpoints { subchannels.push( verify_subchannel_creation_from_policy(&mut rx_events, endpoint.addresses.len()) .await .remove(0), ); } let mut subchannels = subchannels.into_iter(); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::TransientFailure, ); move_subchannel_to_state( &mut child_manager, subchannels.next().unwrap(), tcc.as_mut(), ConnectivityState::TransientFailure, ); assert_eq!( child_manager.aggregate_states(), ConnectivityState::TransientFailure ); } struct ScheduleWorkStubData { requested_work: bool, } fn create_funcs_for_schedule_work_tests(name: &'static str) -> StubPolicyFuncs { StubPolicyFuncs { resolver_update: Some(Arc::new(move |data, _update, lbcfg, _controller| { if data.test_data.is_none() { data.test_data = Some(Box::new(ScheduleWorkStubData { requested_work: false, })); } let stubdata = data .test_data .as_mut() .unwrap() .downcast_mut::() .unwrap(); assert!(!stubdata.requested_work); if lbcfg .unwrap() .convert_to::>>() .unwrap() .lock() .unwrap() .contains_key(name) { stubdata.requested_work = true; data.lb_policy_options.work_scheduler.schedule_work(); } Ok(()) })), subchannel_update: None, work: Some(Arc::new(move |data, _controller| { println!("work called for {name}"); let stubdata = data .test_data .as_mut() .unwrap() .downcast_mut::() .unwrap(); stubdata.requested_work = false; })), } } // Tests that the child manager properly delegates to the children that // called schedule_work when work is called. #[tokio::test] async fn childmanager_schedule_work_works() { let name1 = "childmanager_schedule_work_works-one"; let name2 = "childmanager_schedule_work_works-two"; test_utils::reg_stub_policy(name1, create_funcs_for_schedule_work_tests(name1)); test_utils::reg_stub_policy(name2, create_funcs_for_schedule_work_tests(name2)); let (tx_events, mut rx_events) = mpsc::unbounded_channel::(); let mut tcc = TestChannelController { tx_events: tx_events.clone(), }; let names = [name1, name2]; let mut child_manager = ChildManager::new(default_runtime(), Arc::new(TestWorkScheduler { tx_events })); // Request that child one requests work. let cfg = LbConfig::new(Mutex::new(HashMap::<&'static str, ()>::new())); let children = cfg .convert_to::>>() .unwrap(); children.lock().unwrap().insert(name1, ()); let updates = names.iter().map(|name| { let child_policy_builder: Arc = GLOBAL_LB_REGISTRY.get_policy(name).unwrap(); ChildUpdate { child_identifier: (), child_policy_builder, child_update: Some((ResolverUpdate::default(), Some(cfg.clone()))), } }); child_manager.update(updates.clone(), &mut tcc).unwrap(); // Confirm that child one has requested work. match rx_events.recv().await.unwrap() { TestEvent::ScheduleWork => {} other => panic!("unexpected event {:?}", other), }; assert_eq!(child_manager.pending_work.lock().unwrap().len(), 1); let idx = *child_manager .pending_work .lock() .unwrap() .iter() .next() .unwrap(); assert_eq!(child_manager.children[idx].builder.name(), name1); // Perform the work call and assert the pending_work set is empty. child_manager.work(&mut tcc); assert_eq!(child_manager.pending_work.lock().unwrap().len(), 0); // Now have both children request work. children.lock().unwrap().insert(name2, ()); child_manager.update(updates.clone(), &mut tcc).unwrap(); // Confirm that both children requested work. match rx_events.recv().await.unwrap() { TestEvent::ScheduleWork => {} other => panic!("unexpected event {:?}", other), }; assert_eq!(child_manager.pending_work.lock().unwrap().len(), 2); // Perform the work call and assert the pending_work set is empty. child_manager.work(&mut tcc); assert_eq!(child_manager.pending_work.lock().unwrap().len(), 0); // Perform one final call to resolver_update which asserts that both // child policies had their work methods called. child_manager.update(updates, &mut tcc).unwrap(); } } ================================================ FILE: grpc/src/client/load_balancing/graceful_switch.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::collections::HashMap; use std::error::Error; use std::sync::Arc; use crate::client::ConnectivityState; use crate::client::load_balancing::ChannelController; use crate::client::load_balancing::GLOBAL_LB_REGISTRY; use crate::client::load_balancing::LbConfig; use crate::client::load_balancing::LbPolicy; use crate::client::load_balancing::LbPolicyBuilder; use crate::client::load_balancing::LbState; use crate::client::load_balancing::ParsedJsonLbConfig; use crate::client::load_balancing::Subchannel; use crate::client::load_balancing::SubchannelState; use crate::client::load_balancing::WorkScheduler; use crate::client::load_balancing::child_manager::ChildManager; use crate::client::load_balancing::child_manager::ChildUpdate; use crate::client::name_resolution::ResolverUpdate; use crate::rt::GrpcRuntime; #[derive(Debug, Clone)] struct GracefulSwitchLbConfig { child_builder: Arc, child_config: Option, } /// A graceful switching load balancing policy. In graceful switch, there is /// always either one or two child policies. When there is one policy, all /// operations are delegated to it. When the child policy type needs to change, /// graceful switch creates a "pending" child policy alongside the "active" /// policy. When the pending policy leaves the CONNECTING state, or when the /// active policy is not READY, graceful switch will promote the pending policy /// to to active and tear down the previously active policy. #[derive(Debug)] pub(crate) struct GracefulSwitchPolicy { child_manager: ChildManager<()>, // Child ID empty - only the name of the child LB policy matters. last_update: Option, // Saves the last output LbState to determine if an update is needed. active_child_builder: Option>, } impl LbPolicy for GracefulSwitchPolicy { fn resolver_update( &mut self, update: ResolverUpdate, config: Option<&LbConfig>, channel_controller: &mut dyn ChannelController, ) -> Result<(), Box> { let config = config .ok_or("graceful switch received no config")? .convert_to::() .ok_or_else(|| format!("invalid config: {config:?}"))?; if self.active_child_builder.is_none() { // When there are no children yet, the current update immediately // becomes the active child. self.active_child_builder = Some(config.child_builder.clone()); } let active_child_builder = self.active_child_builder.as_ref().unwrap(); let mut children = Vec::with_capacity(2); // Always include the incoming update. children.push(ChildUpdate { child_policy_builder: config.child_builder.clone(), child_identifier: (), child_update: Some((update, config.child_config.clone())), }); // Include the active child if it does not match the updated child so // that the child manager will not delete it. if config.child_builder.name() != active_child_builder.name() { children.push(ChildUpdate { child_policy_builder: active_child_builder.clone(), child_identifier: (), child_update: None, }); } let res = self.child_manager.update(children, channel_controller); self.update_picker(channel_controller); res } fn subchannel_update( &mut self, subchannel: Arc, state: &SubchannelState, channel_controller: &mut dyn ChannelController, ) { self.child_manager .subchannel_update(subchannel, state, channel_controller); self.update_picker(channel_controller); } fn work(&mut self, channel_controller: &mut dyn ChannelController) { self.child_manager.work(channel_controller); self.update_picker(channel_controller); } fn exit_idle(&mut self, channel_controller: &mut dyn ChannelController) { self.child_manager.exit_idle(channel_controller); self.update_picker(channel_controller); } } #[derive(Debug, PartialEq, Eq, Clone)] enum ChildKind { Current, Pending, } impl GracefulSwitchPolicy { /// Creates a new Graceful Switch policy. pub fn new(runtime: GrpcRuntime, work_scheduler: Arc) -> Self { GracefulSwitchPolicy { child_manager: ChildManager::new(runtime, work_scheduler), last_update: None, active_child_builder: None, } } /// Parses a child config list and returns a LB config for the /// GracefulSwitchPolicy. Config is expected to contain a JSON array of LB /// policy names + configs matching the format of the "loadBalancingConfig" /// field in the gRPC ServiceConfig. It returns a type that should be passed /// to resolver_update in the LbConfig.config field. pub fn parse_config( config: &ParsedJsonLbConfig, ) -> Result> { let cfg: Vec> = match config.convert_to() { Ok(c) => c, Err(e) => { return Err(format!("failed to parse JSON config: {}", e).into()); } }; for c in cfg { if c.len() != 1 { return Err(format!( "Each element in array must contain exactly one policy name/config; found {:?}", c.keys() ) .into()); } let (policy_name, policy_config) = c.into_iter().next().unwrap(); let Some(child_builder) = GLOBAL_LB_REGISTRY.get_policy(policy_name.as_str()) else { continue; }; let parsed_config = ParsedJsonLbConfig { value: policy_config, }; let child_config = child_builder.parse_config(&parsed_config)?; let gsb_config = GracefulSwitchLbConfig { child_builder, child_config, }; return Ok(LbConfig::new(gsb_config)); } Err("no supported policies found in config".into()) } fn update_picker(&mut self, channel_controller: &mut dyn ChannelController) { // If maybe_swap returns a None, then no update needs to happen. let Some(update) = self.maybe_swap(channel_controller) else { return; }; // If the current update is the same as the last update, skip it. if self.last_update.as_ref().is_some_and(|lu| lu == &update) { return; } channel_controller.update_picker(update.clone()); self.last_update = Some(update); } // Determines the appropriate state to output fn maybe_swap(&mut self, channel_controller: &mut dyn ChannelController) -> Option { // If no child updated itself, there is nothing we can do. if !self.child_manager.child_updated() { return None; } // If resolver_update has never been called, we have no children, so // there's nothing we can do. let Some(active_child_builder) = &self.active_child_builder else { return None; }; let active_name = active_child_builder.name(); // Scan through the child manager's children for the active and // (optional) pending child. let mut active_child = None; let mut pending_child = None; for child in self.child_manager.children() { if child.builder.name() == active_name { active_child = Some(child); } else { pending_child = Some(child); } } let active_child = active_child.expect("There should always be an active child policy"); // If no pending child exists, we will update the active child's state. let Some(pending_child) = pending_child else { return Some(active_child.state.clone()); }; // If the active child is still reading and the pending child is still // connecting, keep using the active child's state. if active_child.state.connectivity_state == ConnectivityState::Ready && pending_child.state.connectivity_state == ConnectivityState::Connecting { return Some(active_child.state.clone()); } // Transition to the pending child and remove the active child. // Clone some things from child_manager.children to release the // child_manager reference. let pending_child_builder = pending_child.builder.clone(); let pending_state = pending_child.state.clone(); self.active_child_builder = Some(pending_child_builder.clone()); self.child_manager .retain_children([((), pending_child_builder)]); Some(pending_state) } } #[cfg(test)] mod test { use crate::client::ConnectivityState; use crate::client::load_balancing::ChannelController; use crate::client::load_balancing::LbPolicy; use crate::client::load_balancing::LbState; use crate::client::load_balancing::ParsedJsonLbConfig; use crate::client::load_balancing::Pick; use crate::client::load_balancing::PickResult; use crate::client::load_balancing::Picker; use crate::client::load_balancing::Subchannel; use crate::client::load_balancing::SubchannelState; use crate::client::load_balancing::graceful_switch::GracefulSwitchPolicy; use crate::client::load_balancing::test_utils::StubPolicyData; use crate::client::load_balancing::test_utils::StubPolicyFuncs; use crate::client::load_balancing::test_utils::TestChannelController; use crate::client::load_balancing::test_utils::TestEvent; use crate::client::load_balancing::test_utils::TestSubchannel; use crate::client::load_balancing::test_utils::TestWorkScheduler; use crate::client::load_balancing::test_utils::reg_stub_policy; use crate::client::load_balancing::test_utils::{self}; use crate::client::name_resolution::Address; use crate::client::name_resolution::Endpoint; use crate::client::name_resolution::ResolverUpdate; use crate::core::RequestHeaders; use crate::rt::default_runtime; use std::panic; use std::sync::Arc; use std::time::Duration; use tokio::select; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::{self}; use tonic::metadata::MetadataMap; const DEFAULT_TEST_SHORT_TIMEOUT: Duration = Duration::from_millis(10); struct TestSubchannelList { subchannels: Vec>, } impl TestSubchannelList { fn new(addresses: &Vec
, channel_controller: &mut dyn ChannelController) -> Self { let mut scl = TestSubchannelList { subchannels: Vec::new(), }; for address in addresses { let sc = channel_controller.new_subchannel(address); scl.subchannels.push(sc.clone()); } scl } fn contains(&self, sc: &Arc) -> bool { self.subchannels.contains(sc) } } #[derive(Debug)] struct TestPicker { name: &'static str, } impl TestPicker { fn new(name: &'static str) -> Self { Self { name } } } impl Picker for TestPicker { fn pick(&self, _req: &RequestHeaders) -> PickResult { PickResult::Pick(Pick { subchannel: Arc::new(TestSubchannel::new( Address { address: self.name.to_string().into(), ..Default::default() }, mpsc::unbounded_channel().0, )), metadata: MetadataMap::new(), on_complete: None, }) } } struct TestState { subchannel_list: TestSubchannelList, } // Defines the functions resolver_update and subchannel_update to test graceful switch fn create_funcs_for_gracefulswitch_tests(name: &'static str) -> StubPolicyFuncs { StubPolicyFuncs { // Closure for resolver_update. It creates a subchannel for the // endpoint it receives and stores which endpoint it received and // which subchannel this child created in the data field. resolver_update: Some(Arc::new( move |data: &mut StubPolicyData, update: ResolverUpdate, _, channel_controller| { if let Ok(ref endpoints) = update.endpoints { let addresses: Vec<_> = endpoints .iter() .flat_map(|ep| ep.addresses.clone()) .collect(); let scl = TestSubchannelList::new(&addresses, channel_controller); let child_state = TestState { subchannel_list: scl, }; data.test_data = Some(Box::new(child_state)); } else { data.test_data = None; } Ok(()) }, )), // Closure for subchannel_update. Verify that the subchannel that // being updated now is the same one that this child policy created // in resolver_update. It then sends a picker of the same state that // was passed to it. subchannel_update: Some(Arc::new( move |data: &mut StubPolicyData, updated_subchannel, state, channel_controller| { // Retrieve the specific TestState from the generic test_data field. // This downcasts the `Any` trait object. let test_data = data.test_data.as_mut().unwrap(); let test_state = test_data.downcast_mut::().unwrap(); let scl = &mut test_state.subchannel_list; assert!( scl.contains(&updated_subchannel), "subchannel_update received an update for a subchannel it does not own." ); channel_controller.update_picker(LbState { connectivity_state: state.connectivity_state, picker: Arc::new(TestPicker { name }), }); }, )), work: None, } } // Sets up the test environment. // // Performs the following: // 1. Creates a work scheduler. // 2. Creates a fake channel that acts as a channel controller. // 3. Creates an StubPolicyBuilder with StubFuncs that each test will define // and name of the test. // 5. Creates a GracefulSwitch. // // Returns the following: // 1. A receiver for events initiated by the LB policy (like creating a new // subchannel, sending a new picker etc). // 2. The GracefulSwitch to send resolver and subchannel updates from the // test. // 3. The controller to pass to the LB policy as part of the updates. fn setup() -> ( mpsc::UnboundedReceiver, Box, Box, ) { let (tx_events, rx_events) = mpsc::unbounded_channel::(); let work_scheduler = Arc::new(TestWorkScheduler { tx_events: tx_events.clone(), }); let tcc = Box::new(TestChannelController { tx_events: tx_events.clone(), }); let graceful_switch = GracefulSwitchPolicy::new(default_runtime(), Arc::new(TestWorkScheduler { tx_events })); (rx_events, Box::new(graceful_switch), tcc) } fn create_endpoint_with_one_address(addr: String) -> Endpoint { Endpoint { addresses: vec![Address { address: addr.into(), ..Default::default() }], ..Default::default() } } // Verifies that the next event on rx_events channel is NewSubchannel. // Returns the subchannel created. async fn verify_subchannel_creation_from_policy( rx_events: &mut mpsc::UnboundedReceiver, ) -> Arc { match rx_events.recv().await.unwrap() { TestEvent::NewSubchannel(sc) => sc, other => panic!("unexpected event {:?}", other), } } // Verifies that the channel moves to READY state with a picker that returns the // given subchannel. // // Returns the picker for tests to make more picks, if required. async fn verify_correct_picker_from_policy( rx_events: &mut mpsc::UnboundedReceiver, name: &str, ) { println!("verify ready picker"); let event = rx_events.recv().await.unwrap(); let TestEvent::UpdatePicker(update) = event else { panic!("unexpected event {:?}", event); }; let req = test_utils::new_request_headers(); println!("{:?}", update.connectivity_state); let pick = update.picker.pick(&req); let PickResult::Pick(pick) = pick else { panic!("unexpected pick result: {:?}", pick); }; let received_address = &pick.subchannel.address().address.to_string(); // It's good practice to create the expected value once. let expected_address = name.to_string(); // Check for inequality and panic with a detailed message if they don't match. assert_eq!(received_address, &expected_address); } fn move_subchannel_to_state( lb_policy: &mut dyn LbPolicy, subchannel: Arc, tcc: &mut dyn ChannelController, state: ConnectivityState, ) { lb_policy.subchannel_update( subchannel, &SubchannelState { connectivity_state: state, ..Default::default() }, tcc, ); } // Tests that the gracefulswitch policy correctly sets a child and sends // updates to that child when it receives its first config. #[tokio::test] async fn gracefulswitch_successful_first_update() { reg_stub_policy( "stub-gracefulswitch_successful_first_update-one", create_funcs_for_gracefulswitch_tests( "stub-gracefulswitch_successful_first_update-one", ), ); reg_stub_policy( "stub-gracefulswitch_successful_first_update-two", create_funcs_for_gracefulswitch_tests( "stub-gracefulswitch_successful_first_update-two", ), ); let (mut rx_events, mut graceful_switch, mut tcc) = setup(); let service_config = serde_json::json!([ { "stub-gracefulswitch_successful_first_update-one": serde_json::json!({}) }, { "stub-gracefulswitch_successful_first_update-two": serde_json::json!({}) } ] ); let parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: service_config, }) .unwrap(); let endpoint = create_endpoint_with_one_address("127.0.0.1:1234".to_string()); let update = ResolverUpdate { endpoints: Ok(vec![endpoint.clone()]), ..Default::default() }; graceful_switch .resolver_update(update.clone(), Some(&parsed_config), &mut *tcc) .unwrap(); let subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, subchannel, tcc.as_mut(), ConnectivityState::Ready, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_successful_first_update-one", ) .await; } // Tests that the gracefulswitch policy correctly sets a pending child and // sends subchannel updates to that child when it receives a new config. #[tokio::test] async fn gracefulswitch_switching_to_resolver_update() { let (mut rx_events, mut graceful_switch, mut tcc) = setup(); reg_stub_policy( "stub-gracefulswitch_switching_to_resolver_update-one", create_funcs_for_gracefulswitch_tests( "stub-gracefulswitch_switching_to_resolver_update-one", ), ); reg_stub_policy( "stub-gracefulswitch_switching_to_resolver_update-two", create_funcs_for_gracefulswitch_tests( "stub-gracefulswitch_switching_to_resolver_update-two", ), ); let service_config = serde_json::json!([ { "stub-gracefulswitch_switching_to_resolver_update-one": serde_json::json!({}) } ] ); let parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: service_config, }) .unwrap(); let endpoint = create_endpoint_with_one_address("127.0.0.1:1234".to_string()); let update = ResolverUpdate { endpoints: Ok(vec![endpoint.clone()]), ..Default::default() }; graceful_switch .resolver_update(update.clone(), Some(&parsed_config), &mut *tcc) .unwrap(); // Subchannel creation and ready let subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, subchannel, tcc.as_mut(), ConnectivityState::Ready, ); // Assert picker is TestPickerOne by checking subchannel address verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_switching_to_resolver_update-one", ) .await; // 2. Switch to mock_policy_two as pending let new_service_config = serde_json::json!([ { "stub-gracefulswitch_switching_to_resolver_update-two": serde_json::json!({}) } ] ); let new_parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: new_service_config, }) .unwrap(); graceful_switch .resolver_update(update.clone(), Some(&new_parsed_config), &mut *tcc) .unwrap(); // Simulate subchannel creation and ready for pending let subchannel_two = verify_subchannel_creation_from_policy(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, subchannel_two, tcc.as_mut(), ConnectivityState::Ready, ); // Assert picker is TestPickerTwo by checking subchannel address verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_switching_to_resolver_update-two", ) .await; assert_channel_empty(&mut rx_events).await; } async fn assert_channel_empty(rx_events: &mut UnboundedReceiver) { select! { event = rx_events.recv() => { panic!("Received unexpected event from policy: {event:?}"); } _ = tokio::time::sleep(DEFAULT_TEST_SHORT_TIMEOUT) => {} }; } // Tests that the gracefulswitch policy should do nothing when it receives a // new config of the same policy that it received before. #[tokio::test] async fn gracefulswitch_two_policies_same_type() { let (mut rx_events, mut graceful_switch, mut tcc) = setup(); reg_stub_policy( "stub-gracefulswitch_two_policies_same_type-one", create_funcs_for_gracefulswitch_tests("stub-gracefulswitch_two_policies_same_type-one"), ); let service_config = serde_json::json!( [ { "stub-gracefulswitch_two_policies_same_type-one": serde_json::json!({}) } ] ); let parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: service_config, }) .unwrap(); let endpoint = create_endpoint_with_one_address("127.0.0.1:1234".to_string()); let update = ResolverUpdate { endpoints: Ok(vec![endpoint.clone()]), ..Default::default() }; graceful_switch .resolver_update(update.clone(), Some(&parsed_config), &mut *tcc) .unwrap(); let subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, subchannel, tcc.as_mut(), ConnectivityState::Ready, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_two_policies_same_type-one", ) .await; let service_config2 = serde_json::json!( [ { "stub-gracefulswitch_two_policies_same_type-one": serde_json::json!({}) } ] ); let parsed_config2 = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: service_config2, }) .unwrap(); graceful_switch .resolver_update(update.clone(), Some(&parsed_config2), &mut *tcc) .unwrap(); let subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; assert_eq!(&*subchannel.address().address, "127.0.0.1:1234"); assert_channel_empty(&mut rx_events).await; } // Tests that the gracefulswitch policy should replace the current child // with the pending child if the current child isn't ready. #[tokio::test] async fn gracefulswitch_current_not_ready_pending_update() { let (mut rx_events, mut graceful_switch, mut tcc) = setup(); reg_stub_policy( "stub-gracefulswitch_current_not_ready_pending_update-one", create_funcs_for_gracefulswitch_tests( "stub-gracefulswitch_current_not_ready_pending_update-one", ), ); reg_stub_policy( "stub-gracefulswitch_current_not_ready_pending_update-two", create_funcs_for_gracefulswitch_tests( "stub-gracefulswitch_current_not_ready_pending_update-two", ), ); let service_config = serde_json::json!([ { "stub-gracefulswitch_current_not_ready_pending_update-one": serde_json::json!({}) } ] ); let parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: service_config, }) .unwrap(); let endpoint = create_endpoint_with_one_address("127.0.0.1:1234".to_string()); let second_endpoint = create_endpoint_with_one_address("0.0.0.0.0".to_string()); let update = ResolverUpdate { endpoints: Ok(vec![endpoint.clone()]), ..Default::default() }; // Switch to first one (current) graceful_switch .resolver_update(update.clone(), Some(&parsed_config), &mut *tcc) .unwrap(); let current_subchannels = verify_subchannel_creation_from_policy(&mut rx_events).await; assert_channel_empty(&mut rx_events).await; let new_service_config = serde_json::json!([ { "stub-gracefulswitch_current_not_ready_pending_update-two": serde_json::json!({ "shuffleAddressList": false }) }, ] ); let second_update = ResolverUpdate { endpoints: Ok(vec![second_endpoint.clone()]), ..Default::default() }; let new_parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: new_service_config, }) .unwrap(); graceful_switch .resolver_update(second_update.clone(), Some(&new_parsed_config), &mut *tcc) .unwrap(); let second_subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; assert_channel_empty(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, second_subchannel, tcc.as_mut(), ConnectivityState::Ready, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_current_not_ready_pending_update-two", ) .await; assert_channel_empty(&mut rx_events).await; } // Tests that the gracefulswitch policy should replace the current child // with the pending child if the current child was ready but then leaves ready. #[tokio::test] async fn gracefulswitch_current_leaving_ready() { let (mut rx_events, mut graceful_switch, mut tcc) = setup(); reg_stub_policy( "stub-gracefulswitch_current_leaving_ready-one", create_funcs_for_gracefulswitch_tests("stub-gracefulswitch_current_leaving_ready-one"), ); reg_stub_policy( "stub-gracefulswitch_current_leaving_ready-two", create_funcs_for_gracefulswitch_tests("stub-gracefulswitch_current_leaving_ready-two"), ); let service_config = serde_json::json!([ { "stub-gracefulswitch_current_leaving_ready-one": serde_json::json!({}) } ] ); let parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: service_config, }) .unwrap(); let endpoint = create_endpoint_with_one_address("127.0.0.1:1234".to_string()); let endpoint2 = create_endpoint_with_one_address("127.0.0.1:1235".to_string()); let update = ResolverUpdate { endpoints: Ok(vec![endpoint.clone()]), ..Default::default() }; // Switch to first one (current) graceful_switch .resolver_update(update.clone(), Some(&parsed_config), &mut *tcc) .unwrap(); let current_subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, current_subchannel.clone(), tcc.as_mut(), ConnectivityState::Ready, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_current_leaving_ready-one", ) .await; let new_service_config = serde_json::json!( [ { "stub-gracefulswitch_current_leaving_ready-two": serde_json::json!({}) }, ] ); let new_update = ResolverUpdate { endpoints: Ok(vec![endpoint2.clone()]), ..Default::default() }; let new_parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: new_service_config, }) .unwrap(); graceful_switch .resolver_update(new_update.clone(), Some(&new_parsed_config), &mut *tcc) .unwrap(); let pending_subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, pending_subchannel, tcc.as_mut(), ConnectivityState::Connecting, ); // This should not produce an update. assert_channel_empty(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, current_subchannel, tcc.as_mut(), ConnectivityState::Connecting, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_current_leaving_ready-two", ) .await; } // Tests that the gracefulswitch policy should replace the current child // with the pending child if the pending child leaves connecting. #[tokio::test] async fn gracefulswitch_pending_leaving_connecting() { let (mut rx_events, mut graceful_switch, mut tcc) = setup(); reg_stub_policy( "stub-gracefulswitch_current_leaving_ready-one", create_funcs_for_gracefulswitch_tests("stub-gracefulswitch_current_leaving_ready-one"), ); reg_stub_policy( "stub-gracefulswitch_current_leaving_ready-two", create_funcs_for_gracefulswitch_tests("stub-gracefulswitch_current_leaving_ready-two"), ); let service_config = serde_json::json!( [ { "stub-gracefulswitch_current_leaving_ready-one": serde_json::json!({}) } ] ); let parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: service_config, }) .unwrap(); let endpoint = create_endpoint_with_one_address("127.0.0.1:1234".to_string()); let endpoint2 = create_endpoint_with_one_address("127.0.0.1:1235".to_string()); let update = ResolverUpdate { endpoints: Ok(vec![endpoint.clone()]), ..Default::default() }; // Switch to first one (current) graceful_switch .resolver_update(update.clone(), Some(&parsed_config), &mut *tcc) .unwrap(); let current_subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, current_subchannel, tcc.as_mut(), ConnectivityState::Ready, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_current_leaving_ready-one", ) .await; let new_service_config = serde_json::json!( [ { "stub-gracefulswitch_current_leaving_ready-two": serde_json::json!({}) }, ] ); let new_update = ResolverUpdate { endpoints: Ok(vec![endpoint2.clone()]), ..Default::default() }; let new_parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: new_service_config, }) .unwrap(); graceful_switch .resolver_update(new_update.clone(), Some(&new_parsed_config), &mut *tcc) .unwrap(); let pending_subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, pending_subchannel.clone(), tcc.as_mut(), ConnectivityState::TransientFailure, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_current_leaving_ready-two", ) .await; move_subchannel_to_state( &mut *graceful_switch, pending_subchannel, tcc.as_mut(), ConnectivityState::Connecting, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_current_leaving_ready-two", ) .await; } // Tests that the gracefulswitch policy should remove the current child's // subchannels after swapping. #[tokio::test] async fn gracefulswitch_subchannels_removed_after_current_child_swapped() { let (mut rx_events, mut graceful_switch, mut tcc) = setup(); reg_stub_policy( "stub-gracefulswitch_subchannels_removed_after_current_child_swapped-one", create_funcs_for_gracefulswitch_tests( "stub-gracefulswitch_subchannels_removed_after_current_child_swapped-one", ), ); reg_stub_policy( "stub-gracefulswitch_subchannels_removed_after_current_child_swapped-two", create_funcs_for_gracefulswitch_tests( "stub-gracefulswitch_subchannels_removed_after_current_child_swapped-two", ), ); let service_config = serde_json::json!( [ { "stub-gracefulswitch_subchannels_removed_after_current_child_swapped-one": serde_json::json!({}) } ] ); let parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: service_config, }) .unwrap(); let endpoint = create_endpoint_with_one_address("127.0.0.1:1234".to_string()); let update = ResolverUpdate { endpoints: Ok(vec![endpoint.clone()]), ..Default::default() }; graceful_switch .resolver_update(update.clone(), Some(&parsed_config), &mut *tcc) .unwrap(); let current_subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; move_subchannel_to_state( &mut *graceful_switch, current_subchannel.clone(), tcc.as_mut(), ConnectivityState::Ready, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_subchannels_removed_after_current_child_swapped-one", ) .await; let new_service_config = serde_json::json!( [ { "stub-gracefulswitch_subchannels_removed_after_current_child_swapped-two": serde_json::json!({ "shuffleAddressList": false }) }, ] ); let second_endpoint = create_endpoint_with_one_address("127.0.0.1:1235".to_string()); let second_update = ResolverUpdate { endpoints: Ok(vec![second_endpoint.clone()]), ..Default::default() }; let new_parsed_config = GracefulSwitchPolicy::parse_config(&ParsedJsonLbConfig { value: new_service_config, }) .unwrap(); graceful_switch .resolver_update(second_update.clone(), Some(&new_parsed_config), &mut *tcc) .unwrap(); let pending_subchannel = verify_subchannel_creation_from_policy(&mut rx_events).await; println!("moving subchannel to idle"); move_subchannel_to_state( &mut *graceful_switch, pending_subchannel, tcc.as_mut(), ConnectivityState::Idle, ); verify_correct_picker_from_policy( &mut rx_events, "stub-gracefulswitch_subchannels_removed_after_current_child_swapped-two", ) .await; assert!(Arc::strong_count(¤t_subchannel) == 1); } } ================================================ FILE: grpc/src/client/load_balancing/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use core::panic; use std::any::Any; use std::error::Error; use std::fmt::Debug; use std::fmt::Display; use std::hash::Hash; use std::hash::Hasher; use std::ptr::addr_eq; use std::sync::Arc; use std::sync::Mutex; use std::sync::Weak; use tonic::Status; use tonic::metadata::MetadataMap; use crate::client::ConnectivityState; use crate::client::channel::InternalChannelController; use crate::client::channel::WorkQueueItem; use crate::client::channel::WorkQueueTx; use crate::client::name_resolution::Address; use crate::client::name_resolution::ResolverUpdate; use crate::client::service_config::LbConfig; use crate::client::subchannel::InternalSubchannel; use crate::client::subchannel::SubchannelStateWatcher; use crate::core::RequestHeaders; use crate::rt::GrpcRuntime; pub(crate) mod child_manager; pub(crate) mod graceful_switch; pub(crate) mod pick_first; pub(crate) mod round_robin; #[cfg(test)] pub(crate) mod test_utils; pub(crate) mod registry; pub(crate) use registry::GLOBAL_LB_REGISTRY; /// A collection of data configured on the channel that is constructing this /// LbPolicy. #[derive(Debug)] pub(crate) struct LbPolicyOptions { /// A hook into the channel's work scheduler that allows the LbPolicy to /// request the ability to perform operations on the ChannelController. pub work_scheduler: Arc, pub runtime: GrpcRuntime, } /// Used to asynchronously request a call into the LbPolicy's work method if /// the LbPolicy needs to provide an update without waiting for an update /// from the channel first. pub(crate) trait WorkScheduler: Send + Sync + Debug { // Schedules a call into the LbPolicy's work method. If there is already a // pending work call that has not yet started, this may not schedule another // call. fn schedule_work(&self); } /// Abstract representation of the configuration for any LB policy, stored as /// JSON. Hides internal storage details and includes a method to deserialize /// the JSON into a concrete policy struct. #[derive(Debug)] pub(crate) struct ParsedJsonLbConfig { value: serde_json::Value, } impl ParsedJsonLbConfig { /// Creates a new ParsedJsonLbConfig from the provided JSON string. pub fn new(json: &str) -> Result { match serde_json::from_str(json) { Ok(value) => Ok(ParsedJsonLbConfig { value }), Err(e) => Err(format!("failed to parse LB config JSON: {e}")), } } pub(crate) fn from_value(value: serde_json::Value) -> Self { Self { value } } /// Converts the JSON configuration into a concrete type that represents the /// configuration of an LB policy. /// /// This will typically be used by the LB policy builder to parse the /// configuration into a type that can be used by the LB policy. pub fn convert_to( &self, ) -> Result> { let res: T = match serde_json::from_value(self.value.clone()) { Ok(v) => v, Err(e) => { return Err(format!("{e}").into()); } }; Ok(res) } } /// An LB policy factory that produces LbPolicy instances used by the channel /// to manage connections and pick connections for RPCs. pub(crate) trait LbPolicyBuilder: Send + Sync + Debug { /// Builds and returns a new LB policy instance. /// /// Note that build must not fail. Any optional configuration is delivered /// via the LbPolicy's resolver_update method. /// /// An LbPolicy instance is assumed to begin in a Connecting state that /// queues RPCs until its first update. fn build(&self, options: LbPolicyOptions) -> Box; /// Reports the name of the LB Policy. fn name(&self) -> &'static str; /// Parses the JSON LB policy configuration into an internal representation. /// /// LB policies do not need to accept a configuration, in which case the /// default implementation returns Ok(None). fn parse_config( &self, _config: &ParsedJsonLbConfig, ) -> Result, Box> { Ok(None) } } /// An LB policy instance. /// /// LB policies are responsible for creating connections (modeled as /// Subchannels) and producing Picker instances for picking connections for /// RPCs. pub(crate) trait LbPolicy: Send + Debug { /// Called by the channel when the name resolver produces a new set of /// resolved addresses or a new service config. fn resolver_update( &mut self, update: ResolverUpdate, config: Option<&LbConfig>, channel_controller: &mut dyn ChannelController, ) -> Result<(), Box>; /// Called by the channel when any subchannel created by the LB policy /// changes state. fn subchannel_update( &mut self, subchannel: Arc, state: &SubchannelState, channel_controller: &mut dyn ChannelController, ); /// Called by the channel in response to a call from the LB policy to the /// WorkScheduler's request_work method. fn work(&mut self, channel_controller: &mut dyn ChannelController); /// Called by the channel when an LbPolicy goes idle and the channel /// wants it to start connecting to subchannels again. fn exit_idle(&mut self, channel_controller: &mut dyn ChannelController); } /// Controls channel behaviors. pub(crate) trait ChannelController: Send + Sync { /// Creates a new subchannel in IDLE state. fn new_subchannel(&mut self, address: &Address) -> Arc; /// Provides a new snapshot of the LB policy's state to the channel. fn update_picker(&mut self, update: LbState); /// Signals the name resolver to attempt to re-resolve addresses. Typically /// used when connections fail, indicating a possible change in the overall /// network configuration. fn request_resolution(&mut self); } /// Represents the current state of a Subchannel. #[derive(Debug, Clone)] pub(crate) struct SubchannelState { /// The connectivity state of the subchannel. See SubChannel for a /// description of the various states and their valid transitions. pub connectivity_state: ConnectivityState, // Set if connectivity state is TransientFailure to describe the most recent // connection error. None for any other connectivity_state value. pub last_connection_error: Option>, } impl Default for SubchannelState { fn default() -> Self { Self { connectivity_state: ConnectivityState::Idle, last_connection_error: None, } } } impl Display for SubchannelState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "connectivity_state: {}", self.connectivity_state)?; if let Some(err) = &self.last_connection_error { write!(f, ", last_connection_error: {err}")?; } Ok(()) } } /// A Picker is responsible for deciding what Subchannel to use for any given /// request. A Picker is only used once for any RPC. If pick() returns Queue, /// the channel will queue the RPC until a new Picker is produced by the /// LbPolicy, and will call pick() on the new Picker for the request. /// /// Pickers are always paired with a ConnectivityState which the channel will /// expose to applications so they can predict what might happens when /// performing RPCs: /// /// If the ConnectivityState is Idle, the Picker should ensure connections are /// initiated by the LbPolicy that produced the Picker, and return a Queue /// result so the request is attempted the next time a Picker is produced. /// /// If the ConnectivityState is Connecting, the Picker should return a Queue /// result and continue to wait for pending connections. /// /// If the ConnectivityState is Ready, the Picker should return a Ready /// Subchannel. /// /// If the ConnectivityState is TransientFailure, the Picker should return an /// Err with an error that describes why connections are failing. pub(crate) trait Picker: Send + Sync + Debug { /// Picks a connection to use for the request. /// /// This function should not block. If the Picker needs to do blocking or /// time-consuming work to service this request, it should return Queue, and /// the Pick call will be repeated by the channel when a new Picker is /// produced by the LbPolicy. fn pick(&self, request: &RequestHeaders) -> PickResult; } #[derive(Debug)] pub(crate) enum PickResult { /// Indicates the Subchannel in the Pick should be used for the request. Pick(Pick), /// Indicates the LbPolicy is attempting to connect to a server to use for /// the request. Queue, /// Indicates that the request should fail with the included error status /// (with the code converted to UNAVAILABLE). If the RPC is wait-for-ready, /// then it will not be terminated, but instead attempted on a new picker if /// one is produced before it is cancelled. Fail(Status), /// Indicates that the request should fail with the included status /// immediately, even if the RPC is wait-for-ready. The channel will /// convert the status code to INTERNAL if it is not a valid code for the /// gRPC library to produce, per [gRFC A54]. /// /// [gRFC A54]: /// https://github.com/grpc/proposal/blob/master/A54-restrict-control-plane-status-codes.md Drop(Status), } impl PickResult { pub fn unwrap_pick(self) -> Pick { let PickResult::Pick(pick) = self else { panic!("Called `PickResult::unwrap_pick` on a `Queue` or `Err` value"); }; pick } } impl PartialEq for PickResult { fn eq(&self, other: &Self) -> bool { match self { PickResult::Pick(pick) => match other { PickResult::Pick(other_pick) => pick.subchannel == other_pick.subchannel.clone(), _ => false, }, PickResult::Queue => matches!(other, PickResult::Queue), PickResult::Fail(status) => { // TODO: implement me. false } PickResult::Drop(status) => { // TODO: implement me. false } } } } impl Display for PickResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Pick(_) => write!(f, "Pick"), Self::Queue => write!(f, "Queue"), Self::Fail(st) => write!(f, "Fail({st})"), Self::Drop(st) => write!(f, "Drop({st})"), } } } /// Data provided by the LB policy. #[derive(Clone, Debug)] pub(crate) struct LbState { pub connectivity_state: super::ConnectivityState, pub picker: Arc, } impl PartialEq for LbState { /// Equality for two LbStates. /// /// Two `LbState`s are equal if and only if they have the same connectivity /// state and the same Picker allocation. Even if two Pickers have the same /// behavior or the same underlying implementation, they will be considered /// distinct unless they are the same Picker instance. fn eq(&self, other: &Self) -> bool { self.connectivity_state == other.connectivity_state && std::ptr::addr_eq(Arc::as_ptr(&self.picker), Arc::as_ptr(&other.picker)) } } impl Eq for LbState {} impl LbState { /// Returns a generic initial LbState which is Connecting and a picker which /// queues all picks. pub fn initial() -> Self { Self { connectivity_state: ConnectivityState::Connecting, picker: Arc::new(QueuingPicker {}), } } } /// Type alias for the completion callback function. pub(crate) type CompletionCallback = Box; /// A collection of data used by the channel for routing a request. pub(crate) struct Pick { /// The Subchannel for the request. pub subchannel: Arc, // Metadata to be added to existing outgoing metadata. pub metadata: MetadataMap, // Callback to be invoked once the RPC completes. pub on_complete: Option, } impl Debug for Pick { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Pick") .field("subchannel", &self.subchannel) .field("metadata", &self.metadata) .field("on_complete", &format_args!("{:p}", &self.on_complete)) .finish() } } pub(crate) trait DynHash { #[allow(clippy::redundant_allocation)] fn dyn_hash(&self, state: &mut Box<&mut dyn Hasher>); } impl DynHash for T { fn dyn_hash(&self, state: &mut Box<&mut dyn Hasher>) { self.hash(state); } } pub(crate) trait DynPartialEq { fn dyn_eq(&self, other: &&dyn Any) -> bool; } impl DynPartialEq for T { fn dyn_eq(&self, other: &&dyn Any) -> bool { let Some(other) = other.downcast_ref::() else { return false; }; self.eq(other) } } mod private { pub trait Sealed {} } pub(crate) trait SealedSubchannel: private::Sealed {} /// A Subchannel represents a method of communicating with a server which may be /// connected or disconnected many times across its lifetime. /// /// - Subchannels start IDLE. /// /// - IDLE transitions to CONNECTING when connect() is called. /// /// - CONNECTING transitions to READY on success or TRANSIENT_FAILURE on error. /// /// - READY transitions to IDLE when the connection is lost. /// /// - TRANSIENT_FAILURE transitions to IDLE when the reconnect backoff timer has /// expired. This timer scales exponentially and is reset when the subchannel /// becomes READY. /// /// When a Subchannel is dropped, it is disconnected automatically, and no /// subsequent state updates will be provided for it to the LB policy. pub(crate) trait Subchannel: SealedSubchannel + DynHash + DynPartialEq + Any + Send + Sync { /// Returns the address of the Subchannel. /// TODO: Consider whether this should really be public. fn address(&self) -> Address; /// Notifies the Subchannel to connect. fn connect(&self); } impl dyn Subchannel { pub fn downcast_ref(&self) -> Option<&T> where T: 'static, { (self as &dyn Any).downcast_ref() } } impl Hash for dyn Subchannel { fn hash(&self, state: &mut H) { self.dyn_hash(&mut Box::new(state as &mut dyn Hasher)); } } impl PartialEq for dyn Subchannel { fn eq(&self, other: &Self) -> bool { self.dyn_eq(&Box::new(other as &dyn Any)) } } impl Eq for dyn Subchannel {} impl Debug for dyn Subchannel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Subchannel: {}", self.address()) } } impl Display for dyn Subchannel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Subchannel: {}", self.address()) } } #[derive(Debug)] struct WeakSubchannel(Weak); impl From> for WeakSubchannel { fn from(subchannel: Arc) -> Self { WeakSubchannel(Arc::downgrade(&subchannel)) } } impl WeakSubchannel { pub fn new(subchannel: &Arc) -> Self { WeakSubchannel(Arc::downgrade(subchannel)) } pub fn upgrade(&self) -> Option> { self.0.upgrade() } } impl Hash for WeakSubchannel { fn hash(&self, state: &mut H) { (self.0.as_ptr() as *const () as usize).hash(state); } } impl PartialEq for WeakSubchannel { fn eq(&self, other: &Self) -> bool { addr_eq(self.0.as_ptr(), other.0.as_ptr()) } } impl Eq for WeakSubchannel {} pub(crate) struct ExternalSubchannel { pub(crate) isc: Option>, work_scheduler: WorkQueueTx, watcher: Mutex>>, } impl ExternalSubchannel { pub(super) fn new(isc: Arc, work_scheduler: WorkQueueTx) -> Self { ExternalSubchannel { isc: Some(isc), work_scheduler, watcher: Mutex::default(), } } pub(super) fn set_watcher(&self, watcher: Arc) { self.watcher.lock().unwrap().replace(watcher); } } impl Hash for ExternalSubchannel { fn hash(&self, state: &mut H) { self.address().hash(state); } } impl PartialEq for ExternalSubchannel { fn eq(&self, other: &Self) -> bool { self.address() == other.address() } } impl Eq for ExternalSubchannel {} impl Subchannel for ExternalSubchannel { fn address(&self) -> Address { self.isc.as_ref().unwrap().address() } fn connect(&self) { println!("connect called for subchannel: {self}"); self.isc.as_ref().unwrap().connect(); } } impl SealedSubchannel for ExternalSubchannel {} impl private::Sealed for ExternalSubchannel {} impl Drop for ExternalSubchannel { fn drop(&mut self) { let watcher = self.watcher.lock().unwrap().take(); let address = self.address().address.clone(); let isc = self.isc.take(); let _ = self.work_scheduler.send(WorkQueueItem::Closure(Box::new( move |c: &mut InternalChannelController| { println!("unregistering connectivity state watcher for {address:?}"); isc.as_ref() .unwrap() .unregister_connectivity_state_watcher(watcher.unwrap()); }, // The internal subchannel is dropped from here (i.e., from inside // the work serializer), if this is the last reference to it. ))); } } impl Debug for ExternalSubchannel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Subchannel {}", self.address()) } } impl Display for ExternalSubchannel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Subchannel {}", self.address()) } } pub(crate) trait ForwardingSubchannel: DynHash + DynPartialEq + Any + Send + Sync { fn delegate(&self) -> Arc; fn address(&self) -> Address { self.delegate().address() } fn connect(&self) { self.delegate().connect() } } impl Subchannel for T { fn address(&self) -> Address { self.address() } fn connect(&self) { self.connect() } } impl SealedSubchannel for T {} impl private::Sealed for T {} /// QueuingPicker always returns Queue. LB policies that are not actively /// Connecting should not use this picker. #[derive(Debug)] pub(crate) struct QueuingPicker {} impl Picker for QueuingPicker { fn pick(&self, _request: &RequestHeaders) -> PickResult { PickResult::Queue } } #[derive(Debug)] pub(crate) struct FailingPicker { pub error: String, } impl Picker for FailingPicker { fn pick(&self, _: &RequestHeaders) -> PickResult { PickResult::Fail(Status::unavailable(self.error.clone())) } } ================================================ FILE: grpc/src/client/load_balancing/pick_first.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::error::Error; use std::sync::Arc; use std::time::Duration; use tonic::metadata::MetadataMap; use crate::client::ConnectivityState; use crate::client::load_balancing::ChannelController; use crate::client::load_balancing::LbConfig; use crate::client::load_balancing::LbPolicy; use crate::client::load_balancing::LbPolicyBuilder; use crate::client::load_balancing::LbPolicyOptions; use crate::client::load_balancing::LbState; use crate::client::load_balancing::Pick; use crate::client::load_balancing::PickResult; use crate::client::load_balancing::Picker; use crate::client::load_balancing::Subchannel; use crate::client::load_balancing::SubchannelState; use crate::client::load_balancing::WorkScheduler; use crate::client::name_resolution::Address; use crate::client::name_resolution::ResolverUpdate; use crate::core::RequestHeaders; use crate::rt::GrpcRuntime; pub(crate) static POLICY_NAME: &str = "pick_first"; #[derive(Debug)] struct Builder {} impl LbPolicyBuilder for Builder { fn build(&self, options: LbPolicyOptions) -> Box { Box::new(PickFirstPolicy { work_scheduler: options.work_scheduler, subchannel: None, next_addresses: Vec::default(), runtime: options.runtime, }) } fn name(&self) -> &'static str { POLICY_NAME } } pub(crate) fn reg() { super::GLOBAL_LB_REGISTRY.add_builder(Builder {}) } #[derive(Debug)] struct PickFirstPolicy { work_scheduler: Arc, subchannel: Option>, next_addresses: Vec
, runtime: GrpcRuntime, } impl LbPolicy for PickFirstPolicy { fn resolver_update( &mut self, update: ResolverUpdate, config: Option<&LbConfig>, channel_controller: &mut dyn ChannelController, ) -> Result<(), Box> { let mut addresses = update .endpoints .unwrap() .into_iter() .next() .ok_or("no endpoints")? .addresses; let address = addresses.pop().ok_or("no addresses")?; let sc = channel_controller.new_subchannel(&address); sc.connect(); self.subchannel = Some(sc); self.next_addresses = addresses; let work_scheduler = self.work_scheduler.clone(); let runtime = self.runtime.clone(); // TODO: Implement Drop that cancels this task. self.runtime.spawn(Box::pin(async move { runtime.sleep(Duration::from_millis(200)).await; work_scheduler.schedule_work(); })); // TODO: return a picker that queues RPCs. Ok(()) } fn subchannel_update( &mut self, subchannel: Arc, state: &SubchannelState, channel_controller: &mut dyn ChannelController, ) { // Assume the update is for our subchannel. if state.connectivity_state == ConnectivityState::Ready { channel_controller.update_picker(LbState { connectivity_state: ConnectivityState::Ready, picker: Arc::new(OneSubchannelPicker { sc: self.subchannel.as_ref().unwrap().clone(), }), }); } } fn work(&mut self, channel_controller: &mut dyn ChannelController) {} fn exit_idle(&mut self, _channel_controller: &mut dyn ChannelController) { todo!("implement exit_idle") } } #[derive(Debug)] struct OneSubchannelPicker { sc: Arc, } impl Picker for OneSubchannelPicker { fn pick(&self, _: &RequestHeaders) -> PickResult { PickResult::Pick(Pick { subchannel: self.sc.clone(), // on_complete: None, metadata: MetadataMap::new(), on_complete: None, }) } } ================================================ FILE: grpc/src/client/load_balancing/registry.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::collections::HashMap; use std::sync::Arc; use std::sync::LazyLock; use std::sync::Mutex; use crate::client::load_balancing::LbPolicyBuilder; /// A registry to store and retrieve LB policies. LB policies are indexed by /// their names. pub(crate) struct LbPolicyRegistry { m: Arc>>>, } impl LbPolicyRegistry { /// Construct an empty LB policy registry. pub fn new() -> Self { Self { m: Arc::default() } } /// Add a LB policy into the registry. pub(crate) fn add_builder(&self, builder: impl LbPolicyBuilder + 'static) { self.m .lock() .unwrap() .insert(builder.name().to_string(), Arc::new(builder)); } /// Retrieve a LB policy from the registry, or None if not found. pub(crate) fn get_policy(&self, name: &str) -> Option> { self.m.lock().unwrap().get(name).cloned() } } impl Default for LbPolicyRegistry { fn default() -> Self { Self::new() } } /// The registry used if a local registry is not provided to a channel or if it /// does not exist in the local registry. pub(crate) static GLOBAL_LB_REGISTRY: LazyLock = LazyLock::new(LbPolicyRegistry::new); ================================================ FILE: grpc/src/client/load_balancing/round_robin.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::error::Error; use std::sync::Arc; use std::sync::Once; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use crate::client::ConnectivityState; use crate::client::load_balancing::ChannelController; use crate::client::load_balancing::FailingPicker; use crate::client::load_balancing::GLOBAL_LB_REGISTRY; use crate::client::load_balancing::LbConfig; use crate::client::load_balancing::LbPolicy; use crate::client::load_balancing::LbPolicyBuilder; use crate::client::load_balancing::LbPolicyOptions; use crate::client::load_balancing::LbState; use crate::client::load_balancing::PickResult; use crate::client::load_balancing::Picker; use crate::client::load_balancing::Subchannel; use crate::client::load_balancing::SubchannelState; use crate::client::load_balancing::child_manager::ChildManager; use crate::client::load_balancing::child_manager::ChildUpdate; use crate::client::load_balancing::pick_first; use crate::client::name_resolution::Endpoint; use crate::client::name_resolution::ResolverUpdate; use crate::core::RequestHeaders; pub(crate) static POLICY_NAME: &str = "round_robin"; static START: Once = Once::new(); #[derive(Debug)] struct RoundRobinBuilder {} impl LbPolicyBuilder for RoundRobinBuilder { fn build(&self, options: LbPolicyOptions) -> Box { let child_manager = ChildManager::new(options.runtime, options.work_scheduler); Box::new(RoundRobinPolicy::new( child_manager, GLOBAL_LB_REGISTRY .get_policy(pick_first::POLICY_NAME) .unwrap(), )) } fn name(&self) -> &'static str { POLICY_NAME } } #[derive(Debug)] struct RoundRobinPolicy { child_manager: ChildManager, pick_first_builder: Arc, } impl RoundRobinPolicy { fn new( child_manager: ChildManager, pick_first_builder: Arc, ) -> Self { Self { child_manager, pick_first_builder, } } // Sets the policy's state to TRANSIENT_FAILURE with a picker returning the // error string provided, then requests re-resolution from the channel. fn move_to_transient_failure( &mut self, error: String, channel_controller: &mut dyn ChannelController, ) { channel_controller.update_picker(LbState { connectivity_state: ConnectivityState::TransientFailure, picker: Arc::new(FailingPicker { error }), }); channel_controller.request_resolution(); } // Sends an aggregate picker based on states of children. // // The state is determined according to normal state aggregation rules, and // the picker round-robins between all children in that state. fn update_picker(&mut self, channel_controller: &mut dyn ChannelController) { if !self.child_manager.child_updated() { return; } let aggregate_state = self.child_manager.aggregate_states(); let pickers = self .child_manager .children() .filter(|cs| cs.state.connectivity_state == aggregate_state) .map(|cs| cs.state.picker.clone()) .collect(); let picker_update = LbState { connectivity_state: aggregate_state, picker: Arc::new(RoundRobinPicker::new(pickers)), }; channel_controller.update_picker(picker_update); } // Responds to an incoming ResolverUpdate containing an Err in endpoints by // forwarding it to all children unconditionally. Updates the picker as // needed. fn handle_resolver_error( &mut self, resolver_update: ResolverUpdate, channel_controller: &mut dyn ChannelController, ) -> Result<(), Box> { let err = format!( "Received error from name resolver: {}", resolver_update.endpoints.as_ref().unwrap_err() ); if self.child_manager.children().next().is_none() { // We had no children so we must produce an erroring picker. self.move_to_transient_failure(err.clone(), channel_controller); return Err(err.into()); } // Forward the error to each child, ignoring their responses. let _ = self .child_manager .resolver_update(resolver_update, None, channel_controller); self.update_picker(channel_controller); Err(err.into()) } } impl LbPolicy for RoundRobinPolicy { fn resolver_update( &mut self, update: ResolverUpdate, config: Option<&LbConfig>, channel_controller: &mut dyn ChannelController, ) -> Result<(), Box> { if update.endpoints.is_err() { return self.handle_resolver_error(update, channel_controller); } // Shard the update by endpoint. let updates = update.endpoints.as_ref().unwrap().iter().map(|e| { let update = ResolverUpdate { attributes: crate::attributes::Attributes::default(), endpoints: Ok(vec![e.clone()]), service_config: update.service_config.clone(), resolution_note: None, }; ChildUpdate { child_identifier: e.clone(), child_policy_builder: self.pick_first_builder.clone(), child_update: Some((update, config.cloned())), } }); self.child_manager .update(updates, channel_controller) .unwrap(); if self.child_manager.children().next().is_none() { // There are no children remaining, so report this error and produce // an erroring picker. let err = "Received empty address list from the name resolver"; self.move_to_transient_failure(err.into(), channel_controller); return Err(err.into()); } self.update_picker(channel_controller); Ok(()) } fn subchannel_update( &mut self, subchannel: Arc, state: &SubchannelState, channel_controller: &mut dyn ChannelController, ) { self.child_manager .subchannel_update(subchannel, state, channel_controller); self.update_picker(channel_controller); } fn work(&mut self, channel_controller: &mut dyn ChannelController) { self.child_manager.work(channel_controller); self.update_picker(channel_controller); } fn exit_idle(&mut self, channel_controller: &mut dyn ChannelController) { self.child_manager.exit_idle(channel_controller); self.update_picker(channel_controller); } } /// Register round robin as a LbPolicy. pub(crate) fn reg() { START.call_once(|| { GLOBAL_LB_REGISTRY.add_builder(RoundRobinBuilder {}); }); } #[derive(Debug)] struct RoundRobinPicker { pickers: Vec>, next: AtomicUsize, } impl RoundRobinPicker { fn new(pickers: Vec>) -> Self { let random_index: usize = rand::random_range(..pickers.len()); Self { pickers, next: AtomicUsize::new(random_index), } } } impl Picker for RoundRobinPicker { fn pick(&self, request_headers: &RequestHeaders) -> PickResult { let len = self.pickers.len(); let idx = self.next.fetch_add(1, Ordering::Relaxed) % len; self.pickers[idx].pick(request_headers) } } #[cfg(test)] mod test { use crate::client::ConnectivityState; use crate::client::load_balancing::ChannelController; use crate::client::load_balancing::FailingPicker; use crate::client::load_balancing::GLOBAL_LB_REGISTRY; use crate::client::load_balancing::LbPolicy; use crate::client::load_balancing::LbState; use crate::client::load_balancing::Pick; use crate::client::load_balancing::PickResult; use crate::client::load_balancing::Picker; use crate::client::load_balancing::QueuingPicker; use crate::client::load_balancing::Subchannel; use crate::client::load_balancing::SubchannelState; use crate::client::load_balancing::child_manager::ChildManager; use crate::client::load_balancing::pick_first; use crate::client::load_balancing::round_robin::RoundRobinPolicy; use crate::client::load_balancing::round_robin::{self}; use crate::client::load_balancing::test_utils::StubPolicyData; use crate::client::load_balancing::test_utils::StubPolicyFuncs; use crate::client::load_balancing::test_utils::TestChannelController; use crate::client::load_balancing::test_utils::TestEvent; use crate::client::load_balancing::test_utils::TestWorkScheduler; use crate::client::load_balancing::test_utils::{self}; use crate::client::name_resolution::Address; use crate::client::name_resolution::Endpoint; use crate::client::name_resolution::ResolverUpdate; use crate::core::RequestHeaders; use crate::rt::default_runtime; use std::collections::HashSet; use std::panic; use std::sync::Arc; use tokio::sync::mpsc; use tonic::metadata::MetadataMap; const DEFAULT_TEST_SHORT_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(100); // Sets up the test environment. // // Performs the following: // 1. Creates a work scheduler. // 2. Creates a fake channel that acts as a channel controller. // 3. Creates an StubPolicyBuilder with StubFuncs and the name of the test // passed in. // 4. Create a Round Robin policy with the StubPolicyBuilder. // // Returns the following: // 1. A receiver for events initiated by the LB policy (like creating a new // subchannel, sending a new picker etc). // 2. The Round Robin to send resolver and subchannel updates from the test. // 3. The controller to pass to the LB policy as part of the updates. fn setup( test_name: &'static str, ) -> ( mpsc::UnboundedReceiver, impl LbPolicy, Box, ) { pick_first::reg(); round_robin::reg(); test_utils::reg_stub_policy(test_name, create_funcs_for_roundrobin_tests()); let (tx_events, rx_events) = mpsc::unbounded_channel(); let work_scheduler = Arc::new(TestWorkScheduler { tx_events: tx_events.clone(), }); let child_manager = ChildManager::new(default_runtime(), work_scheduler); let tcc = Box::new(TestChannelController { tx_events }); let child_policy_builder = GLOBAL_LB_REGISTRY.get_policy(test_name).unwrap(); let lb_policy = RoundRobinPolicy::new(child_manager, child_policy_builder); (rx_events, lb_policy, tcc) } struct TestSubchannelList { subchannels: Vec>, } impl TestSubchannelList { fn new(addresses: &[Address], channel_controller: &mut dyn ChannelController) -> Self { TestSubchannelList { subchannels: addresses .iter() .map(|a| channel_controller.new_subchannel(a)) .collect(), } } fn contains(&self, sc: &Arc) -> bool { self.subchannels.contains(sc) } } fn create_endpoints(num_endpoints: usize, num_addresses: usize) -> Vec { let mut endpoints = Vec::with_capacity(num_endpoints); for i in 0..num_endpoints { let mut addresses: Vec
= Vec::with_capacity(num_addresses); for j in 0..num_addresses { addresses.push(Address { address: format!("{}.{}.{}.{}:{}", i + 1, i + 1, i + 1, i + 1, j).into(), ..Default::default() }); } endpoints.push(Endpoint { addresses, ..Default::default() }) } endpoints } // Sends a resolver update to the LB policy with the specified endpoint. fn send_resolver_update_to_policy( lb_policy: &mut impl LbPolicy, endpoints: Vec, tcc: &mut dyn ChannelController, ) { let update = ResolverUpdate { endpoints: Ok(endpoints), ..Default::default() }; let _ = lb_policy.resolver_update(update, None, tcc); } fn send_resolver_error_to_policy( lb_policy: &mut impl LbPolicy, err: String, tcc: &mut dyn ChannelController, ) { let update = ResolverUpdate { endpoints: Err(err), ..Default::default() }; let _ = lb_policy.resolver_update(update, None, tcc); } fn move_subchannel_to_state( lb_policy: &mut impl LbPolicy, subchannel: Arc, state: ConnectivityState, tcc: &mut dyn ChannelController, ) { lb_policy.subchannel_update( subchannel, &SubchannelState { connectivity_state: state, ..Default::default() }, tcc, ); } fn move_subchannel_to_transient_failure( lb_policy: &mut impl LbPolicy, subchannel: Arc, err: &str, tcc: &mut dyn ChannelController, ) { lb_policy.subchannel_update( subchannel, &SubchannelState { connectivity_state: ConnectivityState::TransientFailure, last_connection_error: Some(Arc::from(Box::from(err.to_owned()))), }, tcc, ); } #[derive(Debug)] struct OneSubchannelPicker { sc: Arc, } impl Picker for OneSubchannelPicker { fn pick(&self, _: &RequestHeaders) -> PickResult { PickResult::Pick(Pick { subchannel: self.sc.clone(), on_complete: None, metadata: MetadataMap::new(), }) } } fn addresses_from_endpoints(endpoints: &[Endpoint]) -> Vec
{ let mut addresses: Vec
= endpoints .iter() .flat_map(|ep| ep.addresses.clone()) .collect(); let mut uniques = HashSet::new(); addresses.retain(|e| uniques.insert(e.clone())); addresses } struct PickFirstState { subchannel_list: Option, selected_subchannel: Option>, addresses: Vec
, connectivity_state: ConnectivityState, } // TODO: Replace with Pick First child once merged. // Defines the functions resolver_update and subchannel_update to test round // robin. This is a simplified version of PickFirst. It just creates a // subchannel and then sends the appropriate picker update. fn create_funcs_for_roundrobin_tests() -> StubPolicyFuncs { StubPolicyFuncs { // Closure for resolver_update. It creates a subchannel for the // endpoint it receives and stores which endpoint it received and // which subchannel this child created in the data field. resolver_update: Some(Arc::new( |data: &mut StubPolicyData, update: ResolverUpdate, _, channel_controller| { let state = data .test_data .get_or_insert_with(|| { Box::new(PickFirstState { subchannel_list: None, selected_subchannel: None, addresses: vec![], connectivity_state: ConnectivityState::Connecting, }) }) .downcast_mut::() .unwrap(); if let Err(error) = update.endpoints { if state.addresses.is_empty() || state.connectivity_state == ConnectivityState::TransientFailure { channel_controller.update_picker(LbState { connectivity_state: ConnectivityState::TransientFailure, picker: Arc::new(FailingPicker { error: error.to_string(), }), }); state.connectivity_state = ConnectivityState::TransientFailure; channel_controller.request_resolution(); } return Ok(()); }; let endpoints = update.endpoints.unwrap(); let new_addresses = addresses_from_endpoints(&endpoints); if new_addresses.is_empty() { channel_controller.update_picker(LbState { connectivity_state: ConnectivityState::TransientFailure, picker: Arc::new(FailingPicker { error: "Received empty address list from the name resolver" .to_string(), }), }); state.connectivity_state = ConnectivityState::TransientFailure; channel_controller.request_resolution(); return Err("Received empty address list from the name resolver".into()); } if state.connectivity_state != ConnectivityState::Idle { state.subchannel_list = Some(TestSubchannelList::new(&new_addresses, channel_controller)); } state.addresses = new_addresses; Ok(()) }, )), // Closure for subchannel_update. Verify that the subchannel being // updated is the same one that this child policy created in // resolver_update. It then sends a picker of the same state that // was passed to it. subchannel_update: Some(Arc::new( |data: &mut StubPolicyData, subchannel, state, channel_controller| { // Retrieve the specific TestState from the generic test_data field. // This downcasts the `Any` trait object let test_data = data.test_data.as_mut().unwrap(); // ? ignore? let test_state = test_data.downcast_mut::().unwrap(); let scl = &mut test_state.subchannel_list.as_ref().unwrap(); assert!( scl.contains(&subchannel), "subchannel_update received an update for a subchannel it does not own." ); test_state.connectivity_state = state.connectivity_state; match state.connectivity_state { ConnectivityState::Ready => { channel_controller.update_picker(LbState { connectivity_state: state.connectivity_state, picker: Arc::new(OneSubchannelPicker { sc: subchannel }), }); } ConnectivityState::Idle => {} ConnectivityState::Connecting => { channel_controller.update_picker(LbState { connectivity_state: state.connectivity_state, picker: Arc::new(QueuingPicker {}), }); } ConnectivityState::TransientFailure => { channel_controller.update_picker(LbState { connectivity_state: state.connectivity_state, picker: Arc::new(FailingPicker { error: state .last_connection_error .as_ref() .unwrap() .to_string(), }), }); } } }, )), work: None, } } // Creates a new endpoint with the specified number of addresses. fn create_endpoint(num_addresses: usize) -> Endpoint { let mut addresses = Vec::with_capacity(num_addresses); for i in 0..num_addresses { addresses.push(Address { address: format!("{}.{}.{}.{}:{}", i, i, i, i, i).into(), ..Default::default() }); } Endpoint { addresses, ..Default::default() } } // Verifies that the expected number of subchannels is created. Returns the // subchannels created. async fn verify_subchannel_creation( rx_events: &mut mpsc::UnboundedReceiver, number_of_subchannels: usize, ) -> Vec> { let mut subchannels = Vec::new(); for _ in 0..number_of_subchannels { match rx_events.recv().await.unwrap() { TestEvent::NewSubchannel(sc) => { subchannels.push(sc); } other => panic!("unexpected event {:?}", other), }; } subchannels } // Verifies that the channel moves to CONNECTING state with a queuing picker. // // Returns the picker for tests to make more picks, if required. async fn verify_connecting_picker( rx_events: &mut mpsc::UnboundedReceiver, ) -> Arc { println!("verify connecting picker"); match rx_events.recv().await.unwrap() { TestEvent::UpdatePicker(update) => { println!("connectivity state is {}", update.connectivity_state); assert!(update.connectivity_state == ConnectivityState::Connecting); let req = test_utils::new_request_headers(); assert!(update.picker.pick(&req) == PickResult::Queue); update.picker } other => panic!("unexpected event {:?}", other), } } // Verifies that the channel moves to READY state with a picker that returns // the given subchannel. // // Returns the picker for tests to make more picks, if required. async fn verify_ready_picker( rx_events: &mut mpsc::UnboundedReceiver, subchannel: Arc, ) -> Arc { println!("verify ready picker"); match rx_events.recv().await.unwrap() { TestEvent::UpdatePicker(update) => { println!( "connectivity state for ready picker is {}", update.connectivity_state ); assert!(update.connectivity_state == ConnectivityState::Ready); let req = test_utils::new_request_headers(); match update.picker.pick(&req) { PickResult::Pick(pick) => { println!("selected subchannel is {}", pick.subchannel); println!("should've been selected subchannel is {}", subchannel); assert!(pick.subchannel == subchannel.clone()); update.picker.clone() } other => panic!("unexpected pick result {}", other), } } other => panic!("unexpected event {:?}", other), } } // Returns the picker for when there are multiple pickers in the ready // picker. async fn verify_roundrobin_ready_picker( rx_events: &mut mpsc::UnboundedReceiver, ) -> Arc { println!("verify ready picker"); match rx_events.recv().await.unwrap() { TestEvent::UpdatePicker(update) => { println!( "connectivity state for ready picker is {}", update.connectivity_state ); assert!(update.connectivity_state == ConnectivityState::Ready); let req = test_utils::new_request_headers(); match update.picker.pick(&req) { PickResult::Pick(pick) => update.picker.clone(), other => panic!("unexpected pick result {}", other), } } other => panic!("unexpected event {:?}", other), } } // Verifies that the channel moves to TRANSIENT_FAILURE state with a picker // that returns an error with the given message. The error code should be // UNAVAILABLE.. // // Returns the picker for tests to make more picks, if required. async fn verify_transient_failure_picker( rx_events: &mut mpsc::UnboundedReceiver, want_error: String, ) -> Arc { (match rx_events.recv().await.unwrap() { TestEvent::UpdatePicker(update) => { assert!(update.connectivity_state == ConnectivityState::TransientFailure); let req = test_utils::new_request_headers(); match update.picker.pick(&req) { PickResult::Fail(status) => { assert!(status.code() == tonic::Code::Unavailable); dbg!(status.message()); dbg!(&want_error); assert!(status.message().contains(&want_error)); update.picker.clone() } other => panic!("unexpected pick result {}", other), } } other => panic!("unexpected event {:?}", other), }) as _ } // Verifies that the LB policy requests re-resolution. async fn verify_resolution_request(rx_events: &mut mpsc::UnboundedReceiver) { println!("verifying resolution request"); match rx_events.recv().await.unwrap() { TestEvent::RequestResolution => {} other => panic!("unexpected event {:?}", other), }; } async fn verify_no_activity(rx_events: &mut mpsc::UnboundedReceiver) { tokio::select! { _ = tokio::time::sleep(DEFAULT_TEST_SHORT_TIMEOUT) => {} event = rx_events.recv() => { panic!("unexpected event {:?}", event.unwrap()); } } } // Tests the scenario where the resolver returns an error before a valid // update. The LB policy should move to TRANSIENT_FAILURE state with a // failing picker. #[tokio::test] async fn roundrobin_resolver_error_before_a_valid_update() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_resolver_error_before_a_valid_update"); let tcc = tcc.as_mut(); let resolver_error = String::from("resolver error"); send_resolver_error_to_policy(&mut lb_policy, resolver_error.clone(), tcc); verify_transient_failure_picker(&mut rx_events, resolver_error).await; } // Tests the scenario where the resolver returns an error after a valid update // and the LB policy has moved to READY. The LB policy should ignore the error // and continue using the previously received update. #[tokio::test] async fn roundrobin_resolver_error_after_a_valid_update_in_ready() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_resolver_error_after_a_valid_update_in_ready"); let tcc = tcc.as_mut(); let endpoint = create_endpoint(1); send_resolver_update_to_policy(&mut lb_policy, vec![endpoint], tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 1).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Ready, tcc, ); let picker = verify_ready_picker(&mut rx_events, subchannels[0].clone()).await; let resolver_error = String::from("resolver error"); send_resolver_error_to_policy(&mut lb_policy, resolver_error.clone(), tcc); verify_no_activity(&mut rx_events).await; let req = test_utils::new_request_headers(); match picker.pick(&req) { PickResult::Pick(pick) => { assert!(pick.subchannel == subchannels[0].clone()); } other => panic!("unexpected pick result {}", other), } } // Tests the scenario where the resolver returns an error after a valid update // and the LB policy is still trying to connect. The LB policy should ignore the // error and continue using the previously received update. #[tokio::test] async fn roundrobin_resolver_error_after_a_valid_update_in_connecting() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_resolver_error_after_a_valid_update_in_connecting"); let tcc = tcc.as_mut(); let endpoint = create_endpoint(1); send_resolver_update_to_policy(&mut lb_policy, vec![endpoint], tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 1).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); let picker = verify_connecting_picker(&mut rx_events).await; let resolver_error = String::from("resolver error"); send_resolver_error_to_policy(&mut lb_policy, resolver_error, tcc); verify_no_activity(&mut rx_events).await; let req = test_utils::new_request_headers(); match picker.pick(&req) { PickResult::Queue => {} other => panic!("unexpected pick result {}", other), } } // Tests the scenario where the resolver returns an error after a valid // update and the LB policy has moved to TRANSIENT_FAILURE after attempting // to connect to all addresses. The LB policy should send a new picker that // returns the error from the resolver. #[tokio::test] async fn roundrobin_resolver_error_after_a_valid_update_in_tf() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_resolver_error_after_a_valid_update_in_tf"); let tcc = tcc.as_mut(); let endpoint = create_endpoint(1); send_resolver_update_to_policy(&mut lb_policy, vec![endpoint], tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 1).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; let connection_error = String::from("test connection error"); move_subchannel_to_transient_failure( &mut lb_policy, subchannels[0].clone(), &connection_error, tcc, ); verify_transient_failure_picker(&mut rx_events, connection_error).await; let resolver_error = String::from("resolver error"); send_resolver_error_to_policy(&mut lb_policy, resolver_error.clone(), tcc); verify_resolution_request(&mut rx_events).await; verify_transient_failure_picker(&mut rx_events, resolver_error).await; } // Round Robin should round robin across endpoints. #[tokio::test] async fn roundrobin_picks_are_round_robin() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_picks_are_round_robin"); let tcc = tcc.as_mut(); let endpoints = create_endpoints(2, 1); send_resolver_update_to_policy(&mut lb_policy, endpoints, tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 2).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Ready, tcc, ); verify_ready_picker(&mut rx_events, subchannels[0].clone()).await; move_subchannel_to_state( &mut lb_policy, subchannels[1].clone(), ConnectivityState::Ready, tcc, ); let picker = verify_roundrobin_ready_picker(&mut rx_events).await; let req = test_utils::new_request_headers(); let mut picked = Vec::new(); for _ in 0..4 { match picker.pick(&req) { PickResult::Pick(pick) => { println!("picked subchannel is {}", pick.subchannel); picked.push(pick.subchannel.clone()) } other => panic!("unexpected pick result {}", other), } } assert!( picked[0] != picked[1].clone(), "Should alternate between subchannels" ); assert_eq!(&picked[0], &picked[2]); assert_eq!(&picked[1], &picked[3]); assert!(picked.contains(&subchannels[0])); assert!(picked.contains(&subchannels[1])); } // If round robin receives no endpoints in a resolver update, // it should go into transient failure. #[tokio::test] async fn roundrobin_endpoints_removed() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_addresses_removed"); let tcc = tcc.as_mut(); let endpoints = create_endpoints(2, 1); send_resolver_update_to_policy(&mut lb_policy, endpoints, tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 2).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; let update = ResolverUpdate { endpoints: Ok(vec![]), ..Default::default() }; let _ = lb_policy.resolver_update(update, None, tcc); let want_error = "Received empty address list from the name resolver"; verify_transient_failure_picker(&mut rx_events, want_error.to_string()).await; verify_resolution_request(&mut rx_events).await; } // Round robin should only round robin across children that are ready. // If a child leaves the ready state, Round Robin should only // pick from the children that are still Ready. #[tokio::test] async fn roundrobin_one_endpoint_down() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_one_endpoint_down"); let tcc = tcc.as_mut(); let endpoints = create_endpoints(2, 1); send_resolver_update_to_policy(&mut lb_policy, endpoints, tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 2).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Ready, tcc, ); let picker = verify_ready_picker(&mut rx_events, subchannels[0].clone()).await; move_subchannel_to_state( &mut lb_policy, subchannels[1].clone(), ConnectivityState::Ready, tcc, ); let picker = verify_roundrobin_ready_picker(&mut rx_events).await; let req = test_utils::new_request_headers(); let mut picked = Vec::new(); for _ in 0..4 { match picker.pick(&req) { PickResult::Pick(pick) => { println!("picked subchannel is {}", pick.subchannel); picked.push(pick.subchannel.clone()) } other => panic!("unexpected pick result {}", other), } } assert!( picked[0] != picked[1].clone(), "Should alternate between subchannels" ); assert_eq!(&picked[0], &picked[2]); assert_eq!(&picked[1], &picked[3]); assert!(picked.contains(&subchannels[0])); assert!(picked.contains(&subchannels[1])); let subchannel_being_removed = subchannels[1].clone(); let error = "endpoint down"; move_subchannel_to_transient_failure(&mut lb_policy, subchannels[1].clone(), error, tcc); let new_picker = verify_roundrobin_ready_picker(&mut rx_events).await; let req = test_utils::new_request_headers(); let mut picked = Vec::new(); for _ in 0..4 { match new_picker.pick(&req) { PickResult::Pick(pick) => { println!("picked subchannel is {}", pick.subchannel); picked.push(pick.subchannel.clone()) } other => panic!("unexpected pick result {}", other), } } assert_eq!(&picked[0], &picked[2]); assert_eq!(&picked[1], &picked[3]); assert!(picked.contains(&subchannels[0])); assert!(!picked.contains(&subchannel_being_removed)); } // If Round Robin receives a resolver update that removes an endpoint and // adds a new endpoint from a previous update, that endpoint's subchannels // should not be a part of its picks anymore and should be removed. It should // then roundrobin across the endpoints it still has and the new one. #[tokio::test] async fn roundrobin_pick_after_resolved_updated_hosts() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_pick_after_resolved_updated_hosts"); let tcc = tcc.as_mut(); // Two initial endpoints: subchannel_one, subchannel_two let addr_one = Address { address: "subchannel_one".to_string().into(), ..Default::default() }; let addr_two = Address { address: "subchannel_two".to_string().into(), ..Default::default() }; let endpoint_one = Endpoint { addresses: vec![addr_one], ..Default::default() }; let endpoint_two = Endpoint { addresses: vec![addr_two], ..Default::default() }; send_resolver_update_to_policy( &mut lb_policy, vec![endpoint_one, endpoint_two.clone()], tcc, ); // Start with two subchannels created let all_subchannels = verify_subchannel_creation(&mut rx_events, 2).await; let subchannel_one = all_subchannels .iter() .find(|sc| sc.address().address == "subchannel_one".to_string().into()) .unwrap(); let subchannel_two = all_subchannels .iter() .find(|sc| sc.address().address == "subchannel_two".to_string().into()) .unwrap(); move_subchannel_to_state( &mut lb_policy, subchannel_one.clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannel_two.clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannel_one.clone(), ConnectivityState::Ready, tcc, ); verify_ready_picker(&mut rx_events, subchannel_one.clone()).await; move_subchannel_to_state( &mut lb_policy, subchannel_two.clone(), ConnectivityState::Ready, tcc, ); let picker = verify_roundrobin_ready_picker(&mut rx_events).await; let req = test_utils::new_request_headers(); let mut picked = Vec::new(); for _ in 0..4 { match picker.pick(&req) { PickResult::Pick(pick) => picked.push(pick.subchannel.clone()), other => panic!("unexpected pick result {}", other), } } assert!(picked.contains(subchannel_one)); assert!(picked.contains(subchannel_two)); // Resolver update removes subchannel_one and adds "new" let new_addr = Address { address: "new".to_string().into(), ..Default::default() }; let new_endpoint = Endpoint { addresses: vec![new_addr], ..Default::default() }; send_resolver_update_to_policy(&mut lb_policy, vec![endpoint_two, new_endpoint], tcc); let new_subchannels = verify_subchannel_creation(&mut rx_events, 2).await; let new_sc = new_subchannels .iter() .find(|sc| sc.address().address == "new".to_string().into()) .unwrap(); let old_sc = new_subchannels .iter() .find(|sc| sc.address().address == "subchannel_two".to_string().into()) .unwrap(); move_subchannel_to_state( &mut lb_policy, old_sc.clone(), ConnectivityState::Ready, tcc, ); let _ = verify_roundrobin_ready_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, new_sc.clone(), ConnectivityState::Connecting, tcc, ); let _ = verify_roundrobin_ready_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, new_sc.clone(), ConnectivityState::Ready, tcc, ); let new_picker = verify_roundrobin_ready_picker(&mut rx_events).await; let req = test_utils::new_request_headers(); let mut picked = Vec::new(); for _ in 0..4 { match new_picker.pick(&req) { PickResult::Pick(pick) => picked.push(pick.subchannel.clone()), other => panic!("unexpected pick result {}", other), } } assert!(picked.contains(old_sc)); assert!(picked.contains(new_sc)); assert!(!picked.contains(subchannel_one)); } // Round robin should stay in transient failure until a child reports ready #[tokio::test] async fn roundrobin_stay_transient_failure_until_ready() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_stay_transient_failure_until_ready"); let tcc = tcc.as_mut(); let endpoints = create_endpoints(2, 1); send_resolver_update_to_policy(&mut lb_policy, endpoints, tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 2).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannels[1].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; let first_error = String::from("test connection error 1"); move_subchannel_to_transient_failure( &mut lb_policy, subchannels[0].clone(), &first_error, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_transient_failure( &mut lb_policy, subchannels[1].clone(), &first_error, tcc, ); verify_transient_failure_picker(&mut rx_events, first_error).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Ready, tcc, ); verify_ready_picker(&mut rx_events, subchannels[0].clone()).await; } // Tests the scenario where the resolver returns an update with no endpoints // (before sending any valid update). The LB policy should move to // TRANSIENT_FAILURE state with a failing picker. #[tokio::test] async fn roundrobin_zero_endpoints_from_resolver_before_valid_update() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_zero_endpoints_from_resolver_before_valid_update"); let tcc = tcc.as_mut(); send_resolver_update_to_policy(&mut lb_policy, vec![], tcc); verify_transient_failure_picker( &mut rx_events, "Received empty address list from the name resolver".to_string(), ) .await; } // Tests the scenario where the resolver returns an update with no endpoints // after sending a valid update (and the LB policy has moved to READY). The LB // policy should move to TRANSIENT_FAILURE state with a failing picker. #[tokio::test] async fn roundrobin_zero_endpoints_from_resolver_after_valid_update() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_zero_endpoints_from_resolver_after_valid_update"); let tcc = tcc.as_mut(); let endpoint = create_endpoint(1); send_resolver_update_to_policy(&mut lb_policy, vec![endpoint], tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 1).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Ready, tcc, ); verify_ready_picker(&mut rx_events, subchannels[0].clone()).await; let update = ResolverUpdate { endpoints: Ok(vec![]), ..Default::default() }; assert!(lb_policy.resolver_update(update, None, tcc).is_err()); verify_transient_failure_picker( &mut rx_events, "Received empty address list from the name resolver".to_string(), ) .await; verify_resolution_request(&mut rx_events).await; } // Tests the scenario where the resolver returns an update with multiple // address. The LB policy should create subchannels for all address, and attempt // to connect to them in order, until a connection succeeds, at which point it // should move to READY state with a picker that returns that subchannel. #[tokio::test] async fn roundrobin_with_multiple_backends_first_backend_is_ready() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_with_multiple_backends_first_backend_is_ready"); let tcc = tcc.as_mut(); let endpoint = create_endpoints(2, 1); send_resolver_update_to_policy(&mut lb_policy, endpoint, tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 2).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Ready, tcc, ); let picker = verify_ready_picker(&mut rx_events, subchannels[0].clone()).await; let req = test_utils::new_request_headers(); // First pick determines the only subchannel the picker should yield let first_sc = match picker.pick(&req) { PickResult::Pick(p) => p.subchannel.clone(), other => panic!("unexpected pick result {}", other), }; for _ in 0..7 { match picker.pick(&req) { PickResult::Pick(p) => { assert!( Arc::ptr_eq(&first_sc, &p.subchannel), "READY picker should contain exactly one subchannel" ); } other => panic!("unexpected pick result {}", other), } } } // Tests the scenario where the resolver returns an update with multiple // addresses and the LB policy successfully connects to first one and moves to // READY. The resolver then returns an update with a new address list that // contains the address of the currently connected subchannel. The LB policy // should create subchannels for the new addresses, and then see that the // currently connected subchannel is in the new address list. It should then // send a new READY picker that returns the currently connected subchannel. #[tokio::test] async fn roundrobin_resolver_update_contains_currently_ready_subchannel() { let (mut rx_events, mut lb_policy, mut tcc) = setup("stub-roundrobin_resolver_update_contains_currently_ready_subchannel"); let tcc = tcc.as_mut(); let endpoints = create_endpoint(2); send_resolver_update_to_policy(&mut lb_policy, vec![endpoints], tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 2).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Connecting, tcc, ); verify_connecting_picker(&mut rx_events).await; move_subchannel_to_state( &mut lb_policy, subchannels[0].clone(), ConnectivityState::Ready, tcc, ); verify_ready_picker(&mut rx_events, subchannels[0].clone()).await; let mut endpoints = create_endpoint(4); endpoints.addresses.reverse(); send_resolver_update_to_policy(&mut lb_policy, vec![endpoints], tcc); let subchannels = verify_subchannel_creation(&mut rx_events, 4).await; lb_policy.subchannel_update(subchannels[0].clone(), &SubchannelState::default(), tcc); lb_policy.subchannel_update(subchannels[1].clone(), &SubchannelState::default(), tcc); lb_policy.subchannel_update(subchannels[2].clone(), &SubchannelState::default(), tcc); lb_policy.subchannel_update( subchannels[3].clone(), &SubchannelState { connectivity_state: ConnectivityState::Ready, ..Default::default() }, tcc, ); verify_ready_picker(&mut rx_events, subchannels[3].clone()).await; } } ================================================ FILE: grpc/src/client/load_balancing/test_utils.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::any::Any; use std::error::Error; use std::fmt::Debug; use std::hash::Hash; use std::sync::Arc; use serde::Deserialize; use serde::Serialize; use tokio::sync::Notify; use tokio::sync::mpsc; use crate::client::load_balancing::ChannelController; use crate::client::load_balancing::ForwardingSubchannel; use crate::client::load_balancing::LbPolicy; use crate::client::load_balancing::LbPolicyBuilder; use crate::client::load_balancing::LbPolicyOptions; use crate::client::load_balancing::LbState; use crate::client::load_balancing::ParsedJsonLbConfig; use crate::client::load_balancing::Subchannel; use crate::client::load_balancing::SubchannelState; use crate::client::load_balancing::WorkScheduler; use crate::client::name_resolution::Address; use crate::client::name_resolution::ResolverUpdate; use crate::client::service_config::LbConfig; use crate::core::RequestHeaders; pub(crate) fn new_request_headers() -> RequestHeaders { RequestHeaders::default() } // A test subchannel that forwards connect calls to a channel. // This allows tests to verify when a subchannel is asked to connect. pub(crate) struct TestSubchannel { address: Address, tx_connect: mpsc::UnboundedSender, } impl TestSubchannel { pub fn new(address: Address, tx_connect: mpsc::UnboundedSender) -> Self { Self { address, tx_connect, } } } impl ForwardingSubchannel for TestSubchannel { fn delegate(&self) -> Arc { panic!("unsupported operation on a test subchannel"); } fn address(&self) -> Address { self.address.clone() } fn connect(&self) { println!("connect called for subchannel {}", self.address); self.tx_connect .send(TestEvent::Connect(self.address.clone())) .unwrap(); } } impl Hash for TestSubchannel { fn hash(&self, state: &mut H) { self.address.hash(state); } } impl PartialEq for TestSubchannel { fn eq(&self, other: &Self) -> bool { std::ptr::eq(self, other) } } impl Eq for TestSubchannel {} pub(crate) enum TestEvent { NewSubchannel(Arc), UpdatePicker(LbState), RequestResolution, Connect(Address), ScheduleWork, } // TODO(easwars): Remove this and instead derive Debug. impl Debug for TestEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::NewSubchannel(sc) => write!(f, "NewSubchannel({})", sc.address()), Self::UpdatePicker(state) => write!(f, "UpdatePicker({})", state.connectivity_state), Self::RequestResolution => write!(f, "RequestResolution"), Self::Connect(addr) => write!(f, "Connect({:?})", addr.address), Self::ScheduleWork => write!(f, "ScheduleWork"), } } } /// A test channel controller that forwards calls to a channel. This allows /// tests to verify when a channel controller is asked to create subchannels or /// update the picker. pub(crate) struct TestChannelController { pub(crate) tx_events: mpsc::UnboundedSender, } impl ChannelController for TestChannelController { fn new_subchannel(&mut self, address: &Address) -> Arc { println!("new_subchannel called for address {}", address); let notify = Arc::new(Notify::new()); let subchannel: Arc = Arc::new(TestSubchannel::new(address.clone(), self.tx_events.clone())); self.tx_events .send(TestEvent::NewSubchannel(subchannel.clone())) .unwrap(); subchannel } fn update_picker(&mut self, update: LbState) { println!("picker_update called with {}", update.connectivity_state); self.tx_events .send(TestEvent::UpdatePicker(update)) .unwrap(); } fn request_resolution(&mut self) { self.tx_events.send(TestEvent::RequestResolution).unwrap(); } } #[derive(Debug)] pub(crate) struct TestWorkScheduler { pub(crate) tx_events: mpsc::UnboundedSender, } impl WorkScheduler for TestWorkScheduler { fn schedule_work(&self) { self.tx_events.send(TestEvent::ScheduleWork).unwrap(); } } // The callback to invoke when resolver_update is invoked on the stub policy. type ResolverUpdateFn = Arc< dyn Fn( &mut StubPolicyData, ResolverUpdate, Option<&LbConfig>, &mut dyn ChannelController, ) -> Result<(), Box> + Send + Sync, >; // The callback to invoke when subchannel_update is invoked on the stub policy. type SubchannelUpdateFn = Arc< dyn Fn(&mut StubPolicyData, Arc, &SubchannelState, &mut dyn ChannelController) + Send + Sync, >; type WorkFn = Arc; /// This struct holds `LbPolicy` trait stub functions that tests are expected to /// implement. #[derive(Clone)] pub(crate) struct StubPolicyFuncs { pub resolver_update: Option, pub subchannel_update: Option, pub work: Option, } impl Debug for StubPolicyFuncs { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "stub funcs") } } /// Data holds test data that will be passed all to functions in PolicyFuncs #[derive(Debug)] pub(crate) struct StubPolicyData { pub lb_policy_options: LbPolicyOptions, pub test_data: Option>, } impl StubPolicyData { /// Creates an instance of StubPolicyData. pub fn new(lb_policy_options: LbPolicyOptions) -> Self { Self { test_data: None, lb_policy_options, } } } /// The stub `LbPolicy` that calls the provided functions. #[derive(Debug)] pub(crate) struct StubPolicy { funcs: StubPolicyFuncs, data: StubPolicyData, } impl LbPolicy for StubPolicy { fn resolver_update( &mut self, update: ResolverUpdate, config: Option<&LbConfig>, channel_controller: &mut dyn ChannelController, ) -> Result<(), Box> { if let Some(f) = &mut self.funcs.resolver_update { return f(&mut self.data, update, config, channel_controller); } Ok(()) } fn subchannel_update( &mut self, subchannel: Arc, state: &SubchannelState, channel_controller: &mut dyn ChannelController, ) { if let Some(f) = &self.funcs.subchannel_update { f(&mut self.data, subchannel, state, channel_controller); } } fn exit_idle(&mut self, channel_controller: &mut dyn ChannelController) { todo!("Implement exit_idle for StubPolicy") } fn work(&mut self, channel_controller: &mut dyn ChannelController) { if let Some(f) = &self.funcs.work { f(&mut self.data, channel_controller); } } } /// StubPolicyBuilder builds a StubLbPolicy. #[derive(Debug)] pub(crate) struct StubPolicyBuilder { name: &'static str, funcs: StubPolicyFuncs, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub(super) struct MockConfig { shuffle_address_list: Option, } impl LbPolicyBuilder for StubPolicyBuilder { fn build(&self, options: LbPolicyOptions) -> Box { let data = StubPolicyData::new(options); Box::new(StubPolicy { funcs: self.funcs.clone(), data, }) } fn name(&self) -> &'static str { self.name } fn parse_config( &self, config: &ParsedJsonLbConfig, ) -> Result, Box> { let cfg: MockConfig = match config.convert_to() { Ok(c) => c, Err(e) => { return Err(format!("failed to parse JSON config: {}", e).into()); } }; Ok(Some(LbConfig::new(cfg))) } } pub(crate) fn reg_stub_policy(name: &'static str, funcs: StubPolicyFuncs) { super::GLOBAL_LB_REGISTRY.add_builder(StubPolicyBuilder { name, funcs }) } ================================================ FILE: grpc/src/client/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::fmt::Display; use std::time::Instant; use tonic::async_trait; use crate::core::ClientResponseStreamItem; use crate::core::RecvMessage; use crate::core::RequestHeaders; use crate::core::SendMessage; pub mod channel; pub mod interceptor; pub mod service_config; pub mod stream_util; pub use channel::Channel; pub use channel::ChannelOptions; pub(crate) mod load_balancing; pub(crate) mod name_resolution; mod subchannel; pub(crate) mod transport; /// A representation of the current state of a gRPC channel, also used for the /// state of subchannels (individual connections within the channel). /// /// A gRPC channel begins in the Idle state. When an RPC is attempted, the /// channel will automatically transition to Connecting. If connections to a /// backend service are available, the state becomes Ready. Otherwise, if RPCs /// would fail due to a lack of connections, the state becomes TransientFailure /// and continues to attempt to reconnect. /// /// Channels may re-enter the Idle state if they are unused for longer than /// their configured idleness timeout. #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] pub enum ConnectivityState { #[default] Idle, Connecting, Ready, TransientFailure, } impl Display for ConnectivityState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ConnectivityState::Idle => write!(f, "Idle"), ConnectivityState::Connecting => write!(f, "Connecting"), ConnectivityState::Ready => write!(f, "Ready"), ConnectivityState::TransientFailure => write!(f, "TransientFailure"), } } } /// Contains settings to configure an RPC. /// /// Most applications will not need this type, and will set options via the /// generated (e.g. protobuf) APIs instead. #[derive(Default, Clone)] #[non_exhaustive] pub struct CallOptions { /// The deadline for the call. If unset, the call may run indefinitely. deadline: Option, } /// A trait which may be implemented by types to perform RPCs (Remote Procedure /// Calls, often shortened to "call"). /// /// Most applications will not use this type directly, and will instead use the /// generated APIs (e.g. protobuf) to perform RPCs instead. #[trait_variant::make(Send)] pub trait Invoke: Sync { type SendStream: SendStream + 'static; type RecvStream: RecvStream + 'static; /// Starts an RPC, returning the send and receive streams to interact with /// it. /// /// Note that invoke is synchronous, which implies no pushback may be /// enforced via execution flow. If a call cannot be started or queued /// locally, the returned SendStream and RecvStream may represent a /// locally-erroring stream immediately instead. However, SendStream and /// RecvStream are asynchronous, and may block their first operations until /// quota is available, a connection is ready, etc. async fn invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream); } #[async_trait] pub trait DynInvoke: Send + Sync { async fn dyn_invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Box, Box); } #[async_trait] impl DynInvoke for T { async fn dyn_invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Box, Box) { let (tx, rx) = self.invoke(headers, options).await; (Box::new(tx), Box::new(rx)) } } // Like `Invoke`, but not reusable. It is blanket implemented on references to // `Invoke`s. #[trait_variant::make(Send)] pub trait InvokeOnce: Sync { type SendStream: SendStream + 'static; type RecvStream: RecvStream + 'static; async fn invoke_once( self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream); } impl InvokeOnce for &T { type SendStream = T::SendStream; type RecvStream = T::RecvStream; async fn invoke_once( self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { self.invoke(headers, options).await } } /// Represents the sending side of a client stream. When a `SendStream` is /// dropped, the send side of the stream is closed. Clients may continue to /// read from the RecvStream /// /// Most applications will not need this type directly, and will use the /// generated APIs (e.g. protobuf) to perform RPCs instead. #[trait_variant::make(Send)] pub trait SendStream { /// Sends T on the stream. If Err(()) is returned, the message could not be /// delivered because the stream was closed. Future calls to SendStream /// will do nothing. /// /// # Cancel safety /// /// This method is not intended to be cancellation safe. If the returned /// future is not polled to completion, the behavior of any subsequent calls /// to the SendStream are undefined and data may be lost. async fn send(&mut self, msg: &dyn SendMessage, options: SendOptions) -> Result<(), ()>; } #[async_trait] pub trait DynSendStream: Send { async fn dyn_send(&mut self, msg: &dyn SendMessage, options: SendOptions) -> Result<(), ()>; } #[async_trait] impl DynSendStream for T { async fn dyn_send(&mut self, msg: &dyn SendMessage, options: SendOptions) -> Result<(), ()> { self.send(msg, options).await } } impl SendStream for Box { async fn send(&mut self, msg: &dyn SendMessage, options: SendOptions) -> Result<(), ()> { (**self).dyn_send(msg, options).await } } /// Contains settings to configure a send operation on a SendStream. /// /// Most applications will not need this type directly, and will use the /// generated (e.g. protobuf) APIs to configure RPCs instead. #[derive(Default, Clone)] #[non_exhaustive] pub struct SendOptions { /// Closes the stream immediately after sending this message. pub final_msg: bool, /// If set, compression will be disabled for this message. pub disable_compression: bool, } /// Represents the receiving side of a client stream. When a `RecvStream` is /// dropped, the associated call is cancelled if the server has not already /// terminated the stream. /// /// Most applications will not need this type directly, and will use the /// generated APIs (e.g. protobuf) to perform RPCs instead. #[trait_variant::make(Send)] pub trait RecvStream { /// Returns the next item on the stream. If that item represents a message, /// `msg` has been updated directly to contain the received message. /// /// # Cancel safety /// /// This method is not intended to be cancellation safe. If the returned /// future is not polled to completion, the behavior of any subsequent calls /// to the RecvStream are undefined and data may be lost. async fn next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem; } #[async_trait] pub trait DynRecvStream: Send { async fn dyn_next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem; } #[async_trait] impl DynRecvStream for T { async fn dyn_next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem { self.next(msg).await } } impl RecvStream for Box { async fn next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem { (**self).dyn_next(msg).await } } ================================================ FILE: grpc/src/client/name_resolution/backoff.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::time::Duration; use rand::Rng; #[derive(Clone)] pub(crate) struct BackoffConfig { /// The amount of time to backoff after the first failure. pub base_delay: Duration, /// The factor with which to multiply backoffs after a /// failed retry. Should ideally be greater than 1. pub multiplier: f64, /// The factor with which backoffs are randomized. pub jitter: f64, /// The upper bound of backoff delay. pub max_delay: Duration, } pub(crate) struct ExponentialBackoff { config: BackoffConfig, /// The delay for the next retry, without the random jitter. Store as f64 /// to avoid rounding errors. next_delay_secs: f64, } /// This is a backoff configuration with the default values specified /// at https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md. /// /// This should be useful for callers who want to configure backoff with /// non-default values only for a subset of the options. pub(crate) const DEFAULT_EXPONENTIAL_CONFIG: BackoffConfig = BackoffConfig { base_delay: Duration::from_secs(1), multiplier: 1.6, jitter: 0.2, max_delay: Duration::from_secs(120), }; impl BackoffConfig { fn validate(&self) -> Result<(), &'static str> { // Check that the arguments are in valid ranges. // 0 <= base_dealy <= max_delay if self.base_delay > self.max_delay { Err("base_delay must be greater than max_delay")?; } // 1 <= multiplier if self.multiplier < 1.0 { Err("multiplier must be greater than 1.0")?; } // 0 <= jitter <= 1 if self.jitter < 0.0 { Err("jitter must be greater than or equal to 0")?; } if self.jitter > 1.0 { Err("jitter must be less than or equal to 1")? } Ok(()) } } impl ExponentialBackoff { pub fn new(config: BackoffConfig) -> Result { config.validate()?; let next_delay_secs = config.base_delay.as_secs_f64(); Ok(ExponentialBackoff { config, next_delay_secs, }) } pub fn reset(&mut self) { self.next_delay_secs = self.config.base_delay.as_secs_f64(); } pub fn backoff_duration(&mut self) -> Duration { let next_delay = self.next_delay_secs; let cur_delay = next_delay * (1.0 + self.config.jitter * rand::rng().random_range(-1.0..1.0)); self.next_delay_secs = self .config .max_delay .as_secs_f64() .min(next_delay * self.config.multiplier); Duration::from_secs_f64(cur_delay) } } #[cfg(test)] mod tests { use std::time::Duration; use crate::client::name_resolution::backoff::BackoffConfig; use crate::client::name_resolution::backoff::DEFAULT_EXPONENTIAL_CONFIG; use crate::client::name_resolution::backoff::ExponentialBackoff; // Epsilon for floating point comparisons if needed, though Duration // comparisons are often better. const EPSILON: f64 = 1e-9; #[test] fn default_config_is_valid() { let result = ExponentialBackoff::new(DEFAULT_EXPONENTIAL_CONFIG.clone()); assert!(result.is_ok()); } #[test] fn base_less_than_max() { let config = BackoffConfig { base_delay: Duration::from_secs(10), multiplier: 123.0, jitter: 0.0, max_delay: Duration::from_secs(100), }; let mut backoff = ExponentialBackoff::new(config).unwrap(); assert_eq!(backoff.backoff_duration(), Duration::from_secs(10)); } #[test] fn base_more_than_max() { let config = BackoffConfig { multiplier: 123.0, jitter: 0.0, base_delay: Duration::from_secs(100), max_delay: Duration::from_secs(10), }; let result = ExponentialBackoff::new(config); assert!(result.is_err()); } #[test] fn negative_multiplier() { let config = BackoffConfig { multiplier: -123.0, jitter: 0.0, base_delay: Duration::from_secs(10), max_delay: Duration::from_secs(100), }; let result = ExponentialBackoff::new(config); assert!(result.is_err()); } #[test] fn negative_jitter() { let config = BackoffConfig { multiplier: 1.0, jitter: -10.0, base_delay: Duration::from_secs(10), max_delay: Duration::from_secs(100), }; let result = ExponentialBackoff::new(config); assert!(result.is_err()); } #[test] fn jitter_greater_than_one() { let config = BackoffConfig { multiplier: 1.0, jitter: 2.0, base_delay: Duration::from_secs(10), max_delay: Duration::from_secs(100), }; let result = ExponentialBackoff::new(config); assert!(result.is_err()); } #[test] fn backoff_reset_no_jitter() { let config = BackoffConfig { multiplier: 2.0, jitter: 0.0, base_delay: Duration::from_secs(1), max_delay: Duration::from_secs(15), }; let mut backoff = ExponentialBackoff::new(config.clone()).unwrap(); assert_eq!(backoff.backoff_duration(), Duration::from_secs(1)); assert_eq!(backoff.backoff_duration(), Duration::from_secs(2)); assert_eq!(backoff.backoff_duration(), Duration::from_secs(4)); assert_eq!(backoff.backoff_duration(), Duration::from_secs(8)); assert_eq!(backoff.backoff_duration(), Duration::from_secs(15)); // Duration is capped to max_delay. assert_eq!(backoff.backoff_duration(), Duration::from_secs(15)); // reset and repeat. backoff.reset(); assert_eq!(backoff.backoff_duration(), Duration::from_secs(1)); assert_eq!(backoff.backoff_duration(), Duration::from_secs(2)); assert_eq!(backoff.backoff_duration(), Duration::from_secs(4)); assert_eq!(backoff.backoff_duration(), Duration::from_secs(8)); assert_eq!(backoff.backoff_duration(), Duration::from_secs(15)); // Duration is capped to max_delay. assert_eq!(backoff.backoff_duration(), Duration::from_secs(15)); } #[test] fn backoff_with_jitter() { let config = BackoffConfig { multiplier: 2.0, jitter: 0.2, base_delay: Duration::from_secs(1), max_delay: Duration::from_secs(15), }; let mut backoff = ExponentialBackoff::new(config.clone()).unwrap(); // 0.8 <= duration <= 1.2. let duration = backoff.backoff_duration(); assert!(duration.gt(&Duration::from_secs_f64(0.8 - EPSILON))); assert!(duration.lt(&Duration::from_secs_f64(1.2 + EPSILON))); // 1.6 <= duration <= 2.4. let duration = backoff.backoff_duration(); assert!(duration.gt(&Duration::from_secs_f64(1.6 - EPSILON))); assert!(duration.lt(&Duration::from_secs_f64(2.4 + EPSILON))); // 3.2 <= duration <= 4.8. let duration = backoff.backoff_duration(); assert!(duration.gt(&Duration::from_secs_f64(3.2 - EPSILON))); assert!(duration.lt(&Duration::from_secs_f64(4.8 + EPSILON))); } } ================================================ FILE: grpc/src/client/name_resolution/dns/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ //! This module implements a DNS resolver to be installed as the default resolver //! in grpc. use std::net::IpAddr; use std::net::SocketAddr; use std::sync::Arc; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::time::Duration; use std::time::SystemTime; use parking_lot::Mutex; use tokio::sync::Notify; use url::Host; use crate::byte_str::ByteStr; use crate::client::name_resolution::Address; use crate::client::name_resolution::ChannelController; use crate::client::name_resolution::Endpoint; use crate::client::name_resolution::NopResolver; use crate::client::name_resolution::Resolver; use crate::client::name_resolution::ResolverBuilder; use crate::client::name_resolution::ResolverOptions; use crate::client::name_resolution::ResolverUpdate; use crate::client::name_resolution::TCP_IP_NETWORK_TYPE; use crate::client::name_resolution::Target; use crate::client::name_resolution::backoff::BackoffConfig; use crate::client::name_resolution::backoff::DEFAULT_EXPONENTIAL_CONFIG; use crate::client::name_resolution::backoff::ExponentialBackoff; use crate::client::name_resolution::global_registry; use crate::rt::BoxedTaskHandle; use crate::rt::{self}; #[cfg(test)] mod test; const DEFAULT_PORT: u16 = 443; const DEFAULT_DNS_PORT: u16 = 53; /// This specifies the maximum duration for a DNS resolution request. /// If the timeout expires before a response is received, the request will be /// canceled. /// /// It is recommended to set this value at application startup. Avoid modifying /// this variable after initialization. static RESOLVING_TIMEOUT_MS: AtomicU64 = AtomicU64::new(30_000); // 30 seconds /// This is the minimum interval at which re-resolutions are allowed. This helps /// to prevent excessive re-resolution. static MIN_RESOLUTION_INTERVAL_MS: AtomicU64 = AtomicU64::new(30_000); // 30 seconds fn get_resolving_timeout() -> Duration { Duration::from_millis(RESOLVING_TIMEOUT_MS.load(Ordering::Relaxed)) } /// Sets the maximum duration for DNS resolution requests. /// /// This function affects the global timeout used by all channels using the DNS /// name resolver scheme. /// /// It must be called only at application startup, before any gRPC calls are /// made. /// /// The default value is 30 seconds. Setting the timeout too low may result in /// premature timeouts during resolution, while setting it too high may lead to /// unnecessary delays in service discovery. Choose a value appropriate for your /// specific needs and network environment. pub(crate) fn set_resolving_timeout(duration: Duration) { RESOLVING_TIMEOUT_MS.store(duration.as_millis() as u64, Ordering::Relaxed); } fn get_min_resolution_interval() -> Duration { Duration::from_millis(MIN_RESOLUTION_INTERVAL_MS.load(Ordering::Relaxed)) } /// Sets the default minimum interval at which DNS re-resolutions are allowed. /// This helps to prevent excessive re-resolution. /// /// It must be called only at application startup, before any gRPC calls are /// made. pub(crate) fn set_min_resolution_interval(duration: Duration) { MIN_RESOLUTION_INTERVAL_MS.store(duration.as_millis() as u64, Ordering::Relaxed); } pub(crate) fn reg() { global_registry().add_builder(Box::new(Builder {})); } struct Builder {} struct DnsOptions { min_resolution_interval: Duration, resolving_timeout: Duration, backoff_config: BackoffConfig, host: String, port: u16, } impl DnsResolver { fn new( dns_client: Box, options: ResolverOptions, dns_opts: DnsOptions, ) -> Self { let state = Arc::new(Mutex::new(InternalState { addrs: Ok(Vec::new()), channel_response: None, })); let state_copy = state.clone(); let resolve_now_notify = Arc::new(Notify::new()); let channel_updated_notify = Arc::new(Notify::new()); let channel_updated_rx = channel_updated_notify.clone(); let resolve_now_rx = resolve_now_notify.clone(); let runtime = options.runtime.clone(); let work_scheduler = options.work_scheduler.clone(); let handle = options.runtime.spawn(Box::pin(async move { let mut backoff = ExponentialBackoff::new(dns_opts.backoff_config.clone()) .expect("default exponential config must be valid"); let state = state_copy; loop { let mut lookup_fut = dns_client.lookup_host_name(&dns_opts.host); let mut timeout_fut = runtime.sleep(dns_opts.resolving_timeout); let addrs = tokio::select! { result = &mut lookup_fut => { match result { Ok(ips) => { let addrs = ips .into_iter() .map(|ip| SocketAddr::new(ip, dns_opts.port)) .collect(); Ok(addrs) } Err(err) => Err(err), } } _ = &mut timeout_fut => { Err("Timed out waiting for DNS resolution".to_string()) } }; { state.lock().addrs = addrs; } work_scheduler.schedule_work(); channel_updated_rx.notified().await; let channel_response = { state.lock().channel_response.take() }; let next_resoltion_time = if channel_response.is_some() { SystemTime::now() .checked_add(backoff.backoff_duration()) .unwrap() } else { // Success resolving, wait for the next resolve_now. However, // also wait MIN_RESOLUTION_INTERVAL at the very least to prevent // constantly re-resolving. backoff.reset(); let res_time = SystemTime::now() .checked_add(dns_opts.min_resolution_interval) .unwrap(); _ = resolve_now_rx.notified().await; res_time }; // Wait till next resolution time. let Ok(duration) = next_resoltion_time.duration_since(SystemTime::now()) else { continue; // Time has already passed. }; runtime.sleep(duration).await; } })); Self { state, task_handle: handle, resolve_now_notifier: resolve_now_notify, channel_update_notifier: channel_updated_notify, } } } impl ResolverBuilder for Builder { fn build(&self, target: &Target, options: ResolverOptions) -> Box { let parsed = match parse_endpoint_and_authority(target) { Ok(res) => res, Err(err) => return nop_resolver_for_err(err.to_string(), options), }; let endpoint = parsed.endpoint; let host = match endpoint.host { Host::Domain(d) => d, Host::Ipv4(ipv4) => { return nop_resolver_for_ip(IpAddr::V4(ipv4), endpoint.port, options); } Host::Ipv6(ipv6) => { return nop_resolver_for_ip(IpAddr::V6(ipv6), endpoint.port, options); } }; let authority = parsed.authority; let dns_client = match options.runtime.get_dns_resolver(rt::ResolverOptions { server_addr: authority, }) { Ok(dns) => dns, Err(err) => return nop_resolver_for_err(err.to_string(), options), }; let dns_opts = DnsOptions { min_resolution_interval: get_min_resolution_interval(), resolving_timeout: get_resolving_timeout(), backoff_config: DEFAULT_EXPONENTIAL_CONFIG, host, port: endpoint.port, }; Box::new(DnsResolver::new(dns_client, options, dns_opts)) } fn scheme(&self) -> &'static str { "dns" } fn is_valid_uri(&self, target: &Target) -> bool { if let Err(err) = parse_endpoint_and_authority(target) { eprintln!("{err}"); false } else { true } } } struct DnsResolver { state: Arc>, task_handle: BoxedTaskHandle, resolve_now_notifier: Arc, channel_update_notifier: Arc, } struct InternalState { addrs: Result, String>, // Error from the latest call to channel_controller.update(). channel_response: Option, } impl Resolver for DnsResolver { fn resolve_now(&mut self) { self.resolve_now_notifier.notify_one(); } fn work(&mut self, channel_controller: &mut dyn ChannelController) { let mut state = self.state.lock(); let endpoint_result = match &state.addrs { Ok(addrs) => { let endpoints: Vec<_> = addrs .iter() .map(|a| Endpoint { addresses: vec![Address { network_type: TCP_IP_NETWORK_TYPE, address: ByteStr::from(a.to_string()), ..Default::default() }], ..Default::default() }) .collect(); Ok(endpoints) } Err(err) => Err(err.to_string()), }; let update = ResolverUpdate { endpoints: endpoint_result, ..Default::default() }; let status = channel_controller.update(update); state.channel_response = status.err(); self.channel_update_notifier.notify_one(); } } impl Drop for DnsResolver { fn drop(&mut self) { self.task_handle.abort(); } } #[derive(Eq, PartialEq, Debug)] struct HostPort { host: Host, port: u16, } #[derive(Eq, PartialEq, Debug)] struct ParseResult { endpoint: HostPort, authority: Option, } fn parse_endpoint_and_authority(target: &Target) -> Result { // Parse the endpoint. let endpoint = target.path(); let endpoint = endpoint.strip_prefix("/").unwrap_or(endpoint); let parse_result = parse_host_port(endpoint, DEFAULT_PORT) .map_err(|err| format!("Failed to parse target {target}: {err}"))?; let endpoint = parse_result.ok_or("Received empty endpoint host.".to_string())?; // Parse the authority. let authority = target.authority_host_port(); if authority.is_empty() { return Ok(ParseResult { endpoint, authority: None, }); } let parse_result = parse_host_port(&authority, DEFAULT_DNS_PORT) .map_err(|err| format!("Failed to parse DNS authority {target}: {err}"))?; let Some(authority) = parse_result else { return Ok(ParseResult { endpoint, authority: None, }); }; let authority = match authority.host { Host::Ipv4(ipv4) => SocketAddr::new(IpAddr::V4(ipv4), authority.port), Host::Ipv6(ipv6) => SocketAddr::new(IpAddr::V6(ipv6), authority.port), _ => { return Err(format!("Received non-IP DNS authority {}", authority.host)); } }; Ok(ParseResult { endpoint, authority: Some(authority), }) } /// Takes the user input string of the format "host:port" and default port, /// returns the parsed host and port. If string doesn't specify a port, the /// default_port is returned. If the string doesn't specify the host, /// Ok(None) is returned. fn parse_host_port(host_and_port: &str, default_port: u16) -> Result, String> { // We need to use the https scheme otherwise url::Url::parse doesn't convert // IP addresses to Host::Ipv4 or Host::Ipv6 if they could represent valid // domains. let url = format!("https://{host_and_port}"); let url = url.parse::().map_err(|err| err.to_string())?; let port = url.port().unwrap_or(default_port); let host = match url.host() { Some(host) => host, None => return Ok(None), }; // Convert the domain to an owned string. let host = match host { Host::Domain(s) => Host::Domain(s.to_owned()), Host::Ipv4(ip) => Host::Ipv4(ip), Host::Ipv6(ip) => Host::Ipv6(ip), }; Ok(Some(HostPort { host, port })) } fn nop_resolver_for_ip(ip: IpAddr, port: u16, options: ResolverOptions) -> Box { options.work_scheduler.schedule_work(); Box::new(NopResolver { update: ResolverUpdate { endpoints: Ok(vec![Endpoint { addresses: vec![Address { network_type: TCP_IP_NETWORK_TYPE, address: ByteStr::from(SocketAddr::new(ip, port).to_string()), ..Default::default() }], ..Default::default() }]), ..Default::default() }, }) } fn nop_resolver_for_err(err: String, options: ResolverOptions) -> Box { options.work_scheduler.schedule_work(); Box::new(NopResolver { update: ResolverUpdate { endpoints: Err(err), ..Default::default() }, }) } ================================================ FILE: grpc/src/client/name_resolution/dns/test.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::future::Future; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::{self}; use url::Host; use crate::client::name_resolution::ChannelController; use crate::client::name_resolution::Resolver; use crate::client::name_resolution::ResolverOptions; use crate::client::name_resolution::ResolverUpdate; use crate::client::name_resolution::Target; use crate::client::name_resolution::WorkScheduler; use crate::client::name_resolution::backoff::BackoffConfig; use crate::client::name_resolution::backoff::DEFAULT_EXPONENTIAL_CONFIG; use crate::client::name_resolution::dns::DnsOptions; use crate::client::name_resolution::dns::DnsResolver; use crate::client::name_resolution::dns::HostPort; use crate::client::name_resolution::dns::ParseResult; use crate::client::name_resolution::dns::get_min_resolution_interval; use crate::client::name_resolution::dns::get_resolving_timeout; use crate::client::name_resolution::dns::parse_endpoint_and_authority; use crate::client::name_resolution::dns::reg; use crate::client::name_resolution::global_registry; use crate::client::service_config::ServiceConfig; use crate::rt::BoxFuture; use crate::rt::GrpcRuntime; use crate::rt::TcpOptions; use crate::rt::tokio::TokioRuntime; use crate::rt::{self}; const DEFAULT_TEST_SHORT_TIMEOUT: Duration = Duration::from_millis(10); #[test] pub(crate) fn target_parsing() { struct TestCase { input: &'static str, want_result: Result, } let test_cases = vec![ TestCase { input: "dns:///grpc.io", want_result: Ok(ParseResult { endpoint: HostPort { host: Host::Domain("grpc.io".to_string()), port: 443, }, authority: None, }), }, TestCase { input: "dns:///grpc.io:1234", want_result: Ok(ParseResult { endpoint: HostPort { host: Host::Domain("grpc.io".to_string()), port: 1234, }, authority: None, }), }, TestCase { input: "dns://8.8.8.8/grpc.io:1234", want_result: Ok(ParseResult { endpoint: HostPort { host: Host::Domain("grpc.io".to_string()), port: 1234, }, authority: Some("8.8.8.8:53".parse().unwrap()), }), }, TestCase { input: "dns://8.8.8.8:5678/grpc.io:1234/abc", want_result: Ok(ParseResult { endpoint: HostPort { host: Host::Domain("grpc.io".to_string()), port: 1234, }, authority: Some("8.8.8.8:5678".parse().unwrap()), }), }, TestCase { input: "dns://[::1]:5678/grpc.io:1234/abc", want_result: Ok(ParseResult { endpoint: HostPort { host: Host::Domain("grpc.io".to_string()), port: 1234, }, authority: Some("[::1]:5678".parse().unwrap()), }), }, TestCase { input: "dns://[fe80::1]:5678/127.0.0.1:1234/abc", want_result: Ok(ParseResult { endpoint: HostPort { host: Host::Ipv4("127.0.0.1".parse().unwrap()), port: 1234, }, authority: Some("[fe80::1]:5678".parse().unwrap()), }), }, TestCase { input: "dns:///[fe80::1%80]:5678/abc", want_result: Err("SocketAddr doesn't support IPv6 addresses with zones".to_string()), }, TestCase { input: "dns:///:5678/abc", want_result: Err("Empty host with port".to_string()), }, TestCase { input: "dns:///grpc.io:abc/abc", want_result: Err("Non numeric port".to_string()), }, TestCase { input: "dns:///grpc.io:/", want_result: Ok(ParseResult { endpoint: HostPort { host: Host::Domain("grpc.io".to_string()), port: 443, }, authority: None, }), }, TestCase { input: "dns:///:", want_result: Err("No host and port".to_string()), }, TestCase { input: "dns:///[2001:db8:a0b:12f0::1", want_result: Err("Invalid address".to_string()), }, ]; for tc in test_cases { let target: Target = tc.input.parse().unwrap(); let got = parse_endpoint_and_authority(&target); if got.is_err() != tc.want_result.is_err() { panic!( "Got error {:?}, want error: {:?}", got.err(), tc.want_result.err() ); } if got.is_err() { continue; } assert_eq!(got.unwrap(), tc.want_result.unwrap()); } } struct FakeWorkScheduler { work_tx: UnboundedSender<()>, } impl WorkScheduler for FakeWorkScheduler { fn schedule_work(&self) { self.work_tx.send(()).unwrap(); } } struct FakeChannelController { update_result: Result<(), String>, update_tx: UnboundedSender, } impl ChannelController for FakeChannelController { fn update(&mut self, update: ResolverUpdate) -> Result<(), String> { println!("Received resolver update: {:?}", &update); self.update_tx.send(update).unwrap(); self.update_result.clone() } fn parse_service_config(&self, _: &str) -> Result { Err("Unimplemented".to_string()) } } #[tokio::test] pub(crate) async fn dns_basic() { reg(); let builder = global_registry().get("dns").unwrap(); let target = &"dns:///localhost:1234".parse().unwrap(); let (work_tx, mut work_rx) = mpsc::unbounded_channel(); let work_scheduler = Arc::new(FakeWorkScheduler { work_tx: work_tx.clone(), }); let opts = ResolverOptions { authority: "ignored".to_string(), runtime: rt::default_runtime(), work_scheduler: work_scheduler.clone(), }; let mut resolver = builder.build(target, opts); // Wait for schedule work to be called. work_rx.recv().await.unwrap(); let (update_tx, mut update_rx) = mpsc::unbounded_channel(); let mut channel_controller = FakeChannelController { update_tx, update_result: Ok(()), }; resolver.work(&mut channel_controller); // A successful endpoint update should be received. let update = update_rx.recv().await.unwrap(); assert!(update.endpoints.unwrap().len() > 1); } #[tokio::test] pub(crate) async fn invalid_target() { reg(); let builder = global_registry().get("dns").unwrap(); let target = &"dns:///:1234".parse().unwrap(); let (work_tx, mut work_rx) = mpsc::unbounded_channel(); let work_scheduler = Arc::new(FakeWorkScheduler { work_tx: work_tx.clone(), }); let opts = ResolverOptions { authority: "ignored".to_string(), runtime: rt::default_runtime(), work_scheduler: work_scheduler.clone(), }; let mut resolver = builder.build(target, opts); // Wait for schedule work to be called. work_rx.recv().await.unwrap(); let (update_tx, mut update_rx) = mpsc::unbounded_channel(); let mut channel_controller = FakeChannelController { update_tx, update_result: Ok(()), }; resolver.work(&mut channel_controller); // An error endpoint update should be received. let update = update_rx.recv().await.unwrap(); assert!( update .endpoints .err() .unwrap() .contains(&target.to_string()) ); } #[derive(Clone, Debug)] struct FakeDns { latency: Duration, lookup_result: Result, String>, } #[tonic::async_trait] impl rt::DnsResolver for FakeDns { async fn lookup_host_name(&self, _: &str) -> Result, String> { tokio::time::sleep(self.latency).await; self.lookup_result.clone() } async fn lookup_txt(&self, _: &str) -> Result, String> { Err("unimplemented".to_string()) } } #[derive(Debug)] struct FakeRuntime { inner: TokioRuntime, dns: FakeDns, } impl rt::Runtime for FakeRuntime { fn spawn( &self, task: Pin + Send + 'static>>, ) -> Box { self.inner.spawn(task) } fn get_dns_resolver(&self, _: rt::ResolverOptions) -> Result, String> { Ok(Box::new(self.dns.clone())) } fn sleep(&self, duration: std::time::Duration) -> Pin> { self.inner.sleep(duration) } fn tcp_stream( &self, target: std::net::SocketAddr, opts: rt::TcpOptions, ) -> Pin, String>> + Send>> { self.inner.tcp_stream(target, opts) } fn listen_tcp( &self, _addr: std::net::SocketAddr, _opts: TcpOptions, ) -> BoxFuture, String>> { unimplemented!() } } #[tokio::test] pub(crate) async fn dns_lookup_error() { reg(); let builder = global_registry().get("dns").unwrap(); let target = &"dns:///grpc.io:1234".parse().unwrap(); let (work_tx, mut work_rx) = mpsc::unbounded_channel(); let work_scheduler = Arc::new(FakeWorkScheduler { work_tx: work_tx.clone(), }); let runtime = FakeRuntime { inner: TokioRuntime::default(), dns: FakeDns { latency: Duration::from_secs(0), lookup_result: Err("test_error".to_string()), }, }; let opts = ResolverOptions { authority: "ignored".to_string(), runtime: GrpcRuntime::new(runtime), work_scheduler: work_scheduler.clone(), }; let mut resolver = builder.build(target, opts); // Wait for schedule work to be called. work_rx.recv().await.unwrap(); let (update_tx, mut update_rx) = mpsc::unbounded_channel(); let mut channel_controller = FakeChannelController { update_tx, update_result: Ok(()), }; resolver.work(&mut channel_controller); // An error endpoint update should be received. let update = update_rx.recv().await.unwrap(); assert!(update.endpoints.err().unwrap().contains("test_error")); } #[tokio::test] pub(crate) async fn dns_lookup_timeout() { let (work_tx, mut work_rx) = mpsc::unbounded_channel(); let work_scheduler = Arc::new(FakeWorkScheduler { work_tx: work_tx.clone(), }); let runtime = FakeRuntime { inner: TokioRuntime::default(), dns: FakeDns { latency: Duration::from_secs(20), lookup_result: Ok(Vec::new()), }, }; let dns_client = runtime.dns.clone(); let opts = ResolverOptions { authority: "ignored".to_string(), runtime: GrpcRuntime::new(runtime), work_scheduler: work_scheduler.clone(), }; let dns_opts = DnsOptions { min_resolution_interval: get_min_resolution_interval(), resolving_timeout: DEFAULT_TEST_SHORT_TIMEOUT, backoff_config: DEFAULT_EXPONENTIAL_CONFIG, host: "grpc.io".to_string(), port: 1234, }; let mut resolver = DnsResolver::new(Box::new(dns_client), opts, dns_opts); // Wait for schedule work to be called. work_rx.recv().await.unwrap(); let (update_tx, mut update_rx) = mpsc::unbounded_channel(); let mut channel_controller = FakeChannelController { update_tx, update_result: Ok(()), }; resolver.work(&mut channel_controller); // An error endpoint update should be received. let update = update_rx.recv().await.unwrap(); assert!(update.endpoints.err().unwrap().contains("Timed out")); } #[tokio::test] pub(crate) async fn rate_limit() { let (work_tx, mut work_rx) = mpsc::unbounded_channel(); let work_scheduler = Arc::new(FakeWorkScheduler { work_tx: work_tx.clone(), }); let opts = ResolverOptions { authority: "ignored".to_string(), runtime: rt::default_runtime(), work_scheduler: work_scheduler.clone(), }; let dns_client = opts .runtime .get_dns_resolver(rt::ResolverOptions { server_addr: None }) .unwrap(); let dns_opts = DnsOptions { min_resolution_interval: Duration::from_secs(20), resolving_timeout: get_resolving_timeout(), backoff_config: DEFAULT_EXPONENTIAL_CONFIG, host: "localhost".to_string(), port: 1234, }; let mut resolver = DnsResolver::new(dns_client, opts, dns_opts); // Wait for schedule work to be called. work_rx.recv().await.unwrap(); let (update_tx, mut update_rx) = mpsc::unbounded_channel(); let mut channel_controller = FakeChannelController { update_tx, update_result: Ok(()), }; resolver.work(&mut channel_controller); // A successful endpoint update should be received. let update = update_rx.recv().await.unwrap(); assert!(update.endpoints.unwrap().len() > 1); // Call resolve_now repeatedly, new updates should not be produced. for _ in 0..5 { resolver.resolve_now(); tokio::select! { _ = work_rx.recv() => { panic!("Received unexpected work request from resolver"); } _ = tokio::time::sleep(DEFAULT_TEST_SHORT_TIMEOUT) => { println!("No work requested from resolver."); } }; } } #[tokio::test] pub(crate) async fn re_resolution_after_success() { let (work_tx, mut work_rx) = mpsc::unbounded_channel(); let work_scheduler = Arc::new(FakeWorkScheduler { work_tx: work_tx.clone(), }); let opts = ResolverOptions { authority: "ignored".to_string(), runtime: rt::default_runtime(), work_scheduler: work_scheduler.clone(), }; let dns_opts = DnsOptions { min_resolution_interval: Duration::from_millis(1), resolving_timeout: get_resolving_timeout(), backoff_config: DEFAULT_EXPONENTIAL_CONFIG, host: "localhost".to_string(), port: 1234, }; let dns_client = opts .runtime .get_dns_resolver(rt::ResolverOptions { server_addr: None }) .unwrap(); let mut resolver = DnsResolver::new(dns_client, opts, dns_opts); // Wait for schedule work to be called. work_rx.recv().await.unwrap(); let (update_tx, mut update_rx) = mpsc::unbounded_channel(); let mut channel_controller = FakeChannelController { update_tx, update_result: Ok(()), }; resolver.work(&mut channel_controller); // A successful endpoint update should be received. let update = update_rx.recv().await.unwrap(); assert!(update.endpoints.unwrap().len() > 1); // Call resolve_now, a new update should be produced. resolver.resolve_now(); work_rx.recv().await.unwrap(); resolver.work(&mut channel_controller); let update = update_rx.recv().await.unwrap(); assert!(update.endpoints.unwrap().len() > 1); } #[tokio::test] pub(crate) async fn backoff_on_error() { let (work_tx, mut work_rx) = mpsc::unbounded_channel(); let work_scheduler = Arc::new(FakeWorkScheduler { work_tx: work_tx.clone(), }); let opts = ResolverOptions { authority: "ignored".to_string(), runtime: rt::default_runtime(), work_scheduler: work_scheduler.clone(), }; let dns_opts = DnsOptions { min_resolution_interval: Duration::from_millis(1), resolving_timeout: get_resolving_timeout(), // Speed up the backoffs to make the test run faster. backoff_config: BackoffConfig { base_delay: Duration::from_millis(1), multiplier: 1.0, jitter: 0.0, max_delay: Duration::from_millis(1), }, host: "localhost".to_string(), port: 1234, }; let dns_client = opts .runtime .get_dns_resolver(rt::ResolverOptions { server_addr: None }) .unwrap(); let mut resolver = DnsResolver::new(dns_client, opts, dns_opts); let (update_tx, mut update_rx) = mpsc::unbounded_channel(); let mut channel_controller = FakeChannelController { update_tx, update_result: Err("test_error".to_string()), }; // As the channel returned an error to the resolver, the resolver will // backoff and re-attempt resolution. for _ in 0..5 { work_rx.recv().await.unwrap(); resolver.work(&mut channel_controller); let update = update_rx.recv().await.unwrap(); assert!(update.endpoints.unwrap().len() > 1); } // This time the channel accepts the resolver update. channel_controller.update_result = Ok(()); work_rx.recv().await.unwrap(); resolver.work(&mut channel_controller); let update = update_rx.recv().await.unwrap(); assert!(update.endpoints.unwrap().len() > 1); // Since the channel controller returns Ok(), the resolver will stop // producing more updates. tokio::select! { _ = work_rx.recv() => { panic!("Received unexpected work request from resolver."); } _ = tokio::time::sleep(DEFAULT_TEST_SHORT_TIMEOUT) => { println!("No event received from resolver."); } }; } ================================================ FILE: grpc/src/client/name_resolution/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ //! Name Resolution for gRPC. //! //! Name Resolution is the process by which a channel's target is converted into //! network addresses (typically IP addresses) used by the channel to connect to //! a service. use core::fmt; use std::fmt::Display; use std::fmt::Formatter; use std::hash::Hash; use std::str::FromStr; use std::sync::Arc; use url::Url; use crate::attributes::Attributes; use crate::byte_str::ByteStr; use crate::client::service_config::ServiceConfig; use crate::rt::GrpcRuntime; mod backoff; pub(crate) mod dns; mod registry; pub(crate) use registry::global_registry; /// Target represents a target for gRPC, as specified in: /// https://github.com/grpc/grpc/blob/master/doc/naming.md. /// It is parsed from the target string that gets passed during channel creation /// by the user. gRPC passes it to the resolver and the balancer. /// /// If the target follows the naming spec, and the parsed scheme is registered /// with gRPC, we will parse the target string according to the spec. If the /// target does not contain a scheme or if the parsed scheme is not registered /// (i.e. no corresponding resolver available to resolve the endpoint), we will /// apply the default scheme, and will attempt to reparse it. #[derive(Debug, Clone)] pub(crate) struct Target { url: Url, } impl FromStr for Target { type Err = String; fn from_str(s: &str) -> Result { match s.parse::() { Ok(url) => Ok(Target { url }), Err(err) => Err(err.to_string()), } } } impl From for Target { fn from(url: url::Url) -> Self { Target { url } } } /// Target represents a target for gRPC, as specified in: /// https://github.com/grpc/grpc/blob/master/doc/naming.md. /// It is parsed from the target string that gets passed during channel creation /// by the user. gRPC passes it to the resolver and the balancer. /// /// If the target follows the naming spec, and the parsed scheme is registered /// with gRPC, we will parse the target string according to the spec. If the /// target does not contain a scheme or if the parsed scheme is not registered /// (i.e. no corresponding resolver available to resolve the endpoint), we will /// apply the default scheme, and will attempt to reparse it. impl Target { pub fn scheme(&self) -> &str { self.url.scheme() } /// The host part of the authority. pub fn authority_host(&self) -> &str { self.url.host_str().unwrap_or("") } /// The port part of the authority. pub fn authority_port(&self) -> Option { self.url.port() } /// Returns either host:port or host depending on the existence of the port /// in the authority. pub fn authority_host_port(&self) -> String { let host = self.authority_host(); let port = self.authority_port(); if let Some(port) = port { format!("{host}:{port}") } else { host.to_owned() } } /// Retrieves endpoint from `Url.path()`. pub fn path(&self) -> &str { self.url.path() } } impl Display for Target { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}://{}{}", self.scheme(), self.authority_host_port(), self.path() ) } } /// A name resolver factory that produces Resolver instances used by the channel /// to resolve network addresses for the target URI. pub(crate) trait ResolverBuilder: Send + Sync { /// Builds a name resolver instance. /// /// Note that build must not fail. Instead, an erroring Resolver may be /// returned that calls ChannelController.update() with an Err value. fn build(&self, target: &Target, options: ResolverOptions) -> Box; /// Reports the URI scheme handled by this name resolver. fn scheme(&self) -> &str; /// Returns the default authority for a channel using this name resolver /// and target. This refers to the *dataplane authority* — the value used /// in the `:authority` header of HTTP/2 requests — and not to be confused /// with the authority portion of the target URI, which typically specifies /// the name of an external server used for name resolution. /// /// By default, this method returns the path portion of the target URI, /// with the leading prefix removed. fn default_authority(&self, target: &Target) -> String { let path = target.path(); path.strip_prefix("/").unwrap_or(path).to_string() } /// Returns a bool indicating whether the input uri is valid to create a /// resolver. fn is_valid_uri(&self, uri: &Target) -> bool; } /// A collection of data configured on the channel that is constructing this /// name resolver. #[non_exhaustive] pub(crate) struct ResolverOptions { /// The authority that will be used for the channel by default. This refers /// to the `:authority` value sent in HTTP/2 requests — the dataplane /// authority — and not the authority portion of the target URI, which is /// typically used to identify the name resolution server. /// /// This value is either the result of the `default_authority` method of /// this `ResolverBuilder`, or another string if the channel was explicitly /// configured to override the default. pub authority: String, /// The runtime which provides utilities to do async work. pub runtime: GrpcRuntime, /// A hook into the channel's work scheduler that allows the Resolver to /// request the ability to perform operations on the ChannelController. pub work_scheduler: Arc, } /// Used to asynchronously request a call into the Resolver's work method. pub(crate) trait WorkScheduler: Send + Sync { // Schedules a call into the Resolver's work method. If there is already a // pending work call that has not yet started, this may not schedule another // call. fn schedule_work(&self); } /// Resolver watches for the updates on the specified target. /// Updates include address updates and service config updates. // This trait may not need the Sync sub-trait if the channel implementation can // ensure that the resolver is accessed serially. The sub-trait can be removed // in that case. pub(crate) trait Resolver: Send + Sync { /// Asks the resolver to obtain an updated resolver result, if applicable. /// /// This is useful for polling resolvers to decide when to re-resolve. /// However, the implementation is not required to re-resolve immediately /// upon receiving this call; it may instead elect to delay based on some /// configured minimum time between queries, to avoid hammering the name /// service with queries. /// /// For watch based resolvers, this may be a no-op. fn resolve_now(&mut self); /// Called serially by the channel to provide access to the /// `ChannelController`. fn work(&mut self, channel_controller: &mut dyn ChannelController); } /// The `ChannelController` trait provides the resolver with functionality /// to interact with the channel. pub(crate) trait ChannelController: Send + Sync { /// Notifies the channel about the current state of the name resolver. If /// an error value is returned, the name resolver should attempt to /// re-resolve, if possible. The resolver is responsible for applying an /// appropriate backoff mechanism to avoid overloading the system or the /// remote resolver. fn update(&mut self, update: ResolverUpdate) -> Result<(), String>; /// Parses the provided JSON service config and returns an instance of a /// ParsedServiceConfig. fn parse_service_config(&self, config: &str) -> Result; } #[derive(Clone, Debug)] #[non_exhaustive] /// ResolverUpdate contains the current Resolver state relevant to the /// channel. pub(crate) struct ResolverUpdate { /// Attributes contains arbitrary data about the resolver intended for /// consumption by the load balancing policy. pub attributes: Attributes, /// A list of endpoints which each identify a logical host serving the /// service indicated by the target URI. pub endpoints: Result, String>, /// The service config which the client should use for communicating with /// the service. If it is None, it indicates no service config is present or /// the resolver does not provide service configs. pub service_config: Result, String>, /// An optional human-readable note describing context about the /// resolution, to be passed along to the LB policy for inclusion in /// RPC failure status messages in cases where neither endpoints nor /// service_config has a non-OK status. For example, a resolver that /// returns an empty endpoint list but a valid service config may set /// to this to something like "no DNS entries found for ". pub resolution_note: Option, } impl Default for ResolverUpdate { fn default() -> Self { ResolverUpdate { service_config: Ok(Default::default()), attributes: Default::default(), endpoints: Ok(Default::default()), resolution_note: Default::default(), } } } /// An Endpoint is an address or a collection of addresses which reference one /// logical server. Multiple addresses may be used if there are multiple ways /// which the server can be reached, e.g. via IPv4 and IPv6 addresses. #[derive(Debug, Default, Clone, PartialEq, Eq)] #[non_exhaustive] pub(crate) struct Endpoint { /// Addresses contains a list of addresses used to access this endpoint. pub addresses: Vec
, /// Attributes contains arbitrary data about this endpoint intended for /// consumption by the LB policy. pub attributes: Attributes, } impl Hash for Endpoint { fn hash(&self, state: &mut H) { self.addresses.hash(state); } } /// An Address is an identifier that indicates how to connect to a server. #[non_exhaustive] #[derive(Debug, Clone, Default, Ord, PartialOrd)] pub(crate) struct Address { /// The network type is used to identify what kind of transport to create /// when connecting to this address. Typically TCP_IP_ADDRESS_TYPE. pub network_type: &'static str, /// The address itself is passed to the transport in order to create a /// connection to it. pub address: ByteStr, /// Attributes contains arbitrary data about this address intended for /// consumption by the subchannel. pub attributes: Attributes, } impl Eq for Address {} impl PartialEq for Address { fn eq(&self, other: &Self) -> bool { self.network_type == other.network_type && self.address == other.address } } impl Hash for Address { fn hash(&self, state: &mut H) { self.network_type.hash(state); self.address.hash(state); } } impl Display for Address { #[allow(clippy::to_string_in_format_args)] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}:{}", self.network_type, self.address.to_string()) } } /// Indicates the address is an IPv4 or IPv6 address that should be connected to /// via TCP/IP. pub(crate) static TCP_IP_NETWORK_TYPE: &str = "tcp"; // A resolver that returns the same result every time its work method is called. // It can be used to return an error to the channel when a resolver fails to // build. struct NopResolver { pub update: ResolverUpdate, } impl Resolver for NopResolver { fn resolve_now(&mut self) {} fn work(&mut self, channel_controller: &mut dyn ChannelController) { let _ = channel_controller.update(self.update.clone()); } } #[cfg(test)] mod test { use super::Target; #[test] pub fn parse_target() { #[derive(Default)] struct TestCase { input: &'static str, want_scheme: &'static str, want_host: &'static str, want_port: Option, want_host_port: &'static str, want_path: &'static str, want_str: &'static str, } let test_cases = vec![ TestCase { input: "dns:///grpc.io", want_scheme: "dns", want_host_port: "", want_host: "", want_port: None, want_path: "/grpc.io", want_str: "dns:///grpc.io", }, TestCase { input: "dns://8.8.8.8:53/grpc.io/docs", want_scheme: "dns", want_host_port: "8.8.8.8:53", want_host: "8.8.8.8", want_port: Some(53), want_path: "/grpc.io/docs", want_str: "dns://8.8.8.8:53/grpc.io/docs", }, TestCase { input: "unix:path/to/file", want_scheme: "unix", want_host_port: "", want_host: "", want_port: None, want_path: "path/to/file", want_str: "unix://path/to/file", }, TestCase { input: "unix:///run/containerd/containerd.sock", want_scheme: "unix", want_host_port: "", want_host: "", want_port: None, want_path: "/run/containerd/containerd.sock", want_str: "unix:///run/containerd/containerd.sock", }, ]; for tc in test_cases { let target: Target = tc.input.parse().unwrap(); assert_eq!(target.scheme(), tc.want_scheme); assert_eq!(target.authority_host(), tc.want_host); assert_eq!(target.authority_port(), tc.want_port); assert_eq!(target.authority_host_port(), tc.want_host_port); assert_eq!(target.path(), tc.want_path); assert_eq!(&target.to_string(), tc.want_str); } } } ================================================ FILE: grpc/src/client/name_resolution/registry.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex; use std::sync::OnceLock; use crate::client::name_resolution::ResolverBuilder; static GLOBAL_RESOLVER_REGISTRY: OnceLock = OnceLock::new(); /// A registry to store and retrieve name resolvers. Resolvers are indexed by /// the URI scheme they are intended to handle. #[derive(Default)] pub(crate) struct ResolverRegistry { inner: Arc>>>, } impl ResolverRegistry { /// Construct an empty name resolver registry. fn new() -> Self { Self { inner: Arc::default(), } } /// Add a name resolver into the registry. builder.scheme() will /// be used as the scheme registered with this builder. If multiple /// resolvers are registered with the same name, the one registered last /// will take effect. /// /// # Panics /// /// Panics if the given scheme contains uppercase characters. pub fn add_builder(&self, builder: Box) { self.try_add_builder(builder).unwrap(); } /// Add a name resolver into the registry. builder.scheme() will /// be used as the scheme registered with this builder. If multiple /// resolvers are registered with the same name, the one registered last /// will take effect. pub fn try_add_builder(&self, builder: Box) -> Result<(), String> { let scheme = builder.scheme(); if scheme.chars().any(|c| c.is_ascii_uppercase()) { return Err(format!( "Scheme must not contain uppercase characters: {scheme}" )); } self.inner .lock() .unwrap() .insert(scheme.to_string(), Arc::from(builder)); Ok(()) } /// Returns the resolver builder registered for the given scheme, if any. /// /// The provided scheme is case-insensitive; any uppercase characters /// will be converted to lowercase before lookup. pub fn get(&self, scheme: &str) -> Option> { self.inner .lock() .unwrap() .get(&scheme.to_lowercase()) .cloned() } } /// Global registry for resolver builders. pub(crate) fn global_registry() -> &'static ResolverRegistry { GLOBAL_RESOLVER_REGISTRY.get_or_init(ResolverRegistry::new) } ================================================ FILE: grpc/src/client/service_config.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::any::Any; use std::sync::Arc; /// An in-memory representation of a service config, usually provided to gRPC as /// a JSON object. // TODO: this shouldn't be public; users should set with JSON instead. #[derive(Debug, Default, Clone)] pub(crate) struct ServiceConfig { pub load_balancing_policy: Option, } #[derive(Debug, Default, Clone, PartialEq, Eq)] pub enum LbPolicyType { #[default] PickFirst, RoundRobin, } /// A convenience wrapper for an LB policy's configuration object. #[derive(Debug, Clone)] pub(crate) struct LbConfig { config: Arc, } impl LbConfig { /// Create a new LbConfig wrapper containing the provided config. pub fn new(config: impl Any + Send + Sync) -> Self { LbConfig { config: Arc::new(config), } } /// Convenience method to extract the LB policy's configuration object. pub fn convert_to(&self) -> Option> { self.config.clone().downcast::().ok() } } ================================================ FILE: grpc/src/client/stream_util.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use crate::Status; use crate::StatusCode; use crate::client::CallOptions; use crate::client::InvokeOnce; use crate::client::RecvStream; use crate::client::interceptor::Intercept; use crate::core::ClientResponseStreamItem; use crate::core::RecvMessage; use crate::core::RequestHeaders; use crate::core::ResponseStreamItem; use crate::core::Trailers; /// An interceptor that enforces proper gRPC semantics on the response stream. #[derive(Clone)] pub struct ResponseValidator { unary: bool, } impl ResponseValidator { /// Constructs a new instance of the response validator. If `unary` is set, /// the validator will enforce proper unary protocol for the stream (e.g. /// exactly one message or an error). /// /// Note that wrapping an entire channel with this interceptor is likely /// inappropraite if `unary` is set. pub fn new(unary: bool) -> Self { Self { unary } } } impl Intercept for ResponseValidator { type SendStream = I::SendStream; type RecvStream = RecvStreamValidator; async fn intercept( &self, headers: RequestHeaders, options: CallOptions, next: I, ) -> (Self::SendStream, Self::RecvStream) { let (tx, rx) = next.invoke_once(headers, options).await; (tx, RecvStreamValidator::new(rx, self.unary)) } } /// RecvStreamValidator wraps a client's RecvStream and enforces proper /// RecvStream semantics on it so that protocol validation does not need to be /// handled by the consumer. pub struct RecvStreamValidator { recv_stream: R, state: RecvStreamState, unary_response: bool, } enum RecvStreamState { AwaitingHeaders, AwaitingMessagesOrTrailers, AwaitingTrailers, Done, } impl RecvStreamValidator where R: RecvStream, { /// Constructs a new `RecvStreamValidator` for converting an untrusted /// `RecvStream` into one that enforces the proper gRPC response stream /// protocol. If the protocol is violated an error will be synthesized. /// Any calls to the `RecvStream` impl's `next` method beyond `Trailers` /// will not be propagated and will immediately return `StreamClosed`. pub fn new(recv_stream: R, unary_response: bool) -> Self { Self { recv_stream, state: RecvStreamState::AwaitingHeaders, unary_response, } } /// Sets the state to Done and produces a synthesized trailer item /// containing the error message. fn error(&mut self, s: impl Into) -> ClientResponseStreamItem { self.state = RecvStreamState::Done; ResponseStreamItem::Trailers(Trailers::new(Status::new(StatusCode::Internal, s))) } } impl RecvStream for RecvStreamValidator where R: RecvStream, { async fn next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem { // Never call the underlying RecvStream if done. if matches!(self.state, RecvStreamState::Done) { return ResponseStreamItem::StreamClosed; } let item = self.recv_stream.next(msg).await; match item { ResponseStreamItem::Headers(_) => { if matches!(self.state, RecvStreamState::AwaitingHeaders) { self.state = RecvStreamState::AwaitingMessagesOrTrailers; item } else { self.error("stream received multiple headers") } } ResponseStreamItem::Message(_) => { if matches!(self.state, RecvStreamState::AwaitingMessagesOrTrailers) { if self.unary_response { self.state = RecvStreamState::AwaitingTrailers; } item } else if matches!(self.state, RecvStreamState::AwaitingTrailers) { self.error("unary stream received multiple messages") } else { self.error("stream received messages without headers") } } ResponseStreamItem::Trailers(t) => { if self.unary_response && !matches!(self.state, RecvStreamState::AwaitingTrailers) && t.status().code() == StatusCode::Ok { return self.error("unary stream received zero messages"); } // Always return a trailers result immediately - it is valid any // time but sets the stream's state to Done. self.state = RecvStreamState::Done; ResponseStreamItem::Trailers(t) } ResponseStreamItem::StreamClosed => { // Trailers were never received or we would be Done. self.error("stream ended without trailers") } } } } #[cfg(test)] mod test { use std::mem::discriminant; use std::vec; use bytes::Buf; use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Sender; use super::*; use crate::client::SendOptions; use crate::client::SendStream; use crate::client::interceptor::InvokeOnceExt as _; use crate::core::ResponseHeaders; use crate::core::SendMessage; // Tests that an error occurs if messages are received before headers. #[tokio::test] async fn test_validator_messages_before_headers() { let scenarios = [vec![ResponseStreamItem::Message(())]]; for scenario in scenarios { validate_scenario( &scenario, ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Internal, "received messages without headers", ))), false, ) .await; } } // Tests that an error occurs if StreamClosed is received early. #[tokio::test] async fn test_validator_stream_closed_before_trailers() { let scenarios = [ vec![ResponseStreamItem::StreamClosed], vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::StreamClosed, ], vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Message(()), ResponseStreamItem::StreamClosed, ], ]; for scenario in &scenarios { validate_scenario( scenario, ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Internal, "ended without trailers", ))), false, ) .await; } } // Tests that an error occurs if headers are received twice. #[tokio::test] async fn test_validator_headers_repeated() { let scenarios = [ vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Headers(ResponseHeaders::default()), ], vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Message(()), ResponseStreamItem::Headers(ResponseHeaders::default()), ], ]; for scenario in &scenarios { validate_scenario( scenario, ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Internal, "received multiple headers", ))), false, ) .await; } } #[tokio::test] async fn test_validator_unary_ok_without_message() { let scenarios = [ vec![ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Ok, "", )))], vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Trailers(Trailers::new(Status::new(StatusCode::Ok, ""))), ], ]; for scenario in &scenarios { validate_scenario( scenario, ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Internal, "received zero messages", ))), true, ) .await; } } #[tokio::test] async fn test_validator_unary_multiple_messages() { let scenarios = [vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Message(()), ResponseStreamItem::Message(()), ]]; for scenario in &scenarios { validate_scenario( scenario, ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Internal, "received multiple messages", ))), true, ) .await; } } #[tokio::test] async fn test_validator_successful_stream() { let scenarios = [vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Message(()), ResponseStreamItem::Message(()), ResponseStreamItem::Message(()), ResponseStreamItem::Trailers(Trailers::new(Status::new(StatusCode::Ok, ""))), ]]; for scenario in &scenarios { validate_scenario( scenario, ResponseStreamItem::Trailers(Trailers::new(Status::new(StatusCode::Ok, ""))), false, ) .await; } } #[tokio::test] async fn test_validator_erroring_stream() { let scenarios = [vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Message(()), ResponseStreamItem::Message(()), ResponseStreamItem::Message(()), ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Aborted, "some err", ))), ]]; for scenario in &scenarios { validate_scenario( scenario, ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Aborted, "some err", ))), false, ) .await; } } #[tokio::test] async fn test_validator_successful_unary() { let scenarios = [vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Message(()), ResponseStreamItem::Trailers(Trailers::new(Status::new(StatusCode::Ok, ""))), ]]; for scenario in &scenarios { validate_scenario( scenario, ResponseStreamItem::Trailers(Trailers::new(Status::new(StatusCode::Ok, ""))), true, ) .await; } } #[tokio::test] async fn test_validator_erroring_unary() { let scenarios = [ vec![ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Aborted, "some err", )))], vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Aborted, "some err", ))), ], vec![ ResponseStreamItem::Headers(ResponseHeaders::default()), ResponseStreamItem::Message(()), ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Aborted, "some err", ))), ], ]; for scenario in &scenarios { validate_scenario( scenario, ResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::Aborted, "some err", ))), true, ) .await; } } async fn validate_scenario( scenario: &[ResponseStreamItem<()>], expect: ResponseStreamItem<()>, unary: bool, ) { let (channel, tx) = MockRecvStream::new(); let channel = channel.with_interceptor(ResponseValidator::new(unary)); let (_, recv_stream) = channel .invoke_once(RequestHeaders::default(), CallOptions::default()) .await; let mut validator = RecvStreamValidator::new(recv_stream, unary); // Send all but the last item, verifying it is returned by the // validator. for item in &scenario[..scenario.len() - 1] { tx.send(item.clone()).await.unwrap(); let got = validator.next(&mut NopRecvMessage).await; // Assert that the item sent is the same type as the item received. println!("{got:?} vs {item:?}"); assert_eq!(discriminant(&got), discriminant(item)); } // Send the final item. tx.send(scenario[scenario.len() - 1].clone()).await.unwrap(); let got = validator.next(&mut NopRecvMessage).await; assert!(matches!(&got, expect)); if let ResponseStreamItem::Trailers(got_t) = got { let ResponseStreamItem::Trailers(expect_t) = expect else { unreachable!(); // per matches check above }; // Assert the codes match. assert_eq!(got_t.status().code(), expect_t.status().code()); // Assert the status received contains the expected status error message. assert!( got_t .status() .message() .contains(expect_t.status().message()) ); } } struct NopSendStream; impl SendStream for NopSendStream { async fn send(&mut self, _item: &dyn SendMessage, _options: SendOptions) -> Result<(), ()> { Ok(()) } } struct NopRecvMessage; impl RecvMessage for NopRecvMessage { fn decode(&mut self, data: &mut dyn Buf) -> Result<(), String> { Ok(()) } } /// Implements a RecvStream and an InvokeOnce that can be directed what to /// return manually by writing to the channel returned by `new`. struct MockRecvStream { rx: Receiver, } impl InvokeOnce for MockRecvStream { type SendStream = NopSendStream; type RecvStream = Self; async fn invoke_once( self, _headers: RequestHeaders, _options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { (NopSendStream, self) } } impl RecvStream for MockRecvStream { async fn next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem { self.rx.recv().await.unwrap() } } impl MockRecvStream { fn new() -> (Self, Sender) { let (tx, rx) = tokio::sync::mpsc::channel(1); (Self { rx }, tx) } } } ================================================ FILE: grpc/src/client/subchannel.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use core::panic; use std::collections::BTreeMap; use std::error::Error; use std::fmt::Debug; use std::fmt::Display; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; use std::sync::Weak; use std::time::Duration; use std::time::Instant; use tokio::sync::Notify; use tokio::sync::oneshot; use tonic::async_trait; use crate::client::CallOptions; use crate::client::ConnectivityState; use crate::client::DynInvoke; use crate::client::DynRecvStream; use crate::client::DynSendStream; use crate::client::channel::InternalChannelController; use crate::client::channel::WorkQueueItem; use crate::client::channel::WorkQueueTx; use crate::client::load_balancing::ExternalSubchannel; use crate::client::load_balancing::SubchannelState; use crate::client::name_resolution::Address; use crate::client::transport::DynTransport; use crate::client::transport::TransportOptions; use crate::core::RequestHeaders; use crate::rt::GrpcRuntime; type SharedInvoke = Arc; pub trait Backoff: Send + Sync { fn backoff_until(&self) -> Instant; fn reset(&self); fn min_connect_timeout(&self) -> Duration; } // TODO(easwars): Move this somewhere else, where appropriate. pub(crate) struct NopBackoff {} impl Backoff for NopBackoff { fn backoff_until(&self) -> Instant { Instant::now() } fn reset(&self) {} fn min_connect_timeout(&self) -> Duration { Duration::from_secs(20) } } enum InternalSubchannelState { Idle, Connecting, Ready(Arc), TransientFailure(String), } impl<'a> From<&'a InternalSubchannelState> for SubchannelState { fn from(iss: &'a InternalSubchannelState) -> SubchannelState { match &iss { InternalSubchannelState::Idle => SubchannelState { connectivity_state: ConnectivityState::Idle, last_connection_error: None, }, InternalSubchannelState::Connecting => SubchannelState { connectivity_state: ConnectivityState::Connecting, last_connection_error: None, }, InternalSubchannelState::Ready(_) => SubchannelState { connectivity_state: ConnectivityState::Ready, last_connection_error: None, }, InternalSubchannelState::TransientFailure(err) => { let arc_err: Arc = Arc::from(Box::from(err.clone())); SubchannelState { connectivity_state: ConnectivityState::TransientFailure, last_connection_error: Some(arc_err), } } } } } impl Display for InternalSubchannelState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Idle => write!(f, "Idle"), Self::Connecting => write!(f, "Connecting"), Self::Ready(_) => write!(f, "Ready"), Self::TransientFailure(_) => write!(f, "TransientFailure"), } } } impl Debug for InternalSubchannelState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Idle => write!(f, "Idle"), Self::Connecting => write!(f, "Connecting"), Self::Ready(_) => write!(f, "Ready"), Self::TransientFailure(_) => write!(f, "TransientFailure"), } } } impl PartialEq for InternalSubchannelState { fn eq(&self, other: &Self) -> bool { match &self { Self::Idle => { if let Self::Idle = other { return true; } } Self::Connecting => { if let Self::Connecting = other { return true; } } Self::Ready(_) => { if let Self::Ready(_) = other { return true; } } Self::TransientFailure(_) => { if let Self::TransientFailure(_) = other { return true; } } } false } } #[async_trait] impl DynInvoke for InternalSubchannel { async fn dyn_invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Box, Box) { let svc = match &self.inner.data.lock().unwrap().state { InternalSubchannelState::Ready(s) => s.clone(), _ => todo!("handle non-READY subchannel"), }; svc.dyn_invoke(headers, options).await } } pub(crate) struct InternalSubchannel { unregister_fn: Option>, key: SubchannelKey, inner: InnerSubchannel, on_drop: Arc, } #[derive(Clone)] struct InnerSubchannel { data: Arc>, } struct SharedInnerSubchannelData { address: String, state: InternalSubchannelState, watchers: Vec>, // TODO(easwars): Revisit the choice for this data structure. on_drop: Arc, transport_builder: Arc, backoff: Arc, runtime: GrpcRuntime, transport_options: TransportOptions, } impl SharedInnerSubchannelData { fn update_state(&mut self, state: InternalSubchannelState) { self.state = state; let state: SubchannelState = (&self.state).into(); for w in &self.watchers { w.on_state_change(state.clone()); } } } impl InternalSubchannel { pub(super) fn new( key: SubchannelKey, transport: Arc, backoff: Arc, unregister_fn: Box, runtime: GrpcRuntime, ) -> Arc { println!("creating new internal subchannel for: {:?}", &key); let address = key.address.address.to_string(); let on_drop = Arc::new(Notify::new()); Arc::new(Self { key, on_drop: on_drop.clone(), unregister_fn: Some(unregister_fn), inner: InnerSubchannel { data: Arc::new(Mutex::new(SharedInnerSubchannelData { address, transport_builder: transport, backoff, runtime, state: InternalSubchannelState::Idle, watchers: Vec::new(), on_drop, transport_options: TransportOptions::default(), // TODO: should be configurable })), }, }) } pub(super) fn address(&self) -> Address { self.key.address.clone() } /// Begins connecting the subchannel asynchronously. Does nothing if the /// subchannel is not currently idle. pub(super) fn connect(self: &Arc) { self.inner.begin_connecting(); } pub(super) fn register_connectivity_state_watcher(&self, watcher: Arc) { let mut data = self.inner.data.lock().unwrap(); data.watchers.push(watcher.clone()); let state = (&data.state).into(); watcher.on_state_change(state); } pub(super) fn unregister_connectivity_state_watcher( &self, watcher: Arc, ) { self.inner .data .lock() .unwrap() .watchers .retain(|x| !Arc::ptr_eq(x, &watcher)); } } // The InnerSubchannel states progress as follows: // // Idle -> Connecting -> Ready -> Idle [after disconnect] // or // Idle -> Connecting -> TransientFailure -> Idle [after backoff] // // Idle is always a terminal state. impl InnerSubchannel { fn move_to_idle(&self) { self.data .lock() .unwrap() .update_state(InternalSubchannelState::Idle); } // Starts connecting in the background and manages the full lifecycle of the // subchannel until it returns back to idle in that background task. fn begin_connecting(&self) { let mut data = self.data.lock().unwrap(); if data.state != InternalSubchannelState::Idle { return; } data.update_state(InternalSubchannelState::Connecting); let self_clone = self.clone(); let connect_timeout = data.backoff.min_connect_timeout(); let transport_builder = data.transport_builder.clone(); let address = data.address.clone(); let runtime = data.runtime.clone(); let on_drop = data.on_drop.clone(); let transport_opts = data.transport_options.clone(); data.runtime.spawn(Box::pin(async move { tokio::select! { _ = runtime.sleep(connect_timeout) => { self_clone.move_to_transient_failure("connect timeout expired".into()).await; } _ = on_drop.notified() => { } result = transport_builder.dyn_connect(address, runtime, &transport_opts) => { match result { Ok((service, disconnection_listener)) => { self_clone.move_to_ready(Arc::from(service), disconnection_listener).await; } Err(e) => { self_clone.move_to_transient_failure(e).await; } } }, } })); } // Sets the state to ready and then waits until the subchannel is dropped or // the connection is lost. Moves to idle upon connection loss. async fn move_to_ready( &self, svc: Arc, closed_rx: oneshot::Receiver>, ) { let on_drop; { let mut data = self.data.lock().unwrap(); // Reset connection backoff upon successfully moving to ready. data.backoff.reset(); on_drop = data.on_drop.clone(); data.update_state(InternalSubchannelState::Ready(svc.clone())); } // TODO(easwars): Does it make sense for disconnected() to return an // error string containing information about why the connection // terminated? But what can we do with that error other than logging // it, which the transport can do as well? tokio::select! { _ = on_drop.notified() => {} e = closed_rx => { eprintln!("Transport closed: {e:?}"); self.move_to_idle(); } } } // Sets the state to transient failure and then waits until the subchannel // is dropped or the backoff expires. Moves to idle upon backoff expiry. async fn move_to_transient_failure(&self, err: String) { let runtime; let on_drop; let backoff_interval; { let mut data = self.data.lock().unwrap(); data.update_state(InternalSubchannelState::TransientFailure(err.clone())); backoff_interval = data.backoff.backoff_until(); runtime = data.runtime.clone(); on_drop = data.on_drop.clone(); } tokio::select! { _ = on_drop.notified() => {} _ = runtime.sleep(backoff_interval.saturating_duration_since(Instant::now())) => { self.move_to_idle(); } } } } impl Drop for InternalSubchannel { fn drop(&mut self) { let unregister_fn = self.unregister_fn.take(); unregister_fn.unwrap()(self.key.clone()); self.on_drop.notify_waiters(); } } // SubchannelKey uniiquely identifies a subchannel in the pool. #[derive(PartialEq, PartialOrd, Eq, Ord, Clone)] pub(crate) struct SubchannelKey { address: Address, } impl SubchannelKey { pub(crate) fn new(address: Address) -> Self { Self { address } } } impl Display for SubchannelKey { #[allow(clippy::to_string_in_format_args)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.address.address.to_string()) } } impl Debug for SubchannelKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.address) } } pub(super) struct InternalSubchannelPool { subchannels: RwLock>>, } impl InternalSubchannelPool { pub(super) fn new() -> Self { Self { subchannels: RwLock::new(BTreeMap::new()), } } pub(super) fn lookup_subchannel(&self, key: &SubchannelKey) -> Option> { println!("looking up subchannel for: {key:?} in the pool"); if let Some(weak_isc) = self.subchannels.read().unwrap().get(key) && let Some(isc) = weak_isc.upgrade() { return Some(isc); } None } pub(super) fn register_subchannel( &self, key: &SubchannelKey, isc: Arc, ) -> Arc { println!("registering subchannel for: {key:?} with the pool"); self.subchannels .write() .unwrap() .insert(key.clone(), Arc::downgrade(&isc)); isc } pub(super) fn unregister_subchannel(&self, key: &SubchannelKey) { let mut subchannels = self.subchannels.write().unwrap(); if let Some(weak_isc) = subchannels.get(key) { if let Some(isc) = weak_isc.upgrade() { return; } println!("removing subchannel for: {key:?} from the pool"); subchannels.remove(key); return; } panic!("attempt to unregister subchannel for unknown key {:?}", key); } } #[derive(Clone)] pub(super) struct SubchannelStateWatcher { subchannel: Weak, work_scheduler: WorkQueueTx, } impl SubchannelStateWatcher { pub(super) fn new(sc: Arc, work_scheduler: WorkQueueTx) -> Self { Self { subchannel: Arc::downgrade(&sc), work_scheduler, } } fn on_state_change(&self, state: SubchannelState) { // Ignore internal subchannel state changes if the external subchannel // was dropped but its state watcher is still pending unregistration; // such updates are inconsequential. if let Some(sc) = self.subchannel.upgrade() { let _ = self.work_scheduler.send(WorkQueueItem::Closure(Box::new( move |c: &mut InternalChannelController| { c.lb.clone() .policy .lock() .unwrap() .as_mut() .unwrap() .subchannel_update(sc, &state, c); }, ))); } } } ================================================ FILE: grpc/src/client/transport/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::time::Duration; use std::time::Instant; use crate::client::DynInvoke; use crate::client::Invoke; use crate::rt::GrpcRuntime; mod registry; // Using tower/buffer enables tokio's rt feature even though it's possible to // create Buffers with a user provided executor. #[cfg(feature = "_runtime-tokio")] mod tonic; use ::tonic::async_trait; pub(crate) use registry::GLOBAL_TRANSPORT_REGISTRY; pub(crate) use registry::TransportRegistry; use tokio::sync::oneshot; // TODO: The following options are specific to HTTP/2. We should // instead pass an `Attribute` like struct to the connect method instead which // can hold config relevant to a particular transport. #[derive(Default, Clone)] pub(crate) struct TransportOptions { pub(crate) init_stream_window_size: Option, pub(crate) init_connection_window_size: Option, pub(crate) http2_keep_alive_interval: Option, pub(crate) http2_keep_alive_timeout: Option, pub(crate) http2_keep_alive_while_idle: Option, pub(crate) http2_max_header_list_size: Option, pub(crate) http2_adaptive_window: Option, pub(crate) concurrency_limit: Option, pub(crate) rate_limit: Option<(u64, Duration)>, pub(crate) tcp_keepalive: Option, pub(crate) tcp_nodelay: bool, pub(crate) connect_deadline: Option, } #[trait_variant::make(Send)] pub(crate) trait Transport: Sync { type Service: Invoke + 'static; async fn connect( &self, address: String, runtime: GrpcRuntime, opts: &TransportOptions, ) -> Result<(Self::Service, oneshot::Receiver>), String>; } #[async_trait] pub(crate) trait DynTransport: Send + Sync { async fn dyn_connect( &self, address: String, runtime: GrpcRuntime, opts: &TransportOptions, ) -> Result<(Box, oneshot::Receiver>), String>; } #[async_trait] impl DynTransport for T { async fn dyn_connect( &self, address: String, runtime: GrpcRuntime, opts: &TransportOptions, ) -> Result<(Box, oneshot::Receiver>), String> { let (i, rx) = self.connect(address, runtime, opts).await?; Ok((Box::new(i), rx)) } } ================================================ FILE: grpc/src/client/transport/registry.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::collections::HashMap; use std::fmt::Debug; use std::sync::Arc; use std::sync::LazyLock; use std::sync::Mutex; use crate::client::transport::DynTransport; use crate::client::transport::Transport; /// A registry to store and retrieve transports. Transports are indexed by /// the address type they are intended to handle. #[derive(Default, Clone)] pub(crate) struct TransportRegistry { inner: Arc>>>, } impl Debug for TransportRegistry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let m = self.inner.lock().unwrap(); for key in m.keys() { write!(f, "k: {key:?}")? } Ok(()) } } impl TransportRegistry { /// Construct an empty name resolver registry. pub(crate) fn new() -> Self { Self::default() } pub(crate) fn add_transport(&self, address_type: &str, transport: T) where T: Transport + 'static, { self.inner .lock() .unwrap() .insert(address_type.to_string(), Arc::new(transport)); } /// Retrieve a name resolver from the registry, or None if not found. pub(crate) fn get_transport( &self, address_type: &str, ) -> Result, String> { self.inner .lock() .unwrap() .get(address_type) .ok_or(format!( "no transport found for address type {address_type}" )) .cloned() } } /// The registry used if a local registry is not provided to a channel or if it /// does not exist in the local registry. pub(crate) static GLOBAL_TRANSPORT_REGISTRY: LazyLock = LazyLock::new(TransportRegistry::new); ================================================ FILE: grpc/src/client/transport/tonic/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::error::Error; use std::future::Future; use std::net::SocketAddr; use std::pin::Pin; use std::str::FromStr; use std::task::Context; use std::task::Poll; use std::time::Instant; use bytes::Buf; use bytes::BufMut as _; use bytes::Bytes; use http::Request as HttpRequest; use http::Response as HttpResponse; use http::Uri; use http::uri::PathAndQuery; use hyper::client::conn::http2::Builder; use hyper::client::conn::http2::SendRequest; use tokio::sync::mpsc; use tokio::sync::oneshot; use tokio_stream::Stream; use tokio_stream::wrappers::ReceiverStream; use tonic::Request as TonicRequest; use tonic::Status as TonicStatus; use tonic::Streaming; use tonic::body::Body; use tonic::client::Grpc; use tonic::client::GrpcService; use tonic::codec::Codec; use tonic::codec::Decoder; use tonic::codec::EncodeBuf; use tonic::codec::Encoder; use tower::ServiceBuilder; use tower::buffer::Buffer; use tower::buffer::future::ResponseFuture as BufferResponseFuture; use tower::limit::ConcurrencyLimitLayer; use tower::limit::RateLimitLayer; use tower::util::BoxService; use tower_service::Service as TowerService; use crate::Status; use crate::StatusCode; use crate::client::CallOptions; use crate::client::Invoke; use crate::client::RecvStream; use crate::client::SendOptions; use crate::client::SendStream; use crate::client::name_resolution::TCP_IP_NETWORK_TYPE; use crate::client::transport::Transport; use crate::client::transport::TransportOptions; use crate::client::transport::registry::GLOBAL_TRANSPORT_REGISTRY; use crate::core::ClientResponseStreamItem; use crate::core::RecvMessage; use crate::core::RequestHeaders; use crate::core::ResponseHeaders; use crate::core::SendMessage; use crate::core::Trailers; use crate::rt::BoxedTaskHandle; use crate::rt::GrpcRuntime; use crate::rt::TcpOptions; use crate::rt::hyper_wrapper::HyperCompatExec; use crate::rt::hyper_wrapper::HyperCompatTimer; use crate::rt::hyper_wrapper::HyperStream; #[cfg(test)] mod test; const DEFAULT_BUFFER_SIZE: usize = 1024; pub(crate) type BoxError = Box; type BoxFuture<'a, T> = Pin + Send + 'a>>; type BoxStream = Pin> + Send>>; pub(crate) fn reg() { GLOBAL_TRANSPORT_REGISTRY.add_transport(TCP_IP_NETWORK_TYPE, TransportBuilder {}); } struct TransportBuilder {} struct TonicTransport { grpc: Grpc, task_handle: BoxedTaskHandle, runtime: GrpcRuntime, } impl Drop for TonicTransport { fn drop(&mut self) { self.task_handle.abort(); } } impl Invoke for TonicTransport { type SendStream = TonicSendStream; type RecvStream = TonicRecvStream; async fn invoke( &self, headers: RequestHeaders, options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { let (req_tx, req_rx) = mpsc::channel(1); let request_stream = ReceiverStream::new(req_rx); let mut request = TonicRequest::new(Box::pin(request_stream)); let (method, metadata) = headers.into_parts(); *request.metadata_mut() = metadata; let Ok(path) = PathAndQuery::from_maybe_shared(method) else { return err_streams(Status::new(StatusCode::Internal, "invalid path")); }; let mut grpc = self.grpc.clone(); if let Err(e) = grpc.ready().await { return err_streams(Status::new( StatusCode::Unavailable, format!("Service was not ready: {e}"), )); } // Note that Tonic's streaming call blocks until the server's headers // are received. The client needs a SendStream to provide the request // message(s), which the server may be awaiting before sending its // headers. So, we spawn a task for this period of time, and then we // send the response (headers, stream) to the TonicRecvStream when it is // available. let (resp_tx, resp_rx) = oneshot::channel(); self.runtime.spawn(Box::pin(async move { let response = grpc.streaming(request, path, BufCodec {}).await; let _ = resp_tx.send(response); })); ( TonicSendStream { sender: Ok(req_tx) }, TonicRecvStream { state: StreamState::AwaitingHeaders(resp_rx), }, ) } } // Converts from a tonic status to a trailers stream item. fn trailers_from_tonic_status(status: TonicStatus) -> ClientResponseStreamItem { ClientResponseStreamItem::Trailers(Trailers::new(Status::new( StatusCode::from(status.code() as i32), status.message(), ))) } // Builds a trailers with a status fn trailers_from_status(code: StatusCode, msg: impl Into) -> ClientResponseStreamItem { ClientResponseStreamItem::Trailers(Trailers::new(Status::new(code, msg))) } struct TonicSendStream { sender: Result>, ()>, } impl SendStream for TonicSendStream { async fn send(&mut self, msg: &dyn SendMessage, options: SendOptions) -> Result<(), ()> { if let Ok(tx) = &self.sender && let Ok(buf) = msg.encode() && tx.send(buf).await.is_ok() { if options.final_msg { self.sender = Err(()); } return Ok(()); } Err(()) } } struct TonicRecvStream { state: StreamState, } enum StreamState { Error(Status), AwaitingHeaders(oneshot::Receiver>, TonicStatus>>), Streaming(Streaming), Closed, } impl RecvStream for TonicRecvStream { async fn next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem { // Take the current state, leaving `Closed` in its place temporarily let state = std::mem::replace(&mut self.state, StreamState::Closed); match state { // Closed is terminal. StreamState::Closed => ClientResponseStreamItem::StreamClosed, // Stay closed after sending trailers. StreamState::Error(error) => ClientResponseStreamItem::Trailers(Trailers::new(error)), StreamState::AwaitingHeaders(rx) => match rx.await { Ok(Ok(response)) => { let (metadata, stream, _extensions) = response.into_parts(); // Start streaming and return the headers. self.state = StreamState::Streaming(stream); ClientResponseStreamItem::Headers( ResponseHeaders::new().with_metadata(metadata), ) } // Stay closed after sending trailers. Err(_) => trailers_from_status(StatusCode::Unknown, "Task cancelled"), Ok(Err(status)) => trailers_from_tonic_status(status), }, StreamState::Streaming(mut stream) => match stream.message().await { Ok(Some(mut buf)) => match msg.decode(&mut buf) { Ok(()) => { // More messages may remain in the stream; set receiver again. self.state = StreamState::Streaming(stream); ClientResponseStreamItem::Message(()) } // TODO: in this case, tonic believes the stream is still // running, but our decoding failed -- do we need to terminate // the request stream now even though the Streaming is dropped? Err(e) => trailers_from_status( StatusCode::Internal, format!("error decoding response: {e}"), ), }, // Stay closed after sending trailers. Err(status) => trailers_from_tonic_status(status), Ok(None) => trailers_from_status(StatusCode::Ok, ""), }, } } } fn err_streams(status: Status) -> (TonicSendStream, TonicRecvStream) { ( TonicSendStream { sender: Err(()) }, TonicRecvStream { state: StreamState::Error(status), }, ) } impl Transport for TransportBuilder { type Service = TonicTransport; async fn connect( &self, address: String, runtime: GrpcRuntime, opts: &TransportOptions, ) -> Result<(Self::Service, oneshot::Receiver>), String> { let runtime = runtime.clone(); let mut settings = Builder::::new(HyperCompatExec { inner: runtime.clone(), }) .timer(HyperCompatTimer { inner: runtime.clone(), }) .initial_stream_window_size(opts.init_stream_window_size) .initial_connection_window_size(opts.init_connection_window_size) .keep_alive_interval(opts.http2_keep_alive_interval) .clone(); if let Some(val) = opts.http2_keep_alive_timeout { settings.keep_alive_timeout(val); } if let Some(val) = opts.http2_keep_alive_while_idle { settings.keep_alive_while_idle(val); } if let Some(val) = opts.http2_adaptive_window { settings.adaptive_window(val); } if let Some(val) = opts.http2_max_header_list_size { settings.max_header_list_size(val); } let addr: SocketAddr = SocketAddr::from_str(&address).map_err(|err| err.to_string())?; let tcp_stream_fut = runtime.tcp_stream( addr, TcpOptions { enable_nodelay: opts.tcp_nodelay, keepalive: opts.tcp_keepalive, }, ); let tcp_stream = if let Some(deadline) = opts.connect_deadline { let timeout = deadline.saturating_duration_since(Instant::now()); tokio::select! { _ = runtime.sleep(timeout) => { return Err("timed out waiting for TCP stream to connect".to_string()) } tcp_stream = tcp_stream_fut => { tcp_stream? } } } else { tcp_stream_fut.await? }; let tcp_stream = HyperStream::new(tcp_stream); let (sender, connection) = settings .handshake(tcp_stream) .await .map_err(|err| err.to_string())?; let (tx, rx) = oneshot::channel(); let task_handle = runtime.spawn(Box::pin(async move { if let Err(err) = connection.await { let _ = tx.send(Err(err.to_string())); } else { let _ = tx.send(Ok(())); } })); let sender = SendRequestWrapper::from(sender); let service = ServiceBuilder::new() .option_layer(opts.concurrency_limit.map(ConcurrencyLimitLayer::new)) .option_layer(opts.rate_limit.map(|(l, d)| RateLimitLayer::new(l, d))) .map_err(Into::::into) .service(sender); let service = BoxService::new(service); let (service, worker) = Buffer::pair(service, DEFAULT_BUFFER_SIZE); runtime.spawn(Box::pin(worker)); let uri = Uri::from_maybe_shared(format!("http://{}", &address)).map_err(|e| e.to_string())?; // TODO: err msg let grpc = Grpc::with_origin(TonicService { inner: service }, uri); let service = TonicTransport { grpc, task_handle, runtime, }; Ok((service, rx)) } } struct SendRequestWrapper { inner: SendRequest, } impl From> for SendRequestWrapper { fn from(inner: SendRequest) -> Self { Self { inner } } } impl TowerService> for SendRequestWrapper { type Response = HttpResponse; type Error = BoxError; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, req: http::Request) -> Self::Future { let fut = self.inner.send_request(req); Box::pin(async move { fut.await.map_err(Into::into).map(|res| res.map(Body::new)) }) } } #[derive(Clone)] struct TonicService { inner: Buffer, BoxFuture<'static, Result, BoxError>>>, } impl GrpcService for TonicService { type ResponseBody = Body; type Error = BoxError; type Future = ResponseFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { tower::Service::poll_ready(&mut self.inner, cx) } fn call(&mut self, request: http::Request) -> Self::Future { ResponseFuture { inner: tower::Service::call(&mut self.inner, request), } } } /// A future that resolves to an HTTP response. /// /// This is returned by the `Service::call` on [`Channel`]. pub(crate) struct ResponseFuture { inner: BufferResponseFuture, BoxError>>>, } impl Future for ResponseFuture { type Output = Result, BoxError>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { Pin::new(&mut self.inner).poll(cx) } } pub(crate) struct BufCodec {} impl Codec for BufCodec { type Encode = Box; type Decode = Bytes; type Encoder = BufEncoder; type Decoder = BytesDecoder; fn encoder(&mut self) -> Self::Encoder { BufEncoder {} } fn decoder(&mut self) -> Self::Decoder { BytesDecoder {} } } pub struct BytesEncoder {} impl Encoder for BytesEncoder { type Item = Bytes; type Error = TonicStatus; fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { dst.put_slice(&item); Ok(()) } } pub struct BufEncoder {} impl Encoder for BufEncoder { type Item = Box; type Error = TonicStatus; fn encode(&mut self, mut item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { dst.put(&mut *item); Ok(()) } } #[derive(Debug)] pub struct BytesDecoder {} impl Decoder for BytesDecoder { type Item = Bytes; type Error = TonicStatus; fn decode( &mut self, src: &mut tonic::codec::DecodeBuf<'_>, ) -> Result, Self::Error> { Ok(Some(src.copy_to_bytes(src.remaining()))) } } ================================================ FILE: grpc/src/client/transport/tonic/test.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::pin::Pin; use std::sync::Arc; use std::time::Duration; use bytes::Buf; use bytes::Bytes; use tokio::net::TcpListener; use tokio::sync::Notify; use tokio::sync::oneshot; use tokio::time::timeout; use tokio_stream::Stream; use tokio_stream::StreamExt; use tokio_stream::wrappers::ReceiverStream; use tonic::Response; use tonic::Status; use tonic::async_trait; use tonic::transport::Server; use tonic_prost::prost::Message as ProstMessage; use crate::client::CallOptions; use crate::client::Channel; use crate::client::Invoke as _; use crate::client::RecvStream as _; use crate::client::SendOptions; use crate::client::SendStream as _; use crate::client::name_resolution::TCP_IP_NETWORK_TYPE; use crate::client::transport::TransportOptions; use crate::client::transport::registry::GLOBAL_TRANSPORT_REGISTRY; use crate::core::ClientResponseStreamItem; use crate::core::RecvMessage; use crate::core::RequestHeaders; use crate::core::SendMessage; use crate::credentials::InsecureChannelCredentials; use crate::echo_pb::EchoRequest; use crate::echo_pb::EchoResponse; use crate::echo_pb::echo_server::Echo; use crate::echo_pb::echo_server::EchoServer; use crate::rt::GrpcRuntime; use crate::rt::tokio::TokioRuntime; const DEFAULT_TEST_DURATION: Duration = Duration::from_secs(10); const DEFAULT_TEST_SHORT_DURATION: Duration = Duration::from_millis(10); // Tests the tonic transport by creating a bi-di stream with a tonic server. #[tokio::test] pub(crate) async fn tonic_transport_rpc() { super::reg(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); // get the assigned address let shutdown_notify = Arc::new(Notify::new()); let shutdown_notify_copy = shutdown_notify.clone(); println!("EchoServer listening on: {addr}"); let server_handle = tokio::spawn(async move { let echo_server = EchoService {}; let svc = EchoServer::new(echo_server); let _ = Server::builder() .add_service(svc) .serve_with_incoming_shutdown( tokio_stream::wrappers::TcpListenerStream::new(listener), shutdown_notify_copy.notified(), ) .await; }); let builder = GLOBAL_TRANSPORT_REGISTRY .get_transport(TCP_IP_NETWORK_TYPE) .unwrap(); let config = Arc::new(TransportOptions::default()); let (conn, mut disconnection_listener) = builder .dyn_connect( addr.to_string(), GrpcRuntime::new(TokioRuntime::default()), &config, ) .await .unwrap(); let (mut tx, mut rx) = conn .dyn_invoke( RequestHeaders::new() .with_method_name("/grpc.examples.echo.Echo/BidirectionalStreamingEcho"), CallOptions::default(), ) .await; // Spawn a sender task let client_handle = tokio::spawn(async move { let mut dummy_msg = WrappedEchoResponse(EchoResponse { message: "".into() }); match rx.next(&mut dummy_msg).await { ClientResponseStreamItem::Headers(_) => { println!("Got headers"); } item => panic!("Expected headers, got {:?}", item), } for i in 0..5 { let message = format!("message {i}"); let request = EchoRequest { message: message.clone(), }; let req = WrappedEchoRequest(request); println!("Sent request: {:?}", req.0); assert!( tx.send(&req, SendOptions::default()).await.is_ok(), "Receiver dropped" ); // Wait for the reply let mut recv_msg = WrappedEchoResponse(EchoResponse { message: "".into() }); match rx.next(&mut recv_msg).await { ClientResponseStreamItem::Message(()) => { let echo_response = recv_msg.0; println!("Got response: {echo_response:?}"); assert_eq!(echo_response.message, message); } item => panic!("Expected message, got {:?}", item), } } }); client_handle.await.unwrap(); // The connection should break only after the server is stopped. assert_eq!( disconnection_listener.try_recv(), Err(oneshot::error::TryRecvError::Empty), ); shutdown_notify.notify_waiters(); let res = timeout(DEFAULT_TEST_DURATION, disconnection_listener) .await .unwrap() .unwrap(); assert_eq!(res, Ok(())); server_handle.await.unwrap(); } #[tokio::test] async fn grpc_invoke_tonic_unary() { // Register DNS & Tonic. super::reg(); crate::client::name_resolution::dns::reg(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let shutdown_notify = Arc::new(Notify::new()); let shutdown_notify_copy = shutdown_notify.clone(); // Spawn a task for the server. let server_handle = tokio::spawn(async move { let echo_server = EchoService {}; let svc = EchoServer::new(echo_server); let _ = Server::builder() .add_service(svc) .serve_with_incoming_shutdown( tokio_stream::wrappers::TcpListenerStream::new(listener), shutdown_notify_copy.notified(), ) .await; }); // Create the channel. let target = format!("dns:///{}", addr); let channel = Channel::new( &target, InsecureChannelCredentials::new(), Default::default(), ); // Start the call. let (mut tx, mut rx) = channel .invoke( RequestHeaders::new().with_method_name("/grpc.examples.echo.Echo/UnaryEcho"), CallOptions::default(), ) .await; // Send the request. let req = WrappedEchoRequest(EchoRequest { message: "hello interop".into(), }); tx.send( &req, SendOptions { final_msg: true, ..Default::default() }, ) .await .unwrap(); // Response should be Headers, Message ("hello interop"), Trailers (OK). let mut resp = WrappedEchoResponse(EchoResponse::default()); let ClientResponseStreamItem::Headers(_) = rx.next(&mut resp).await else { panic!("Expected Headers first"); }; let ClientResponseStreamItem::Message(()) = rx.next(&mut resp).await else { panic!("Expected Message after Headers"); }; assert_eq!(resp.0.message, "hello interop"); let ClientResponseStreamItem::Trailers(t) = rx.next(&mut resp).await else { panic!("Expected Trailers, got StreamClosed or other item"); }; assert_eq!( t.status().code(), crate::StatusCode::Ok, "RPC failed: {:?}", t.status() ); shutdown_notify.notify_one(); server_handle.await.unwrap(); } struct WrappedEchoRequest(EchoRequest); struct WrappedEchoResponse(EchoResponse); impl SendMessage for WrappedEchoRequest { fn encode(&self) -> Result, String> { Ok(Box::new(Bytes::from(self.0.encode_to_vec()))) } } impl RecvMessage for WrappedEchoResponse { fn decode(&mut self, data: &mut dyn Buf) -> Result<(), String> { let buf = data.copy_to_bytes(data.remaining()); self.0 = EchoResponse::decode(buf).map_err(|e| e.to_string())?; Ok(()) } } #[derive(Debug)] pub(crate) struct EchoService {} #[async_trait] impl Echo for EchoService { async fn unary_echo( &self, request: tonic::Request, ) -> std::result::Result, tonic::Status> { let message = request.into_inner().message; Ok(tonic::Response::new(EchoResponse { message })) } type ServerStreamingEchoStream = ReceiverStream>; async fn server_streaming_echo( &self, _: tonic::Request, ) -> std::result::Result, tonic::Status> { unimplemented!() } async fn client_streaming_echo( &self, _: tonic::Request>, ) -> std::result::Result, tonic::Status> { unimplemented!() } type BidirectionalStreamingEchoStream = Pin> + Send + 'static>>; async fn bidirectional_streaming_echo( &self, request: tonic::Request>, ) -> std::result::Result, tonic::Status> { let mut inbound = request.into_inner(); // Map each request to a corresponding EchoResponse let outbound = async_stream::try_stream! { while let Some(req) = inbound.next().await { let req = req?; // Return Err(Status) if stream item is error let reply = EchoResponse { message: req.message.clone(), }; yield reply; } println!("Server closing stream"); }; Ok(Response::new( Box::pin(outbound) as Self::BidirectionalStreamingEchoStream )) } } ================================================ FILE: grpc/src/core/mod.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ //! Types used to implement core gRPC functionality common to clients and //! servers. Note that most gRPC applications should not need these types //! unless they are implementing custom interceptors. use std::any::TypeId; use bytes::Buf; use tonic::metadata::MetadataMap; use crate::Status; #[allow(unused)] pub trait SendMessage: Send + Sync { fn encode(&self) -> Result, String>; #[doc(hidden)] unsafe fn _ptr_for(&self, id: TypeId) -> Option<*const ()> { None } } #[allow(unused)] pub trait RecvMessage: Send + Sync { fn decode(&mut self, data: &mut dyn Buf) -> Result<(), String>; #[doc(hidden)] unsafe fn _ptr_for(&mut self, id: TypeId) -> Option<*mut ()> { None } } /// A MessageType describes what underlying message is inside a SendMessage or /// RecvMessage so that it can be downcast, e.g. by interceptors. It allows for /// safe downcasting to views containing a lifetime. pub trait MessageType { /// The message view's type, which may have a lifetime. type Target<'a>; } fn msg_type_id() -> TypeId where T::Target<'static>: 'static, { TypeId::of::>() } impl dyn SendMessage + '_ { /// Downcasts the SendMessage to T::Target if the SendMessage contains a T. pub fn downcast_ref(&self) -> Option<&T::Target<'_>> where T::Target<'static>: 'static, { unsafe { if let Some(ptr) = self._ptr_for(msg_type_id::()) { Some(&*(ptr as *mut T::Target<'_>)) } else { None } } } } #[allow(unused)] impl dyn RecvMessage + '_ { /// Downcasts the RecvMessage to T::Target if the RecvMessage contains a T. pub fn downcast_mut(&mut self) -> Option<&mut T::Target<'_>> where T::Target<'static>: 'static, { unsafe { if let Some(ptr) = self._ptr_for(msg_type_id::()) { Some(&mut *(ptr as *mut T::Target<'_>)) } else { None } } } } /// ResponseStreamItem represents an item in a response stream (either server /// sending or client receiving). /// /// A response stream must always contain items exactly as follows: /// /// [Headers *Message] Trailers *StreamClosed /// /// That is: optionaly, a Headers value and any number of Message values /// (including zero), followed by a required Trailers value. A response stream /// should not be used after Trailers, but reads should return StreamClosed if /// it is. #[derive(Debug, Clone)] pub enum ResponseStreamItem { /// Indicates the headers for the stream. Headers(ResponseHeaders), /// Indicates a message on the stream. Message(M), /// Indicates trailers were received on the stream and includes the trailers. Trailers(Trailers), /// Indicates the response stream was closed. Trailers must have been /// provided before this value may be used. StreamClosed, } /// The client's view of a ResponseStream in a RecvStream: the message type is /// void as the received message is passed in via the `next` method. pub type ClientResponseStreamItem = ResponseStreamItem<()>; /// The server's view of a ResponseStream in a SendStream: the message type is /// part of the payload provided to the `send` method. pub type ServerResponseStreamItem<'a> = ResponseStreamItem<&'a dyn SendMessage>; /// Contains all information transmitted in the response headers of an RPC. #[derive(Debug, Clone, Default)] pub struct ResponseHeaders { metadata: MetadataMap, } impl ResponseHeaders { /// Returns a default ResponseHeaders instance. pub fn new() -> Self { Self::default() } /// Replaces the metadata of self with `metadata`. pub fn with_metadata(mut self, metadata: MetadataMap) -> Self { self.metadata = metadata; self } /// Returns a reference to the metadata in these headers. pub fn metadata(&self) -> &MetadataMap { &self.metadata } /// Returns a mutable reference to the metadata in these headers. pub fn metadata_mut(&mut self) -> &mut MetadataMap { &mut self.metadata } } /// Contains all information transmitted in the request headers of an RPC. #[derive(Debug, Clone, Default)] pub struct RequestHeaders { /// The full (e.g. "/Service/Method") method name specified for the call. method_name: String, /// The application-specified metadata for the call. metadata: MetadataMap, } impl RequestHeaders { /// Returns a default RequestHeaders instance. pub fn new() -> Self { Self::default() } /// Replaces the method name of self with `method_name`. pub fn with_method_name(mut self, method_name: impl Into) -> Self { self.method_name = method_name.into(); self } /// Replaces the metadata of self with `metadata`. pub fn with_metadata(mut self, metadata: MetadataMap) -> Self { self.metadata = metadata; self } /// Returns the full (e.g. "/Service/Method") method name for these headers. pub fn method_name(&self) -> &String { &self.method_name } /// Returns a reference to the metadata in these headers. pub fn metadata(&self) -> &MetadataMap { &self.metadata } /// Returns a mutable reference to the metadata in these headers. pub fn metadata_mut(&mut self) -> &mut MetadataMap { &mut self.metadata } /// Returns the owned fields in the RequestHeaders. // TODO: make public once fields are fixed. pub(crate) fn into_parts(self) -> (String, MetadataMap) { (self.method_name, self.metadata) } } /// Contains all information transmitted in the response trailers of an RPC. /// gRPC does not support request trailers. #[derive(Debug, Clone)] pub struct Trailers { status: Status, metadata: MetadataMap, } impl Trailers { /// Returns a default RequestHeaders instance. pub fn new(status: Status) -> Self { Self { status, metadata: MetadataMap::default(), } } /// Replaces the status of self with `status`. pub fn with_status(mut self, status: Status) -> Self { self.status = status; self } /// Returns a reference to the status contained in these trailers. pub fn status(&self) -> &Status { &self.status } /// Replaces the metadata of self with `metadata`. pub fn with_metadata(mut self, metadata: MetadataMap) -> Self { self.metadata = metadata; self } /// Returns a mutable reference to the metadata in these trailers. pub fn metadata_mut(&mut self) -> &mut MetadataMap { &mut self.metadata } /// Returns a reference to the metadata in these trailers. pub fn metadata(&self) -> &MetadataMap { &self.metadata } } ================================================ FILE: grpc/src/credentials/call.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::fmt::Debug; use std::sync::Arc; use tonic::Status; use tonic::async_trait; use tonic::metadata::MetadataMap; use crate::attributes::Attributes; use crate::credentials::SecurityLevel; /// Details regarding the call. /// /// The fully qualified method name is constructed as: /// `service_url` + "/" + `method_name` pub struct CallDetails { service_url: String, method_name: String, } impl CallDetails { pub(crate) fn new(service_url: String, method_name: String) -> Self { Self { service_url, method_name, } } /// Returns the base URL of the service for this call. pub fn service_url(&self) -> &str { &self.service_url } /// The method name suffix (e.g., `Method` in `package.Service/Method`). pub fn method_name(&self) -> &str { &self.method_name } } pub struct ChannelSecurityInfo { security_protocol: &'static str, security_level: SecurityLevel, /// Stores extra data derived from the underlying protocol. attributes: Attributes, } impl ChannelSecurityInfo { pub(crate) fn new( security_protocol: &'static str, security_level: SecurityLevel, attributes: Attributes, ) -> Self { Self { security_protocol, security_level, attributes, } } pub fn security_protocol(&self) -> &'static str { self.security_protocol } pub fn security_level(&self) -> SecurityLevel { self.security_level } pub fn attributes(&self) -> &Attributes { &self.attributes } } /// Defines the interface for credentials that need to attach security /// information to every individual RPC (e.g., OAuth2 tokens, JWTs). #[async_trait] pub trait CallCredentials: Send + Sync + Debug { /// Generates the authentication metadata for a specific call. /// /// This method is called by the transport layer on each request. /// Implementations should populate the provided `metadata` map with the /// necessary authorization headers (e.g., `authorization: Bearer `). /// /// If this returns an `Err`, the RPC will fail immediately with a status /// derived from the error if the status code is in the range defined in /// gRFC A54. Otherwise, the RPC is failed with an internal status. /// /// # Cancellation Safety /// /// Implementations of this method must be cancel safe as the future may be /// dropped due to RPC timeouts. async fn get_metadata( &self, call_details: &CallDetails, auth_info: &ChannelSecurityInfo, metadata: &mut MetadataMap, ) -> Result<(), Status>; /// Indicates the minimum transport security level required to send /// these credentials. /// **Default:** Returns [`SecurityLevel::PrivacyAndIntegrity`]. fn minimum_channel_security_level(&self) -> SecurityLevel { SecurityLevel::PrivacyAndIntegrity } } /// A composite implementation of [`CallCredentials`] that combines /// multiple credentials. /// /// The inner credentials are invoked sequentially during metadata retrieval. #[derive(Debug)] pub struct CompositeCallCredentials { creds: Vec>, } impl CompositeCallCredentials { /// Creates a new [`CompositeCallCredentials`] with the first two credentials. pub fn new(first: Arc, second: Arc) -> Self { Self { creds: vec![first, second], } } /// Adds an additional [`CallCredentials`] to the composite. pub fn with_call_credentials(mut self, creds: Arc) -> Self { self.creds.push(creds); self } } #[async_trait] impl CallCredentials for CompositeCallCredentials { async fn get_metadata( &self, call_details: &CallDetails, auth_info: &ChannelSecurityInfo, metadata: &mut MetadataMap, ) -> Result<(), Status> { for cred in &self.creds { cred.get_metadata(call_details, auth_info, metadata).await?; } Ok(()) } fn minimum_channel_security_level(&self) -> SecurityLevel { self.creds .iter() .map(|c| c.minimum_channel_security_level()) .max() .expect("CompositeCallCredentials must hold at least two children.") } } #[cfg(test)] mod tests { use tonic::metadata::MetadataValue; use super::*; #[derive(Debug)] struct MockCallCredentials { key: String, value: String, security_level: SecurityLevel, } #[async_trait] impl CallCredentials for MockCallCredentials { async fn get_metadata( &self, _call_details: &CallDetails, _auth_info: &ChannelSecurityInfo, metadata: &mut MetadataMap, ) -> Result<(), Status> { metadata.insert( self.key .parse::>() .unwrap(), MetadataValue::try_from(&self.value).unwrap(), ); Ok(()) } fn minimum_channel_security_level(&self) -> SecurityLevel { self.security_level } } #[tokio::test] async fn test_composite_call_credentials() { let cred1 = Arc::new(MockCallCredentials { key: "key1".to_string(), value: "value1".to_string(), security_level: SecurityLevel::IntegrityOnly, }); let cred2 = Arc::new(MockCallCredentials { key: "key2".to_string(), value: "value2".to_string(), security_level: SecurityLevel::PrivacyAndIntegrity, }); let composite = CompositeCallCredentials::new(cred1, cred2); let call_details = CallDetails { service_url: "url".to_string(), method_name: "method".to_string(), }; let auth_info = ChannelSecurityInfo::new( "test", SecurityLevel::PrivacyAndIntegrity, Attributes::new(), ); let mut metadata = MetadataMap::new(); composite .get_metadata(&call_details, &auth_info, &mut metadata) .await .unwrap(); assert_eq!(metadata.get("key1").unwrap(), "value1"); assert_eq!(metadata.get("key2").unwrap(), "value2"); assert_eq!( composite.minimum_channel_security_level(), SecurityLevel::PrivacyAndIntegrity ); } } ================================================ FILE: grpc/src/credentials/client.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::sync::Arc; use crate::attributes::Attributes; use crate::credentials::ChannelCredentials; use crate::credentials::ProtocolInfo; use crate::credentials::SecurityLevel; use crate::credentials::call::CallCredentials; use crate::credentials::call::CompositeCallCredentials; use crate::credentials::common::Authority; use crate::credentials::insecure; use crate::rt::GrpcEndpoint; use crate::rt::GrpcRuntime; #[trait_variant::make(Send)] pub trait ChannelCredsInternal { type ContextType: ClientConnectionSecurityContext; type Output; /// Performs the client-side authentication handshake on a raw endpoint. /// /// This method wraps the provided `source` endpoint with the security protocol /// (e.g., TLS) and returns the authenticated endpoint along with its /// security details. /// /// # Arguments /// /// * `authority` - The `:authority` header value to be used when creating /// new streams. /// **Important:** Implementations must use this value as the server name /// (e.g., for SNI) during the handshake. /// * `source` - The raw connection handle. /// * `info` - Additional context passed from the resolver or load balancer. async fn connect( &self, authority: &Authority, source: Input, info: ClientHandshakeInfo, runtime: GrpcRuntime, ) -> Result, Self::ContextType>, String>; /// Returns call credentials to be used for all RPCs made on a connection. fn get_call_credentials(&self) -> Option<&Arc>; } pub struct HandshakeOutput { pub endpoint: T, pub security: ClientConnectionSecurityInfo, } pub trait ClientConnectionSecurityContext: Send + Sync + 'static { /// Checks if the established connection is authorized to send requests to /// the given authority. /// /// This is primarily used for HTTP/2 connection reuse (coalescing). If the /// underlying security handshake (e.g., a TLS certificate) covers the provided /// `authority`, the existing connection may be reused for that host. /// /// # Returns /// /// * `true` - The connection is valid for this authority. /// * `false` - The connection cannot be reused; a new connection must be created. fn validate_authority(&self, authority: &Authority) -> bool { false } } impl ClientConnectionSecurityContext for Box { fn validate_authority(&self, authority: &Authority) -> bool { (**self).validate_authority(authority) } } /// Represents the security state of an established client-side connection. pub struct ClientConnectionSecurityInfo { security_protocol: &'static str, security_level: SecurityLevel, security_context: C, /// Stores extra data derived from the underlying protocol. attributes: Attributes, } impl ClientConnectionSecurityInfo { pub fn new( security_protocol: &'static str, security_level: SecurityLevel, security_context: C, attributes: Attributes, ) -> Self { Self { security_protocol, security_level, security_context, attributes, } } pub fn security_protocol(&self) -> &'static str { self.security_protocol } pub fn security_level(&self) -> SecurityLevel { self.security_level } pub fn security_context(&self) -> &C { &self.security_context } pub fn attributes(&self) -> &Attributes { &self.attributes } pub fn into_boxed( self, ) -> ClientConnectionSecurityInfo> where C: ClientConnectionSecurityContext + 'static, { ClientConnectionSecurityInfo { security_protocol: self.security_protocol, security_level: self.security_level, security_context: Box::new(self.security_context), attributes: self.attributes, } } } /// Holds data to be passed during the connection handshake. /// /// This mechanism allows arbitrary data to flow from gRPC core components—such /// as resolvers and load balancers—down to the credential implementations. /// /// Individual credential implementations are responsible for validating and /// interpreting the format of the data they receive. #[derive(Default)] pub struct ClientHandshakeInfo { /// The bag of attributes containing the handshake data. attributes: Attributes, } impl ClientHandshakeInfo { pub fn new(attributes: Attributes) -> Self { Self { attributes } } pub fn attributes(&self) -> &Attributes { &self.attributes } } /// A credential that combines [`ChannelCredentials`] with [`CallCredentials`]. /// /// This is used to attach per-call authentication (like OAuth2 tokens) to a /// secure channel (like TLS). pub struct CompositeChannelCredentials { channel_creds: T, call_creds: Arc, } impl CompositeChannelCredentials { pub fn new(channel_creds: T, call_creds: Arc) -> Result { if channel_creds.info().security_protocol() == insecure::PROTOCOL_NAME { return Err("using tokens on an insecure credentials is disallowed".to_string()); } let combined_call_creds = if let Some(existing) = channel_creds.get_call_credentials() { let composite_creds = CompositeCallCredentials::new(existing.clone(), call_creds); Arc::new(composite_creds) } else { call_creds }; Ok(Self { channel_creds, call_creds: combined_call_creds, }) } } impl ChannelCredsInternal for CompositeChannelCredentials { type ContextType = T::ContextType; type Output = T::Output; async fn connect( &self, authority: &Authority, source: Input, info: ClientHandshakeInfo, runtime: GrpcRuntime, ) -> Result, Self::ContextType>, String> { self.channel_creds .connect(authority, source, info, runtime) .await } fn get_call_credentials(&self) -> Option<&Arc> { Some(&self.call_creds) } } impl ChannelCredentials for CompositeChannelCredentials { fn info(&self) -> &ProtocolInfo { self.channel_creds.info() } } #[cfg(test)] mod tests { use tokio::net::TcpListener; use tonic::Status; use tonic::async_trait; use tonic::metadata::MetadataMap; use tonic::metadata::MetadataValue; use super::*; use crate::credentials::call::CallCredentials; use crate::credentials::call::CallDetails; use crate::credentials::call::ChannelSecurityInfo; use crate::credentials::insecure::InsecureChannelCredentials; use crate::credentials::local::LocalChannelCredentials; use crate::rt; use crate::rt::TcpOptions; #[derive(Debug)] struct MockCallCredentials { key: &'static str, value: &'static str, min_security_level: SecurityLevel, } #[async_trait] impl CallCredentials for MockCallCredentials { async fn get_metadata( &self, _call_details: &CallDetails, _auth_info: &ChannelSecurityInfo, metadata: &mut MetadataMap, ) -> Result<(), Status> { metadata.insert( self.key .parse::>() .unwrap(), MetadataValue::try_from(self.value).unwrap(), ); Ok(()) } fn minimum_channel_security_level(&self) -> SecurityLevel { self.min_security_level } } #[tokio::test] async fn test_multiple_composition() { let channel_creds = LocalChannelCredentials::new(); let call_creds1 = Arc::new(MockCallCredentials { key: "auth1", value: "val1", min_security_level: SecurityLevel::IntegrityOnly, }); let call_creds2 = Arc::new(MockCallCredentials { key: "auth2", value: "val2", min_security_level: SecurityLevel::PrivacyAndIntegrity, }); // First composition. let composite1 = CompositeChannelCredentials::new(channel_creds, call_creds1).unwrap(); // Second composition (using the first composite as base). let composite2 = CompositeChannelCredentials::new(composite1, call_creds2).unwrap(); // Verify call credentials let combined_call_creds = composite2.get_call_credentials().unwrap(); let call_details = CallDetails::new("service".to_string(), "method".to_string()); let auth_info = ChannelSecurityInfo::new("local", SecurityLevel::NoSecurity, Attributes::new()); let mut metadata = MetadataMap::new(); combined_call_creds .get_metadata(&call_details, &auth_info, &mut metadata) .await .unwrap(); assert_eq!(metadata.get("auth1").unwrap(), "val1"); assert_eq!(metadata.get("auth2").unwrap(), "val2"); // Verify min security level is the max of both. assert_eq!( combined_call_creds.minimum_channel_security_level(), SecurityLevel::PrivacyAndIntegrity ); // Verify security level let addr = "127.0.0.1:0"; let listener = TcpListener::bind(addr).await.unwrap(); let server_addr = listener.local_addr().unwrap(); let authority = Authority::new("localhost".to_string(), Some(server_addr.port())); let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(server_addr, TcpOptions::default()) .await .unwrap(); let output = composite2 .connect( &authority, endpoint, ClientHandshakeInfo::default(), runtime, ) .await .unwrap(); assert_eq!(output.security.security_level(), SecurityLevel::NoSecurity); assert_eq!(output.security.security_protocol(), "local"); } #[test] fn test_composite_channel_credentials_insecure() { let channel_creds = InsecureChannelCredentials::new(); let call_creds = Arc::new(MockCallCredentials { key: "auth", value: "val", min_security_level: SecurityLevel::NoSecurity, }); let result = CompositeChannelCredentials::new(channel_creds, call_creds); assert!(result.is_err()); } } ================================================ FILE: grpc/src/credentials/dyn_wrapper.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use tonic::async_trait; use crate::credentials::ChannelCredentials; use crate::credentials::ProtocolInfo; use crate::credentials::ServerCredentials; use crate::credentials::client::ClientConnectionSecurityContext; use crate::credentials::client::ClientHandshakeInfo; use crate::credentials::client::HandshakeOutput; use crate::credentials::common::Authority; use crate::credentials::server::HandshakeOutput as ServerHandshakeOutput; use crate::rt::GrpcEndpoint; use crate::rt::GrpcRuntime; use crate::send_future::SendFuture; type BoxEndpoint = Box; // Bridge trait for type erasure. #[async_trait] pub(crate) trait DynChannelCredentials: Send + Sync { async fn connect( &self, authority: &Authority, source: BoxEndpoint, info: ClientHandshakeInfo, runtime: GrpcRuntime, ) -> Result>, String>; fn info(&self) -> &ProtocolInfo; } #[async_trait] impl DynChannelCredentials for T where T: ChannelCredentials, T::Output: GrpcEndpoint, { async fn connect( &self, authority: &Authority, source: BoxEndpoint, info: ClientHandshakeInfo, runtime: GrpcRuntime, ) -> Result>, String> { let output = self .connect(authority, source, info, runtime) .make_send() .await?; let stream = output.endpoint; let sec_info = output.security; Ok(HandshakeOutput { endpoint: Box::new(stream), security: sec_info.into_boxed(), }) } fn info(&self) -> &ProtocolInfo { self.info() } } // Bridge trait for type erasure. #[async_trait] pub(crate) trait DynServerCredentials: Send + Sync { async fn accept( &self, source: BoxEndpoint, runtime: GrpcRuntime, ) -> Result, String>; fn info(&self) -> &ProtocolInfo; } #[async_trait] impl DynServerCredentials for T where T: ServerCredentials, T::Output: GrpcEndpoint, { async fn accept( &self, source: BoxEndpoint, runtime: GrpcRuntime, ) -> Result, String> { let output = SendFuture::make_send(self.accept(source, runtime)).await?; Ok(ServerHandshakeOutput { endpoint: Box::new(output.endpoint), security: output.security, }) } fn info(&self) -> &ProtocolInfo { self.info() } } #[cfg(test)] mod tests { use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use super::*; use crate::credentials::InsecureServerCredentials; use crate::credentials::SecurityLevel; use crate::credentials::client::ClientHandshakeInfo; use crate::credentials::common::Authority; use crate::credentials::insecure::InsecureChannelCredentials; use crate::rt::TcpOptions; use crate::rt::{self}; #[tokio::test] async fn test_dyn_client_credential_dispatch() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let creds = InsecureChannelCredentials::new(); let dyn_creds: Box = Box::new(creds); let authority = Authority::new("localhost".to_string(), Some(addr.port())); let runtime = crate::rt::default_runtime(); let source = runtime .tcp_stream(addr, TcpOptions::default()) .await .unwrap(); let info = ClientHandshakeInfo::default(); let result = dyn_creds.connect(&authority, source, info, runtime).await; assert!(result.is_ok()); let output = result.unwrap(); let mut endpoint = output.endpoint; let security_info = output.security; assert!(!endpoint.get_local_address().is_empty()); assert_eq!(security_info.security_protocol(), "insecure"); assert_eq!(security_info.security_level(), SecurityLevel::NoSecurity); // Verify data transfer. let (mut server_stream, _) = listener.accept().await.unwrap(); assert_eq!( endpoint.get_local_address(), &server_stream.peer_addr().unwrap().to_string() ); let test_data = b"hello dynamic grpc"; server_stream.write_all(test_data).await.unwrap(); let mut buf = vec![0u8; test_data.len()]; endpoint.read_exact(&mut buf).await.unwrap(); assert_eq!(buf, test_data); // Validate arbitrary authority. assert!( security_info .security_context() .validate_authority(&authority) ); } #[tokio::test] async fn test_dyn_server_credential_dispatch() { let creds = InsecureServerCredentials::new(); let dyn_creds: Box = Box::new(creds); let info = dyn_creds.info(); assert_eq!(info.security_protocol, "insecure"); let addr = "127.0.0.1:0"; let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp(addr.parse().unwrap(), TcpOptions::default()) .await .unwrap(); let server_addr = *listener.local_addr(); let client_handle = tokio::spawn(async move { let mut stream = tokio::net::TcpStream::connect(server_addr).await.unwrap(); let data = b"hello dynamic grpc server"; stream.write_all(data).await.unwrap(); // Keep the connection alive for a bit so server can read let mut buf = vec![0u8; 1]; let _ = stream.read(&mut buf).await; }); let (server_stream, _) = listener.accept().await.unwrap(); let result = dyn_creds.accept(server_stream, runtime).await; assert!(result.is_ok()); let output = result.unwrap(); let mut endpoint = output.endpoint; let security_info = output.security; assert_eq!(security_info.security_protocol(), "insecure"); assert_eq!(security_info.security_level(), SecurityLevel::NoSecurity); let mut buf = vec![0u8; 25]; endpoint.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf[..], b"hello dynamic grpc server"); client_handle.abort(); } } ================================================ FILE: grpc/src/credentials/insecure.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::sync::Arc; use crate::attributes::Attributes; use crate::credentials::ChannelCredentials; use crate::credentials::ProtocolInfo; use crate::credentials::SecurityLevel; use crate::credentials::ServerCredentials; use crate::credentials::call::CallCredentials; use crate::credentials::client::ClientConnectionSecurityContext; use crate::credentials::client::ClientConnectionSecurityInfo; use crate::credentials::client::ClientHandshakeInfo; use crate::credentials::client::HandshakeOutput; use crate::credentials::client::{self}; use crate::credentials::common::Authority; use crate::credentials::server::ServerConnectionSecurityInfo; use crate::credentials::server::{self}; use crate::rt::GrpcEndpoint; use crate::rt::GrpcRuntime; /// An implementation of [`ChannelCredentials`] for insecure connections. /// /// This credential type does not perform any encryption or authentication. It /// simply passes the raw underlying transport as the output. #[derive(Debug, Clone, Default)] pub struct InsecureChannelCredentials { _private: (), } pub const PROTOCOL_NAME: &str = "insecure"; impl InsecureChannelCredentials { /// Creates a new instance of `InsecureChannelCredentials`. pub fn new() -> Self { Self { _private: () } } } /// An implementation of [`ClientConnectionSecurityContext`] for insecure connections. #[derive(Debug, Clone)] pub struct InsecureConnectionSecurityContext; impl ClientConnectionSecurityContext for InsecureConnectionSecurityContext { fn validate_authority(&self, _authority: &Authority) -> bool { true } } impl client::ChannelCredsInternal for InsecureChannelCredentials { type ContextType = InsecureConnectionSecurityContext; type Output = I; async fn connect( &self, _authority: &Authority, source: Input, _info: ClientHandshakeInfo, _runtime: GrpcRuntime, ) -> Result, Self::ContextType>, String> { Ok(HandshakeOutput { endpoint: source, security: ClientConnectionSecurityInfo::new( PROTOCOL_NAME, SecurityLevel::NoSecurity, InsecureConnectionSecurityContext, Attributes::new(), ), }) } fn get_call_credentials(&self) -> Option<&Arc> { None } } impl ChannelCredentials for InsecureChannelCredentials { fn info(&self) -> &ProtocolInfo { static INFO: ProtocolInfo = ProtocolInfo::new(PROTOCOL_NAME); &INFO } } /// An implementation of [`ServerCredentials`] for insecure connections. #[derive(Debug, Clone, Default)] pub struct InsecureServerCredentials { _private: (), } impl InsecureServerCredentials { pub fn new() -> Self { Self { _private: () } } } impl server::ServerCredsInternal for InsecureServerCredentials { type Output = I; async fn accept( &self, source: Input, _runtime: GrpcRuntime, ) -> Result>, String> { Ok(server::HandshakeOutput { endpoint: source, security: ServerConnectionSecurityInfo::new( PROTOCOL_NAME, SecurityLevel::NoSecurity, Attributes::new(), ), }) } } impl ServerCredentials for InsecureServerCredentials { fn info(&self) -> &ProtocolInfo { static INFO: ProtocolInfo = ProtocolInfo::new(PROTOCOL_NAME); &INFO } } #[cfg(test)] mod test { use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use tokio::net::TcpStream; use super::*; use crate::credentials::ChannelCredentials; use crate::credentials::InsecureChannelCredentials; use crate::credentials::InsecureServerCredentials; use crate::credentials::SecurityLevel; use crate::credentials::ServerCredentials; use crate::credentials::client::ChannelCredsInternal as ClientSealed; use crate::credentials::client::ClientConnectionSecurityContext; use crate::credentials::client::ClientHandshakeInfo; use crate::credentials::common::Authority; use crate::credentials::server::ServerCredsInternal; use crate::rt::GrpcEndpoint; use crate::rt::TcpOptions; use crate::rt::{self}; #[tokio::test] async fn test_insecure_client_credentials() { let creds = InsecureChannelCredentials::new(); let info = creds.info(); assert_eq!(info.security_protocol(), PROTOCOL_NAME); let addr = "127.0.0.1:0"; let listener = TcpListener::bind(addr).await.unwrap(); let server_addr = listener.local_addr().unwrap(); let authority = Authority::new("localhost".to_string(), Some(server_addr.port())); let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(server_addr, TcpOptions::default()) .await .unwrap(); let handshake_info = ClientHandshakeInfo::default(); let output = creds .connect(&authority, endpoint, handshake_info, runtime) .await .unwrap(); let mut endpoint = output.endpoint; let security_info = output.security; // Verify security info. assert_eq!(security_info.security_protocol(), PROTOCOL_NAME); assert_eq!(security_info.security_level(), SecurityLevel::NoSecurity); // Verify data transfer. let (mut server_stream, _) = listener.accept().await.unwrap(); assert_eq!( endpoint.get_local_address(), &server_stream.peer_addr().unwrap().to_string() ); let test_data = b"hello grpc"; server_stream.write_all(test_data).await.unwrap(); let mut buf = vec![0u8; test_data.len()]; endpoint.read_exact(&mut buf).await.unwrap(); assert_eq!(buf, test_data); // Validate arbitrary authority. assert!( security_info .security_context() .validate_authority(&authority) ); } #[tokio::test] async fn test_insecure_server_credentials() { let creds = InsecureServerCredentials::new(); let info = creds.info(); assert_eq!(info.security_protocol, PROTOCOL_NAME); let addr = "127.0.0.1:0"; let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp(addr.parse().unwrap(), TcpOptions::default()) .await .unwrap(); let server_addr = *listener.local_addr(); let client_handle = tokio::spawn(async move { let mut stream = TcpStream::connect(server_addr).await.unwrap(); let data = b"hello grpc"; stream.write_all(data).await.unwrap(); // Keep the connection alive for a bit so server can read. let mut buf = vec![0u8; 1]; let _ = stream.read(&mut buf).await; }); let (server_stream, _) = listener.accept().await.unwrap(); let output = creds.accept(server_stream, runtime).await.unwrap(); let mut endpoint = output.endpoint; let security_info = output.security; assert_eq!(security_info.security_protocol(), PROTOCOL_NAME); assert_eq!(security_info.security_level(), SecurityLevel::NoSecurity); let mut buf = vec![0u8; 10]; endpoint.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf[..], b"hello grpc"); client_handle.abort(); } } ================================================ FILE: grpc/src/credentials/local.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::net::SocketAddr; use std::str::FromStr; use std::sync::Arc; use crate::attributes::Attributes; use crate::client::name_resolution::TCP_IP_NETWORK_TYPE; use crate::credentials::ChannelCredentials; use crate::credentials::ProtocolInfo; use crate::credentials::SecurityLevel; use crate::credentials::ServerCredentials; use crate::credentials::call::CallCredentials; use crate::credentials::client; use crate::credentials::client::ClientConnectionSecurityContext; use crate::credentials::client::ClientConnectionSecurityInfo; use crate::credentials::client::ClientHandshakeInfo; use crate::credentials::client::HandshakeOutput; use crate::credentials::common::Authority; use crate::credentials::server; use crate::credentials::server::ServerConnectionSecurityInfo; use crate::rt::GrpcEndpoint; use crate::rt::GrpcRuntime; pub const PROTOCOL_NAME: &str = "local"; /// An implementation of [`ChannelCredentials`] for connections on the same /// machine. /// /// This credential type does not perform any encryption or authentication. It /// simply passes the raw underlying transport as the output. #[derive(Debug, Clone, Default)] pub struct LocalChannelCredentials { _private: (), } impl LocalChannelCredentials { /// Creates a new instance of `InsecureChannelCredentials`. pub fn new() -> Self { Self { _private: () } } } /// An implementation of [`ClientConnectionSecurityContext`] for local /// connections. #[derive(Debug, Clone)] pub struct LocalConnectionSecurityContext; impl ClientConnectionSecurityContext for LocalConnectionSecurityContext { fn validate_authority(&self, _authority: &Authority) -> bool { true } } /// Returns the security level for a local connection. /// It returns an error if a connection is not local. /// Refer to L62: https://github.com/grpc/proposal/blob/master/L62-core-call-credential-security-level.md fn security_level_for_endpoint( peer_addr: &str, network_type: &str, ) -> Result { if network_type == TCP_IP_NETWORK_TYPE && SocketAddr::from_str(peer_addr) .map_err(|e| e.to_string())? .ip() .is_loopback() { return Ok(SecurityLevel::NoSecurity); } // TODO: Add support for unix sockets. Err(format!( "local credentials rejected connection to non-local address {}", peer_addr )) } impl client::ChannelCredsInternal for LocalChannelCredentials { type ContextType = LocalConnectionSecurityContext; type Output = I; async fn connect( &self, _authority: &Authority, source: Input, _info: ClientHandshakeInfo, _runtime: GrpcRuntime, ) -> Result, Self::ContextType>, String> { let security_level = security_level_for_endpoint(source.get_peer_address(), source.get_network_type())?; Ok(HandshakeOutput { endpoint: source, security: ClientConnectionSecurityInfo::new( PROTOCOL_NAME, security_level, LocalConnectionSecurityContext, Attributes::new(), ), }) } fn get_call_credentials(&self) -> Option<&Arc> { None } } impl ChannelCredentials for LocalChannelCredentials { fn info(&self) -> &ProtocolInfo { static INFO: ProtocolInfo = ProtocolInfo::new(PROTOCOL_NAME); &INFO } } /// An implementation of [`ServerCredentials`] for local connections. #[derive(Debug, Clone, Default)] pub struct LocalServerCredentials { _private: (), } impl LocalServerCredentials { pub fn new() -> Self { Self { _private: () } } } impl server::ServerCredsInternal for LocalServerCredentials { type Output = I; async fn accept( &self, source: Input, _runtime: GrpcRuntime, ) -> Result>, String> { let security_level = security_level_for_endpoint(source.get_peer_address(), source.get_network_type())?; Ok(server::HandshakeOutput { endpoint: source, security: ServerConnectionSecurityInfo::new( PROTOCOL_NAME, security_level, Attributes::new(), ), }) } } impl ServerCredentials for LocalServerCredentials { fn info(&self) -> &ProtocolInfo { static INFO: ProtocolInfo = ProtocolInfo::new(PROTOCOL_NAME); &INFO } } #[cfg(test)] mod test { use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use tokio::net::TcpStream; use super::*; use crate::credentials::ChannelCredentials; use crate::credentials::SecurityLevel; use crate::credentials::ServerCredentials; use crate::credentials::client::ChannelCredsInternal as ClientSealed; use crate::credentials::client::ClientConnectionSecurityContext; use crate::credentials::client::ClientHandshakeInfo; use crate::credentials::common::Authority; use crate::credentials::server::ServerCredsInternal; use crate::rt; use crate::rt::GrpcEndpoint; use crate::rt::TcpOptions; #[test] fn test_security_level_for_endpoint_success() { assert_eq!( security_level_for_endpoint("127.0.0.1:8080", TCP_IP_NETWORK_TYPE), Ok(SecurityLevel::NoSecurity) ); assert_eq!( security_level_for_endpoint("[::1]:8080", TCP_IP_NETWORK_TYPE), Ok(SecurityLevel::NoSecurity) ); } #[test] fn test_security_level_for_endpoint_failure() { assert!(security_level_for_endpoint("192.168.1.1:8080", TCP_IP_NETWORK_TYPE).is_err()); assert!(security_level_for_endpoint("127.0.0.1:8080", "unix").is_err()); assert!(security_level_for_endpoint("invalid", TCP_IP_NETWORK_TYPE).is_err()); } #[tokio::test] async fn test_local_client_credentials() { let creds = LocalChannelCredentials::new(); let info = creds.info(); assert_eq!(info.security_protocol(), "local"); let addr = "127.0.0.1:0"; let listener = TcpListener::bind(addr).await.unwrap(); let server_addr = listener.local_addr().unwrap(); let authority = Authority::new("localhost".to_string(), Some(server_addr.port())); let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(server_addr, TcpOptions::default()) .await .unwrap(); let handshake_info = ClientHandshakeInfo::default(); let output = creds .connect(&authority, endpoint, handshake_info, runtime) .await .unwrap(); let mut endpoint = output.endpoint; let security_info = output.security; // Verify security info. assert_eq!(security_info.security_protocol(), "local"); assert_eq!(security_info.security_level(), SecurityLevel::NoSecurity); // Verify data transfer. let (mut server_stream, _) = listener.accept().await.unwrap(); assert_eq!( endpoint.get_local_address(), &server_stream.peer_addr().unwrap().to_string() ); let test_data = b"hello grpc"; server_stream.write_all(test_data).await.unwrap(); let mut buf = vec![0u8; test_data.len()]; endpoint.read_exact(&mut buf).await.unwrap(); assert_eq!(buf, test_data); // Validate arbitrary authority. assert!( security_info .security_context() .validate_authority(&authority) ); } #[tokio::test] async fn test_local_server_credentials() { let creds = LocalServerCredentials::new(); let info = creds.info(); assert_eq!(info.security_protocol, "local"); let addr = "127.0.0.1:0"; let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp(addr.parse().unwrap(), TcpOptions::default()) .await .unwrap(); let server_addr = *listener.local_addr(); let client_handle = tokio::spawn(async move { let mut stream = TcpStream::connect(server_addr).await.unwrap(); let data = b"hello grpc"; stream.write_all(data).await.unwrap(); // Keep the connection alive for a bit so server can read. let mut buf = vec![0u8; 1]; let _ = stream.read(&mut buf).await; }); let (server_stream, _) = listener.accept().await.unwrap(); let output = creds.accept(server_stream, runtime).await.unwrap(); let mut endpoint = output.endpoint; let security_info = output.security; assert_eq!(security_info.security_protocol(), "local"); assert_eq!(security_info.security_level(), SecurityLevel::NoSecurity); let mut buf = vec![0u8; 10]; endpoint.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf[..], b"hello grpc"); client_handle.abort(); } } ================================================ FILE: grpc/src/credentials/mod.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ pub mod call; pub(crate) mod client; pub(crate) mod dyn_wrapper; mod insecure; mod local; #[cfg(feature = "tls-rustls")] pub mod rustls; pub(crate) mod server; pub use client::CompositeChannelCredentials; pub use insecure::InsecureChannelCredentials; pub use insecure::InsecureServerCredentials; pub use local::LocalChannelCredentials; pub use local::LocalServerCredentials; /// Defines the common interface for all live gRPC wire protocols and supported /// transport security protocols (e.g., TLS, ALTS). pub trait ChannelCredentials: client::ChannelCredsInternal + Sync + 'static { //// Provides the ProtocolInfo of these credentials. fn info(&self) -> &ProtocolInfo; } pub trait ServerCredentials: server::ServerCredsInternal + Sync + 'static { //// Provides the ProtocolInfo of this credentials. fn info(&self) -> &ProtocolInfo; } /// Defines the level of protection provided by an established connection. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[non_exhaustive] pub enum SecurityLevel { /// The connection is insecure; no protection is applied. NoSecurity, /// The connection guarantees data integrity (tamper-proofing) but not /// privacy. /// /// Payloads are visible to observers but cannot be modified without /// detection. IntegrityOnly, /// The connection guarantees both privacy (confidentiality) and data /// integrity. /// /// This is the standard level for secure transports like TLS. PrivacyAndIntegrity, } pub(crate) mod common { /// Represents the value passed as the `:authority` pseudo-header, typically /// in the form `host:port`. pub struct Authority { host: String, port: Option, } impl Authority { pub fn new(host: String, port: Option) -> Self { Self { host, port } } pub fn host(&self) -> &str { &self.host } pub fn port(&self) -> Option { self.port } } } pub struct ProtocolInfo { security_protocol: &'static str, } impl ProtocolInfo { pub(crate) const fn new(security_protocol: &'static str) -> Self { Self { security_protocol } } pub fn security_protocol(&self) -> &'static str { self.security_protocol } } ================================================ FILE: grpc/src/credentials/rustls/client/mod.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::path::PathBuf; use std::sync::Arc; use rustls::crypto::CryptoProvider; use rustls_pki_types::CertificateDer; use rustls_pki_types::ServerName; use rustls_platform_verifier::BuilderVerifierExt; use tokio::sync::watch::Receiver; use tokio_rustls::TlsConnector; use tokio_rustls::TlsStream as RustlsStream; use crate::attributes::Attributes; use crate::credentials::ChannelCredentials; use crate::credentials::ProtocolInfo; use crate::credentials::SecurityLevel; use crate::credentials::call::CallCredentials; use crate::credentials::client; use crate::credentials::client::ClientConnectionSecurityContext; use crate::credentials::client::ClientConnectionSecurityInfo; use crate::credentials::client::ClientHandshakeInfo; use crate::credentials::client::HandshakeOutput; use crate::credentials::common::Authority; use crate::credentials::rustls::ALPN_PROTO_STR_H2; use crate::credentials::rustls::Identity; use crate::credentials::rustls::Provider; use crate::credentials::rustls::RootCertificates; use crate::credentials::rustls::TLS_PROTO_INFO; use crate::credentials::rustls::key_log::KeyLogFile; use crate::credentials::rustls::parse_certs; use crate::credentials::rustls::parse_key; use crate::credentials::rustls::sanitize_crypto_provider; use crate::credentials::rustls::tls_stream::TlsStream; use crate::rt::GrpcEndpoint; use crate::rt::GrpcRuntime; #[cfg(test)] mod test; /// Configuration for client-side TLS settings. pub struct ClientTlsConfig { pem_roots_provider: Option>, identity_provider: Option>, key_log_path: Option, } impl ClientTlsConfig { pub fn new() -> Self { ClientTlsConfig { pem_roots_provider: None, identity_provider: None, key_log_path: None, } } /// Configures the set of PEM-encoded root certificates (CA) to trust. /// /// These certificates are used to validate the server's certificate chain. /// If this is not called, the client generally defaults to using the /// system's native certificate store. pub fn with_root_certificates_provider(mut self, provider: R) -> Self where R: Provider, { self.pem_roots_provider = Some(provider.get_receiver()); self } /// Configures the client's identity for Mutual TLS (mTLS). /// /// This provides the client's certificate chain and private key. /// If this is not called, the client will not present a certificate /// to the server (standard one-way TLS). pub fn with_identity_provider(mut self, provider: I) -> Self where I: Provider, { self.identity_provider = Some(provider.get_receiver()); self } /// Sets the path where TLS session keys will be logged. /// /// # Security /// /// This should be used **only for debugging purposes**. It should never be /// used in a production environment due to security concerns. pub fn insecure_with_key_log_path(mut self, path: impl Into) -> Self { self.key_log_path = Some(path.into()); self } } impl Default for ClientTlsConfig { fn default() -> Self { Self::new() } } #[derive(Clone)] pub struct RustlsClientTlsCredendials { connector: TlsConnector, } impl RustlsClientTlsCredendials { /// Constructs a new `ClientTlsCredendials` instance from the provided /// configuration. pub fn new(config: ClientTlsConfig) -> Result { let provider = if let Some(p) = CryptoProvider::get_default() { p.as_ref().clone() } else { return Err( "No crypto provider installed. Enable `tls-aws-lc` feature in rustls or install one manually." .to_string() ); }; Self::new_impl(config, provider) } fn new_impl( mut config: ClientTlsConfig, provider: CryptoProvider, ) -> Result { let provider = sanitize_crypto_provider(provider)?; let builder = rustls::ClientConfig::builder_with_provider(Arc::new(provider)) .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12]) .map_err(|e| e.to_string())?; let builder = if let Some(mut roots_provider) = config.pem_roots_provider.take() { let mut root_store = rustls::RootCertStore::empty(); let ca_pem = roots_provider.borrow_and_update(); let certs = parse_certs(ca_pem.get_ref())?; for cert in certs { root_store.add(cert).map_err(|e| e.to_string())?; } builder.with_root_certificates(root_store) } else { // Use system root certificates. builder .with_platform_verifier() .map_err(|e| e.to_string())? }; let mut client_config = if let Some(mut identity_provider) = config.identity_provider.take() { let identity = identity_provider.borrow_and_update(); let certs = parse_certs(&identity.certs)?; let key = parse_key(&identity.key)?; builder .with_client_auth_cert(certs, key) .map_err(|e| e.to_string())? } else { builder.with_no_client_auth() }; client_config.alpn_protocols = vec![ALPN_PROTO_STR_H2.to_vec()]; client_config.resumption = rustls::client::Resumption::disabled(); if let Some(path) = config.key_log_path { client_config.key_log = Arc::new(KeyLogFile::new(&path)) } Ok(RustlsClientTlsCredendials { connector: TlsConnector::from(Arc::new(client_config)), }) } } pub struct ClientTlsSecContext { verified_peer_cert: Option>, } impl ClientConnectionSecurityContext for ClientTlsSecContext { fn validate_authority(&self, authority: &Authority) -> bool { let server_name = match ServerName::try_from(authority.host()) { Ok(n) => n, Err(_) => return false, }; let cert_der = match &self.verified_peer_cert { Some(c) => c, None => return false, }; let cert = match webpki::EndEntityCert::try_from(cert_der) { Ok(c) => c, Err(_) => return false, }; cert.verify_is_valid_for_subject_name(&server_name).is_ok() } } impl client::ChannelCredsInternal for RustlsClientTlsCredendials { type ContextType = ClientTlsSecContext; type Output = TlsStream; async fn connect( &self, authority: &Authority, source: Input, _info: ClientHandshakeInfo, _rt: GrpcRuntime, ) -> Result, ClientTlsSecContext>, String> { let server_name = ServerName::try_from(authority.host()) .map_err(|e| format!("invalid authority: {}", e))? .to_owned(); let tls_stream = self .connector .connect(server_name, source) .await .map_err(|e| e.to_string())?; let (_io, connection) = tls_stream.get_ref(); if let Some(negotiated) = connection.alpn_protocol() { if negotiated != ALPN_PROTO_STR_H2 { return Err("Server negotiated unexpected ALPN protocol".into()); } } else { // Strict Enforcement: Fail if server didn't select ALPN return Err("Server did not negotiate ALPN (h2 required)".into()); } let peer_cert = connection .peer_certificates() .and_then(|certs| certs.first()) .map(|c| c.clone().into_owned()); let cs_info = ClientConnectionSecurityInfo::new( "tls", SecurityLevel::PrivacyAndIntegrity, ClientTlsSecContext { verified_peer_cert: peer_cert, }, Attributes::new(), ); let ep = TlsStream::new(RustlsStream::Client(tls_stream)); Ok(HandshakeOutput { endpoint: ep, security: cs_info, }) } fn get_call_credentials(&self) -> Option<&Arc> { None } } impl ChannelCredentials for RustlsClientTlsCredendials { fn info(&self) -> &ProtocolInfo { &TLS_PROTO_INFO } } ================================================ FILE: grpc/src/credentials/rustls/client/test.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; use std::sync::Once; use rustls::HandshakeKind; use rustls::ServerConfig; use rustls::crypto::ring; use rustls_pki_types::CertificateDer; use rustls_pki_types::PrivateKeyDer; use tempfile::NamedTempFile; use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use tokio::task::JoinHandle; use tokio_rustls::TlsAcceptor; use crate::credentials::client::ChannelCredsInternal; use crate::credentials::client::ClientConnectionSecurityContext; use crate::credentials::client::ClientHandshakeInfo; use crate::credentials::common::Authority; use crate::credentials::rustls::ALPN_PROTO_STR_H2; use crate::credentials::rustls::Identity; use crate::credentials::rustls::RootCertificates; use crate::credentials::rustls::StaticProvider; use crate::credentials::rustls::client::ClientTlsConfig; use crate::credentials::rustls::client::RustlsClientTlsCredendials; use crate::rt; use crate::rt::TcpOptions; static INIT: Once = Once::new(); fn init_provider() { INIT.call_once(|| { let _ = ring::default_provider().install_default(); }); } #[tokio::test] async fn test_tls_handshake() { init_provider(); run_handshake_test(vec![ALPN_PROTO_STR_H2.to_vec()], true).await; } #[tokio::test] async fn test_tls_handshake_no_alpn() { init_provider(); // Server provides NO ALPN. Client requires "h2". run_handshake_test(vec![], false).await; } #[tokio::test] async fn test_tls_handshake_bad_alpn() { init_provider(); // Server provides HTTP/1.1 ALPN. Client requires "h2". run_handshake_test(vec![b"http/1.1".to_vec()], false).await; } #[tokio::test] async fn test_tls_handshake_alpn_h1_and_h2() { init_provider(); // Server provides HTTP/1.1 and H2 ALPN. Client requires "h2". run_handshake_test(vec![b"http/1.1".to_vec(), b"h2".to_vec()], true).await; } #[tokio::test] async fn test_tls_cipher_suites_secure() { init_provider(); let root_certs = load_root_certs("ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ClientTlsConfig::new().with_root_certificates_provider(root_provider); let provider = rustls::crypto::CryptoProvider::get_default() .expect("No default crypto provider found") .as_ref() .clone(); // This should succeed as default provider usually has secure suites. let creds = RustlsClientTlsCredendials::new_impl(config, provider); assert!( creds.is_ok(), "Failed to create creds with secure provider: {:?}", creds.err() ); } #[tokio::test] async fn test_tls_cipher_suites_insecure() { init_provider(); let root_certs = load_root_certs("ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ClientTlsConfig::new().with_root_certificates_provider(root_provider); let mut provider = rustls::crypto::CryptoProvider::get_default() .expect("No default crypto provider found") .as_ref() .clone(); fn is_secure(suported_suite: &rustls::SupportedCipherSuite) -> bool { match suported_suite { rustls::SupportedCipherSuite::Tls13(_) => true, rustls::SupportedCipherSuite::Tls12(suite) => matches!( suite.common.suite, rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 | rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 | rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 | rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 | rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ), } } // Remove all cipher suites that are considered secure by our policy provider.cipher_suites.retain(|suite| !is_secure(suite)); let creds = RustlsClientTlsCredendials::new_impl(config, provider); assert!(creds.err().unwrap().contains("no cipher suites matching")); } #[tokio::test] async fn test_tls_key_log() { init_provider(); let key_log_file = NamedTempFile::new().expect("failed to create a temporary file."); // Server setup let server_config = default_server_config(); let (addr, server_task) = setup_server(server_config).await; // Client setup let root_certs = load_root_certs("ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ClientTlsConfig::new() .with_root_certificates_provider(root_provider) .insecure_with_key_log_path(key_log_file.path()); let creds = RustlsClientTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(addr, TcpOptions::default()) .await .unwrap(); let authority = Authority::new("localhost".to_string(), Some(addr.port())); let result = creds .connect( &authority, endpoint, ClientHandshakeInfo::default(), runtime, ) .await .expect("Handshake failed"); let mut stream = result.endpoint; let mut buf = Vec::new(); let _ = stream.read_to_end(&mut buf).await; assert_eq!(buf, b"Hello world"); server_task.await.unwrap(); // Verify key log file has content. let content = std::fs::read_to_string(key_log_file.path()).unwrap(); assert!(!content.is_empty(), "Key log file is empty"); // CLIENT_HANDSHAKE_TRAFFIC_SECRET is standard for TLS 1.3 assert!( content.contains("CLIENT_HANDSHAKE_TRAFFIC_SECRET"), "Key log missing expected content: {}", content ); } #[tokio::test] async fn test_tls_handshake_wrong_server_name() { init_provider(); // Server setup let server_config = default_server_config(); let (addr, server_task) = setup_server(server_config).await; // Client setup let root_certs = load_root_certs("ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ClientTlsConfig::new().with_root_certificates_provider(root_provider); let creds = RustlsClientTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(addr, TcpOptions::default()) .await .unwrap(); let authority = Authority::new( // Use a hostname not in the server cert's SANs "wrong.host.com".to_string(), Some(addr.port()), ); let result = creds .connect( &authority, endpoint, ClientHandshakeInfo::default(), runtime, ) .await; assert!( result.is_err(), "Handshake should fail with wrong server name" ); server_task.await.unwrap(); } #[tokio::test] async fn test_tls_validate_authority() { init_provider(); // Server setup let server_config = default_server_config(); let acceptor = TlsAcceptor::from(Arc::new(server_config)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server_task = tokio::spawn(async move { if let Ok((stream, _)) = listener.accept().await { // Complete handshake and hold connection if let Ok(mut stream) = acceptor.accept(stream).await { // Keep connection open for client to verify let _ = stream.read_u8().await; } } }); // Client setup. let root_certs = load_root_certs("ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ClientTlsConfig::new().with_root_certificates_provider(root_provider); let creds = RustlsClientTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(addr, TcpOptions::default()) .await .unwrap(); let authority = Authority::new("localhost".to_string(), Some(addr.port())); let result = creds .connect( &authority, endpoint, ClientHandshakeInfo::default(), runtime, ) .await .expect("Handshake failed"); let context = result.security.security_context(); // Validate correct authorities assert!(context.validate_authority(&Authority::new("localhost".to_string(), None))); assert!(context.validate_authority(&Authority::new("example.com".to_string(), None))); assert!(context.validate_authority(&Authority::new("127.0.0.1".to_string(), None))); // Validate incorrect authorities assert!(!context.validate_authority(&Authority::new("wrong.host".to_string(), None))); assert!(!context.validate_authority(&Authority::new("grpc.io".to_string(), None))); } #[tokio::test] async fn test_mtls_handshake_no_identity() { init_provider(); // Server setup (Requires Client Auth) let server_config = mtls_server_config(); let (addr, server_task) = setup_server(server_config).await; let config = ClientTlsConfig::new() .with_root_certificates_provider(StaticProvider::new(load_root_certs("ca.pem"))); let creds = RustlsClientTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(addr, TcpOptions::default()) .await .unwrap(); let authority = Authority::new("localhost".to_string(), Some(addr.port())); // In TLS 1.3, the client considers the handshake complete immediately after // sending its Certificate and Finished messages. It does not wait for the // server to validate them. // Consequently, connect() returns success, but the server will subsequently // process the credentials, reject them, and close the connection with an // Alert. Therefore, the connection succeeds, but the first read on the // stream must fail. let result = creds .connect( &authority, endpoint, ClientHandshakeInfo::default(), runtime, ) .await .expect("Client handshake expected to succeed with TLS 1.3"); let mut stream = result.endpoint; let mut buf = Vec::new(); let res = stream.read_to_end(&mut buf).await; assert!( res.is_err(), "read from TLS stream should fail due to missing client identity" ); server_task.await.unwrap(); } #[tokio::test] async fn test_mtls_handshake_with_identitiy() { init_provider(); // Server setup (Requires Client Auth) let server_config = mtls_server_config(); let (addr, server_task) = setup_server(server_config).await; let root_certs = load_root_certs("ca.pem"); let root_provider = StaticProvider::new(root_certs); let identity = load_identity("client1.pem", "client1.key"); let identity_provider = StaticProvider::new(identity); let config = ClientTlsConfig::new() .with_root_certificates_provider(root_provider) .with_identity_provider(identity_provider); let creds = RustlsClientTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(addr, TcpOptions::default()) .await .unwrap(); let authority = Authority::new("localhost".to_string(), Some(addr.port())); let result = creds .connect( &authority, endpoint, ClientHandshakeInfo::default(), runtime, ) .await .expect("Handshake failed with client identity"); let mut stream = result.endpoint; let mut buf = Vec::new(); let _ = stream.read_to_end(&mut buf).await; assert_eq!(buf, b"Hello world"); server_task.await.unwrap(); } async fn check_client_resumption_disabled( versions: Vec<&'static rustls::SupportedProtocolVersion>, ) { init_provider(); // Server setup: Support resumption let certs = load_certs("server.pem"); let key = load_private_key("server.key"); let provider = ring::default_provider(); let mut server_config = ServerConfig::builder_with_provider(Arc::new(provider)) .with_protocol_versions(&versions) .expect("invalid versions") .with_no_client_auth() .with_single_cert(certs, key) .unwrap(); server_config.alpn_protocols = vec![ALPN_PROTO_STR_H2.to_vec()]; // Enable stateful resumption server_config.session_storage = rustls::server::ServerSessionMemoryCache::new(32); // Enable stateless resumption (TLS 1.3 tickets) server_config.send_tls13_tickets = 1; let (addr, server_task) = setup_server_multi_connection(server_config, 2).await; // Client setup let root_certs = load_root_certs("ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ClientTlsConfig::new().with_root_certificates_provider(root_provider); let creds = RustlsClientTlsCredendials::new(config).unwrap(); for i in 0..2 { let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(addr, TcpOptions::default()) .await .unwrap(); let authority = Authority::new("localhost".to_string(), Some(addr.port())); let result = creds .connect( &authority, endpoint, ClientHandshakeInfo::default(), runtime, ) .await .expect("Handshake failed"); let mut tls_stream = result.endpoint; let connection = match tls_stream.inner() { tokio_rustls::TlsStream::Client(conn) => conn.get_ref().1, _ => panic!("Expected client stream"), }; assert_eq!( connection.handshake_kind(), Some(HandshakeKind::Full), "Expected full handshake on attempt {}", i ); let mut buf = Vec::new(); let _ = tls_stream.read_to_end(&mut buf).await; assert_eq!(buf, b"Hello world"); } server_task.await.unwrap(); } #[tokio::test] async fn test_tls_resumption_disabled_tls13() { check_client_resumption_disabled(vec![&rustls::version::TLS13]).await; } #[tokio::test] async fn test_tls_resumption_disabled_tls12() { check_client_resumption_disabled(vec![&rustls::version::TLS12]).await; } fn load_identity(cert_file: &str, key_file: &str) -> Identity { let cert = std::fs::read(test_certs_path().join(cert_file)).expect("cannot read cert file"); let key = std::fs::read(test_certs_path().join(key_file)).expect("cannot read key file"); Identity::from_pem(cert, key) } fn mtls_server_config() -> ServerConfig { let certs = load_certs("server.pem"); let key = load_private_key("server.key"); let client_ca_path = test_certs_path().join("client_ca.pem"); let file = std::fs::File::open(client_ca_path).expect("cannot open client CA file"); let mut reader = std::io::BufReader::new(file); let mut root_store = rustls::RootCertStore::empty(); for cert in rustls_pemfile::certs(&mut reader) { root_store.add(cert.unwrap()).unwrap(); } let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store)) .build() .unwrap(); let mut server_config = ServerConfig::builder() .with_client_cert_verifier(verifier) .with_single_cert(certs, key) .unwrap(); server_config.alpn_protocols = vec![ALPN_PROTO_STR_H2.to_vec()]; server_config } fn test_certs_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("examples/data/tls") } fn load_certs(filename: &str) -> Vec> { let path = test_certs_path().join(filename); let file = std::fs::File::open(path).expect("cannot open certificate file"); let mut reader = std::io::BufReader::new(file); rustls_pemfile::certs(&mut reader) .map(|result| result.unwrap()) .collect() } fn load_private_key(filename: &str) -> PrivateKeyDer<'static> { let path = test_certs_path().join(filename); let file = std::fs::File::open(path).expect("cannot open private key file"); let mut reader = std::io::BufReader::new(file); loop { match rustls_pemfile::read_one(&mut reader).expect("cannot read private key") { Some(rustls_pemfile::Item::Pkcs1Key(key)) => return key.into(), Some(rustls_pemfile::Item::Pkcs8Key(key)) => return key.into(), Some(rustls_pemfile::Item::Sec1Key(key)) => return key.into(), None => panic!("no keys found"), _ => {} } } } fn load_root_certs(filename: &str) -> RootCertificates { let path = test_certs_path().join(filename); let ca_pem = std::fs::read(path).unwrap(); RootCertificates::from_pem(ca_pem) } fn default_server_config() -> ServerConfig { let certs = load_certs("server.pem"); let key = load_private_key("server.key"); let mut server_config = ServerConfig::builder() .with_no_client_auth() .with_single_cert(certs, key) .unwrap(); server_config.alpn_protocols = vec![ALPN_PROTO_STR_H2.to_vec()]; server_config } async fn setup_server(config: ServerConfig) -> (SocketAddr, JoinHandle<()>) { setup_server_multi_connection(config, 1).await } async fn setup_server_multi_connection( config: ServerConfig, count: usize, ) -> (SocketAddr, JoinHandle<()>) { let acceptor = TlsAcceptor::from(Arc::new(config)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let task = tokio::spawn(async move { for _ in 0..count { let (stream, _) = listener.accept().await.unwrap(); let acceptor = acceptor.clone(); tokio::spawn(async move { match acceptor.accept(stream).await { Ok(mut stream) => { let _ = stream.write_all(b"Hello world").await; let _ = stream.shutdown().await; } Err(err) => { println!("TLS handshake failed: {}", err) } } }); } }); (addr, task) } async fn run_handshake_test(server_alpn: Vec>, expect_success: bool) { // Server setup let mut server_config = default_server_config(); server_config.alpn_protocols = server_alpn; let (addr, server_task) = setup_server(server_config).await; // Client setup let root_certs = load_root_certs("ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ClientTlsConfig::new().with_root_certificates_provider(root_provider); let creds = RustlsClientTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let endpoint = runtime .tcp_stream(addr, TcpOptions::default()) .await .unwrap(); let authority = Authority::new("localhost".to_string(), Some(addr.port())); let result = creds .connect( &authority, endpoint, ClientHandshakeInfo::default(), runtime, ) .await; if expect_success { assert!(result.is_ok(), "Handshake failed: {:?}", result.err()); let result = result.unwrap(); let mut stream = result.endpoint; let mut buf = Vec::new(); // Ignore read errors if server closed connection abruptly (which happens in failure cases, but here we expect success) let _ = stream.read_to_end(&mut buf).await; assert_eq!(buf, b"Hello world"); } else { assert!(result.is_err(), "Handshake succeeded but expected failure"); } server_task.await.unwrap(); } ================================================ FILE: grpc/src/credentials/rustls/key_log.rs ================================================ // Copyright (c) 2016 Joseph Birr-Pixton // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Note: This file contains modifications by the gRPC authors; see revision history for details. use core::fmt::Debug; use core::fmt::Formatter; use std::fs::File; use std::fs::OpenOptions; use std::io; use std::io::Write; use std::path::PathBuf; use std::sync::Mutex; use rustls::KeyLog; // Internal mutable state for KeyLogFile struct KeyLogFileInner { file: Option, buf: Vec, } impl KeyLogFileInner { fn new(path: &PathBuf) -> Self { let file = match OpenOptions::new().append(true).create(true).open(path) { Ok(f) => Some(f), Err(e) => { eprintln!("unable to create key log file {path:?}: {e}"); None } }; Self { file, buf: Vec::new(), } } fn try_write(&mut self, label: &str, client_random: &[u8], secret: &[u8]) -> io::Result<()> { let Some(file) = &mut self.file else { return Ok(()); }; self.buf.truncate(0); write!(self.buf, "{label} ")?; for b in client_random.iter() { write!(self.buf, "{b:02x}")?; } write!(self.buf, " ")?; for b in secret.iter() { write!(self.buf, "{b:02x}")?; } writeln!(self.buf)?; file.write_all(&self.buf) } } impl Debug for KeyLogFileInner { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.debug_struct("KeyLogFileInner") // Note: we omit self.buf deliberately as it may contain key data. .field("file", &self.file) .finish_non_exhaustive() } } /// [`KeyLog`] implementation that opens a file whose name is /// given to the constructor, and writes keys into it. /// /// If such a file cannot be opened, or cannot be written then /// this does nothing but logs errors. pub struct KeyLogFile(Mutex); impl KeyLogFile { /// Makes a new `KeyLogFile`. The environment variable is /// inspected and the named file is opened during this call. pub fn new(path: &PathBuf) -> Self { Self(Mutex::new(KeyLogFileInner::new(path))) } } impl KeyLog for KeyLogFile { fn log(&self, label: &str, client_random: &[u8], secret: &[u8]) { match self .0 .lock() .unwrap() .try_write(label, client_random, secret) { Ok(()) => {} Err(e) => { eprintln!("error writing to key log file: {e}"); } } } } impl Debug for KeyLogFile { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { match self.0.try_lock() { Ok(key_log_file) => write!(f, "{key_log_file:?}"), Err(_) => write!(f, "KeyLogFile {{ }}"), } } } ================================================ FILE: grpc/src/credentials/rustls/mod.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::io::BufReader; use rustls::crypto::CryptoProvider; use rustls::pki_types::PrivateKeyDer; use rustls_pki_types::CertificateDer; use tokio::sync::watch; use crate::credentials::ProtocolInfo; pub mod client; mod key_log; pub mod server; mod tls_stream; const ALPN_PROTO_STR_H2: &[u8; 2] = b"h2"; /// Represents a X509 certificate chain. #[derive(Debug, Clone)] pub struct RootCertificates { pem: Vec, } impl RootCertificates { /// Parse a PEM encoded X509 Certificate. /// /// The provided PEM should include at least one PEM encoded certificate. pub fn from_pem(pem: impl AsRef<[u8]>) -> Self { let pem = pem.as_ref().into(); Self { pem } } /// Get a immutable reference to underlying certificate fn get_ref(&self) -> &[u8] { self.pem.as_slice() } } /// Represents a private key and X509 certificate chain. #[derive(Debug, Clone)] pub struct Identity { certs: Vec, key: Vec, } pub type IdentityList = Vec; impl Identity { /// Parse a PEM encoded certificate and private key. /// /// The provided cert must contain at least one PEM encoded certificate. pub fn from_pem(cert: impl AsRef<[u8]>, key: impl AsRef<[u8]>) -> Self { let cert = cert.as_ref().into(); let key = key.as_ref().into(); Self { certs: cert, key } } } mod provider { use tokio::sync::watch::Receiver; /// A sealed trait to prevent downstream implementations of `Provider`. /// /// This trait exposes the internal mechanism (Tokio watch channel) used to /// receive updates. It is kept private/restricted to ensure that `Provider` /// can only be implemented by types defined within this crate. pub trait ProviderInternal { /// Returns a clone of the underlying watch receiver. /// /// This allows the consumer to observe the current value and await /// future updates. fn get_receiver(self) -> Receiver; } } /// A source of configuration or state of type `T` that allows for dynamic /// updates. /// /// This trait abstracts over the source of the data (e.g., static memory, /// file system, network) and provides a uniform interface for consumers to /// access the current value and subscribe to changes. /// /// # Sealed Trait /// /// This trait is **sealed**. It cannot be implemented by downstream crates. /// Users should rely on the provided implementations (e.g., /// `StaticIdentityProvider`, `StaticRootsProvider`). pub trait Provider: provider::ProviderInternal {} /// A provider that supplies a constant, immutable value. pub struct StaticProvider { inner: T, } impl StaticProvider { /// Creates a new `StaticProvider` with the given fixed value. pub fn new(value: T) -> Self { Self { inner: value } } } impl provider::ProviderInternal for StaticProvider { fn get_receiver(self) -> watch::Receiver { // We drop the sender (_) immediately. // This ensures the receiver sees the initial value but knows // no future updates will arrive. let (_tx, rx) = watch::channel(self.inner); rx } } impl Provider for StaticProvider {} pub type StaticRootCertificatesProvider = StaticProvider; pub type StaticIdentityProvider = StaticProvider; static TLS_PROTO_INFO: ProtocolInfo = ProtocolInfo { security_protocol: "tls", }; fn sanitize_crypto_provider(mut crypto_provider: CryptoProvider) -> Result { crypto_provider.cipher_suites.retain(|suite| match suite { rustls::SupportedCipherSuite::Tls13(suite) => true, rustls::SupportedCipherSuite::Tls12(suite) => { matches!( suite.common.suite, rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 | rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 | rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 | rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 | rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ) } }); if crypto_provider.cipher_suites.is_empty() { return Err("Crypto provider has no cipher suites matching the security policy (TLS1.3 or TLS1.2+ECDHE)".to_string()); } Ok(crypto_provider) } fn parse_certs(pem: &[u8]) -> Result>, String> { let mut reader = BufReader::new(pem); rustls_pemfile::certs(&mut reader) .map(|result| result.map_err(|e| e.to_string())) .collect() } fn parse_key(pem: &[u8]) -> Result, String> { let mut reader = BufReader::new(pem); loop { match rustls_pemfile::read_one(&mut reader).map_err(|e| e.to_string())? { Some(rustls_pemfile::Item::Pkcs1Key(key)) => return Ok(key.into()), Some(rustls_pemfile::Item::Pkcs8Key(key)) => return Ok(key.into()), Some(rustls_pemfile::Item::Sec1Key(key)) => return Ok(key.into()), None => return Err("no private key found".to_string()), _ => continue, } } } ================================================ FILE: grpc/src/credentials/rustls/server/mod.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::path::PathBuf; use std::sync::Arc; use rustls::crypto::CryptoProvider; use rustls::server::ClientHello; use rustls::server::ProducesTickets; use rustls::server::ResolvesServerCert; use rustls::sign::CertifiedKey; use tokio::sync::watch::Receiver; use tokio_rustls::TlsAcceptor; use tokio_rustls::TlsStream as RustlsStream; use webpki::EndEntityCert; use crate::attributes::Attributes; use crate::credentials::ProtocolInfo; use crate::credentials::SecurityLevel; use crate::credentials::ServerCredentials; use crate::credentials::rustls::ALPN_PROTO_STR_H2; use crate::credentials::rustls::IdentityList; use crate::credentials::rustls::Provider; use crate::credentials::rustls::RootCertificates; use crate::credentials::rustls::StaticRootCertificatesProvider; use crate::credentials::rustls::TLS_PROTO_INFO; use crate::credentials::rustls::key_log::KeyLogFile; use crate::credentials::rustls::parse_certs; use crate::credentials::rustls::parse_key; use crate::credentials::rustls::provider::ProviderInternal; use crate::credentials::rustls::sanitize_crypto_provider; use crate::credentials::rustls::tls_stream::TlsStream; use crate::credentials::server::HandshakeOutput; use crate::credentials::server::ServerConnectionSecurityInfo; use crate::credentials::server::ServerCredsInternal; use crate::rt::GrpcEndpoint; use crate::rt::GrpcRuntime; #[cfg(test)] mod test; #[derive(Debug)] struct SniResolver { keys: Vec>, } impl ResolvesServerCert for SniResolver { fn resolve(&self, client_hello: ClientHello) -> Option> { if self.keys.len() == 1 { return Some(self.keys[0].clone()); } if let Some(subject_name) = client_hello .server_name() .and_then(|sni| rustls_pki_types::ServerName::try_from(sni).ok()) { for key in &self.keys { let Some(cert) = key.cert.first() else { continue; }; let Ok(end_entity_cert) = EndEntityCert::try_from(cert) else { continue; }; if end_entity_cert .verify_is_valid_for_subject_name(&subject_name) .is_ok() { return Some(key.clone()); } } } self.keys.first().cloned() } } #[non_exhaustive] pub enum TlsClientCertificateRequestType { /// Server does not request client certificate. /// /// This is the default behavior. /// /// The certificate presented by the client is not checked by the server at /// all. (A client may present a self-signed or signed certificate or not /// present a certificate at all and any of those option would be accepted). DontRequest, /// Server requests client certificate but does not enforce that the client /// presents a certificate. /// /// If the client presents a certificate, the client authentication is done by /// the gRPC framework. For a successful connection the client needs to either /// present a certificate that can be verified against the `pem_root_certs` /// or not present a certificate at all. /// /// The client's key certificate pair must be valid for the TLS connection to /// be established. RequestAndVerify { roots_provider: R }, /// Server requests client certificate and enforces that the client presents a /// certificate. /// /// The certificate presented by the client is verified by the gRPC framework. /// For a successful connection the client needs to present a certificate that /// can be verified against the `pem_root_certs`. /// /// The client's key certificate pair must be valid for the TLS connection to /// be established. RequireAndVerify { roots_provider: R }, } enum InnerClientCertificateRequestType { DontRequestClientCertificate, RequestClientCertificateAndVerify { roots_provider: Receiver, }, RequestAndRequireClientCertificateAndVerify { roots_provider: Receiver, }, } impl From for InnerClientCertificateRequestType { fn from(value: TlsClientCertificateRequestType) -> Self { match value { TlsClientCertificateRequestType::DontRequest => { InnerClientCertificateRequestType::DontRequestClientCertificate } TlsClientCertificateRequestType::RequestAndVerify { roots_provider } => { InnerClientCertificateRequestType::RequestClientCertificateAndVerify { roots_provider: roots_provider.get_receiver(), } } TlsClientCertificateRequestType::RequireAndVerify { roots_provider } => { InnerClientCertificateRequestType::RequestAndRequireClientCertificateAndVerify { roots_provider: roots_provider.get_receiver(), } } } } } #[derive(Clone)] pub struct RustlsServerTlsCredendials { acceptor: TlsAcceptor, } /// Configuration for server-side TLS settings. pub struct ServerTlsConfig { identities_provider: Receiver, request_type: InnerClientCertificateRequestType, key_log_path: Option, } impl ServerTlsConfig { pub fn new(identities_provider: I) -> Self where I: Provider, { ServerTlsConfig { identities_provider: identities_provider.get_receiver(), request_type: TlsClientCertificateRequestType::DontRequest.into(), key_log_path: None, } } /// Configures the client certificate request policy for the server. /// /// This determines whether the server requests a client certificate and how /// it verifies it. pub fn with_request_type(mut self, request_type: TlsClientCertificateRequestType) -> Self { self.request_type = request_type.into(); self } /// Sets the path where TLS session keys will be logged. /// /// # Security /// /// This should be used **only for debugging purposes**. It should never be /// used in a production environment due to security concerns. pub fn insecure_with_key_log_path(mut self, path: impl Into) -> Self { self.key_log_path = Some(path.into()); self } } impl RustlsServerTlsCredendials { pub fn new(config: ServerTlsConfig) -> Result { let provider = if let Some(p) = CryptoProvider::get_default() { p.as_ref().clone() } else { return Err( "No crypto provider installed. Enable `tls-aws-lc` feature in rustls or install one manually." .to_string() ); }; Self::new_impl(config, provider) } fn new_impl( mut config: ServerTlsConfig, provider: CryptoProvider, ) -> Result { let provider = sanitize_crypto_provider(provider)?; let id_list = config.identities_provider.borrow_and_update().clone(); if id_list.is_empty() { return Err("need at least one server identity.".to_string()); } let verifier = match config.request_type { InnerClientCertificateRequestType::DontRequestClientCertificate => { rustls::server::WebPkiClientVerifier::no_client_auth() } InnerClientCertificateRequestType::RequestClientCertificateAndVerify { mut roots_provider, } => { let roots = roots_provider.borrow_and_update(); let certs = parse_certs(&roots.pem)?; let mut root_store = rustls::RootCertStore::empty(); for cert in certs { root_store.add(cert).map_err(|e| e.to_string())?; } rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store)) .allow_unauthenticated() .build() .map_err(|e| e.to_string())? } InnerClientCertificateRequestType::RequestAndRequireClientCertificateAndVerify { mut roots_provider, } => { let roots = roots_provider.borrow_and_update(); let certs = parse_certs(&roots.pem)?; let mut root_store = rustls::RootCertStore::empty(); for cert in certs { root_store.add(cert).map_err(|e| e.to_string())?; } rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store)) .build() .map_err(|e| e.to_string())? } }; let builder = rustls::ServerConfig::builder_with_provider(Arc::new(provider.clone())) .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12]) .map_err(|e| e.to_string())? .with_client_cert_verifier(verifier); let mut keys = Vec::with_capacity(id_list.len()); for identity in id_list { let certs = parse_certs(&identity.certs)?; let key = parse_key(&identity.key)?; let signing_key = provider .key_provider .load_private_key(key) .map_err(|e| e.to_string())?; keys.push(Arc::new(CertifiedKey::new(certs, signing_key))); } let resolver = Arc::new(SniResolver { keys }); let mut server_config = builder.with_cert_resolver(resolver); server_config.alpn_protocols = vec![ALPN_PROTO_STR_H2.to_vec()]; if let Some(path) = config.key_log_path { server_config.key_log = Arc::new(KeyLogFile::new(&path)); } // Disable Stateful Resumption (Session IDs). server_config.session_storage = Arc::new(rustls::server::NoServerSessionStorage {}); // Disable Stateless Resumption (TLS 1.3 Tickets). server_config.send_tls13_tickets = 0; // Disable Stateless Resumption (TLS 1.2 Tickets) // Install a dummy ticketer that refuses to issue tickets. server_config.ticketer = Arc::new(NoTicketer); Ok(RustlsServerTlsCredendials { acceptor: TlsAcceptor::from(Arc::new(server_config)), }) } } // Helper Struct to Disable Tickets. #[derive(Debug)] struct NoTicketer; impl ProducesTickets for NoTicketer { fn enabled(&self) -> bool { false } fn lifetime(&self) -> u32 { 0 } fn encrypt(&self, _plain: &[u8]) -> Option> { None } fn decrypt(&self, _cipher: &[u8]) -> Option> { None } } impl ServerCredsInternal for RustlsServerTlsCredendials { type Output = TlsStream; async fn accept( &self, source: Input, _runtime: GrpcRuntime, ) -> Result>, String> { let tls_stream = self .acceptor .accept(source) .await .map_err(|e| e.to_string())?; let (_io, conn) = tls_stream.get_ref(); if conn.alpn_protocol() != Some(ALPN_PROTO_STR_H2) { return Err("Client ignored ALPN requirements".into()); } let auth_info = ServerConnectionSecurityInfo::new( "tls", SecurityLevel::PrivacyAndIntegrity, Attributes::new(), ); let endpoint = TlsStream::new(RustlsStream::Server(tls_stream)); Ok(HandshakeOutput { endpoint, security: auth_info, }) } } impl ServerCredentials for RustlsServerTlsCredendials { fn info(&self) -> &ProtocolInfo { &TLS_PROTO_INFO } } ================================================ FILE: grpc/src/credentials/rustls/server/test.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::path::PathBuf; use std::sync::Arc; use std::sync::Once; use rustls::HandshakeKind; use rustls::crypto::ring; use rustls_pki_types::ServerName; use tempfile::NamedTempFile; use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; use tokio::net::TcpStream; use tokio_rustls::TlsConnector; use crate::credentials::rustls::ALPN_PROTO_STR_H2; use crate::credentials::rustls::Identity; use crate::credentials::rustls::RootCertificates; use crate::credentials::rustls::StaticProvider; use crate::credentials::rustls::server::RustlsServerTlsCredendials; use crate::credentials::rustls::server::ServerTlsConfig; use crate::credentials::rustls::server::TlsClientCertificateRequestType; use crate::credentials::server::ServerCredsInternal; use crate::rt::TcpOptions; use crate::rt::{self}; static INIT: Once = Once::new(); fn init_provider() { INIT.call_once(|| { let _ = ring::default_provider().install_default(); }); } #[tokio::test] async fn test_tls_server_handshake() { init_provider(); let client_alpn = vec![ALPN_PROTO_STR_H2.to_vec()]; let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let config = ServerTlsConfig::new(identity_provider); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); let result = creds.accept(stream, runtime).await; assert!( result.is_ok(), "Server handshake failed: {:?}", result.err() ); let mut stream = result.unwrap().endpoint; let mut buf = [0u8; 5]; stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"ping!"); stream.write_all(b"pong!").await.unwrap(); }); // Client setup let mut client_config = rustls::ClientConfig::builder() .with_root_certificates(create_root_store()) .with_no_client_auth(); client_config.alpn_protocols = client_alpn; let connector = TlsConnector::from(Arc::new(client_config)); let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("localhost").unwrap(); let result = connector.connect(domain, stream).await; assert!( result.is_ok(), "Client handshake failed: {:?}", result.err() ); let mut tls_stream = result.unwrap(); tls_stream.write_all(b"ping!").await.unwrap(); let mut buf = [0u8; 5]; tls_stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"pong!"); server_task.await.unwrap(); } #[tokio::test] async fn test_tls_server_handshake_no_alpn() { init_provider(); let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let config = ServerTlsConfig::new(identity_provider); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); let result = creds.accept(stream, runtime).await; assert!(result.is_err(), "Server handshake should have failed"); }); // Client setup let mut client_config = rustls::ClientConfig::builder() .with_root_certificates(create_root_store()) .with_no_client_auth(); client_config.alpn_protocols = vec![]; let connector = TlsConnector::from(Arc::new(client_config)); let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("localhost").unwrap(); let result = connector.connect(domain, stream).await; // Handshake will fail after the handshake is complete no ALPN is skipped. let mut tls_stream = result.unwrap(); let _ = tls_stream.write_all(b"ping!").await; let mut buf = [0u8; 5]; let res = tls_stream.read_exact(&mut buf).await; assert!(res.is_err()); server_task.await.unwrap(); } #[tokio::test] async fn test_tls_server_handshake_bad_alpn() { init_provider(); let client_alpn = vec![b"http/1.1".to_vec()]; let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let config = ServerTlsConfig::new(identity_provider); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); let runtime = rt::default_runtime(); let result = creds.accept(stream, runtime).await; assert!(result.is_err(), "Server handshake should have failed"); }); // Client setup let mut client_config = rustls::ClientConfig::builder() .with_root_certificates(create_root_store()) .with_no_client_auth(); client_config.alpn_protocols = client_alpn; let connector = TlsConnector::from(Arc::new(client_config)); let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("localhost").unwrap(); // Handshake should fail due to incompatible application protocols. let result = connector.connect(domain, stream).await; server_task.await.unwrap(); } #[tokio::test] async fn test_tls_handshake_alpn_h1_and_h2() { init_provider(); let client_alpn = vec![b"http/1.1".to_vec(), ALPN_PROTO_STR_H2.to_vec()]; let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let config = ServerTlsConfig::new(identity_provider); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); let runtime = rt::default_runtime(); let result = creds.accept(stream, runtime).await.unwrap(); }); // Client setup let mut client_config = rustls::ClientConfig::builder() .with_root_certificates(create_root_store()) .with_no_client_auth(); client_config.alpn_protocols = client_alpn; let connector = TlsConnector::from(Arc::new(client_config)); let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("localhost").unwrap(); // Handshake should succeed. let result = connector.connect(domain, stream).await.unwrap(); server_task.await.unwrap(); } #[tokio::test] async fn test_tls_server_mtls_require_fail() { init_provider(); let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let root_certs = load_root_certs("ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ServerTlsConfig::new(identity_provider).with_request_type( TlsClientCertificateRequestType::RequireAndVerify { roots_provider: root_provider, }, ); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); let result = creds.accept(stream, runtime).await; assert!(result.is_err(), "Handshake should fail without client cert"); }); // Client setup: No identity let mut client_config = rustls::ClientConfig::builder() .with_root_certificates(create_root_store()) .with_no_client_auth(); client_config.alpn_protocols = vec![b"h2".to_vec()]; let connector = TlsConnector::from(Arc::new(client_config)); let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("localhost").unwrap(); let result = connector.connect(domain, stream).await; // In TLS 1.3 client assumes the handshake succeeded but first read/write // fails. let mut tls_stream = result.unwrap(); let mut buf = [0u8; 1]; let res = tls_stream.read(&mut buf).await; assert!(res.is_err()); server_task.await.unwrap(); } #[tokio::test] async fn test_tls_server_mtls_success() { init_provider(); let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let root_certs = load_root_certs("client_ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ServerTlsConfig::new(identity_provider).with_request_type( TlsClientCertificateRequestType::RequireAndVerify { roots_provider: root_provider, }, ); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); let result = creds .accept(stream, runtime) .await .expect("Server handshake failed"); let mut stream = result.endpoint; let mut buf = [0u8; 5]; stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"ping!"); stream.write_all(b"pong!").await.unwrap(); }); // Client setup: With identity let client_identity_cert = load_certs("client1.pem"); let client_identity_key = load_private_key("client1.key"); let client_config = rustls::ClientConfig::builder() .with_root_certificates(create_root_store()) .with_client_auth_cert(client_identity_cert, client_identity_key) .unwrap(); let mut client_config = client_config; client_config.alpn_protocols = vec![b"h2".to_vec()]; let connector = TlsConnector::from(Arc::new(client_config)); let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("localhost").unwrap(); let mut tls_stream = connector.connect(domain, stream).await.unwrap(); tls_stream.write_all(b"ping!").await.unwrap(); let mut buf = [0u8; 5]; tls_stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"pong!"); server_task.await.unwrap(); } #[tokio::test] async fn test_tls_server_mtls_optional() { init_provider(); let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let root_certs = load_root_certs("client_ca.pem"); let root_provider = StaticProvider::new(root_certs); let config = ServerTlsConfig::new(identity_provider).with_request_type( TlsClientCertificateRequestType::RequestAndVerify { roots_provider: root_provider, }, ); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); let result = creds .accept(stream, runtime) .await .expect("Server handshake failed"); let mut stream = result.endpoint; let mut buf = [0u8; 5]; stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"ping!"); stream.write_all(b"pong!").await.unwrap(); }); // Client setup: Without identity let client_config = rustls::ClientConfig::builder() .with_root_certificates(create_root_store()) .with_no_client_auth(); let mut client_config = client_config; client_config.alpn_protocols = vec![b"h2".to_vec()]; let connector = TlsConnector::from(Arc::new(client_config)); let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("localhost").unwrap(); let mut tls_stream = connector.connect(domain, stream).await.unwrap(); tls_stream.write_all(b"ping!").await.unwrap(); let mut buf = [0u8; 5]; tls_stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"pong!"); server_task.await.unwrap(); } #[tokio::test] async fn test_tls_server_key_log() { init_provider(); let key_log_file = NamedTempFile::new().expect("failed to create a temporary file."); let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let config = ServerTlsConfig::new(identity_provider).insecure_with_key_log_path(key_log_file.path()); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { let (stream, _) = listener.accept().await.unwrap(); let result = creds .accept(stream, runtime) .await .expect("Server handshake failed"); let mut stream = result.endpoint; let mut buf = [0u8; 5]; stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"ping!"); stream.write_all(b"pong!").await.unwrap(); }); // Client setup let mut client_config = rustls::ClientConfig::builder() .with_root_certificates(create_root_store()) .with_no_client_auth(); client_config.alpn_protocols = vec![b"h2".to_vec()]; let connector = TlsConnector::from(Arc::new(client_config)); let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("localhost").unwrap(); let mut tls_stream = connector.connect(domain, stream).await.unwrap(); tls_stream.write_all(b"ping!").await.unwrap(); let mut buf = [0u8; 5]; tls_stream.read_exact(&mut buf).await.unwrap(); server_task.await.unwrap(); // Verify key log file exists and has content let content = std::fs::read_to_string(key_log_file.path()).unwrap(); assert!(!content.is_empty(), "Key log file is empty"); assert!( content.contains("SERVER_HANDSHAKE_TRAFFIC_SECRET"), "Key log missing expected content: {}", content ); } async fn check_resumption_disabled(versions: Vec<&'static rustls::SupportedProtocolVersion>) { init_provider(); let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let config = ServerTlsConfig::new(identity_provider); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { for _ in 0..2 { let (stream, _) = listener.accept().await.unwrap(); let runtime = rt::default_runtime(); let result = creds.accept(stream, runtime).await; assert!(result.is_ok()); let mut stream = result.unwrap().endpoint; stream.write_all(b"pong!").await.unwrap(); } }); // Client setup with session cache let provider = rustls::crypto::CryptoProvider::get_default() .cloned() .unwrap(); let mut client_config = rustls::ClientConfig::builder_with_provider(provider) .with_protocol_versions(&versions) .expect("invalid versions") .with_root_certificates(create_root_store()) .with_no_client_auth(); client_config.resumption = rustls::client::Resumption::in_memory_sessions(32); client_config.alpn_protocols = vec![ALPN_PROTO_STR_H2.to_vec()]; let connector = TlsConnector::from(Arc::new(client_config)); for i in 0..2 { let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("localhost").unwrap(); let mut tls_stream = connector.connect(domain, stream).await.unwrap(); let (_, conn) = tls_stream.get_ref(); assert_eq!( conn.handshake_kind(), Some(HandshakeKind::Full), "Expected full handshake on attempt {}", i ); let mut buf = [0u8; 5]; tls_stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"pong!"); } server_task.await.unwrap(); } #[tokio::test] async fn test_tls_server_resumption_disabled_tls13() { check_resumption_disabled(vec![&rustls::version::TLS13]).await; } #[tokio::test] async fn test_tls_server_resumption_disabled_tls12() { check_resumption_disabled(vec![&rustls::version::TLS12]).await; } #[tokio::test] async fn test_tls_server_sni() { init_provider(); let identity1 = load_identity("server.pem", "server.key"); let identity2 = load_identity("server2.pem", "server2.key"); // identity2 has *.test.com let identity_provider = StaticProvider::new(vec![identity1, identity2]); let config = ServerTlsConfig::new(identity_provider); let creds = RustlsServerTlsCredendials::new(config).unwrap(); let runtime = rt::default_runtime(); let mut listener = runtime .listen_tcp("127.0.0.1:0".parse().unwrap(), TcpOptions::default()) .await .unwrap(); let addr = *listener.local_addr(); let server_task = tokio::spawn(async move { for _ in 0..2 { let (stream, _) = listener.accept().await.unwrap(); let runtime = rt::default_runtime(); let result = creds.accept(stream, runtime).await; assert!( result.is_ok(), "Server handshake failed: {:?}", result.err() ); let mut stream = result.unwrap().endpoint; let mut buf = [0u8; 5]; stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"ping!"); stream.write_all(b"pong!").await.unwrap(); } }); // Client setup let mut client_config = rustls::ClientConfig::builder() .with_root_certificates(create_root_store()) .with_no_client_auth(); client_config.alpn_protocols = vec![ALPN_PROTO_STR_H2.to_vec()]; let connector = TlsConnector::from(Arc::new(client_config)); let test_com = ServerName::try_from("abc.test.com").unwrap(); // Request 1: abc.example.com { let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("abc.example.com").unwrap(); let mut tls_stream = connector.connect(domain, stream).await.unwrap(); let (_, conn) = tls_stream.get_ref(); let certs = conn.peer_certificates().unwrap(); let end_entity = webpki::EndEntityCert::try_from(&certs[0]).unwrap(); // verify it doesn't have a DNS name of *.test.com assert!( end_entity .verify_is_valid_for_subject_name(&test_com) .is_err() ); tls_stream.write_all(b"ping!").await.unwrap(); let mut buf = [0u8; 5]; tls_stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"pong!"); } // Request 2: abc.test.com { let stream = TcpStream::connect(addr).await.unwrap(); let domain = ServerName::try_from("abc.test.com").unwrap(); let mut tls_stream = connector.connect(domain, stream).await.unwrap(); let (_, conn) = tls_stream.get_ref(); let certs = conn.peer_certificates().unwrap(); let end_entity = webpki::EndEntityCert::try_from(&certs[0]).unwrap(); // verify that the peer has a certificate with DNS name of *.test.com assert!( end_entity .verify_is_valid_for_subject_name(&test_com) .is_ok() ); tls_stream.write_all(b"ping!").await.unwrap(); let mut buf = [0u8; 5]; tls_stream.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b"pong!"); } server_task.await.unwrap(); } #[tokio::test] async fn test_tls_server_cipher_suites_insecure() { init_provider(); let identity = load_identity("server.pem", "server.key"); let identity_provider = StaticProvider::new(vec![identity]); let config = ServerTlsConfig::new(identity_provider); let mut provider = rustls::crypto::CryptoProvider::get_default() .expect("No default crypto provider found") .as_ref() .clone(); fn is_secure(suported_suite: &rustls::SupportedCipherSuite) -> bool { match suported_suite { rustls::SupportedCipherSuite::Tls13(_) => true, rustls::SupportedCipherSuite::Tls12(suite) => matches!( suite.common.suite, rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 | rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 | rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 | rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 | rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ), } } // Remove all cipher suites that are considered secure by gRPC. provider.cipher_suites.retain(|suite| !is_secure(suite)); let creds = RustlsServerTlsCredendials::new_impl(config, provider); assert!(creds.err().unwrap().contains("no cipher suites matching")); } fn create_root_store() -> rustls::RootCertStore { let root_certs = load_certs("ca.pem"); let mut root_store = rustls::RootCertStore::empty(); for cert in root_certs { root_store.add(cert).unwrap(); } root_store } fn test_certs_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("examples/data/tls") } fn load_certs(filename: &str) -> Vec> { let path = test_certs_path().join(filename); let file = std::fs::File::open(path).expect("cannot open certificate file"); let mut reader = std::io::BufReader::new(file); rustls_pemfile::certs(&mut reader) .map(|result| result.unwrap()) .collect() } fn load_private_key(filename: &str) -> rustls_pki_types::PrivateKeyDer<'static> { let path = test_certs_path().join(filename); let file = std::fs::File::open(path).expect("cannot open private key file"); let mut reader = std::io::BufReader::new(file); loop { match rustls_pemfile::read_one(&mut reader).expect("cannot read private key") { Some(rustls_pemfile::Item::Pkcs1Key(key)) => return key.into(), Some(rustls_pemfile::Item::Pkcs8Key(key)) => return key.into(), Some(rustls_pemfile::Item::Sec1Key(key)) => return key.into(), None => panic!("no keys found"), _ => {} } } } fn load_root_certs(filename: &str) -> RootCertificates { let path = test_certs_path().join(filename); let ca_pem = std::fs::read(path).unwrap(); RootCertificates::from_pem(ca_pem) } fn load_identity(cert_file: &str, key_file: &str) -> Identity { let cert = std::fs::read(test_certs_path().join(cert_file)).expect("cannot read cert file"); let key = std::fs::read(test_certs_path().join(key_file)).expect("cannot read key file"); Identity::from_pem(cert, key) } ================================================ FILE: grpc/src/credentials/rustls/tls_stream.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::io::IoSlice; use std::pin::Pin; use std::task::Context; use std::task::Poll; use tokio::io::AsyncRead; use tokio::io::AsyncWrite; use tokio::io::ReadBuf; use tokio_rustls::TlsStream as RustlsStream; use crate::rt::GrpcEndpoint; use crate::rt::endpoint; pub struct TlsStream { inner: RustlsStream, } impl AsyncRead for TlsStream where T: GrpcEndpoint, { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { let pinned = Pin::new(&mut self.get_mut().inner); AsyncRead::poll_read(pinned, cx, buf) } } impl AsyncWrite for TlsStream where T: GrpcEndpoint, { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { let pinned = Pin::new(&mut self.get_mut().inner); AsyncWrite::poll_write(pinned, cx, buf) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let pinned = Pin::new(&mut self.get_mut().inner); AsyncWrite::poll_flush(pinned, cx) } fn poll_shutdown( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { let pinned = Pin::new(&mut self.get_mut().inner); AsyncWrite::poll_shutdown(pinned, cx) } fn poll_write_vectored( self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[IoSlice<'_>], ) -> Poll> { let pinned = Pin::new(&mut self.get_mut().inner); AsyncWrite::poll_write_vectored(pinned, cx, bufs) } fn is_write_vectored(&self) -> bool { self.inner.is_write_vectored() } } impl endpoint::Sealed for TlsStream where T: GrpcEndpoint {} impl GrpcEndpoint for TlsStream where T: GrpcEndpoint, { fn get_local_address(&self) -> &str { match &self.inner { RustlsStream::Client(s) => s.get_ref().0.get_local_address(), RustlsStream::Server(s) => s.get_ref().0.get_local_address(), } } fn get_peer_address(&self) -> &str { match &self.inner { RustlsStream::Client(s) => s.get_ref().0.get_peer_address(), RustlsStream::Server(s) => s.get_ref().0.get_peer_address(), } } fn get_network_type(&self) -> &'static str { match &self.inner { RustlsStream::Client(s) => s.get_ref().0.get_network_type(), RustlsStream::Server(s) => s.get_ref().0.get_network_type(), } } } impl TlsStream { pub fn new(inner: RustlsStream) -> Self { Self { inner } } pub fn inner(&self) -> &RustlsStream { &self.inner } } ================================================ FILE: grpc/src/credentials/server.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use crate::attributes::Attributes; use crate::credentials::SecurityLevel; use crate::rt::GrpcEndpoint; use crate::rt::GrpcRuntime; #[trait_variant::make(Send)] pub trait ServerCredsInternal { type Output; /// Performs the server-side authentication handshake. /// /// This method wraps the incoming raw `source` connection with the configured /// security protocol (e.g., TLS). /// /// # Returns /// /// A tuple containing: /// 1. The authenticated endpoint (ready for reading/writing frames). async fn accept( &self, source: Input, runtime: GrpcRuntime, ) -> Result>, String>; } pub struct HandshakeOutput { pub endpoint: T, pub security: ServerConnectionSecurityInfo, } /// Represents the security state of an established server-side connection. pub struct ServerConnectionSecurityInfo { security_protocol: &'static str, security_level: SecurityLevel, /// Stores extra data derived from the underlying protocol. attributes: Attributes, } impl ServerConnectionSecurityInfo { /// Creates a new instance of `ServerConnectionSecurityInfo`. pub fn new( security_protocol: &'static str, security_level: SecurityLevel, attributes: Attributes, ) -> Self { Self { security_protocol, security_level, attributes, } } /// Returns the security protocol used. pub fn security_protocol(&self) -> &'static str { self.security_protocol } /// Returns the security level of the connection. pub fn security_level(&self) -> SecurityLevel { self.security_level } /// Returns the attributes associated with the connection. pub fn attributes(&self) -> &Attributes { &self.attributes } } ================================================ FILE: grpc/src/generated/echo_fds.rs ================================================ // This file is @generated by codegen. // // // Copyright 2018 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // /// Byte encoded FILE_DESCRIPTOR_SET. pub const FILE_DESCRIPTOR_SET: &[u8] = &[ 10u8, 246u8, 3u8, 10u8, 15u8, 101u8, 99u8, 104u8, 111u8, 47u8, 101u8, 99u8, 104u8, 111u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 18u8, 18u8, 103u8, 114u8, 112u8, 99u8, 46u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 115u8, 46u8, 101u8, 99u8, 104u8, 111u8, 34u8, 39u8, 10u8, 11u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 24u8, 10u8, 7u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 34u8, 40u8, 10u8, 12u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 24u8, 10u8, 7u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 50u8, 243u8, 2u8, 10u8, 4u8, 69u8, 99u8, 104u8, 111u8, 18u8, 78u8, 10u8, 9u8, 85u8, 110u8, 97u8, 114u8, 121u8, 69u8, 99u8, 104u8, 111u8, 18u8, 31u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 115u8, 46u8, 101u8, 99u8, 104u8, 111u8, 46u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 32u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 115u8, 46u8, 101u8, 99u8, 104u8, 111u8, 46u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 90u8, 10u8, 19u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 83u8, 116u8, 114u8, 101u8, 97u8, 109u8, 105u8, 110u8, 103u8, 69u8, 99u8, 104u8, 111u8, 18u8, 31u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 115u8, 46u8, 101u8, 99u8, 104u8, 111u8, 46u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 32u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 115u8, 46u8, 101u8, 99u8, 104u8, 111u8, 46u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 48u8, 1u8, 18u8, 90u8, 10u8, 19u8, 67u8, 108u8, 105u8, 101u8, 110u8, 116u8, 83u8, 116u8, 114u8, 101u8, 97u8, 109u8, 105u8, 110u8, 103u8, 69u8, 99u8, 104u8, 111u8, 18u8, 31u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 115u8, 46u8, 101u8, 99u8, 104u8, 111u8, 46u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 32u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 115u8, 46u8, 101u8, 99u8, 104u8, 111u8, 46u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 40u8, 1u8, 18u8, 99u8, 10u8, 26u8, 66u8, 105u8, 100u8, 105u8, 114u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 97u8, 108u8, 83u8, 116u8, 114u8, 101u8, 97u8, 109u8, 105u8, 110u8, 103u8, 69u8, 99u8, 104u8, 111u8, 18u8, 31u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 115u8, 46u8, 101u8, 99u8, 104u8, 111u8, 46u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 32u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 101u8, 120u8, 97u8, 109u8, 112u8, 108u8, 101u8, 115u8, 46u8, 101u8, 99u8, 104u8, 111u8, 46u8, 69u8, 99u8, 104u8, 111u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 40u8, 1u8, 48u8, 1u8, 98u8, 6u8, 112u8, 114u8, 111u8, 116u8, 111u8, 51u8, ]; ================================================ FILE: grpc/src/generated/grpc_examples_echo.rs ================================================ // This file is @generated by prost-build. /// EchoRequest is the request for echo. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct EchoRequest { #[prost(string, tag = "1")] pub message: ::prost::alloc::string::String, } /// EchoResponse is the response for echo. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct EchoResponse { #[prost(string, tag = "1")] pub message: ::prost::alloc::string::String, } /// Generated client implementations. pub mod echo_client { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; use tonic::codegen::http::Uri; /// Echo is the echo service. #[derive(Debug, Clone)] pub struct EchoClient { inner: tonic::client::Grpc, } impl EchoClient where T: tonic::client::GrpcService, T::Error: Into, T::ResponseBody: Body + std::marker::Send + 'static, ::Error: Into + std::marker::Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } pub fn with_origin(inner: T, origin: Uri) -> Self { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } pub fn with_interceptor( inner: T, interceptor: F, ) -> EchoClient> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response< >::ResponseBody, >, >, , >>::Error: Into + std::marker::Send + std::marker::Sync, { EchoClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); self } /// Enable decompressing responses. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.accept_compressed(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_decoding_message_size(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_encoding_message_size(limit); self } /// UnaryEcho is unary echo. pub async fn unary_echo( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.examples.echo.Echo/UnaryEcho", ); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("grpc.examples.echo.Echo", "UnaryEcho")); self.inner.unary(req, path, codec).await } /// ServerStreamingEcho is server side streaming. pub async fn server_streaming_echo( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result< tonic::Response>, tonic::Status, > { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.examples.echo.Echo/ServerStreamingEcho", ); let mut req = request.into_request(); req.extensions_mut() .insert( GrpcMethod::new("grpc.examples.echo.Echo", "ServerStreamingEcho"), ); self.inner.server_streaming(req, path, codec).await } /// ClientStreamingEcho is client side streaming. pub async fn client_streaming_echo( &mut self, request: impl tonic::IntoStreamingRequest, ) -> std::result::Result, tonic::Status> { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.examples.echo.Echo/ClientStreamingEcho", ); let mut req = request.into_streaming_request(); req.extensions_mut() .insert( GrpcMethod::new("grpc.examples.echo.Echo", "ClientStreamingEcho"), ); self.inner.client_streaming(req, path, codec).await } /// BidirectionalStreamingEcho is bidi streaming. pub async fn bidirectional_streaming_echo( &mut self, request: impl tonic::IntoStreamingRequest, ) -> std::result::Result< tonic::Response>, tonic::Status, > { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.examples.echo.Echo/BidirectionalStreamingEcho", ); let mut req = request.into_streaming_request(); req.extensions_mut() .insert( GrpcMethod::new( "grpc.examples.echo.Echo", "BidirectionalStreamingEcho", ), ); self.inner.streaming(req, path, codec).await } } } /// Generated server implementations. pub mod echo_server { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with EchoServer. #[async_trait] pub trait Echo: std::marker::Send + std::marker::Sync + 'static { /// UnaryEcho is unary echo. async fn unary_echo( &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; /// Server streaming response type for the ServerStreamingEcho method. type ServerStreamingEchoStream: tonic::codegen::tokio_stream::Stream< Item = std::result::Result, > + std::marker::Send + 'static; /// ServerStreamingEcho is server side streaming. async fn server_streaming_echo( &self, request: tonic::Request, ) -> std::result::Result< tonic::Response, tonic::Status, >; /// ClientStreamingEcho is client side streaming. async fn client_streaming_echo( &self, request: tonic::Request>, ) -> std::result::Result, tonic::Status>; /// Server streaming response type for the BidirectionalStreamingEcho method. type BidirectionalStreamingEchoStream: tonic::codegen::tokio_stream::Stream< Item = std::result::Result, > + std::marker::Send + 'static; /// BidirectionalStreamingEcho is bidi streaming. async fn bidirectional_streaming_echo( &self, request: tonic::Request>, ) -> std::result::Result< tonic::Response, tonic::Status, >; } /// Echo is the echo service. #[derive(Debug)] pub struct EchoServer { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } impl EchoServer { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } pub fn from_arc(inner: Arc) -> Self { Self { inner, accept_compression_encodings: Default::default(), send_compression_encodings: Default::default(), max_decoding_message_size: None, max_encoding_message_size: None, } } pub fn with_interceptor( inner: T, interceptor: F, ) -> InterceptedService where F: tonic::service::Interceptor, { InterceptedService::new(Self::new(inner), interceptor) } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Compress responses with the given encoding, if the client supports it. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.max_encoding_message_size = Some(limit); self } } impl tonic::codegen::Service> for EchoServer where T: Echo, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { type Response = http::Response; type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready( &mut self, _cx: &mut Context<'_>, ) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { "/grpc.examples.echo.Echo/UnaryEcho" => { #[allow(non_camel_case_types)] struct UnaryEchoSvc(pub Arc); impl tonic::server::UnaryService for UnaryEchoSvc { type Response = super::EchoResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::unary_echo(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = UnaryEchoSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) } "/grpc.examples.echo.Echo/ServerStreamingEcho" => { #[allow(non_camel_case_types)] struct ServerStreamingEchoSvc(pub Arc); impl< T: Echo, > tonic::server::ServerStreamingService for ServerStreamingEchoSvc { type Response = super::EchoResponse; type ResponseStream = T::ServerStreamingEchoStream; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::server_streaming_echo(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = ServerStreamingEchoSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.server_streaming(method, req).await; Ok(res) }; Box::pin(fut) } "/grpc.examples.echo.Echo/ClientStreamingEcho" => { #[allow(non_camel_case_types)] struct ClientStreamingEchoSvc(pub Arc); impl< T: Echo, > tonic::server::ClientStreamingService for ClientStreamingEchoSvc { type Response = super::EchoResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request>, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::client_streaming_echo(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = ClientStreamingEchoSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.client_streaming(method, req).await; Ok(res) }; Box::pin(fut) } "/grpc.examples.echo.Echo/BidirectionalStreamingEcho" => { #[allow(non_camel_case_types)] struct BidirectionalStreamingEchoSvc(pub Arc); impl tonic::server::StreamingService for BidirectionalStreamingEchoSvc { type Response = super::EchoResponse; type ResponseStream = T::BidirectionalStreamingEchoStream; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request>, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::bidirectional_streaming_echo(&inner, request) .await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = BidirectionalStreamingEchoSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.streaming(method, req).await; Ok(res) }; Box::pin(fut) } _ => { Box::pin(async move { let mut response = http::Response::new( tonic::body::Body::default(), ); let headers = response.headers_mut(); headers .insert( tonic::Status::GRPC_STATUS, (tonic::Code::Unimplemented as i32).into(), ); headers .insert( http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE, ); Ok(response) }) } } } } impl Clone for EchoServer { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { inner, accept_compression_encodings: self.accept_compression_encodings, send_compression_encodings: self.send_compression_encodings, max_decoding_message_size: self.max_decoding_message_size, max_encoding_message_size: self.max_encoding_message_size, } } } /// Generated gRPC service name pub const SERVICE_NAME: &str = "grpc.examples.echo.Echo"; impl tonic::server::NamedService for EchoServer { const NAME: &'static str = SERVICE_NAME; } } ================================================ FILE: grpc/src/inmemory/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::collections::HashMap; use std::sync::Arc; use std::sync::LazyLock; use std::sync::Mutex; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use bytes::Buf; use tokio::sync::Mutex as TokioMutex; use tokio::sync::Notify; use tokio::sync::mpsc; use tokio::sync::oneshot; use crate::client::CallOptions; use crate::client::DynRecvStream as ClientDynRecvStream; use crate::client::DynSendStream as ClientDynSendStream; use crate::client::Invoke; use crate::client::RecvStream as ClientRecvStream; use crate::client::SendOptions as ClientSendOptions; use crate::client::SendStream as ClientSendStream; use crate::client::name_resolution::Address; use crate::client::name_resolution::ChannelController as ResolverChannelController; use crate::client::name_resolution::Endpoint; use crate::client::name_resolution::Resolver; use crate::client::name_resolution::ResolverBuilder; use crate::client::name_resolution::ResolverOptions; use crate::client::name_resolution::ResolverUpdate; use crate::client::name_resolution::Target; use crate::client::name_resolution::global_registry as global_resolver_registry; use crate::client::service_config::ServiceConfig; use crate::client::transport::GLOBAL_TRANSPORT_REGISTRY; use crate::client::transport::Transport; use crate::client::transport::TransportOptions; use crate::core::ClientResponseStreamItem; use crate::core::RecvMessage; use crate::core::RequestHeaders; use crate::core::ResponseHeaders; use crate::core::ResponseStreamItem; use crate::core::SendMessage; use crate::core::Trailers; use crate::rt::GrpcRuntime; use crate::server::Call as ServerCall; use crate::server::Listener as ServerListener; use crate::server::RecvStream as ServerRecvStream; use crate::server::SendOptions as ServerSendOptions; use crate::server::SendStream as ServerSendStream; static LISTENERS: LazyLock>>> = LazyLock::new(|| Mutex::new(HashMap::new())); static NEXT_ID: AtomicU64 = AtomicU64::new(0); struct InMemoryServerCall { headers: RequestHeaders, req_rx: mpsc::UnboundedReceiver, resp_tx: mpsc::UnboundedSender, } enum InMemoryRequestStreamItem { Message(Box), StreamClosed, } enum InMemoryResponseStreamItem { Headers(ResponseHeaders), Message(Box), Trailers(Trailers), StreamClosed, } #[derive(Clone)] pub struct InMemoryListener { inner: Arc, } struct InMemoryListenerInner { id: String, r: TokioMutex>, close_notify: Arc, drop_notify: Arc, } impl Drop for InMemoryListenerInner { fn drop(&mut self) { self.drop_notify.notify_waiters(); } } impl Default for InMemoryListener { fn default() -> Self { Self::new() } } impl InMemoryListener { pub fn new() -> Self { let id = NEXT_ID.fetch_add(1, Ordering::Relaxed).to_string(); let (s, r) = mpsc::channel(1); let mut listeners = LISTENERS.lock().unwrap(); listeners.insert(id.clone(), s); Self { inner: Arc::new(InMemoryListenerInner { id, r: TokioMutex::new(r), close_notify: Arc::new(Notify::new()), drop_notify: Arc::new(Notify::new()), }), } } pub fn id(&self) -> String { self.inner.id.clone() } pub async fn close(self) { let id = self.inner.id.clone(); let drop_notify = self.inner.drop_notify.clone(); let weak = Arc::downgrade(&self.inner); LISTENERS.lock().unwrap().remove(&id); self.inner.close_notify.notify_waiters(); drop(self); loop { let notified = drop_notify.notified(); if weak.upgrade().is_none() { return; } notified.await; } } pub async fn await_connection(&self) {} } impl ServerListener for InMemoryListener { type SendStream = InMemoryServerSendStream; type RecvStream = InMemoryServerRecvStream; async fn accept(&self) -> Option> { let mut r = self.inner.r.lock().await; tokio::select! { call = r.recv() => { let call = call?; Some(ServerCall { headers: call.headers, send: InMemoryServerSendStream { tx: call.resp_tx }, recv: InMemoryServerRecvStream { rx: call.req_rx }, }) } _ = self.inner.close_notify.notified() => { None } } } } pub struct InMemoryServerSendStream { tx: mpsc::UnboundedSender, } impl ServerSendStream for InMemoryServerSendStream { async fn send<'a>( &mut self, item: crate::core::ServerResponseStreamItem<'a>, _options: ServerSendOptions, ) -> Result<(), ()> { let inmemory_item = match item { ResponseStreamItem::Headers(h) => InMemoryResponseStreamItem::Headers(h), ResponseStreamItem::Message(m) => { let buf = m.encode().map_err(|_| ())?; InMemoryResponseStreamItem::Message(buf) } ResponseStreamItem::Trailers(t) => InMemoryResponseStreamItem::Trailers(t), ResponseStreamItem::StreamClosed => InMemoryResponseStreamItem::StreamClosed, }; self.tx.send(inmemory_item).map_err(|_| ()) } } pub struct InMemoryServerRecvStream { rx: mpsc::UnboundedReceiver, } impl ServerRecvStream for InMemoryServerRecvStream { async fn next(&mut self, msg: &mut dyn RecvMessage) -> Result<(), ()> { match self.rx.recv().await { Some(InMemoryRequestStreamItem::Message(mut buf)) => { msg.decode(&mut buf).map_err(|_| ()) } _ => Err(()), } } } pub struct InMemoryConnection { s: mpsc::Sender, closed_tx: Option>>, } impl Invoke for InMemoryConnection { type SendStream = Box; type RecvStream = Box; async fn invoke( &self, headers: RequestHeaders, _options: CallOptions, ) -> (Self::SendStream, Self::RecvStream) { let (req_tx, req_rx) = mpsc::unbounded_channel::(); let (resp_tx, resp_rx) = mpsc::unbounded_channel::(); let call = InMemoryServerCall { headers, req_rx, resp_tx, }; let _ = self.s.try_send(call); ( Box::new(InMemoryClientSendStream { tx: Some(req_tx) }), Box::new(InMemoryClientRecvStream { rx: resp_rx }), ) } } impl Drop for InMemoryConnection { fn drop(&mut self) { let _ = self.closed_tx.take().unwrap().send(Err("".into())); } } pub struct InMemoryClientSendStream { tx: Option>, } impl ClientSendStream for InMemoryClientSendStream { async fn send(&mut self, msg: &dyn SendMessage, _options: ClientSendOptions) -> Result<(), ()> { let buf = msg.encode().unwrap(); if self .tx .as_mut() .unwrap() .send(InMemoryRequestStreamItem::Message(buf)) .is_err() { self.tx = None; return Err(()); } Ok(()) } } impl Drop for InMemoryClientSendStream { fn drop(&mut self) { if let Some(tx) = self.tx.take() { let _ = tx.send(InMemoryRequestStreamItem::StreamClosed); } } } pub struct InMemoryClientRecvStream { rx: mpsc::UnboundedReceiver, } impl ClientRecvStream for InMemoryClientRecvStream { async fn next(&mut self, msg: &mut dyn RecvMessage) -> ClientResponseStreamItem { match self.rx.recv().await { Some(InMemoryResponseStreamItem::Headers(h)) => ClientResponseStreamItem::Headers(h), Some(InMemoryResponseStreamItem::Message(mut buf)) => { msg.decode(&mut buf).unwrap(); ClientResponseStreamItem::Message(()) } Some(InMemoryResponseStreamItem::Trailers(t)) => ClientResponseStreamItem::Trailers(t), _ => ClientResponseStreamItem::StreamClosed, } } } pub struct InMemoryTransport {} impl Transport for InMemoryTransport { type Service = InMemoryConnection; async fn connect( &self, target: String, _runtime: GrpcRuntime, _options: &TransportOptions, ) -> Result<(Self::Service, oneshot::Receiver>), String> { let listeners = LISTENERS.lock().unwrap(); let s = listeners .get(&target) .ok_or_else(|| format!("no listener for target: {}", target))?; let (closed_tx, closed_rx) = oneshot::channel(); let conn = InMemoryConnection { s: s.clone(), closed_tx: Some(closed_tx), }; Ok((conn, closed_rx)) } } pub struct InMemoryResolverBuilder {} impl ResolverBuilder for InMemoryResolverBuilder { fn build(&self, target: &Target, options: ResolverOptions) -> Box { let path = target.path().strip_prefix('/').unwrap_or(target.path()); let ids: Vec = path.split(',').map(|s| s.to_string()).collect(); options.work_scheduler.schedule_work(); Box::new(InMemoryResolver { ids }) } fn scheme(&self) -> &str { "inmemory" } fn is_valid_uri(&self, _uri: &Target) -> bool { true } } struct InMemoryResolver { ids: Vec, } impl Resolver for InMemoryResolver { fn resolve_now(&mut self) {} fn work(&mut self, channel_controller: &mut dyn ResolverChannelController) { let endpoints = self .ids .iter() .map(|id| Endpoint { addresses: vec![Address { network_type: "inmemory", address: crate::byte_str::ByteStr::from(id.clone()), ..Default::default() }], ..Default::default() }) .collect(); let _ = channel_controller.update(ResolverUpdate { endpoints: Ok(endpoints), service_config: Ok(Some(ServiceConfig { load_balancing_policy: Some( crate::client::service_config::LbPolicyType::RoundRobin, ), })), ..Default::default() }); } } pub fn reg() { GLOBAL_TRANSPORT_REGISTRY.add_transport("inmemory", InMemoryTransport {}); global_resolver_registry().add_builder(Box::new(InMemoryResolverBuilder {})); } ================================================ FILE: grpc/src/lib.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ //! The official Rust implementation of [gRPC], a high performance, open source, //! universal RPC framework //! //! This version is in progress and not recommended for any production use. All //! APIs are unstable. Proceed at your own risk. //! //! [gRPC]: https://grpc.io #![allow(dead_code, unused_variables)] pub mod client; pub mod core; pub mod credentials; pub mod inmemory; pub mod server; mod macros; mod status; pub use status::ServerStatus; pub use status::Status; pub use status::StatusCode; mod attributes; mod byte_str; mod rt; mod send_future; #[cfg(test)] mod echo_pb { include!(concat!( env!("CARGO_MANIFEST_DIR"), "/src/generated/grpc_examples_echo.rs" )); } ================================================ FILE: grpc/src/macros.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ /// Includes generated proto message, client, and server code. /// /// You must specify the path to the `.proto` file **relative to the proto root /// directory**, without the `.proto` extension. /// /// For example, if your proto directory is `path/to/protos` and it contains the /// file `helloworld.proto`, you would write: /// /// ```rust,ignore /// mod pb { /// grpc::include_proto!("path/to/protos", "helloworld"); /// } /// ``` /// /// # Note /// **This macro only works if the gRPC build output directory and message path /// are unmodified.** By default: /// - The output directory is set to the [`OUT_DIR`] environment variable. /// - The message path is set to `self`. /// /// If your `.proto` files are not in a subdirectory, you can omit the first /// parameter. /// /// ```rust,ignore /// mod pb { /// grpc::include_proto!("helloworld"); /// } /// ``` /// /// If you have modified the output directory or message path, you should use /// the include_generated_code macro below instead of using this macro or /// manually include it yourself. /// /// ```rust,ignore /// mod grpc { /// grpc::include_generated_proto!("path/to/protos", "helloworld"); /// } /// ``` /// /// The following example assumes the message code is imported using `self`: /// /// ```rust,ignore /// mod protos { /// // Include message code. /// include!("relative/protobuf/directory/generated.rs"); /// /// // Include service code. /// include!("relative/protobuf/directory/helloworld_grpc.pb.rs"); /// } /// ``` /// /// If the message code and service code are in different modules, and the /// message path specified during code generation is `super::protos`, use: /// /// ```rust,ignore /// mod protos { /// // Include message code. /// include!("relative/protobuf/directory/generated.rs"); /// } /// /// mod grpc { /// // Include service code. /// include!("relative/protobuf/directory/helloworld_grpc.pb.rs"); /// } /// ``` /// /// [`OUT_DIR`]: /// https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts #[macro_export] macro_rules! include_proto { // Assume the generated output dir is OUT_DIR. ($proto_file:literal) => { $crate::include_proto!("", $proto_file); }; ($parent_dir:literal, $proto_file:literal) => { include!(concat!(env!("OUT_DIR"), "/", $parent_dir, "/generated.rs")); include!(concat!( env!("OUT_DIR"), "/", $parent_dir, "/", $proto_file, "_grpc.pb.rs" )); }; } /// Includes generated proto message, client, and server code. This macro is for /// if you manually set output_dir instead of using the default OUT_DIR. /// /// You must specify the path to the `.proto` file **relative to the proto root /// directory**, without the `.proto` extension. /// /// For example, if your proto directory is `path/to/protos` and it contains the /// file `helloworld.proto`, you would write: /// /// ```rust,ignore /// mod pb { /// grpc::include_generated_proto!("path/to/protos", "helloworld"); /// } /// ``` /// /// If your `.proto` files are not in a subdirectory, you can omit the first /// parameter. /// /// ```rust,ignore /// mod pb { /// grpc::include_generated_proto!("helloworld"); /// } /// ``` /// /// [`CARGO_MANIFEST_DIR`]: /// https://doc.rust-lang.org/cargo/reference/environment-variables.html #[macro_export] macro_rules! include_generated_proto { ($proto_file:literal) => { $crate::include_generated_proto!("", $proto_file); }; ($parent_dir:literal, $proto_file:literal) => { include!(concat!( env!("CARGO_MANIFEST_DIR"), "/", $parent_dir, "/generated.rs" )); include!(concat!( env!("CARGO_MANIFEST_DIR"), "/", $parent_dir, "/", $proto_file, "_grpc.pb.rs" )); }; } ================================================ FILE: grpc/src/rt/hyper_wrapper.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::future::Future; use std::io; use std::pin::Pin; use std::task::Context; use std::task::Poll; use std::time::Instant; use hyper::rt::Executor; use hyper::rt::Timer; use pin_project_lite::pin_project; use tokio::io::AsyncRead; use tokio::io::AsyncWrite; use tokio::io::ReadBuf; use crate::rt::GrpcEndpoint; use crate::rt::GrpcRuntime; /// Adapts a runtime to a hyper compatible executor. #[derive(Clone)] pub(crate) struct HyperCompatExec { pub(crate) inner: GrpcRuntime, } impl Executor for HyperCompatExec where F: Future + Send + 'static, F::Output: Send + 'static, { fn execute(&self, fut: F) { self.inner.spawn(Box::pin(async { let _ = fut.await; })); } } struct HyperCompatSleep { inner: Pin>, } impl Future for HyperCompatSleep { type Output = (); fn poll( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { self.inner.as_mut().poll(cx) } } impl hyper::rt::Sleep for HyperCompatSleep {} /// Adapts a runtime to a hyper compatible timer. pub(crate) struct HyperCompatTimer { pub(crate) inner: GrpcRuntime, } impl Timer for HyperCompatTimer { fn sleep(&self, duration: std::time::Duration) -> Pin> { let sleep = self.inner.sleep(duration); Box::pin(HyperCompatSleep { inner: sleep }) } fn sleep_until(&self, deadline: Instant) -> Pin> { let now = Instant::now(); let duration = deadline.saturating_duration_since(now); self.sleep(duration) } } // The following adapters are copied from hyper: // https://github.com/hyperium/hyper/blob/v1.6.0/benches/support/tokiort.rs pin_project! { /// A wrapper to make any `GrpcEndpoint` compatible with Hyper. It implements /// Tokio's async IO traits. pub(crate) struct HyperStream { #[pin] inner: Box, } } impl HyperStream { /// Creates a new `HyperStream` from a type implementing `TcpStream`. pub fn new(stream: Box) -> Self { Self { inner: stream } } } impl AsyncRead for HyperStream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { // Delegate the poll_read call to the inner stream. Pin::new(&mut self.inner).poll_read(cx, buf) } } impl AsyncWrite for HyperStream { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { Pin::new(&mut self.inner).poll_write(cx, buf) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.inner).poll_flush(cx) } fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.inner).poll_shutdown(cx) } } impl hyper::rt::Read for HyperStream { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, mut buf: hyper::rt::ReadBufCursor<'_>, ) -> Poll> { let n = unsafe { let mut tbuf = tokio::io::ReadBuf::uninit(buf.as_mut()); match tokio::io::AsyncRead::poll_read(self.project().inner, cx, &mut tbuf) { Poll::Ready(Ok(())) => tbuf.filled().len(), other => return other, } }; unsafe { buf.advance(n); } Poll::Ready(Ok(())) } } impl hyper::rt::Write for HyperStream { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { AsyncWrite::poll_write(self.project().inner, cx, buf) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { AsyncWrite::poll_flush(self.project().inner, cx) } fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { AsyncWrite::poll_shutdown(self.project().inner, cx) } fn is_write_vectored(&self) -> bool { AsyncWrite::is_write_vectored(&self.inner) } fn poll_write_vectored( self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[io::IoSlice<'_>], ) -> Poll> { AsyncWrite::poll_write_vectored(self.project().inner, cx, bufs) } } ================================================ FILE: grpc/src/rt/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::fmt::Debug; use std::future::Future; use std::net::SocketAddr; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; pub(crate) mod hyper_wrapper; #[cfg(feature = "_runtime-tokio")] pub(crate) mod tokio; pub type BoxFuture = Pin + Send>>; pub type BoxedTaskHandle = Box; pub type BoxEndpoint = Box; pub type ScopedBoxFuture<'a, T> = Pin + Send + 'a>>; /// An abstraction over an asynchronous runtime. /// /// The `Runtime` trait defines the core functionality required for /// executing asynchronous tasks, creating DNS resolvers, and performing /// time-based operations such as sleeping. It provides a uniform interface /// that can be implemented for various async runtimes, enabling pluggable /// and testable infrastructure. pub trait Runtime: Send + Sync + Debug { /// Spawns the given asynchronous task to run in the background. fn spawn(&self, task: Pin + Send + 'static>>) -> BoxedTaskHandle; /// Creates and returns an instance of a DNSResolver, optionally /// configured by the ResolverOptions struct. This method may return an /// error if it fails to create the DNSResolver. fn get_dns_resolver(&self, opts: ResolverOptions) -> Result, String>; /// Returns a future that completes after the specified duration. fn sleep(&self, duration: std::time::Duration) -> Pin>; /// Establishes a TCP connection to the given `target` address with the /// specified `opts`. fn tcp_stream( &self, target: SocketAddr, opts: TcpOptions, ) -> BoxFuture, String>>; /// Create a new listener for the given address. fn listen_tcp( &self, addr: SocketAddr, opts: TcpOptions, ) -> BoxFuture, String>>; } /// A future that resolves after a specified duration. pub trait Sleep: Send + Sync + Future {} pub trait TaskHandle: Send + Sync { /// Abort the associated task. fn abort(&self); } /// A trait for asynchronous DNS resolution. #[tonic::async_trait] pub trait DnsResolver: Send + Sync { /// Resolve an address async fn lookup_host_name(&self, name: &str) -> Result, String>; /// Perform a TXT record lookup. If a txt record contains multiple strings, /// they are concatenated. async fn lookup_txt(&self, name: &str) -> Result, String>; } #[derive(Default)] pub struct ResolverOptions { /// The address of the DNS server in "IP:port" format. If None, the /// system's default DNS server will be used. pub(super) server_addr: Option, } #[derive(Default)] pub struct TcpOptions { pub(crate) enable_nodelay: bool, pub(crate) keepalive: Option, } pub(crate) mod endpoint { /// This trait is sealed since we may need to change the read and write /// methods to align closely with the gRPC C++ implementations. For example, /// the read method may be responsible for allocating the buffer and /// returning it to enable in-place decryption. Since the libraries used /// for http2 and channel credentials use AsyncRead, designing such an API /// today would require adapters which would incur an extra copy, affecting /// performance. pub trait Sealed: tokio::io::AsyncRead + tokio::io::AsyncWrite {} } /// GrpcEndpoint is a generic stream-oriented network connection. pub trait GrpcEndpoint: endpoint::Sealed + Send + Unpin + 'static { /// Returns the local address that this stream is bound to. fn get_local_address(&self) -> &str; /// Returns the remote address that this stream is connected to. fn get_peer_address(&self) -> &str; fn get_network_type(&self) -> &'static str; } impl endpoint::Sealed for Box {} impl GrpcEndpoint for Box { fn get_local_address(&self) -> &str { (**self).get_local_address() } fn get_peer_address(&self) -> &str { (**self).get_peer_address() } fn get_network_type(&self) -> &'static str { (**self).get_network_type() } } /// A trait representing a TCP listener capable of accepting incoming /// connections. pub trait TcpListener: Send + Sync { /// Accepts a new incoming connection. /// /// Returns a future that resolves to a result containing the new /// `GrpcEndpoint` and the remote peer's `SocketAddr`, or an error string /// if acceptance fails. fn accept(&mut self) -> ScopedBoxFuture<'_, Result<(BoxEndpoint, SocketAddr), String>>; /// Returns the local socket address this listener is bound to. fn local_addr(&self) -> &SocketAddr; } /// A fake runtime to satisfy the compiler when no runtime is enabled. This will /// /// # Panics /// /// Panics if any of its functions are called. #[derive(Default, Debug)] pub(crate) struct NoOpRuntime {} impl Runtime for NoOpRuntime { fn spawn(&self, task: Pin + Send + 'static>>) -> BoxedTaskHandle { unimplemented!() } fn get_dns_resolver(&self, opts: ResolverOptions) -> Result, String> { unimplemented!() } fn sleep(&self, duration: std::time::Duration) -> Pin> { unimplemented!() } fn tcp_stream( &self, target: SocketAddr, opts: TcpOptions, ) -> Pin, String>> + Send>> { unimplemented!() } fn listen_tcp( &self, addr: SocketAddr, _opts: TcpOptions, ) -> BoxFuture, String>> { unimplemented!() } } pub(crate) fn default_runtime() -> GrpcRuntime { #[cfg(feature = "_runtime-tokio")] { return GrpcRuntime::new(tokio::TokioRuntime::default()); } #[allow(unreachable_code)] GrpcRuntime::new(NoOpRuntime::default()) } #[derive(Clone, Debug)] pub struct GrpcRuntime { inner: Arc, } impl GrpcRuntime { pub fn new(runtime: T) -> Self { GrpcRuntime { inner: Arc::new(runtime), } } pub fn spawn( &self, task: Pin + Send + 'static>>, ) -> BoxedTaskHandle { self.inner.spawn(task) } pub fn get_dns_resolver(&self, opts: ResolverOptions) -> Result, String> { self.inner.get_dns_resolver(opts) } pub fn sleep(&self, duration: std::time::Duration) -> Pin> { self.inner.sleep(duration) } pub fn tcp_stream( &self, target: SocketAddr, opts: TcpOptions, ) -> BoxFuture, String>> { self.inner.tcp_stream(target, opts) } pub fn listen_tcp( &self, addr: SocketAddr, opts: TcpOptions, ) -> BoxFuture, String>> { self.inner.listen_tcp(addr, opts) } } ================================================ FILE: grpc/src/rt/tokio/hickory_resolver.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::net::IpAddr; use hickory_resolver::TokioResolver; use hickory_resolver::config::LookupIpStrategy; use hickory_resolver::config::NameServerConfigGroup; use hickory_resolver::config::ResolverConfig; use hickory_resolver::config::ResolverOpts; use hickory_resolver::name_server::TokioConnectionProvider; use crate::rt::ResolverOptions; use crate::rt::{self}; /// A DNS resolver that uses hickory with the tokio runtime. This supports txt /// lookups in addition to A and AAAA record lookups. It also supports using /// custom DNS servers. pub(super) struct DnsResolver { resolver: hickory_resolver::TokioResolver, } #[tonic::async_trait] impl rt::DnsResolver for DnsResolver { async fn lookup_host_name(&self, name: &str) -> Result, String> { let response = self .resolver .lookup_ip(name) .await .map_err(|err| err.to_string())?; Ok(response.iter().collect()) } async fn lookup_txt(&self, name: &str) -> Result, String> { let response: Vec<_> = self .resolver .txt_lookup(name) .await .map_err(|err| err.to_string())? .iter() .map(|txt_record| { txt_record .iter() .map(|bytes| String::from_utf8_lossy(bytes).into_owned()) .collect::>() .join("") }) .collect(); Ok(response) } } impl DnsResolver { pub(super) fn new(opts: ResolverOptions) -> Result { let builder = if let Some(server_addr) = opts.server_addr { let provider = TokioConnectionProvider::default(); let name_servers = NameServerConfigGroup::from_ips_clear( &[server_addr.ip()], server_addr.port(), true, ); let config = ResolverConfig::from_parts(None, vec![], name_servers); TokioResolver::builder_with_config(config, provider) } else { TokioResolver::builder_tokio().map_err(|err| err.to_string())? }; let mut resolver_opts = ResolverOpts::default(); resolver_opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; Ok(DnsResolver { resolver: builder.with_options(resolver_opts).build(), }) } } #[cfg(test)] mod tests { use std::net::Ipv4Addr; use std::net::SocketAddr; use std::sync::Arc; use hickory_resolver::Name; use hickory_server::ServerFuture; use hickory_server::authority::Catalog; use hickory_server::authority::ZoneType; use hickory_server::proto::rr::LowerName; use hickory_server::proto::rr::RData; use hickory_server::proto::rr::Record; use hickory_server::proto::rr::rdata::A; use hickory_server::proto::rr::rdata::TXT; use hickory_server::store::in_memory::InMemoryAuthority; use tokio::net::UdpSocket; use tokio::sync::oneshot; use tokio::task::JoinHandle; use crate::rt::DnsResolver; use crate::rt::ResolverOptions; use crate::rt::tokio::TokioDefaultDnsResolver; #[tokio::test] async fn compare_hickory_and_default() { let hickory_dns = super::DnsResolver::new(ResolverOptions::default()).unwrap(); let mut ips_hickory = hickory_dns.lookup_host_name("localhost").await.unwrap(); let default_resolver = TokioDefaultDnsResolver::new(ResolverOptions::default()).unwrap(); let mut system_resolver_ips = default_resolver .lookup_host_name("localhost") .await .unwrap(); // Hickory requests A and AAAA records in parallel, so the order of IPv4 // and IPv6 addresses isn't deterministic. ips_hickory.sort(); system_resolver_ips.sort(); assert_eq!( ips_hickory, system_resolver_ips, "both resolvers should produce same IPs for localhost" ) } #[tokio::test] async fn resolve_txt() { let records = vec![ Record::from_rdata( Name::from_ascii("test.local.").unwrap(), 300, RData::TXT(TXT::new(vec![ "one".to_string(), "two".to_string(), "three".to_string(), ])), ), Record::from_rdata( Name::from_ascii("test.local.").unwrap(), 300, RData::TXT(TXT::new(vec![ "abc".to_string(), "def".to_string(), "ghi".to_string(), ])), ), ]; let dns = start_in_memory_dns_server("test.local.", records).await; let opts = ResolverOptions { server_addr: Some(dns.addr), }; let hickory_dns = super::DnsResolver::new(opts).unwrap(); let txt = hickory_dns.lookup_txt("test.local").await.unwrap(); assert_eq!( txt, vec!["onetwothree".to_string(), "abcdefghi".to_string(),] ); dns.shutdown().await; } #[tokio::test] async fn custom_authority() { let record = Record::from_rdata( Name::from_ascii("test.local.").unwrap(), 300, RData::A(A(Ipv4Addr::new(1, 2, 3, 4))), ); let dns = start_in_memory_dns_server("test.local.", vec![record]).await; let opts = ResolverOptions { server_addr: Some(dns.addr), }; let hickory_dns = super::DnsResolver::new(opts).unwrap(); let ips = hickory_dns.lookup_host_name("test.local").await.unwrap(); assert_eq!(ips, vec![Ipv4Addr::new(1, 2, 3, 4)]); dns.shutdown().await } struct FakeDns { tx: Option>, join_handle: Option>, addr: SocketAddr, } impl FakeDns { async fn shutdown(mut self) { let tx = self.tx.take().unwrap(); tx.send(()).unwrap(); let handle = self.join_handle.take().unwrap(); handle.await.unwrap(); } } /// Starts an in-memory DNS server with and adds the given records. Returns /// a DNS server which should be shutdown after the test. It uses a random /// port to bind since tests can run in parallel. The assigned port can be /// read from the returned struct. async fn start_in_memory_dns_server(host: &str, records: Vec) -> FakeDns { // Create a simple A record for `test.local.` let authority = InMemoryAuthority::empty(Name::from_ascii(host).unwrap(), ZoneType::Primary, false); for record in records { authority.upsert(record, 0).await; } let mut catalog = Catalog::new(); catalog.upsert( LowerName::new(&Name::from_ascii(host).unwrap()), vec![Arc::new(authority)], ); let mut server = ServerFuture::new(catalog); let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let addr = socket.local_addr().unwrap(); server.register_socket(socket); println!("DNS server running on {addr}"); let (tx, rx) = oneshot::channel::<()>(); let server_task = tokio::spawn(async move { tokio::select! { _ = server.block_until_done() => {}, _ = rx => { server.shutdown_gracefully().await.unwrap(); } } }); FakeDns { tx: Some(tx), join_handle: Some(server_task), addr, } } } ================================================ FILE: grpc/src/rt/tokio/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::future::Future; use std::net::IpAddr; use std::net::SocketAddr; use std::pin::Pin; use std::time::Duration; use tokio::io::AsyncRead; use tokio::io::AsyncWrite; use tokio::net::TcpStream; use tokio::task::JoinHandle; use crate::client::name_resolution::TCP_IP_NETWORK_TYPE; use crate::rt::BoxEndpoint; use crate::rt::BoxFuture; use crate::rt::BoxedTaskHandle; use crate::rt::DnsResolver; use crate::rt::GrpcEndpoint; use crate::rt::ResolverOptions; use crate::rt::Runtime; use crate::rt::ScopedBoxFuture; use crate::rt::Sleep; use crate::rt::TaskHandle; use crate::rt::TcpOptions; use crate::rt::endpoint; #[cfg(feature = "dns")] mod hickory_resolver; /// A DNS resolver that uses tokio::net::lookup_host for resolution. It only /// supports host lookups. struct TokioDefaultDnsResolver { _priv: (), } #[tonic::async_trait] impl DnsResolver for TokioDefaultDnsResolver { async fn lookup_host_name(&self, name: &str) -> Result, String> { let name_with_port = match name.parse::() { Ok(ip) => SocketAddr::new(ip, 0).to_string(), Err(_) => format!("{name}:0"), }; let ips = tokio::net::lookup_host(name_with_port) .await .map_err(|err| err.to_string())? .map(|socket_addr| socket_addr.ip()) .collect(); Ok(ips) } async fn lookup_txt(&self, _name: &str) -> Result, String> { Err("TXT record lookup unavailable. Enable the optional 'dns' feature to enable service config lookups.".to_string()) } } #[derive(Debug, Default)] pub(crate) struct TokioRuntime { _priv: (), } impl TaskHandle for JoinHandle<()> { fn abort(&self) { self.abort() } } impl Sleep for tokio::time::Sleep {} impl Runtime for TokioRuntime { fn spawn(&self, task: Pin + Send + 'static>>) -> BoxedTaskHandle { Box::new(tokio::spawn(task)) } fn get_dns_resolver(&self, opts: ResolverOptions) -> Result, String> { #[cfg(feature = "dns")] { Ok(Box::new(hickory_resolver::DnsResolver::new(opts)?)) } #[cfg(not(feature = "dns"))] { Ok(Box::new(TokioDefaultDnsResolver::new(opts)?)) } } fn sleep(&self, duration: Duration) -> Pin> { Box::pin(tokio::time::sleep(duration)) } fn tcp_stream( &self, target: SocketAddr, opts: super::TcpOptions, ) -> Pin, String>> + Send>> { Box::pin(async move { let stream = TcpStream::connect(target) .await .map_err(|err| err.to_string())?; if let Some(duration) = opts.keepalive { let sock_ref = socket2::SockRef::from(&stream); let mut ka = socket2::TcpKeepalive::new(); ka = ka.with_time(duration); sock_ref .set_tcp_keepalive(&ka) .map_err(|err| err.to_string())?; } let stream: Box = Box::new(TokioTcpStream { peer_addr: target.to_string().into_boxed_str(), local_addr: stream .local_addr() .map_err(|err| err.to_string())? .to_string() .into_boxed_str(), inner: stream, }); Ok(stream) }) } fn listen_tcp( &self, addr: SocketAddr, _opts: TcpOptions, ) -> BoxFuture, String>> { Box::pin(async move { let listener = tokio::net::TcpListener::bind(addr) .await .map_err(|err| err.to_string())?; let local_addr = listener.local_addr().map_err(|e| e.to_string())?; let listener = TokioListener { inner: listener, local_addr, }; Ok(Box::new(listener) as Box) }) } } impl TokioDefaultDnsResolver { pub fn new(opts: ResolverOptions) -> Result { if opts.server_addr.is_some() { return Err("Custom DNS server are not supported, enable optional feature 'dns' to enable support.".to_string()); } Ok(TokioDefaultDnsResolver { _priv: () }) } } struct TokioTcpStream { inner: TcpStream, peer_addr: Box, local_addr: Box, } impl AsyncRead for TokioTcpStream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &mut tokio::io::ReadBuf<'_>, ) -> std::task::Poll> { Pin::new(&mut self.inner).poll_read(cx, buf) } } impl AsyncWrite for TokioTcpStream { fn poll_write( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &[u8], ) -> std::task::Poll> { Pin::new(&mut self.inner).poll_write(cx, buf) } fn poll_write_vectored( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, bufs: &[std::io::IoSlice<'_>], ) -> std::task::Poll> { Pin::new(&mut self.inner).poll_write_vectored(cx, bufs) } fn is_write_vectored(&self) -> bool { self.inner.is_write_vectored() } fn poll_flush( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { Pin::new(&mut self.inner).poll_flush(cx) } fn poll_shutdown( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { Pin::new(&mut self.inner).poll_shutdown(cx) } } impl endpoint::Sealed for TokioTcpStream {} impl super::GrpcEndpoint for TokioTcpStream { fn get_local_address(&self) -> &str { &self.local_addr } fn get_peer_address(&self) -> &str { &self.peer_addr } fn get_network_type(&self) -> &'static str { TCP_IP_NETWORK_TYPE } } struct TokioListener { inner: tokio::net::TcpListener, local_addr: SocketAddr, } impl super::TcpListener for TokioListener { fn accept(&mut self) -> ScopedBoxFuture<'_, Result<(BoxEndpoint, SocketAddr), String>> { Box::pin(async move { let (stream, addr) = self.inner.accept().await.map_err(|e| e.to_string())?; Ok(( Box::new(TokioTcpStream { local_addr: stream .local_addr() .map_err(|err| err.to_string())? .to_string() .into_boxed_str(), peer_addr: addr.to_string().into_boxed_str(), inner: stream, }) as Box, addr, )) }) } fn local_addr(&self) -> &SocketAddr { &self.local_addr } } #[cfg(test)] mod tests { use super::DnsResolver; use super::ResolverOptions; use super::Runtime; use super::TokioDefaultDnsResolver; use super::TokioRuntime; #[tokio::test] async fn lookup_hostname() { let runtime = TokioRuntime::default(); let dns = runtime .get_dns_resolver(ResolverOptions::default()) .unwrap(); let ips = dns.lookup_host_name("localhost").await.unwrap(); assert!( !ips.is_empty(), "Expect localhost to resolve to more than 1 IPs." ) } #[tokio::test] async fn default_resolver_txt_fails() { let default_resolver = TokioDefaultDnsResolver::new(ResolverOptions::default()).unwrap(); let txt = default_resolver.lookup_txt("google.com").await; assert!(txt.is_err()) } #[tokio::test] async fn default_resolver_custom_authority() { let opts = ResolverOptions { server_addr: Some("8.8.8.8:53".parse().unwrap()), }; let default_resolver = TokioDefaultDnsResolver::new(opts); assert!(default_resolver.is_err()) } } ================================================ FILE: grpc/src/send_future.rs ================================================ /* * * Copyright 2026 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use core::future::Future; /// A helper trait to enforce and explicitly bound a [`Future`] as [`Send`]. /// /// This trait provides a mechanism to work around specific Rust compiler /// limitations and bugs where the compiler's borrow checker or drop analysis /// conservatively concludes that an `async` block is `!Send` (not safe to send /// across threads), /// even when it logically should be. /// /// # Problem Context /// /// As detailed in issues [#64552], [#102211], and [#96865], there are scenarios /// where: /// * An `async` function captures a reference to a type that is `!Sync`. /// * A variable is dropped before an `.await` point, but the compiler's liveness /// analysis incorrectly believes it is held across the await. /// * Complex control flow confuses the auto-trait deduction for `Send`. /// /// These scenarios often result in obscure error messages when trying to spawn /// the future on an executor (like `tokio::spawn`), claiming the future is not /// `Send`. /// /// # The Solution /// /// The `make_send()` method acts as an identity function (a no-op at runtime) but /// performs two critical compile-time tasks: /// /// 1. **Explicit Assertion:** It requires `Self` to implement `Send` at the /// call site. This moves the error message from the deep internals of an /// executor's spawn function to the specific line where the future is created, /// making debugging significantly easier. /// 2. **Type Erasure / Coercion:** By returning `impl Future<...> + Send`, it /// creates an opaque type boundary. This can sometimes help the compiler's /// trait solver "lock in" the `Send` guarantee and disregard phantom lifetime /// issues that might otherwise propagate. /// /// [#64552]: https://github.com/rust-lang/rust/issues/64552 /// [#102211]: https://github.com/rust-lang/rust/issues/102211 /// [#96865]: https://github.com/rust-lang/rust/issues/96865 /// [`Future`]: core::future::Future /// [`Send`]: core::marker::Send pub trait SendFuture: Future { /// Consumes the future and returns it as an opaque type that is guaranteed /// to be [`Send`]. /// /// This is a zero-cost abstraction (it simply returns `self`) used primarily /// to help the compiler resolve auto-traits or to produce better error /// diagnostics. fn make_send(self) -> impl Future + Send where Self: Sized + Send, { self } } impl SendFuture for T {} ================================================ FILE: grpc/src/server/mod.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::sync::Arc; use tonic::async_trait; use crate::core::RecvMessage; use crate::core::RequestHeaders; use crate::core::ServerResponseStreamItem; pub struct Server { handler: Option>, } pub struct Call { pub headers: RequestHeaders, pub send: SS, pub recv: RS, } #[trait_variant::make(Send)] pub trait Listener { type SendStream: SendStream + 'static; type RecvStream: RecvStream + 'static; async fn accept(&self) -> Option>; } impl Server { pub fn new() -> Self { Self { handler: None } } pub fn set_handler(&mut self, h: H) where H: Handle + Send + Sync + 'static, { self.handler = Some(Arc::new(h)) } pub async fn serve(&self, l: &impl Listener) { while let Some(call) = l.accept().await { let mut send: Box = Box::new(call.send); let recv: Box = Box::new(call.recv); self.handler .as_ref() .unwrap() .dyn_handle(call.headers, &mut *send, recv) .await; } } } impl Default for Server { fn default() -> Self { Self::new() } } /// A trait which may be implemented by types to handle server-side logic of /// RPCs (Remote Procedure Calls, often shortened to "call"). #[trait_variant::make(Send)] pub trait Handle: Send + Sync { /// Handles an RPC, accepting the send and receive streams that are used to /// interact with the call. Note that `tx` is not static, so it cannot be /// sent to another task, meaning the RPC must end before handle returns. async fn handle( &self, headers: RequestHeaders, tx: &mut impl SendStream, rx: impl RecvStream + 'static, ); } #[async_trait] trait DynHandle: Send + Sync { async fn dyn_handle( &self, headers: RequestHeaders, tx: &mut dyn DynSendStream, rx: Box, ); } #[async_trait] impl DynHandle for T { async fn dyn_handle( &self, headers: RequestHeaders, mut tx: &mut dyn DynSendStream, rx: Box, ) { self.handle(headers, &mut tx, rx).await } } /// Represents the sending side of a server stream. See `ResponseStream` /// documentation for information about the different types of items and the /// order in which they must be sent. #[trait_variant::make(Send)] pub trait SendStream { /// Sends the next item on the stream. /// /// # Cancel safety /// /// This method is not intended to be cancellation safe. If the returned /// future is not polled to completion, the behavior of any subsequent calls /// to the SendStream are undefined and data may be lost. async fn send<'a>( &mut self, item: ServerResponseStreamItem<'a>, options: SendOptions, ) -> Result<(), ()>; } #[async_trait] trait DynSendStream: Send { async fn dyn_send<'a>( &mut self, item: ServerResponseStreamItem<'a>, options: SendOptions, ) -> Result<(), ()>; } #[async_trait] impl DynSendStream for T { async fn dyn_send<'a>( &mut self, item: ServerResponseStreamItem<'a>, options: SendOptions, ) -> Result<(), ()> { self.send(item, options).await } } impl SendStream for &mut dyn DynSendStream { async fn send<'a>( &mut self, item: ServerResponseStreamItem<'a>, options: SendOptions, ) -> Result<(), ()> { (**self).dyn_send(item, options).await } } impl SendStream for Box { async fn send<'a>( &mut self, item: ServerResponseStreamItem<'a>, options: SendOptions, ) -> Result<(), ()> { (**self).dyn_send(item, options).await } } /// Contains settings to configure a send operation on a SendStream. #[derive(Default)] #[non_exhaustive] pub struct SendOptions { /// Delays sending the message until the trailers are provided on the stream /// and batches the two items together if possible. pub final_msg: bool, /// If set, compression will be disabled for this message. pub disable_compression: bool, } /// Represents the receiving side of a server stream. #[trait_variant::make(Send)] pub trait RecvStream { /// Returns the next message on the stream. If an error is returned, the /// stream ended or the client closed the send side of the request stream. /// /// # Cancel safety /// /// This method is not intended to be cancellation safe. If the returned /// future is not polled to completion, the behavior of any subsequent calls /// to the RecvStream are undefined and data may be lost. async fn next(&mut self, msg: &mut dyn RecvMessage) -> Result<(), ()>; } #[async_trait] trait DynRecvStream: Send { async fn dyn_next(&mut self, msg: &mut dyn RecvMessage) -> Result<(), ()>; } #[async_trait] impl DynRecvStream for T { async fn dyn_next(&mut self, msg: &mut dyn RecvMessage) -> Result<(), ()> { self.next(msg).await } } impl RecvStream for Box { async fn next(&mut self, msg: &mut dyn RecvMessage) -> Result<(), ()> { (**self).dyn_next(msg).await } } ================================================ FILE: grpc/src/status/server_status.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use crate::status::Status; use crate::status::status_code::StatusCode; /// Represents a gRPC status on the server. /// /// This is a separate type from `Status` to prevent accidental conversion and /// leaking of sensitive information from the server to the client. #[derive(Debug, Clone)] pub struct ServerStatus(Status); impl std::ops::Deref for ServerStatus { type Target = Status; fn deref(&self) -> &Self::Target { &self.0 } } impl ServerStatus { /// Create a new `ServerStatus` with the given code and message. pub fn new(code: StatusCode, message: impl Into) -> Self { ServerStatus(Status::new(code, message)) } /// Create a new `ServerStatus` from a `Status`. pub fn from_status(status: Status) -> Self { ServerStatus(status) } /// Converts the `ServerStatus` to a `Status` for client responses. pub(crate) fn into_status(self) -> Status { self.0 } } #[cfg(test)] mod tests { use super::*; #[test] fn test_server_status_new() { let status = ServerStatus::new(StatusCode::Ok, "ok"); assert_eq!(status.code(), StatusCode::Ok); assert_eq!(status.message(), "ok"); } #[test] fn test_server_status_deref() { let status = ServerStatus::new(StatusCode::Ok, "ok"); assert_eq!(status.code(), StatusCode::Ok); } #[test] fn test_server_status_from_status() { let status = Status::new(StatusCode::Ok, "ok"); let server_status = ServerStatus::from_status(status); assert_eq!(server_status.code(), StatusCode::Ok); } #[test] fn test_server_status_into_status() { let server_status = ServerStatus::new(StatusCode::Ok, "ok"); let status = server_status.into_status(); assert_eq!(status.code(), StatusCode::Ok); } } ================================================ FILE: grpc/src/status/status_code.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ /// Represents a gRPC status code. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(i32)] pub enum StatusCode { Ok = 0, Cancelled = 1, Unknown = 2, InvalidArgument = 3, DeadlineExceeded = 4, NotFound = 5, AlreadyExists = 6, PermissionDenied = 7, ResourceExhausted = 8, FailedPrecondition = 9, Aborted = 10, OutOfRange = 11, Unimplemented = 12, Internal = 13, Unavailable = 14, DataLoss = 15, Unauthenticated = 16, } impl From for StatusCode { fn from(i: i32) -> Self { match i { 0 => StatusCode::Ok, 1 => StatusCode::Cancelled, 2 => StatusCode::Unknown, 3 => StatusCode::InvalidArgument, 4 => StatusCode::DeadlineExceeded, 5 => StatusCode::NotFound, 6 => StatusCode::AlreadyExists, 7 => StatusCode::PermissionDenied, 8 => StatusCode::ResourceExhausted, 9 => StatusCode::FailedPrecondition, 10 => StatusCode::Aborted, 11 => StatusCode::OutOfRange, 12 => StatusCode::Unimplemented, 13 => StatusCode::Internal, 14 => StatusCode::Unavailable, 15 => StatusCode::DataLoss, 16 => StatusCode::Unauthenticated, _ => StatusCode::Unknown, } } } ================================================ FILE: grpc/src/status.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ mod server_status; mod status_code; pub use server_status::ServerStatus; pub use status_code::StatusCode; /// Represents a gRPC status. #[derive(Debug, Clone)] pub struct Status { code: StatusCode, message: String, } impl Status { /// Create a new `Status` with the given code and message. pub fn new(code: StatusCode, message: impl Into) -> Self { Status { code, message: message.into(), } } /// Get the `StatusCode` of this `Status`. pub fn code(&self) -> StatusCode { self.code } /// Get the message of this `Status`. pub fn message(&self) -> &str { &self.message } } #[cfg(test)] mod tests { use super::*; #[test] fn test_status_new() { let status = Status::new(StatusCode::Ok, "ok"); assert_eq!(status.code(), StatusCode::Ok); assert_eq!(status.message(), "ok"); } #[test] fn test_status_debug() { let status = Status::new(StatusCode::Ok, "ok"); let debug = format!("{:?}", status); assert!(debug.contains("Status")); assert!(debug.contains("Ok")); assert!(debug.contains("ok")); } } ================================================ FILE: interop/Cargo.toml ================================================ [package] authors = ["Lucio Franco "] edition = "2024" license = "MIT" name = "interop" rust-version = { workspace = true } [[bin]] name = "client" path = "src/bin/client.rs" [[bin]] name = "server" path = "src/bin/server.rs" [dependencies] async-stream = "0.3" strum = {version = "0.27", features = ["derive"]} pico-args = {version = "0.5", features = ["eq-separator"]} console = "0.16" http = "1" http-body-util = "0.1" prost = "0.14" tokio = {version = "1.0", features = ["rt-multi-thread", "time", "macros"]} tokio-stream = "0.1" tonic = {path = "../tonic", features = ["tls-ring"]} tonic-prost = {path = "../tonic-prost"} tower = "0.5" tracing-subscriber = {version = "0.3"} grpc = {path = "../grpc"} # TODO: Remove once the protobuf-codegen crate supports configuring the path to # the protobuf crate used in the generated message code, instead of defaulting # to `::protobuf`. protobuf = { version = "4.33.0-release" } tonic-protobuf = {path = "../tonic-protobuf"} [build-dependencies] tonic-prost-build = {path = "../tonic-prost-build"} tonic-protobuf-build = {path = "../tonic-protobuf-build"} ================================================ FILE: interop/bin/client_darwin_amd64 ================================================ [File too large to display: 12.3 MB] ================================================ FILE: interop/bin/client_linux_amd64 ================================================ [File too large to display: 12.4 MB] ================================================ FILE: interop/bin/server_darwin_amd64 ================================================ [File too large to display: 11.3 MB] ================================================ FILE: interop/bin/server_linux_amd64 ================================================ [File too large to display: 11.4 MB] ================================================ FILE: interop/build.rs ================================================ fn main() { let proto = "proto/grpc/testing/test.proto"; eprintln!("{}", tonic_protobuf_build::protoc()); let path = std::env::var("PATH").unwrap_or_default(); unsafe { std::env::set_var("PATH", format!("{}:{}", path, tonic_protobuf_build::bin())); } tonic_prost_build::compile_protos(proto).unwrap(); tonic_protobuf_build::CodeGen::new() .include("proto/grpc/testing") .inputs(["test.proto", "empty.proto", "messages.proto"]) .compile() .unwrap(); // prevent needing to rebuild if files (or deps) haven't changed println!("cargo:rerun-if-changed={proto}"); } ================================================ FILE: interop/data/README.md ================================================ # Tonic Testing Certificates This directory contains certificates used for testing interop between Tonic's implementation of gRPC and the Go implementation. Certificates are generated using [`terraform`][tf]. To regenerate certificates for some reason, do the following: 1. Install Terraform 0.12 (or higher) 1. From the `cert-generator` directory, run: 1. `terraform init` 1. `terraform apply` This will generate certificates and write them to the filesystem. The effective version should be committed to git. [tf]: https://terraform.io ================================================ FILE: interop/data/ca.pem ================================================ -----BEGIN CERTIFICATE----- MIIDRjCCAi6gAwIBAgIQd5pnuFdwgGxb4RiClYEPMTANBgkqhkiG9w0BAQsFADA9 MQ4wDAYDVQQKEwVUb2tpbzEQMA4GA1UECxMHVGVzdGluZzEZMBcGA1UEAxMQVG9u aWMgVGVzdGluZyBDQTAeFw0yNTA1MDExNzQyNThaFw0zNTA0MjkxNzQyNThaMD0x DjAMBgNVBAoTBVRva2lvMRAwDgYDVQQLEwdUZXN0aW5nMRkwFwYDVQQDExBUb25p YyBUZXN0aW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2C37 LVCs4RfNdwv8NMZfIdFNqrUdwzXZ+a5B7Pee1nOL+JD9feOGn1qGZI1ZgFMqVygN ejSkzlbouN9RAGgyBmOFFo3oEc+nz7kPrezBLoM3oVgNzhEixz2IQoafoZX3j48Y fpGYmrTHUp4MAwUAt6Zb+kD7YGqD8//I5OMM4Y5R8yuYGsJHUUSZqYfgXCk0ZvVG EX7zyr31cVLqto1vpuv5Uvp6WX5oGgbZVB0wvlqs9Ak+dblWBZQIsrUPU8kn/6kx HilF8Lw24dRXr5oveFDMdD/n4sIh7Gr/O+VGH83gP/PawXy0WWn5qGAhdx+P99jI UGAWNetu4vGgASLFkwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAgQwDwYDVR0TAQH/ BAUwAwEB/zAdBgNVHQ4EFgQU4AI0rUoGKFxe0gXIYujrhnuNsocwDQYJKoZIhvcN AQELBQADggEBAEtSrTgz+suVMDRsdvxG7+BAcXfVc8pmou3KIFbDtIyog2BEU5nU Z+zBqrVgbGv9B4D7XLcZgTShY17cvJP8QpUwT/gzI9uR8Lig9JGF7n1K43+aiAk9 s7H8e74rwyPX6mRmuznd1sJdDsc5lohUPpZVI+7pRywedQw+QG6/n2cVvR0k0Txh pF1XBpzuFA5t5uqW/v/QFqfGEuIDDMdW2JQSEB7UyH4V2yWswoYb/uf/xoNXWWqs Y6RVSp6qVW8748rPPwmLaN8hHGIUNUnilQIXr67bX8i3FjoLHhQvKqUEKciXJWj9 ssGOvq0QoVZNPltcZp9yID3W2kyxv6Hq8VA= -----END CERTIFICATE----- ================================================ FILE: interop/data/cert-generator/.gitignore ================================================ .terraform/ *.tfstate *.tfstate.backup ================================================ FILE: interop/data/cert-generator/ca.tf ================================================ resource "tls_private_key" "root" { algorithm = "RSA" rsa_bits = "2048" } resource "tls_self_signed_cert" "root" { private_key_pem = tls_private_key.root.private_key_pem validity_period_hours = 87600 early_renewal_hours = 8760 is_ca_certificate = true allowed_uses = ["cert_signing"] subject { common_name = "Tonic Testing CA" organization = "Tokio" organizational_unit = "Testing" } } resource "local_file" "ca_cert" { filename = "../ca.pem" content = tls_self_signed_cert.root.cert_pem } ================================================ FILE: interop/data/cert-generator/server_certs.tf ================================================ resource "tls_private_key" "server" { algorithm = "RSA" rsa_bits = "2048" } resource "tls_cert_request" "server" { private_key_pem = tls_private_key.server.private_key_pem subject { common_name = "Tonic Test Server Cert" } dns_names = [ "*.test.google.fr", ] } resource "tls_locally_signed_cert" "server" { cert_request_pem = tls_cert_request.server.cert_request_pem ca_private_key_pem = tls_private_key.root.private_key_pem ca_cert_pem = tls_self_signed_cert.root.cert_pem validity_period_hours = 43800 early_renewal_hours = 8760 allowed_uses = ["server_auth"] } resource "local_file" "server_cert" { filename = "../server1.pem" content = tls_locally_signed_cert.server.cert_pem } resource "local_file" "server_key" { filename = "../server1.key" content = tls_private_key.server.private_key_pem } ================================================ FILE: interop/data/server1.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA7iJJ8gLlKsp+r15CR15Iz2rmi3f3OZmA8FZ0hpB5hNkQHfVA RlC2yawIfHiLO4tpUmjtX8iq3RXPkKPYP5Zfd1BDLdR/2qhd3vFJnRVfoiqTMNOV 3R3+tIm04gDbtGxIDuWL+No/r/KldFxwbLqYTXDOaa145YI2aZ3GZ3P6GFYls6h7 PeUqlXv7yWx1jfcMIZPeupHwWESYCCLkpvBHFZftWhc/FChUmgr417vmQC2eGwuX LyRdu+Lv9NmSzsO6A+w8ss62ewC02LXkXAG2Prd1GYuScsq94GcE5lguC5TVkdTm tQMDmET/KvitG3vLB9AIkKnjZdi1Ml7Ow6dByQIDAQABAoIBAFo7ketrH2z8d855 mAG0/z/hEOSuG3au7MWk7NiEbBdjrJC9epJqSSjX0AtiHdf9NnZsne2aeuv1NMZo 3ysRDrGGLz5xc9Tl0VQF98/W5nrrSQTKV9IGaJn+SBUPIDEYiqFiZ4xvHozME9eo o0z/03Acm4o9mj7U/Us95o0SzCRl3QKgAWeSS36Ks0OmDJfNuYBWf+WA7Fte9NUp yOOm2fGejoge9eMcJY3/7HckrESscMECZMUL1hBbVD939d4S4AvM6YWTErAa9uq9 APsXdu5IYglonqw6oc4TtN9bI9gbHKTyiFgi42gM6qcN2ixpQ78ufktLcJLBTLzi jP5f5cUCgYEA7whtTRG+KN3nAaRy5gU3JDdOIM1tlAVjwtvUIre3sf6p6Bzs0+RL DVdOidJB+8wnV6hF64+juHS27Y7t4ONt2VRFNmY3yRlb9MwqOYlqGaOOewgY+Gab ZC4GBKmMRKW0LpeRHghpCeyeRRKr5tkYalyU9/C+mxIFpb0/NZZXh6sCgYEA/wmG s+npJH2Xs17Yd3wwroZpFAlG2wRCd0IS14brKr+w5czbGis8Pz/LCVrNH+6ZkvoI gUpTDwY7egt9O2iCIeSeq82Ov+g9WiDa150YTq8av09N7AZ13Na+SU5aNpPwIOEZ WX8dygNloSh4JDjOhrwigRtcMmYCtpKVS792GFsCgYEA6QEB6rp870E/ya4QAoDa +4adtgQJ6NxIHs5Cv4Tun6dq4EQx52sGbf7JJDe88kJTp3L0lWbzZP8AwhktcKbB kbQ/s4N4paL+rGXIU0XMEyoH3Y5LKPh8SO9EFo9fmBsexLwiTXBNU8s/jH1i7Ch7 UFLnM7mNU4QB1Ungr8/ZivkCgYA6sA2ATz5oOEJ1c0jqzfhB4QpDIxNcCPHmkZzW XeS11KC3cNmmfvaBM4PcZjm3tGdArCrS3bCZT3zWS9iImDcB56Mfs9C6lo2vtMnH Pg4+5QqJpY0v2Bi9NelZ4x7dWlOyrTnxH1BSkU+Ms0xaQXw9AwQJo6smqdTMAJU8 dhWN6wKBgQDRAjpfV77FE32XsBkHyGlaIanxu+gSJVLaHStZLV8qTvn//lnmr5Be abK4smlwHp5ZnTwqP3gMh/b8vfyPYHdK/2rCKSCEPf/JHtoABsw9Hvkr/N99MZQd S2l3PYQoQ8smUVYNWhdYvdRER8WFTk5rPX6fEoVne/sArxlwk8+8nw== -----END RSA PRIVATE KEY----- ================================================ FILE: interop/data/server1.pem ================================================ -----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIRAL1ZcIwdi/AfgLm2T41fHO4wDQYJKoZIhvcNAQELBQAw PTEOMAwGA1UEChMFVG9raW8xEDAOBgNVBAsTB1Rlc3RpbmcxGTAXBgNVBAMTEFRv bmljIFRlc3RpbmcgQ0EwHhcNMjUwNTAxMTc0MjU4WhcNMzAwNDMwMTc0MjU4WjAh MR8wHQYDVQQDExZUb25pYyBUZXN0IFNlcnZlciBDZXJ0MIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEA7iJJ8gLlKsp+r15CR15Iz2rmi3f3OZmA8FZ0hpB5 hNkQHfVARlC2yawIfHiLO4tpUmjtX8iq3RXPkKPYP5Zfd1BDLdR/2qhd3vFJnRVf oiqTMNOV3R3+tIm04gDbtGxIDuWL+No/r/KldFxwbLqYTXDOaa145YI2aZ3GZ3P6 GFYls6h7PeUqlXv7yWx1jfcMIZPeupHwWESYCCLkpvBHFZftWhc/FChUmgr417vm QC2eGwuXLyRdu+Lv9NmSzsO6A+w8ss62ewC02LXkXAG2Prd1GYuScsq94GcE5lgu C5TVkdTmtQMDmET/KvitG3vLB9AIkKnjZdi1Ml7Ow6dByQIDAQABo2MwYTATBgNV HSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFOACNK1K BihcXtIFyGLo64Z7jbKHMBsGA1UdEQQUMBKCECoudGVzdC5nb29nbGUuZnIwDQYJ KoZIhvcNAQELBQADggEBAJP9h4voqemt8Jiw9lgXKOfZyydIHKvL8oeeNQLnn+Ch S8D32xRxDeql0oghbTFj1AUxs5X415YgyP4JBoQ8X+L7z3hvSHHildJjbDAM5l+D jHIr/G6+N6DzLi75WUpZkHFa0ZZ+jHkrxRFq3SsS2hzL93sZ8HoLoEXgGJYcuVYh duWmy1pv/TW8j3GcRE358rLyIzsAK2tJZOHC3MeDqvITfGfzeHxy/UG2bbGmXU8Z UoCFUGHhukNuESQFfPxoHsWnsxvCIvcIxGPj4NXSO0WJ9r7/A+UczSr+Vuc55h0E qrAl9EXltUWTjRZwdIvvas9N3y0ApxkMFNIRmMwUBGE= -----END CERTIFICATE----- ================================================ FILE: interop/proto/grpc/testing/empty.proto ================================================ // Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package grpc.testing; // An empty message that you can re-use to avoid defining duplicated empty // messages in your project. A typical example is to use it as argument or the // return value of a service API. For instance: // // service Foo { // rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; // }; // message Empty {} ================================================ FILE: interop/proto/grpc/testing/messages.proto ================================================ // Copyright 2015-2016 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Message definitions to be used by integration test service definitions. syntax = "proto3"; package grpc.testing; // TODO(dgq): Go back to using well-known types once // https://github.com/grpc/grpc/issues/6980 has been fixed. // import "google/protobuf/wrappers.proto"; message BoolValue { // The bool value. bool value = 1; } // DEPRECATED, don't use. To be removed shortly. // The type of payload that should be returned. enum PayloadType { // Compressable text format. COMPRESSABLE = 0; } // A block of data, to simply increase gRPC message size. message Payload { // DEPRECATED, don't use. To be removed shortly. // The type of data in body. PayloadType type = 1; // Primary contents of payload. bytes body = 2; } // A protobuf representation for grpc status. This is used by test // clients to specify a status that the server should attempt to return. message EchoStatus { int32 code = 1; string message = 2; } // Unary request. message SimpleRequest { // DEPRECATED, don't use. To be removed shortly. // Desired payload type in the response from the server. // If response_type is RANDOM, server randomly chooses one from other formats. PayloadType response_type = 1; // Desired payload size in the response from the server. int32 response_size = 2; // Optional input payload sent along with the request. Payload payload = 3; // Whether SimpleResponse should include username. bool fill_username = 4; // Whether SimpleResponse should include OAuth scope. bool fill_oauth_scope = 5; // Whether to request the server to compress the response. This field is // "nullable" in order to interoperate seamlessly with clients not able to // implement the full compression tests by introspecting the call to verify // the response's compression status. BoolValue response_compressed = 6; // Whether server should return a given status EchoStatus response_status = 7; // Whether the server should expect this request to be compressed. BoolValue expect_compressed = 8; } // Unary response, as configured by the request. message SimpleResponse { // Payload to increase message size. Payload payload = 1; // The user the request came from, for verifying authentication was // successful when the client expected it. string username = 2; // OAuth scope. string oauth_scope = 3; } // Client-streaming request. message StreamingInputCallRequest { // Optional input payload sent along with the request. Payload payload = 1; // Whether the server should expect this request to be compressed. This field // is "nullable" in order to interoperate seamlessly with servers not able to // implement the full compression tests by introspecting the call to verify // the request's compression status. BoolValue expect_compressed = 2; // Not expecting any payload from the response. } // Client-streaming response. message StreamingInputCallResponse { // Aggregated size of payloads received from the client. int32 aggregated_payload_size = 1; } // Configuration for a particular response. message ResponseParameters { // Desired payload sizes in responses from the server. int32 size = 1; // Desired interval between consecutive responses in the response stream in // microseconds. int32 interval_us = 2; // Whether to request the server to compress the response. This field is // "nullable" in order to interoperate seamlessly with clients not able to // implement the full compression tests by introspecting the call to verify // the response's compression status. BoolValue compressed = 3; } // Server-streaming request. message StreamingOutputCallRequest { // DEPRECATED, don't use. To be removed shortly. // Desired payload type in the response from the server. // If response_type is RANDOM, the payload from each response in the stream // might be of different types. This is to simulate a mixed type of payload // stream. PayloadType response_type = 1; // Configuration for each expected response message. repeated ResponseParameters response_parameters = 2; // Optional input payload sent along with the request. Payload payload = 3; // Whether server should return a given status EchoStatus response_status = 7; } // Server-streaming response, as configured by the request and parameters. message StreamingOutputCallResponse { // Payload to increase response size. Payload payload = 1; } // For reconnect interop test only. // Client tells server what reconnection parameters it used. message ReconnectParams { int32 max_reconnect_backoff_ms = 1; } // For reconnect interop test only. // Server tells client whether its reconnects are following the spec and the // reconnect backoffs it saw. message ReconnectInfo { bool passed = 1; repeated int32 backoff_ms = 2; } ================================================ FILE: interop/proto/grpc/testing/test.proto ================================================ // Copyright 2015-2016 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // An integration test service that covers all the method signature permutations // of unary/streaming requests/responses. syntax = "proto3"; import "empty.proto"; import "messages.proto"; package grpc.testing; // A simple service to test the various types of RPCs and experiment with // performance with various types of payload. service TestService { // One empty request followed by one empty response. rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); // One request followed by one response. rpc UnaryCall(SimpleRequest) returns (SimpleResponse); // One request followed by one response. Response has cache control // headers set such that a caching HTTP proxy (such as GFE) can // satisfy subsequent requests. rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); // One request followed by a sequence of responses (streamed download). // The server returns the payload with client desired type and sizes. rpc StreamingOutputCall(StreamingOutputCallRequest) returns (stream StreamingOutputCallResponse); // A sequence of requests followed by one response (streamed upload). // The server returns the aggregated size of client payload as the result. rpc StreamingInputCall(stream StreamingInputCallRequest) returns (StreamingInputCallResponse); // A sequence of requests with each request served by the server immediately. // As one request could lead to multiple responses, this interface // demonstrates the idea of full duplexing. rpc FullDuplexCall(stream StreamingOutputCallRequest) returns (stream StreamingOutputCallResponse); // A sequence of requests followed by a sequence of responses. // The server buffers all the client requests and then serves them in order. A // stream of responses are returned to the client when the server starts with // first request. rpc HalfDuplexCall(stream StreamingOutputCallRequest) returns (stream StreamingOutputCallResponse); // The test server will not implement this method. It will be used // to test the behavior when clients call unimplemented methods. rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); } // A simple service NOT implemented at servers so clients can test for // that case. service UnimplementedService { // A call that no server should implement rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); } // A service used to control reconnect server. service ReconnectService { rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); } ================================================ FILE: interop/src/bin/client.rs ================================================ use interop::client::{InteropTest, InteropTestUnimplemented}; use interop::{client_prost, client_protobuf}; use std::{str::FromStr, time::Duration}; use tonic::transport::Endpoint; use tonic::transport::{Certificate, ClientTlsConfig}; #[derive(Debug)] struct Opts { use_tls: bool, test_case: Vec, codec: Codec, } #[derive(Debug)] enum Codec { Prost, Protobuf, } impl FromStr for Codec { type Err = String; fn from_str(s: &str) -> Result { match s { "prost" => Ok(Codec::Prost), "protobuf" => Ok(Codec::Protobuf), _ => Err(format!("Invalid codec: {}", s)), } } } impl Opts { fn parse() -> Result { let mut pargs = pico_args::Arguments::from_env(); Ok(Self { use_tls: pargs.contains("--use_tls"), test_case: pargs.value_from_fn("--test_case", |test_case| { test_case.split(',').map(Testcase::from_str).collect() })?, codec: pargs.value_from_str("--codec")?, }) } } #[tokio::main] async fn main() -> Result<(), Box> { interop::trace_init(); let matches = Opts::parse()?; let test_cases = matches.test_case; let scheme = if matches.use_tls { "https" } else { "http" }; #[allow(unused_mut)] let mut endpoint = Endpoint::try_from(format!("{scheme}://localhost:10000"))? .timeout(Duration::from_secs(5)) .concurrency_limit(30); if matches.use_tls { let pem = std::fs::read_to_string("interop/data/ca.pem")?; let ca = Certificate::from_pem(pem); endpoint = endpoint.tls_config( ClientTlsConfig::new() .ca_certificate(ca) .domain_name("foo.test.google.fr"), )?; } let channel = endpoint.connect().await?; let (mut client, mut unimplemented_client): ( Box, Box, ) = match matches.codec { Codec::Prost => ( Box::new(client_prost::TestClient::new(channel.clone())), Box::new(client_prost::UnimplementedClient::new(channel)), ), Codec::Protobuf => ( Box::new(client_protobuf::TestClient::new(channel.clone())), Box::new(client_protobuf::UnimplementedClient::new(channel)), ), }; let mut failures = Vec::new(); for test_case in test_cases { println!("{test_case:?}:"); let mut test_results = Vec::new(); match test_case { Testcase::EmptyUnary => client.empty_unary(&mut test_results).await, Testcase::LargeUnary => client.large_unary(&mut test_results).await, Testcase::ClientStreaming => client.client_streaming(&mut test_results).await, Testcase::ServerStreaming => client.server_streaming(&mut test_results).await, Testcase::PingPong => client.ping_pong(&mut test_results).await, Testcase::EmptyStream => client.empty_stream(&mut test_results).await, Testcase::StatusCodeAndMessage => { client.status_code_and_message(&mut test_results).await } Testcase::SpecialStatusMessage => { client.special_status_message(&mut test_results).await } Testcase::UnimplementedMethod => client.unimplemented_method(&mut test_results).await, Testcase::UnimplementedService => { unimplemented_client .unimplemented_service(&mut test_results) .await } Testcase::CustomMetadata => client.custom_metadata(&mut test_results).await, _ => unimplemented!(), } for result in test_results { println!(" {result}"); if result.is_failed() { failures.push(result); } } } if !failures.is_empty() { println!("{} tests failed", failures.len()); std::process::exit(1); } Ok(()) } #[derive(Debug, strum::EnumString)] #[strum(serialize_all = "snake_case")] enum Testcase { EmptyUnary, CacheableUnary, LargeUnary, ClientCompressedUnary, ServerCompressedUnary, ClientStreaming, ClientCompressedStreaming, ServerStreaming, ServerCompressedStreaming, PingPong, EmptyStream, ComputeEngineCreds, JwtTokenCreds, Oauth2AuthToken, PerRpcCreds, CustomMetadata, StatusCodeAndMessage, SpecialStatusMessage, UnimplementedMethod, UnimplementedService, CancelAfterBegin, CancelAfterFirstResponse, TimeoutOnSleepingServer, ConcurrentLargeUnary, } ================================================ FILE: interop/src/bin/server.rs ================================================ use interop::{server_prost, server_protobuf}; use std::str::FromStr; use tonic::transport::Server; use tonic::transport::{Identity, ServerTlsConfig}; #[derive(Debug)] struct Opts { use_tls: bool, codec: Codec, } #[derive(Debug)] enum Codec { Prost, Protobuf, } impl FromStr for Codec { type Err = String; fn from_str(s: &str) -> Result { match s { "prost" => Ok(Codec::Prost), "protobuf" => Ok(Codec::Protobuf), _ => Err(format!("Invalid codec: {}", s)), } } } impl Opts { fn parse() -> Result { let mut pargs = pico_args::Arguments::from_env(); Ok(Self { use_tls: pargs.contains("--use_tls"), codec: pargs.value_from_str("--codec")?, }) } } #[tokio::main] async fn main() -> std::result::Result<(), Box> { interop::trace_init(); let matches = Opts::parse()?; let addr = "127.0.0.1:10000".parse().unwrap(); let mut builder = Server::builder(); if matches.use_tls { let cert = std::fs::read_to_string("interop/data/server1.pem")?; let key = std::fs::read_to_string("interop/data/server1.key")?; let identity = Identity::from_pem(cert, key); builder = builder.tls_config(ServerTlsConfig::new().identity(identity))?; } match matches.codec { Codec::Prost => { let test_service = server_prost::TestServiceServer::new(server_prost::TestService::default()); let unimplemented_service = server_prost::UnimplementedServiceServer::new( server_prost::UnimplementedService::default(), ); // Wrap this test_service with a service that will echo headers as trailers. let test_service_svc = server_prost::EchoHeadersSvc::new(test_service); builder .add_service(test_service_svc) .add_service(unimplemented_service) .serve(addr) .await?; } Codec::Protobuf => { let test_service = server_protobuf::TestServiceServer::new(server_protobuf::TestService::default()); let unimplemented_service = server_protobuf::UnimplementedServiceServer::new( server_protobuf::UnimplementedService::default(), ); // Wrap this test_service with a service that will echo headers as trailers. let test_service_svc = server_protobuf::EchoHeadersSvc::new(test_service); builder .add_service(test_service_svc) .add_service(unimplemented_service) .serve(addr) .await?; } }; Ok(()) } ================================================ FILE: interop/src/client.rs ================================================ use crate::TestAssertion; use tonic::async_trait; #[async_trait] pub trait InteropTest: Send { async fn empty_unary(&mut self, assertions: &mut Vec); async fn large_unary(&mut self, assertions: &mut Vec); async fn client_streaming(&mut self, assertions: &mut Vec); async fn server_streaming(&mut self, assertions: &mut Vec); async fn ping_pong(&mut self, assertions: &mut Vec); async fn empty_stream(&mut self, assertions: &mut Vec); async fn status_code_and_message(&mut self, assertions: &mut Vec); async fn special_status_message(&mut self, assertions: &mut Vec); async fn unimplemented_method(&mut self, assertions: &mut Vec); async fn custom_metadata(&mut self, assertions: &mut Vec); } #[async_trait] pub trait InteropTestUnimplemented: Send { async fn unimplemented_service(&mut self, assertions: &mut Vec); } ================================================ FILE: interop/src/client_prost.rs ================================================ use crate::client::{InteropTest, InteropTestUnimplemented}; use crate::{ TestAssertion, pb::test_service_client::*, pb::unimplemented_service_client::*, pb::*, test_assert, }; use tokio::sync::mpsc; use tokio_stream::StreamExt; use tonic::async_trait; use tonic::transport::Channel; use tonic::{Code, Request, Response, Status, metadata::MetadataValue}; pub type TestClient = TestServiceClient; pub type UnimplementedClient = UnimplementedServiceClient; const LARGE_REQ_SIZE: usize = 271_828; const LARGE_RSP_SIZE: i32 = 314_159; const REQUEST_LENGTHS: &[i32] = &[27182, 8, 1828, 45904]; const RESPONSE_LENGTHS: &[i32] = &[31415, 9, 2653, 58979]; const TEST_STATUS_MESSAGE: &str = "test status message"; const SPECIAL_TEST_STATUS_MESSAGE: &str = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n"; #[async_trait] impl InteropTest for TestClient { async fn empty_unary(&mut self, assertions: &mut Vec) { let result = self.empty_call(Request::new(Empty {})).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result { let body = response.into_inner(); assertions.push(test_assert!( "body must not be null", body == Empty {}, format!("body={:?}", body) )); } } async fn large_unary(&mut self, assertions: &mut Vec) { use std::mem; let payload = crate::client_payload(LARGE_REQ_SIZE); let req = SimpleRequest { response_type: PayloadType::Compressable as i32, response_size: LARGE_RSP_SIZE, payload: Some(payload), ..Default::default() }; let result = self.unary_call(Request::new(req)).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result { let body = response.into_inner(); let payload_len = body.payload.as_ref().map(|p| p.body.len()).unwrap_or(0); assertions.push(test_assert!( "body must be 314159 bytes", payload_len == LARGE_RSP_SIZE as usize, format!("mem::size_of_val(&body)={:?}", mem::size_of_val(&body)) )); } } // async fn cachable_unary(client: &mut Client, assertions: &mut Vec) { // let payload = Payload { // r#type: PayloadType::Compressable as i32, // body: format!("{:?}", std::time::Instant::now()).into_bytes(), // }; // let req = SimpleRequest { // response_type: PayloadType::Compressable as i32, // payload: Some(payload), // ..Default::default() // }; // self. // } async fn client_streaming(&mut self, assertions: &mut Vec) { let requests: Vec<_> = REQUEST_LENGTHS .iter() .map(make_streaming_input_request) .collect(); let stream = tokio_stream::iter(requests); let result = self.streaming_input_call(Request::new(stream)).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result { let body = response.into_inner(); assertions.push(test_assert!( "aggregated payload size must be 74922 bytes", body.aggregated_payload_size == 74922, format!("aggregated_payload_size={:?}", body.aggregated_payload_size) )); } } async fn server_streaming(&mut self, assertions: &mut Vec) { let req = StreamingOutputCallRequest { response_parameters: RESPONSE_LENGTHS .iter() .map(|len| ResponseParameters::with_size(*len)) .collect(), ..Default::default() }; let req = Request::new(req); let result = self.streaming_output_call(req).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result { let responses = response .into_inner() .filter_map(|m| m.ok()) .collect::>() .await; let actual_response_lengths = crate::response_lengths(&responses); let asserts = vec![ test_assert!( "there should be four responses", responses.len() == 4, format!("responses.len()={:?}", responses.len()) ), test_assert!( "the response payload sizes should match input", RESPONSE_LENGTHS == actual_response_lengths.as_slice(), format!("{:?}={:?}", RESPONSE_LENGTHS, actual_response_lengths) ), ]; assertions.extend(asserts); } } async fn ping_pong(&mut self, assertions: &mut Vec) { let (tx, rx) = mpsc::unbounded_channel(); tx.send(make_ping_pong_request(0)).unwrap(); let result = self .full_duplex_call(Request::new( tokio_stream::wrappers::UnboundedReceiverStream::new(rx), )) .await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(mut response) = result.map(Response::into_inner) { let mut responses = Vec::new(); loop { match response.next().await { Some(result) => { responses.push(result.unwrap()); if responses.len() == REQUEST_LENGTHS.len() { drop(tx); break; } else { tx.send(make_ping_pong_request(responses.len())).unwrap(); } } None => { assertions.push(TestAssertion::Failed { description: "server should keep the stream open until the client closes it", expression: "Stream terminated unexpectedly early", why: None, }); break; } } } let actual_response_lengths = crate::response_lengths(&responses); assertions.push(test_assert!( "there should be four responses", responses.len() == RESPONSE_LENGTHS.len(), format!("{:?}={:?}", responses.len(), RESPONSE_LENGTHS.len()) )); assertions.push(test_assert!( "the response payload sizes should match input", RESPONSE_LENGTHS == actual_response_lengths.as_slice(), format!("{:?}={:?}", RESPONSE_LENGTHS, actual_response_lengths) )); } } async fn empty_stream(&mut self, assertions: &mut Vec) { let stream = tokio_stream::empty(); let result = self.full_duplex_call(Request::new(stream)).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result.map(Response::into_inner) { let responses = response.collect::>().await; assertions.push(test_assert!( "there should be no responses", responses.is_empty(), format!("responses.len()={:?}", responses.len()) )); } } async fn status_code_and_message(&mut self, assertions: &mut Vec) { fn validate_response(result: Result, assertions: &mut Vec) where T: std::fmt::Debug, { assertions.push(test_assert!( "call must fail with unknown status code", match &result { Err(status) => status.code() == Code::Unknown, _ => false, }, format!("result={:?}", result) )); assertions.push(test_assert!( "call must respsond with expected status message", match &result { Err(status) => status.message() == TEST_STATUS_MESSAGE, _ => false, }, format!("result={:?}", result) )); } let simple_req = SimpleRequest { response_status: Some(EchoStatus { code: 2, message: TEST_STATUS_MESSAGE.to_string(), }), ..Default::default() }; let duplex_req = StreamingOutputCallRequest { response_status: Some(EchoStatus { code: 2, message: TEST_STATUS_MESSAGE.to_string(), }), ..Default::default() }; let result = self.unary_call(Request::new(simple_req)).await; validate_response(result, assertions); let stream = tokio_stream::once(duplex_req); let result = match self.full_duplex_call(Request::new(stream)).await { Ok(response) => { let stream = response.into_inner(); let responses = stream.collect::>().await; Ok(responses) } Err(e) => Err(e), }; validate_response(result, assertions); } async fn special_status_message(&mut self, assertions: &mut Vec) { let req = SimpleRequest { response_status: Some(EchoStatus { code: 2, message: SPECIAL_TEST_STATUS_MESSAGE.to_string(), }), ..Default::default() }; let result = self.unary_call(Request::new(req)).await; assertions.push(test_assert!( "call must fail with unknown status code", match &result { Err(status) => status.code() == Code::Unknown, _ => false, }, format!("result={:?}", result) )); assertions.push(test_assert!( "call must respsond with expected status message", match &result { Err(status) => status.message() == SPECIAL_TEST_STATUS_MESSAGE, _ => false, }, format!("result={:?}", result) )); } async fn unimplemented_method(&mut self, assertions: &mut Vec) { let result = self.unimplemented_call(Request::new(Empty {})).await; assertions.push(test_assert!( "call must fail with unimplemented status code", match &result { Err(status) => status.code() == Code::Unimplemented, _ => false, }, format!("result={:?}", result) )); } async fn custom_metadata(&mut self, assertions: &mut Vec) { let key1 = "x-grpc-test-echo-initial"; let value1: MetadataValue<_> = "test_initial_metadata_value".parse().unwrap(); let key2 = "x-grpc-test-echo-trailing-bin"; let value2 = MetadataValue::from_bytes(&[0xab, 0xab, 0xab]); let req = SimpleRequest { response_type: PayloadType::Compressable as i32, response_size: LARGE_RSP_SIZE, payload: Some(crate::client_payload(LARGE_REQ_SIZE)), ..Default::default() }; let mut req_unary = Request::new(req); req_unary.metadata_mut().insert(key1, value1.clone()); req_unary.metadata_mut().insert_bin(key2, value2.clone()); let stream = tokio_stream::once(make_ping_pong_request(0)); let mut req_stream = Request::new(stream); req_stream.metadata_mut().insert(key1, value1.clone()); req_stream.metadata_mut().insert_bin(key2, value2.clone()); let response = self.unary_call(req_unary).await.expect("call should pass."); assertions.push(test_assert!( "metadata string must match in unary", response.metadata().get(key1) == Some(&value1), format!("result={:?}", response.metadata().get(key1)) )); assertions.push(test_assert!( "metadata bin must match in unary", response.metadata().get_bin(key2) == Some(&value2), format!("result={:?}", response.metadata().get_bin(key1)) )); let response = self .full_duplex_call(req_stream) .await .expect("call should pass."); assertions.push(test_assert!( "metadata string must match in unary", response.metadata().get(key1) == Some(&value1), format!("result={:?}", response.metadata().get(key1)) )); let mut stream = response.into_inner(); let trailers = stream.trailers().await.unwrap().unwrap(); assertions.push(test_assert!( "metadata bin must match in unary", trailers.get_bin(key2) == Some(&value2), format!("result={:?}", trailers.get_bin(key1)) )); } } #[async_trait] impl InteropTestUnimplemented for UnimplementedClient { async fn unimplemented_service(&mut self, assertions: &mut Vec) { let result = self.unimplemented_call(Request::new(Empty {})).await; assertions.push(test_assert!( "call must fail with unimplemented status code", match &result { Err(status) => status.code() == Code::Unimplemented, _ => false, }, format!("result={:?}", result) )); } } fn make_ping_pong_request(idx: usize) -> StreamingOutputCallRequest { let req_len = REQUEST_LENGTHS[idx]; let resp_len = RESPONSE_LENGTHS[idx]; StreamingOutputCallRequest { response_parameters: vec![ResponseParameters::with_size(resp_len)], payload: Some(crate::client_payload(req_len as usize)), ..Default::default() } } fn make_streaming_input_request(len: &i32) -> StreamingInputCallRequest { StreamingInputCallRequest { payload: Some(crate::client_payload(*len as usize)), ..Default::default() } } ================================================ FILE: interop/src/client_protobuf.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use crate::client::{InteropTest, InteropTestUnimplemented}; use crate::{ TestAssertion, grpc_pb::test_service_client::*, grpc_pb::unimplemented_service_client::*, grpc_pb::*, test_assert, }; use tokio::sync::mpsc; use tokio_stream::StreamExt; use tonic::async_trait; use tonic::transport::Channel; use tonic::{Code, Request, Response, Status, metadata::MetadataValue}; use tonic_protobuf::protobuf::__internal::MatcherEq; use tonic_protobuf::protobuf::proto; pub type TestClient = TestServiceClient; pub type UnimplementedClient = UnimplementedServiceClient; const LARGE_REQ_SIZE: usize = 271_828; const LARGE_RSP_SIZE: i32 = 314_159; const REQUEST_LENGTHS: &[i32] = &[27182, 8, 1828, 45904]; const RESPONSE_LENGTHS: &[i32] = &[31415, 9, 2653, 58979]; const TEST_STATUS_MESSAGE: &str = "test status message"; const SPECIAL_TEST_STATUS_MESSAGE: &str = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n"; #[async_trait] impl InteropTest for TestClient { async fn empty_unary(&mut self, assertions: &mut Vec) { let result = self.empty_call(Request::new(Empty::default())).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result { let body = response.into_inner(); assertions.push(test_assert!( "body must not be null", body.matches(&Empty::default()), format!("body={:?}", body) )); } } async fn large_unary(&mut self, assertions: &mut Vec) { use std::mem; let payload = crate::grpc_utils::client_payload(LARGE_REQ_SIZE); let req = proto!(SimpleRequest { response_type: PayloadType::Compressable, response_size: LARGE_RSP_SIZE, payload: payload, }); let result = self.unary_call(Request::new(req)).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result { let body = response.into_inner(); let payload_len = body.payload().body().len(); assertions.push(test_assert!( "body must be 314159 bytes", payload_len == LARGE_RSP_SIZE as usize, format!("mem::size_of_val(&body)={:?}", mem::size_of_val(&body)) )); } } async fn client_streaming(&mut self, assertions: &mut Vec) { let requests: Vec<_> = REQUEST_LENGTHS .iter() .map(make_streaming_input_request) .collect(); let stream = tokio_stream::iter(requests); let result = self.streaming_input_call(Request::new(stream)).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result { let body = response.into_inner(); assertions.push(test_assert!( "aggregated payload size must be 74922 bytes", body.aggregated_payload_size() == 74922, format!( "aggregated_payload_size={:?}", body.aggregated_payload_size() ) )); } } async fn server_streaming(&mut self, assertions: &mut Vec) { let req = proto!(StreamingOutputCallRequest { response_parameters: RESPONSE_LENGTHS .iter() .map(|len| ResponseParameters::with_size(*len)), }); let req = Request::new(req); let result = self.streaming_output_call(req).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result { let responses = response .into_inner() .filter_map(|m| m.ok()) .collect::>() .await; let actual_response_lengths = crate::grpc_utils::response_lengths(&responses); let asserts = vec![ test_assert!( "there should be four responses", responses.len() == 4, format!("responses.len()={:?}", responses.len()) ), test_assert!( "the response payload sizes should match input", RESPONSE_LENGTHS == actual_response_lengths.as_slice(), format!("{:?}={:?}", RESPONSE_LENGTHS, actual_response_lengths) ), ]; assertions.extend(asserts); } } async fn ping_pong(&mut self, assertions: &mut Vec) { let (tx, rx) = mpsc::unbounded_channel(); tx.send(make_ping_pong_request(0)).unwrap(); let result = self .full_duplex_call(Request::new( tokio_stream::wrappers::UnboundedReceiverStream::new(rx), )) .await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(mut response) = result.map(Response::into_inner) { let mut responses = Vec::new(); loop { match response.next().await { Some(result) => { responses.push(result.unwrap()); if responses.len() == REQUEST_LENGTHS.len() { drop(tx); break; } else { tx.send(make_ping_pong_request(responses.len())).unwrap(); } } None => { assertions.push(TestAssertion::Failed { description: "server should keep the stream open until the client closes it", expression: "Stream terminated unexpectedly early", why: None, }); break; } } } let actual_response_lengths = crate::grpc_utils::response_lengths(&responses); assertions.push(test_assert!( "there should be four responses", responses.len() == RESPONSE_LENGTHS.len(), format!("{:?}={:?}", responses.len(), RESPONSE_LENGTHS.len()) )); assertions.push(test_assert!( "the response payload sizes should match input", RESPONSE_LENGTHS == actual_response_lengths.as_slice(), format!("{:?}={:?}", RESPONSE_LENGTHS, actual_response_lengths) )); } } async fn empty_stream(&mut self, assertions: &mut Vec) { let stream = tokio_stream::empty(); let result = self.full_duplex_call(Request::new(stream)).await; assertions.push(test_assert!( "call must be successful", result.is_ok(), format!("result={:?}", result) )); if let Ok(response) = result.map(Response::into_inner) { let responses = response.collect::>().await; assertions.push(test_assert!( "there should be no responses", responses.is_empty(), format!("responses.len()={:?}", responses.len()) )); } } async fn status_code_and_message(&mut self, assertions: &mut Vec) { fn validate_response(result: Result, assertions: &mut Vec) where T: std::fmt::Debug, { assertions.push(test_assert!( "call must fail with unknown status code", match &result { Err(status) => status.code() == Code::Unknown, _ => false, }, format!("result={:?}", result) )); assertions.push(test_assert!( "call must respsond with expected status message", match &result { Err(status) => status.message() == TEST_STATUS_MESSAGE, _ => false, }, format!("result={:?}", result) )); } let simple_req = proto!(SimpleRequest { response_status: EchoStatus { code: 2, message: TEST_STATUS_MESSAGE.to_string(), }, }); let duplex_req = proto!(StreamingOutputCallRequest { response_status: EchoStatus { code: 2, message: TEST_STATUS_MESSAGE.to_string(), }, }); let result = self.unary_call(Request::new(simple_req)).await; validate_response(result, assertions); let stream = tokio_stream::once(duplex_req); let result = match self.full_duplex_call(Request::new(stream)).await { Ok(response) => { let stream = response.into_inner(); let responses = stream.collect::>().await; Ok(responses) } Err(e) => Err(e), }; validate_response(result, assertions); } async fn special_status_message(&mut self, assertions: &mut Vec) { let req = proto!(SimpleRequest { response_status: EchoStatus { code: 2, message: SPECIAL_TEST_STATUS_MESSAGE.to_string(), }, }); let result = self.unary_call(Request::new(req)).await; assertions.push(test_assert!( "call must fail with unknown status code", match &result { Err(status) => status.code() == Code::Unknown, _ => false, }, format!("result={:?}", result) )); assertions.push(test_assert!( "call must respsond with expected status message", match &result { Err(status) => status.message() == SPECIAL_TEST_STATUS_MESSAGE, _ => false, }, format!("result={:?}", result) )); } async fn unimplemented_method(&mut self, assertions: &mut Vec) { let result = self .unimplemented_call(Request::new(Empty::default())) .await; assertions.push(test_assert!( "call must fail with unimplemented status code", match &result { Err(status) => status.code() == Code::Unimplemented, _ => false, }, format!("result={:?}", result) )); } async fn custom_metadata(&mut self, assertions: &mut Vec) { let key1 = "x-grpc-test-echo-initial"; let value1: MetadataValue<_> = "test_initial_metadata_value".parse().unwrap(); let key2 = "x-grpc-test-echo-trailing-bin"; let value2 = MetadataValue::from_bytes(&[0xab, 0xab, 0xab]); let req = proto!(SimpleRequest { response_type: PayloadType::Compressable, response_size: LARGE_RSP_SIZE, payload: crate::grpc_utils::client_payload(LARGE_REQ_SIZE), }); let mut req_unary = Request::new(req); req_unary.metadata_mut().insert(key1, value1.clone()); req_unary.metadata_mut().insert_bin(key2, value2.clone()); let stream = tokio_stream::once(make_ping_pong_request(0)); let mut req_stream = Request::new(stream); req_stream.metadata_mut().insert(key1, value1.clone()); req_stream.metadata_mut().insert_bin(key2, value2.clone()); let response = self.unary_call(req_unary).await.expect("call should pass."); assertions.push(test_assert!( "metadata string must match in unary", response.metadata().get(key1) == Some(&value1), format!("result={:?}", response.metadata().get(key1)) )); assertions.push(test_assert!( "metadata bin must match in unary", response.metadata().get_bin(key2) == Some(&value2), format!("result={:?}", response.metadata().get_bin(key1)) )); let response = self .full_duplex_call(req_stream) .await .expect("call should pass."); assertions.push(test_assert!( "metadata string must match in unary", response.metadata().get(key1) == Some(&value1), format!("result={:?}", response.metadata().get(key1)) )); let mut stream = response.into_inner(); let trailers = stream.trailers().await.unwrap().unwrap(); assertions.push(test_assert!( "metadata bin must match in unary", trailers.get_bin(key2) == Some(&value2), format!("result={:?}", trailers.get_bin(key1)) )); } } #[async_trait] impl InteropTestUnimplemented for UnimplementedClient { async fn unimplemented_service(&mut self, assertions: &mut Vec) { let result = self .unimplemented_call(Request::new(Empty::default())) .await; assertions.push(test_assert!( "call must fail with unimplemented status code", match &result { Err(status) => status.code() == Code::Unimplemented, _ => false, }, format!("result={:?}", result) )); } } fn make_ping_pong_request(idx: usize) -> StreamingOutputCallRequest { let req_len = REQUEST_LENGTHS[idx]; let resp_len = RESPONSE_LENGTHS[idx]; proto!(StreamingOutputCallRequest { response_parameters: std::iter::once(ResponseParameters::with_size(resp_len)), payload: crate::grpc_utils::client_payload(req_len as usize), }) } fn make_streaming_input_request(len: &i32) -> StreamingInputCallRequest { proto!(StreamingInputCallRequest { payload: crate::grpc_utils::client_payload(*len as usize), }) } ================================================ FILE: interop/src/lib.rs ================================================ #![recursion_limit = "256"] pub mod client; pub mod client_prost; pub mod client_protobuf; pub mod server_prost; pub mod server_protobuf; pub mod pb { #![allow(dead_code, unused_imports)] include!(concat!(env!("OUT_DIR"), "/grpc.testing.rs")); } pub mod grpc_pb { #![allow( dead_code, unused_imports, clippy::clone_on_copy, clippy::useless_conversion, clippy::unnecessary_fallible_conversions, clippy::derivable_impls )] grpc::include_proto!("test"); } use std::{default, fmt, iter}; pub fn trace_init() { tracing_subscriber::fmt::init(); } pub fn client_payload(size: usize) -> pb::Payload { pb::Payload { r#type: default::Default::default(), body: iter::repeat_n(0u8, size).collect(), } } pub fn server_payload(size: usize) -> pb::Payload { pb::Payload { r#type: default::Default::default(), body: iter::repeat_n(0u8, size).collect(), } } impl pb::ResponseParameters { fn with_size(size: i32) -> Self { pb::ResponseParameters { size, ..Default::default() } } } fn response_length(response: &pb::StreamingOutputCallResponse) -> i32 { match &response.payload { Some(payload) => payload.body.len() as i32, None => 0, } } fn response_lengths(responses: &[pb::StreamingOutputCallResponse]) -> Vec { responses.iter().map(&response_length).collect() } mod grpc_utils { use super::grpc_pb; use protobuf::proto; use std::iter; pub(crate) fn client_payload(size: usize) -> grpc_pb::Payload { proto!(grpc_pb::Payload { body: iter::repeat_n(0u8, size).collect::>(), }) } impl grpc_pb::ResponseParameters { pub(crate) fn with_size(size: i32) -> Self { proto!(grpc_pb::ResponseParameters { size: size }) } } pub(crate) fn response_length(response: &grpc_pb::StreamingOutputCallResponse) -> i32 { response.payload().body().len() as i32 } pub(crate) fn response_lengths(responses: &[grpc_pb::StreamingOutputCallResponse]) -> Vec { responses.iter().map(&response_length).collect() } pub(crate) fn server_payload(size: usize) -> grpc_pb::Payload { proto!(grpc_pb::Payload { body: iter::repeat_n(0u8, size).collect::>(), }) } } #[derive(Debug)] pub enum TestAssertion { Passed { description: &'static str, }, Failed { description: &'static str, expression: &'static str, why: Option, }, } impl TestAssertion { pub fn is_failed(&self) -> bool { matches!(self, TestAssertion::Failed { .. }) } } impl fmt::Display for TestAssertion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use console::{Emoji, style}; match *self { TestAssertion::Passed { ref description } => write!( f, "{check} {desc}", check = style(Emoji("✔", "+")).green(), desc = style(description).green(), ), TestAssertion::Failed { ref description, ref expression, why: Some(ref why), } => write!( f, "{check} {desc}\n in `{exp}`: {why}", check = style(Emoji("✖", "x")).red(), desc = style(description).red(), exp = style(expression).red(), why = style(why).red(), ), TestAssertion::Failed { ref description, ref expression, why: None, } => write!( f, "{check} {desc}\n in `{exp}`", check = style(Emoji("✖", "x")).red(), desc = style(description).red(), exp = style(expression).red(), ), } } } #[macro_export] macro_rules! test_assert { ($description:expr, $assertion:expr) => { if $assertion { $crate::TestAssertion::Passed { description: $description, } } else { TestAssertion::Failed { description: $description, expression: stringify!($assertion), why: None, } } }; ($description:expr, $assertion:expr, $why:expr) => { if $assertion { $crate::TestAssertion::Passed { description: $description, } } else { $crate::TestAssertion::Failed { description: $description, expression: stringify!($assertion), why: Some($why), } } }; } ================================================ FILE: interop/src/server_prost.rs ================================================ use crate::pb::{self, *}; use async_stream::try_stream; use http::header::{HeaderMap, HeaderName}; use http_body_util::BodyExt; use std::future::Future; use std::pin::Pin; use std::result::Result as StdResult; use std::task::{Context, Poll}; use std::time::Duration; use tokio_stream::StreamExt; use tonic::{Code, Request, Response, Status, body::Body, server::NamedService}; use tower::Service; pub use pb::test_service_server::TestServiceServer; pub use pb::unimplemented_service_server::UnimplementedServiceServer; #[derive(Default, Clone)] pub struct TestService {} type Result = StdResult, Status>; type Streaming = Request>; type Stream = Pin> + Send + 'static>>; type BoxFuture = Pin> + Send + 'static>>; #[tonic::async_trait] impl pb::test_service_server::TestService for TestService { async fn empty_call(&self, _request: Request) -> Result { Ok(Response::new(Empty {})) } async fn unary_call(&self, request: Request) -> Result { let req = request.into_inner(); if let Some(echo_status) = req.response_status { let status = Status::new(Code::from_i32(echo_status.code), echo_status.message); return Err(status); } let res_size = if req.response_size >= 0 { req.response_size as usize } else { let status = Status::new(Code::InvalidArgument, "response_size cannot be negative"); return Err(status); }; let res = SimpleResponse { payload: Some(Payload { body: vec![0; res_size], ..Default::default() }), ..Default::default() }; Ok(Response::new(res)) } async fn cacheable_unary_call(&self, _: Request) -> Result { unimplemented!() } type StreamingOutputCallStream = Stream; async fn streaming_output_call( &self, req: Request, ) -> Result { let StreamingOutputCallRequest { response_parameters, .. } = req.into_inner(); let stream = try_stream! { for param in response_parameters { tokio::time::sleep(Duration::from_micros(param.interval_us as u64)).await; let payload = crate::server_payload(param.size as usize); yield StreamingOutputCallResponse { payload: Some(payload) }; } }; Ok(Response::new( Box::pin(stream) as Self::StreamingOutputCallStream )) } async fn streaming_input_call( &self, req: Streaming, ) -> Result { let mut stream = req.into_inner(); let mut aggregated_payload_size = 0; while let Some(msg) = stream.try_next().await? { aggregated_payload_size += msg.payload.unwrap().body.len() as i32; } let res = StreamingInputCallResponse { aggregated_payload_size, }; Ok(Response::new(res)) } type FullDuplexCallStream = Stream; async fn full_duplex_call( &self, req: Streaming, ) -> Result { let mut stream = req.into_inner(); if let Some(first_msg) = stream.message().await? { if let Some(echo_status) = first_msg.response_status { let status = Status::new(Code::from_i32(echo_status.code), echo_status.message); return Err(status); } let single_message = tokio_stream::once(Ok(first_msg)); let mut stream = single_message.chain(stream); let stream = try_stream! { while let Some(msg) = stream.try_next().await? { if let Some(echo_status) = msg.response_status { let status = Status::new(Code::from_i32(echo_status.code), echo_status.message); Err(status)?; } for param in msg.response_parameters { tokio::time::sleep(Duration::from_micros(param.interval_us as u64)).await; let payload = crate::server_payload(param.size as usize); yield StreamingOutputCallResponse { payload: Some(payload) }; } } }; Ok(Response::new(Box::pin(stream) as Self::FullDuplexCallStream)) } else { let stream = tokio_stream::empty(); Ok(Response::new(Box::pin(stream) as Self::FullDuplexCallStream)) } } type HalfDuplexCallStream = Stream; async fn half_duplex_call( &self, _: Streaming, ) -> Result { Err(Status::unimplemented("TODO")) } async fn unimplemented_call(&self, _: Request) -> Result { Err(Status::unimplemented("")) } } #[derive(Default)] pub struct UnimplementedService {} #[tonic::async_trait] impl pb::unimplemented_service_server::UnimplementedService for UnimplementedService { async fn unimplemented_call(&self, _req: Request) -> Result { Err(Status::unimplemented("")) } } #[derive(Clone, Default)] pub struct EchoHeadersSvc { inner: S, } impl NamedService for EchoHeadersSvc { const NAME: &'static str = S::NAME; } impl EchoHeadersSvc { pub fn new(inner: S) -> Self { Self { inner } } } impl Service> for EchoHeadersSvc where S: Service, Response = http::Response> + Send, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = BoxFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Ok(()).into() } fn call(&mut self, req: http::Request) -> Self::Future { let echo_header = req.headers().get("x-grpc-test-echo-initial").cloned(); let trailer_name = HeaderName::from_static("x-grpc-test-echo-trailing-bin"); let echo_trailer = req .headers() .get(&trailer_name) .cloned() .map(|v| HeaderMap::from_iter(std::iter::once((trailer_name, v)))); let call = self.inner.call(req); Box::pin(async move { let mut res = call.await?; if let Some(echo_header) = echo_header { res.headers_mut() .insert("x-grpc-test-echo-initial", echo_header); Ok(res .map(|b| b.with_trailers(async move { echo_trailer.map(Ok) })) .map(Body::new)) } else { Ok(res) } }) } } ================================================ FILE: interop/src/server_protobuf.rs ================================================ use crate::grpc_pb::{self, *}; use async_stream::try_stream; use http::header::{HeaderMap, HeaderName}; use http_body_util::BodyExt; use std::future::Future; use std::pin::Pin; use std::result::Result as StdResult; use std::task::{Context, Poll}; use std::time::Duration; use tokio_stream::StreamExt; use tonic::codegen::BoxStream; use tonic::{Code, Request, Response, Status, body::Body, server::NamedService}; use tonic_protobuf::protobuf::proto; use tower::Service; pub use grpc_pb::test_service_server::TestServiceServer; pub use grpc_pb::unimplemented_service_server::UnimplementedServiceServer; #[derive(Default, Clone)] pub struct TestService {} type Result = StdResult, Status>; type Streaming = Request>; type BoxFuture = Pin> + Send + 'static>>; #[tonic::async_trait] impl grpc_pb::test_service_server::TestService for TestService { async fn empty_call(&self, _request: Request) -> Result { Ok(Response::new(Empty::default())) } async fn unary_call(&self, request: Request) -> Result { let req = request.into_inner(); if req.response_status().code() != 0 { let echo_status = req.response_status(); let status = Status::new( Code::from_i32(echo_status.code()), echo_status.message().to_string(), ); return Err(status); } let res_size = if req.response_size() >= 0 { req.response_size() as usize } else { let status = Status::new(Code::InvalidArgument, "response_size cannot be negative"); return Err(status); }; let res = proto!(SimpleResponse { payload: Payload { body: vec![0; res_size], }, }); Ok(Response::new(res)) } async fn cacheable_unary_call(&self, _: Request) -> Result { unimplemented!() } async fn streaming_output_call( &self, req: tonic::Request, ) -> std::result::Result>, tonic::Status> { let stream = try_stream! { for param in req.into_inner().response_parameters() { tokio::time::sleep(Duration::from_micros(param.interval_us() as u64)).await; let payload = crate::grpc_utils::server_payload(param.size() as usize); yield proto!(StreamingOutputCallResponse { payload: payload }); } }; Ok(Response::new(Box::pin(stream))) } async fn streaming_input_call( &self, req: Streaming, ) -> Result { let mut stream = req.into_inner(); let mut aggregated_payload_size = 0; while let Some(msg) = stream.try_next().await? { aggregated_payload_size += msg.payload().body().len() as i32; } let res = proto!(StreamingInputCallResponse { aggregated_payload_size: aggregated_payload_size, }); Ok(Response::new(res)) } async fn full_duplex_call( &self, req: tonic::Request>, ) -> std::result::Result>, tonic::Status> { let mut stream = req.into_inner(); if let Some(first_msg) = stream.message().await? { if first_msg.response_status().code() != 0 { let echo_status = first_msg.response_status(); let status = Status::new( Code::from_i32(echo_status.code()), echo_status.message().to_string(), ); return Err(status); } let single_message = tokio_stream::once(Ok(first_msg)); let mut stream = single_message.chain(stream); let stream = try_stream! { while let Some(msg) = stream.try_next().await? { if msg.response_status().code() != 0 { let echo_status = msg.response_status(); let status = Status::new(Code::from_i32(echo_status.code()), echo_status.message().to_string()); Err(status)?; } for param in msg.response_parameters() { tokio::time::sleep(Duration::from_micros(param.interval_us() as u64)).await; let payload = crate::grpc_utils::server_payload(param.size() as usize); yield proto!(StreamingOutputCallResponse { payload: payload }); } } }; Ok(Response::new(Box::pin(stream))) } else { let stream = tokio_stream::empty(); Ok(Response::new(Box::pin(stream))) } } async fn half_duplex_call( &self, _request: tonic::Request>, ) -> std::result::Result>, tonic::Status> { Err(Status::unimplemented("TODO")) } async fn unimplemented_call(&self, _: Request) -> Result { Err(Status::unimplemented("")) } } #[derive(Default)] pub struct UnimplementedService {} #[tonic::async_trait] impl grpc_pb::unimplemented_service_server::UnimplementedService for UnimplementedService { async fn unimplemented_call(&self, _req: Request) -> Result { Err(Status::unimplemented("")) } } #[derive(Clone, Default)] pub struct EchoHeadersSvc { inner: S, } impl NamedService for EchoHeadersSvc { const NAME: &'static str = S::NAME; } impl EchoHeadersSvc { pub fn new(inner: S) -> Self { Self { inner } } } impl Service> for EchoHeadersSvc where S: Service, Response = http::Response> + Send, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = BoxFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Ok(()).into() } fn call(&mut self, req: http::Request) -> Self::Future { let echo_header = req.headers().get("x-grpc-test-echo-initial").cloned(); let trailer_name = HeaderName::from_static("x-grpc-test-echo-trailing-bin"); let echo_trailer = req .headers() .get(&trailer_name) .cloned() .map(|v| HeaderMap::from_iter(std::iter::once((trailer_name, v)))); let call = self.inner.call(req); Box::pin(async move { let mut res = call.await?; if let Some(echo_header) = echo_header { res.headers_mut() .insert("x-grpc-test-echo-initial", echo_header); Ok(res .map(|b| b.with_trailers(async move { echo_trailer.map(Ok) })) .map(Body::new)) } else { Ok(res) } }) } } ================================================ FILE: interop/test.sh ================================================ #!/usr/bin/env bash set -eu set -o pipefail # the go client does not support passing an argument with multiple test cases # so we loop over this array calling the binary each time around TEST_CASES=( "empty_unary" "large_unary" "client_streaming" "server_streaming" "ping_pong" "empty_stream" "status_code_and_message" "special_status_message" "custom_metadata" "unimplemented_method" "unimplemented_service" ) # join all test cases in one comma separated string (dropping the first one) # so we can call the rust client only once, reducing the noise JOINED_TEST_CASES=$(printf ",%s" "${TEST_CASES[@]}") JOINED_TEST_CASES="${JOINED_TEST_CASES:1}" set -x echo "Running for OS: ${OSTYPE}" case "$OSTYPE" in darwin*) OS="darwin"; EXT="" ;; linux*) OS="linux"; EXT="" ;; msys*) OS="windows"; EXT=".exe" ;; *) exit 2 ;; esac ARG="${1:-""}" (cd interop && cargo build --bins) SERVER="interop/bin/server_${OS}_amd64${EXT}" TLS_CA="interop/data/ca.pem" TLS_CRT="interop/data/server1.pem" TLS_KEY="interop/data/server1.key" # run the test server ./"${SERVER}" "${ARG}" --tls_cert_file $TLS_CRT --tls_key_file $TLS_KEY & SERVER_PID=$! echo ":; started grpc-go test server." cleanup() { echo ":; killing test server ${SERVER_PID}" kill "${SERVER_PID}" || true } # trap exits to make sure we kill the server process when the script exits, # regardless of why (errors, SIGTERM, etc). trap cleanup EXIT sleep 3 TARGET_DIR="$(cargo metadata --format-version 1 | jq -r '.target_directory')" "${TARGET_DIR}/debug/client" --codec=prost --test_case="${JOINED_TEST_CASES}" "${ARG}" # Test a grpc rust client against a Go server. "${TARGET_DIR}/debug/client" --codec=protobuf --test_case="${JOINED_TEST_CASES}" ${ARG} echo ":; killing test server"; kill "${SERVER_PID}"; echo "Waiting for test server to exit..." while kill -0 ${SERVER_PID} 2> /dev/null; do sleep 0.5 done CODECS=("prost" "protobuf") for CODEC in "${CODECS[@]}"; do # run the test server "${TARGET_DIR}/debug/server" "${ARG}" --codec "${CODEC}" & SERVER_PID=$! echo ":; started tonic test server with the ${CODEC} codec." sleep 3 "${TARGET_DIR}/debug/client" --codec=prost --test_case="${JOINED_TEST_CASES}" "${ARG}" # Run client test cases if [ -n "${ARG:-}" ]; then TLS_ARRAY=( \ -use_tls \ -use_test_ca \ -server_host_override=foo.test.google.fr \ -ca_file="${TLS_CA}" \ ) else TLS_ARRAY=() fi for CASE in "${TEST_CASES[@]}"; do flags=( "-test_case=${CASE}" ) # Avoid unbound variable errors on MacOS with bash version < 4.4. # See: https://stackoverflow.com/a/61551944 flags+=( ${TLS_ARRAY[@]+"${TLS_ARRAY[@]}"} ) interop/bin/client_"${OS}"_amd64"${EXT}" "${flags[@]}" done echo ":; killing test server"; kill "${SERVER_PID}"; echo "Waiting for test server to exit..." while kill -0 ${SERVER_PID} 2> /dev/null; do sleep 0.5 done done ================================================ FILE: interop/update_binaries.sh ================================================ #!/bin/bash set -e # This script updates server and client go binaries for interop tests. # It clones grpc-go, compiles interop clients and servers for linux, windows # and macos and finally deletes the cloned repo. # # It is not meant to be executed on every test run or CI and should run from # inside tonic/interop. command -v go >/dev/null 2>&1 || { echo >&2 "go executable is not available" exit 1 } if [ ! -d "./grpc-go" ]; then git clone https://github.com/grpc/grpc-go.git fi cd grpc-go PLATFORMS="darwin linux windows" ROLES="client server" ARCH=amd64 for ROLE in $ROLES; do for OS in $PLATFORMS; do FILENAME="${ROLE}_${OS}_${ARCH}" if [[ "${OS}" == "windows" ]]; then FILENAME="${FILENAME}.exe"; fi GOOS=$OS GOARCH=$ARCH go build -o "../bin/$FILENAME" "./interop/$ROLE" done done rm -rf ../grpc-go ================================================ FILE: prepare-release.sh ================================================ #!/usr/bin/env bash # Script which automates modifying source version fields, and creating a release # commit and tag. The commit and tag are not automatically pushed, nor are the # crates published (see publish-release.sh). set -ex if [ "$#" -ne 1 ] then echo "Usage: $0 " exit 1 fi DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" VERSION="$1" MINOR="$( echo "${VERSION}" | cut -d\. -f1-2 )" VERSION_MATCHER="([a-z0-9\\.-]+)" TONIC_CRATE_MATCHER="(tonic|tonic-[a-z]+)" # Update the README.md. sed -i -E "s/${TONIC_CRATE_MATCHER} = \"${VERSION_MATCHER}\"/\1 = \"${MINOR}\"/" "$DIR/examples/helloworld-tutorial.md" sed -i -E "s/${TONIC_CRATE_MATCHER} = \"${VERSION_MATCHER}\"/\1 = \"${MINOR}\"/" "$DIR/examples/routeguide-tutorial.md" CRATES=( \ "tonic" \ "tonic-build" \ "tonic-types" \ "tonic-reflection" \ "tonic-health" \ "tonic-web" \ "tonic-prost" \ "tonic-prost-build" \ ) for CRATE in "${CRATES[@]}"; do # Update Cargo.toml version fields. sed -i -E "s/^version = \"${VERSION_MATCHER}\"$/version = \"${VERSION}\"/" \ "$DIR/$CRATE/Cargo.toml" done ================================================ FILE: protoc-gen-rust-grpc/.bazelrc ================================================ # Define a custom config for common Unix-like flags build:unix --cxxopt=-std=c++17 build:unix --host_cxxopt=-std=c++17 # Inherit the common 'unix' flags for both macOS and Linux build:macos --config=unix build:linux --config=unix # Windows flags remain as they are build:windows --cxxopt=/std:c++17 build:windows --host_cxxopt=/std:c++17 build:windows --define=protobuf_allow_msvc=true ================================================ FILE: protoc-gen-rust-grpc/.gitignore ================================================ # Bazel bazel-bin bazel-genfiles bazel-out bazel-protoc-gen-rust-grpc bazel-testlogs MODULE.bazel.lock build/* CMakeFiles/* ================================================ FILE: protoc-gen-rust-grpc/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(protoc-gen-rust-grpc LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) if(MINGW) add_compile_options("-Wa,-mbig-obj") elseif(MSVC) add_compile_options("/bigobj") endif() # Options set(PROTOBUF_VERSION "33.0" CACHE STRING "Version of protobuf to download") option(BUILD_SHARED_LIBS "Build shared libraries" OFF) # Add cmake directory to module path list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # Include modules include(FetchContent) include(FetchProtobuf) # Download and configure protobuf fetch_protobuf(${PROTOBUF_VERSION}) # Download and configure abseil # Abseil version that's compatible with protobuf set(ABSEIL_VERSION "20240722.0") set(ABSEIL_URL "https://github.com/abseil/abseil-cpp/archive/refs/tags/${ABSEIL_VERSION}.tar.gz") message(STATUS "Downloading abseil-cpp ${ABSEIL_VERSION}") FetchContent_Declare( absl URL ${ABSEIL_URL} URL_HASH SHA256=f50e5ac311a81382da7fa75b97310e4b9006474f9560ac46f54a9967f07d4ae3 DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) # Set abseil build options set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "" FORCE) set(ABSL_BUILD_TESTING OFF CACHE BOOL "" FORCE) set(BUILD_TESTING OFF CACHE BOOL "" FORCE) # Force using the fetched abseil instead of system abseil set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) set(absl_DIR "${CMAKE_CURRENT_BINARY_DIR}/_deps/absl-build" CACHE PATH "" FORCE) # Prevent finding system libraries set(CMAKE_PREFIX_PATH "" CACHE STRING "" FORCE) FetchContent_MakeAvailable(absl) # Add the protoc-gen-rust-grpc executable add_executable(protoc-gen-rust-grpc src/grpc_rust_plugin.cc src/grpc_rust_generator.cc src/grpc_rust_generator.h ) # Link against protobuf and abseil target_link_libraries(protoc-gen-rust-grpc PRIVATE protobuf::libprotoc protobuf::libprotobuf absl::flat_hash_map absl::strings absl::string_view ) # Include directories target_include_directories(protoc-gen-rust-grpc PRIVATE ${protobuf_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR} ) # Set output directory set_target_properties(protoc-gen-rust-grpc PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin ) # Add protoc executable as a custom target that copies the built protoc # We explicitly name it "protoc" without version suffix if(WIN32) set(PROTOC_OUTPUT_NAME "protoc.exe") else() set(PROTOC_OUTPUT_NAME "protoc") endif() add_custom_target(copy-protoc ALL COMMAND ${CMAKE_COMMAND} -E copy_if_different $ ${CMAKE_BINARY_DIR}/bin/${PROTOC_OUTPUT_NAME} DEPENDS protoc COMMENT "Copying protoc to output directory" ) # Installation rules install(TARGETS protoc-gen-rust-grpc RUNTIME DESTINATION bin ) install(PROGRAMS ${CMAKE_BINARY_DIR}/bin/${PROTOC_OUTPUT_NAME} DESTINATION bin ) # Print summary message(STATUS "") message(STATUS "protoc-gen-rust-grpc configuration summary:") message(STATUS " Protobuf version: ${PROTOBUF_VERSION}") message(STATUS " Abseil version: ${ABSEIL_VERSION}") message(STATUS " Build type: ${CMAKE_BUILD_TYPE}") message(STATUS " C++ standard: ${CMAKE_CXX_STANDARD}") message(STATUS " Output directory: ${CMAKE_BINARY_DIR}/bin") message(STATUS " Binaries to build: protoc, protoc-gen-rust-grpc") ================================================ FILE: protoc-gen-rust-grpc/README.md ================================================ # protoc-gen-rust-grpc A protoc plugin that generates Rust gRPC service code using the Tonic framework. ## Build Requirements: - CMake 3.14 or higher - C++17 compatible compiler ```bash # Create build directory mkdir build && cd build # Configure (downloads protobuf and dependencies automatically) cmake .. -DCMAKE_BUILD_TYPE=Release # Build cmake --build . --parallel # Optional: specify a different protobuf version cmake .. -DCMAKE_BUILD_TYPE=Release -DPROTOBUF_VERSION=28.3 ``` The binaries will be in `build/bin/`: - `protoc` - The protobuf compiler - `protoc-gen-rust-grpc` - The Rust gRPC code generator plugin ## Usage **Note:** It's generally recommended to use `tonic_protobuf_build::CodeGen` and/or `protobuf_codegen::CodeGen` instead of invoking `protoc` directly. ```bash # Add the plugin to PATH export PATH="$PWD/build/bin:$PATH" # Generate Rust gRPC code protoc \ --rust_opt="experimental-codegen=enabled,kernel=upb" \ --rust_out=./generated \ --rust-grpc_out=./generated \ your_service.proto ``` ## Available Options * `message_module_path=PATH` (optional): Specifies the Rust path to the module where Protobuf messages are defined. * Default: `self` * Example: `message_module_path=crate::pb::messages` * `crate_mapping=PATH` (optional): Specifies the path to a crate mapping file for multi-crate projects. ================================================ FILE: protoc-gen-rust-grpc/cmake/FetchProtobuf.cmake ================================================ # FetchProtobuf.cmake - Helper to download and configure protobuf # # This file provides a function to download protobuf from GitHub releases # with automatic hash verification. function(fetch_protobuf VERSION) include(FetchContent) # Map of known protobuf versions to their SHA256 hashes # You can add more versions here as needed if(VERSION STREQUAL "33.0") set(HASH "cbc536064706b628dcfe507bef386ef3e2214d563657612296f1781aa155ee07") elseif(VERSION STREQUAL "32.0") set(HASH "9dfdf08129f025a6c5802613b8ee1395044fecb71d38210ca59ecad283ef68bb") elseif(VERSION STREQUAL "31.1") set(HASH "12bfd76d27b9ac3d65c00966901609e020481b9474ef75c7ff4601ac06fa0b82") elseif(VERSION STREQUAL "28.3") set(HASH "35224c34cdc65a0b59938f62aebdc99c6285fc67f5c0ba5e8273b66179e1c106") elseif(VERSION STREQUAL "27.5") set(HASH "5c56c6be6ba37b0551f9c7b69e4e80d3df30bd962aacaed5ebf3e4df4bb0f746") else() message(WARNING "Unknown protobuf version ${VERSION}, downloading without hash verification") set(HASH "") endif() set(PROTOBUF_URL "https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protobuf-${VERSION}.tar.gz") message(STATUS "Fetching protobuf ${VERSION} from ${PROTOBUF_URL}") if(HASH) FetchContent_Declare( protobuf URL ${PROTOBUF_URL} URL_HASH SHA256=${HASH} DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) else() FetchContent_Declare( protobuf URL ${PROTOBUF_URL} DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) endif() # Set protobuf build options before FetchContent_MakeAvailable set(protobuf_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(protobuf_BUILD_CONFORMANCE OFF CACHE BOOL "" FORCE) set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(protobuf_BUILD_PROTOC_BINARIES ON CACHE BOOL "" FORCE) set(protobuf_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS} CACHE BOOL "" FORCE) set(protobuf_INSTALL OFF CACHE BOOL "" FORCE) set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) set(protobuf_MSVC_STATIC_RUNTIME OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(protobuf) # Export the source directory for later use set(protobuf_SOURCE_DIR ${protobuf_SOURCE_DIR} PARENT_SCOPE) endfunction() ================================================ FILE: protoc-gen-rust-grpc/src/BUILD ================================================ # Copyright 2025 gRPC authors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. cc_binary( name = "protoc-gen-rust-grpc", srcs = [ "grpc_rust_plugin.cc", "grpc_rust_generator.h", "grpc_rust_generator.cc", ], visibility = ["//visibility:public"], deps = [ "@com_google_protobuf//:protoc_lib", ], ) ================================================ FILE: protoc-gen-rust-grpc/src/grpc_rust_generator.cc ================================================ // Copyright 2025 gRPC authors. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. #include "src/grpc_rust_generator.h" #include #include #include "absl/strings/str_join.h" #include "absl/strings/str_replace.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" #include "google/protobuf/compiler/rust/naming.h" #include "google/protobuf/descriptor.h" #include "google/protobuf/descriptor.pb.h" namespace rust_grpc_generator { namespace protobuf = google::protobuf; namespace rust = protobuf::compiler::rust; using protobuf::Descriptor; using protobuf::FileDescriptor; using protobuf::MethodDescriptor; using protobuf::ServiceDescriptor; using protobuf::SourceLocation; using protobuf::io::Printer; namespace { template std::string GrpcGetCommentsForDescriptor(const DescriptorType *descriptor) { SourceLocation location; if (descriptor->GetSourceLocation(&location)) { return location.leading_comments.empty() ? location.trailing_comments : location.leading_comments; } return ""; } std::string RustModuleForContainingType(const GrpcOpts &opts, const Descriptor *containing_type, const FileDescriptor &file) { std::vector modules; // Innermost to outermost order. const Descriptor *parent = containing_type; while (parent != nullptr) { modules.push_back(rust::RsSafeName(rust::CamelToSnakeCase(parent->name()))); parent = parent->containing_type(); } // Reverse the vector to get submodules in outer-to-inner order). std::reverse(modules.begin(), modules.end()); // If there are any modules at all, push an empty string on the end so that // we get the trailing :: if (!modules.empty()) { modules.push_back(""); } std::string crate_relative = absl::StrJoin(modules, "::"); if (opts.IsFileInCurrentCrate(file)) { return crate_relative; } std::string crate_name = absl::StrCat("::", rust::RsSafeName(opts.GetCrateName(file.name()))); return absl::StrCat(crate_name, "::", crate_relative); } std::string RsTypePathWithinMessageModule(const GrpcOpts &opts, const Descriptor &msg) { return absl::StrCat( RustModuleForContainingType(opts, msg.containing_type(), *msg.file()), rust::RsSafeName(msg.name())); } std::string RsTypePath(const Descriptor &msg, const GrpcOpts &opts, int depth) { std::string path_within_module = RsTypePathWithinMessageModule(opts, msg); if (!opts.IsFileInCurrentCrate(*msg.file())) { return path_within_module; } std::string path_to_message_module = opts.GetMessageModulePath() + "::"; if (path_to_message_module == "self::") { path_to_message_module = ""; } // If the path to the message module is defined from the crate or global // root, we don't need to add a prefix of "super::"s. if (absl::StartsWith(path_to_message_module, "crate::") || absl::StartsWith(path_to_message_module, "::")) { depth = 0; } std::string prefix = ""; for (int i = 0; i < depth; ++i) { prefix += "super::"; } return prefix + path_to_message_module + std::string(path_within_module); } absl::Status ReadFileToString(const absl::string_view name, std::string *output, bool text_mode) { char buffer[1024]; FILE *file = fopen(name.data(), text_mode ? "rt" : "rb"); if (file == nullptr) return absl::NotFoundError("Could not open file"); while (true) { size_t n = fread(buffer, 1, sizeof(buffer), file); if (n <= 0) break; output->append(buffer, n); } int error = ferror(file); if (fclose(file) != 0) return absl::InternalError("Failed to close file"); if (error != 0) { return absl::ErrnoToStatus(error, absl::StrCat("Failed to read the file ", name, ". Error code: ", error)); } return absl::OkStatus(); } } // namespace absl::StatusOr> GetImportPathToCrateNameMap(const absl::string_view mapping_file_path) { absl::flat_hash_map mapping; std::string mapping_contents; absl::Status status = ReadFileToString(mapping_file_path, &mapping_contents, true); if (!status.ok()) { return status; } std::vector lines = absl::StrSplit(mapping_contents, '\n', absl::SkipEmpty()); size_t len = lines.size(); size_t idx = 0; while (idx < len) { absl::string_view crate_name = lines[idx++]; size_t files_cnt; if (!absl::SimpleAtoi(lines[idx++], &files_cnt)) { return absl::InvalidArgumentError( "Couldn't parse number of import paths in mapping file"); } for (size_t i = 0; i < files_cnt; ++i) { mapping.insert({std::string(lines[idx++]), std::string(crate_name)}); } } return mapping; } // Method generation abstraction. // // Each service contains a set of generic methods that will be used by codegen // to generate abstraction implementations for the provided methods. class Method { public: Method() = delete; explicit Method(const MethodDescriptor *method) : method_(method) {} // The name of the method in Rust style. std::string Name() const { return rust::RsSafeName(rust::CamelToSnakeCase(method_->name())); }; // The fully-qualified name of the method, scope delimited by periods. absl::string_view FullName() const { return method_->full_name(); } // The name of the method as it appears in the .proto file. absl::string_view ProtoFieldName() const { return method_->name(); }; // Checks if the method is streamed by the client. bool IsClientStreaming() const { return method_->client_streaming(); }; // Checks if the method is streamed by the server. bool IsServerStreaming() const { return method_->server_streaming(); }; // Get comments about this method. std::string Comment() const { return GrpcGetCommentsForDescriptor(method_); }; // Checks if the method is deprecated. Default is false. bool IsDeprecated() const { return method_->options().deprecated(); } // Returns the Rust type name of request message. std::string RequestName(const GrpcOpts &opts, int depth) const { const Descriptor *input = method_->input_type(); return RsTypePath(*input, opts, depth); }; // Returns the Rust type name of response message. std::string ResponseName(const GrpcOpts &opts, int depth) const { const Descriptor *output = method_->output_type(); return RsTypePath(*output, opts, depth); }; private: const MethodDescriptor *method_; }; // Service generation abstraction. // // This class is an interface that can be implemented and consumed // by client and server generators to allow any codegen module // to generate service abstractions. class Service { public: Service() = delete; explicit Service(const ServiceDescriptor *service) : service_(service) {} // The name of the service, not including its containing scope. std::string Name() const { return rust::RsSafeName(rust::SnakeToUpperCamelCase(service_->name())); }; // The fully-qualified name of the service, scope delimited by periods. absl::string_view FullName() const { return service_->full_name(); }; // Returns a list of Methods provided by the service. std::vector Methods() const { std::vector ret; int methods_count = service_->method_count(); ret.reserve(methods_count); for (int i = 0; i < methods_count; ++i) { ret.push_back(Method(service_->method(i))); } return ret; }; // Get comments about this service. virtual std::string Comment() const { return GrpcGetCommentsForDescriptor(service_); }; private: const ServiceDescriptor *service_; }; // Formats the full path for a method call. Returns the formatted method path // (e.g., "/package.MyService/MyMethod") static std::string FormatMethodPath(const Service &service, const Method &method) { return absl::StrFormat("/%s/%s", service.FullName(), method.ProtoFieldName()); } static std::string SanitizeForRustDoc(absl::string_view raw_comment) { // 1. Escape the escape character itself first. std::string sanitized = absl::StrReplaceAll(raw_comment, {{"\\", "\\\\"}}); // 2. Escape Markdown and Rustdoc special characters. sanitized = absl::StrReplaceAll(sanitized, { {"`", "\\`"}, {"*", "\\*"}, {"_", "\\_"}, {"[", "\\["}, {"]", "\\]"}, {"#", "\\#"}, {"<", "\\<"}, {">", "\\>"}, }); return sanitized; } static std::string ProtoCommentToRustDoc(absl::string_view proto_comment) { std::string rust_doc; std::vector lines = absl::StrSplit(proto_comment, '\n'); // Remove trailing empty lines. while (!lines.empty() && lines.back().empty()) { lines.pop_back(); } for (const absl::string_view &line : lines) { // Preserve empty lines. if (line.empty()) { rust_doc += ("///\n"); } else { rust_doc += absl::StrFormat("///%s\n", SanitizeForRustDoc(line)); } } return rust_doc; } static void GenerateDeprecated(Printer &ctx) { ctx.Emit("#[deprecated]\n"); } namespace client { static void GenerateMethods(Printer &printer, const Service &service, const GrpcOpts &opts) { static const std::string unary_format = R"rs( pub async fn $ident$( &mut self, request: impl tonic::IntoRequest<$request$>, ) -> std::result::Result, tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::unknown(format!("Service was not ready: {}", e.into())) })?; let codec = $codec_name$::default(); let path = http::uri::PathAndQuery::from_static("$path$"); let mut req = request.into_request(); req.extensions_mut().insert(GrpcMethod::new("$service_name$", "$method_name$")); self.inner.unary(req, path, codec).await } )rs"; static const std::string server_streaming_format = R"rs( pub async fn $ident$( &mut self, request: impl tonic::IntoRequest<$request$>, ) -> std::result::Result>, tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::unknown(format!("Service was not ready: {}", e.into())) })?; let codec = $codec_name$::default(); let path = http::uri::PathAndQuery::from_static("$path$"); let mut req = request.into_request(); req.extensions_mut().insert(GrpcMethod::new("$service_name$", "$method_name$")); self.inner.server_streaming(req, path, codec).await } )rs"; static const std::string client_streaming_format = R"rs( pub async fn $ident$( &mut self, request: impl tonic::IntoStreamingRequest ) -> std::result::Result, tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::unknown(format!("Service was not ready: {}", e.into())) })?; let codec = $codec_name$::default(); let path = http::uri::PathAndQuery::from_static("$path$"); let mut req = request.into_streaming_request(); req.extensions_mut().insert(GrpcMethod::new("$service_name$", "$method_name$")); self.inner.client_streaming(req, path, codec).await } )rs"; static const std::string streaming_format = R"rs( pub async fn $ident$( &mut self, request: impl tonic::IntoStreamingRequest ) -> std::result::Result>, tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::unknown(format!("Service was not ready: {}", e.into())) })?; let codec = $codec_name$::default(); let path = http::uri::PathAndQuery::from_static("$path$"); let mut req = request.into_streaming_request(); req.extensions_mut().insert(GrpcMethod::new("$service_name$", "$method_name$")); self.inner.streaming(req, path, codec).await } )rs"; const std::vector methods = service.Methods(); for (const Method &method : methods) { printer.Emit(ProtoCommentToRustDoc(method.Comment())); if (method.IsDeprecated()) { GenerateDeprecated(printer); } const std::string request_type = method.RequestName(opts, 1); const std::string response_type = method.ResponseName(opts, 1); { auto vars = printer.WithVars({{"codec_name", "tonic_protobuf::ProtoCodec"}, {"ident", method.Name()}, {"request", request_type}, {"response", response_type}, {"service_name", service.FullName()}, {"path", FormatMethodPath(service, method)}, {"method_name", method.ProtoFieldName()}}); if (!method.IsClientStreaming() && !method.IsServerStreaming()) { printer.Emit(unary_format); } else if (!method.IsClientStreaming() && method.IsServerStreaming()) { printer.Emit(server_streaming_format); } else if (method.IsClientStreaming() && !method.IsServerStreaming()) { printer.Emit(client_streaming_format); } else { printer.Emit(streaming_format); } if (&method != &methods.back()) { printer.Emit("\n"); } } } } static void GenerateClient(const Service &service, Printer &printer, const GrpcOpts &opts) { std::string service_ident = absl::StrFormat("%sClient", service.Name()); std::string client_mod = absl::StrFormat("%s_client", rust::CamelToSnakeCase(service.Name())); printer.Emit( { {"client_mod", client_mod}, {"service_ident", service_ident}, {"service_doc", [&] { printer.Emit(ProtoCommentToRustDoc(service.Comment())); }}, {"methods", [&] { GenerateMethods(printer, service, opts); }}, }, R"rs( /// Generated client implementations. pub mod $client_mod$ { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, // will trigger if compression is disabled clippy::let_unit_value, )] use tonic::codegen::*; use tonic::codegen::http::Uri; $service_doc$ #[derive(Debug, Clone)] pub struct $service_ident$ { inner: tonic::client::Grpc, } impl $service_ident$ where T: tonic::client::GrpcService, T::Error: Into, T::ResponseBody: Body + std::marker::Send + 'static, ::Error: Into + std::marker::Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } pub fn with_origin(inner: T, origin: Uri) -> Self { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } pub fn with_interceptor(inner: T, interceptor: F) -> $service_ident$> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response<>::ResponseBody> >, >>::Error: Into + std::marker::Send + std::marker::Sync, { $service_ident$::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); self } /// Enable decompressing responses. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.accept_compressed(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_decoding_message_size(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_encoding_message_size(limit); self } $methods$ } })rs"); } } // namespace client namespace server { static void GenerateTraitMethods(Printer &printer, const Service &service, const GrpcOpts &opts) { static const std::string unary_format = R"rs( $method_doc$ async fn $name$(&self, request: tonic::Request<$request$>) -> std::result::Result, tonic::Status> { Err(tonic::Status::unimplemented("Not yet implemented")) } )rs"; static const std::string client_streaming_format = R"rs( $method_doc$ async fn $name$(&self, request: tonic::Request>) -> std::result::Result, tonic::Status> { Err(tonic::Status::unimplemented("Not yet implemented")) } )rs"; static const std::string server_streaming_format = R"rs( $method_doc$ async fn $name$(&self, request: tonic::Request<$request$>) -> std::result::Result>, tonic::Status> { Err(tonic::Status::unimplemented("Not yet implemented")) } )rs"; static const std::string streaming_format = R"rs( $method_doc$ async fn $name$(&self, request: tonic::Request>) -> std::result::Result>, tonic::Status> { Err(tonic::Status::unimplemented("Not yet implemented")) } )rs"; const std::vector methods = service.Methods(); for (const Method &method : methods) { const std::string request_type = method.RequestName(opts, 1); const std::string response_type = method.ResponseName(opts, 1); auto vars = printer.WithVars({ {"name", method.Name()}, {"request", request_type}, {"response", response_type}, {"method_doc", ProtoCommentToRustDoc(method.Comment())}, }); if (!method.IsClientStreaming() && !method.IsServerStreaming()) { printer.Emit(unary_format); } else if (!method.IsClientStreaming() && method.IsServerStreaming()) { printer.Emit(server_streaming_format); } else if (method.IsClientStreaming() && !method.IsServerStreaming()) { printer.Emit(client_streaming_format); } else { printer.Emit(streaming_format); } if (&method != &methods.back()) { printer.Emit("\n"); } } } static void GenerateTrait(Printer &printer, const Service &service, const GrpcOpts &opts) { const std::string trait_doc = ProtoCommentToRustDoc( " Generated trait containing gRPC methods that should " "be implemented for use with " + service.Name() + "Server."); printer.Emit( { {"trait_doc", trait_doc}, {"methods", [&] { GenerateTraitMethods(printer, service, opts); }}, }, R"rs( $trait_doc$ #[async_trait] pub trait $server_trait$ : std::marker::Send + std::marker::Sync + 'static { $methods$ } )rs"); } static void GenerateMethods(Printer &printer, const Service &service, const GrpcOpts &opts) { static const std::string unary_format = R"rs( #[allow(non_camel_case_types)] struct $service_ident$(pub Arc); impl tonic::server::UnaryService<$request$> for $service_ident$ { type Response = $response$; type Future = BoxFuture, tonic::Status>; fn call(&mut self, request: tonic::Request<$request$>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::$method_ident$(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = $service_ident$(inner); let codec = $codec_name$::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config(accept_compression_encodings, send_compression_encodings) .apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size); let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) )rs"; static const std::string server_streaming_format = R"rs( #[allow(non_camel_case_types)] struct $service_ident$(pub Arc); impl tonic::server::ServerStreamingService<$request$> for $service_ident$ { type Response = $response$; type ResponseStream = BoxStream<$response$>; type Future = BoxFuture, tonic::Status>; fn call(&mut self, request: tonic::Request<$request$>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::$method_ident$(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = $service_ident$(inner); let codec = $codec_name$::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config(accept_compression_encodings, send_compression_encodings) .apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size); let res = grpc.server_streaming(method, req).await; Ok(res) }; Box::pin(fut) )rs"; static const std::string client_streaming_format = R"rs( #[allow(non_camel_case_types)] struct $service_ident$(pub Arc); impl tonic::server::ClientStreamingService<$request$> for $service_ident$ { type Response = $response$; type Future = BoxFuture, tonic::Status>; fn call(&mut self, request: tonic::Request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::$method_ident$(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = $service_ident$(inner); let codec = $codec_name$::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config(accept_compression_encodings, send_compression_encodings) .apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size); let res = grpc.client_streaming(method, req).await; Ok(res) }; Box::pin(fut) )rs"; static const std::string streaming_format = R"rs( #[allow(non_camel_case_types)] struct $service_ident$(pub Arc); impl tonic::server::StreamingService<$request$> for $service_ident$ { type Response = $response$; type ResponseStream = BoxStream<$response$>; type Future = BoxFuture, tonic::Status>; fn call(&mut self, request: tonic::Request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::$method_ident$(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = $service_ident$(inner); let codec = $codec_name$::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config(accept_compression_encodings, send_compression_encodings) .apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size); let res = grpc.streaming(method, req).await; Ok(res) }; Box::pin(fut) )rs"; const std::vector methods = service.Methods(); for (const Method &method : methods) { const std::string request_type = method.RequestName(opts, 1); const std::string response_type = method.ResponseName(opts, 1); printer.Emit( { {"codec_name", "tonic_protobuf::ProtoCodec"}, {"service_ident", method.Name() + "Svc"}, {"method_ident", method.Name()}, {"request", request_type}, {"response", response_type}, {"server_trait", service.Name()}, {"path", FormatMethodPath(service, method)}, {"method_body", [&]() { if (!method.IsClientStreaming() && !method.IsServerStreaming()) { printer.Emit(unary_format); } else if (!method.IsClientStreaming() && method.IsServerStreaming()) { printer.Emit(server_streaming_format); } else if (method.IsClientStreaming() && !method.IsServerStreaming()) { printer.Emit(client_streaming_format); } else { printer.Emit(streaming_format); } }}, }, R"rs( "$path$" => { $method_body$ } )rs"); } } static void GenerateServer(const Service &service, Printer &printer, const GrpcOpts &opts) { std::string server_mod = absl::StrFormat("%s_server", rust::CamelToSnakeCase(service.Name())); printer.Emit( { {"server_mod", server_mod}, {"service_doc", ProtoCommentToRustDoc(service.Comment())}, {"server_service", service.Name() + "Server"}, {"service_name", service.FullName()}, {"server_trait", service.Name()}, {"generated_trait", [&] { GenerateTrait(printer, service, opts); }}, {"methods", [&] { GenerateMethods(printer, service, opts); }}, }, R"rs( /// Generated server implementations. pub mod $server_mod$ { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, // will trigger if compression is disabled clippy::let_unit_value, )] use tonic::codegen::*; $generated_trait$ $service_doc$ #[derive(Debug)] pub struct $server_service$ { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } impl $server_service$ { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } pub fn from_arc(inner: Arc) -> Self { Self { inner, accept_compression_encodings: Default::default(), send_compression_encodings: Default::default(), max_decoding_message_size: None, max_encoding_message_size: None, } } pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService where F: tonic::service::Interceptor, { InterceptedService::new(Self::new(inner), interceptor) } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Compress responses with the given encoding, if the client supports it. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.max_encoding_message_size = Some(limit); self } } impl tonic::codegen::Service> for $server_service$ where T: $server_trait$, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { type Response = http::Response; type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { $methods$ _ => Box::pin(async move { let mut response = http::Response::new(tonic::body::Body::default()); let headers = response.headers_mut(); headers.insert(tonic::Status::GRPC_STATUS, (tonic::Code::Unimplemented as i32).into()); headers.insert(http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE); Ok(response) }), } } } impl Clone for $server_service$ { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { inner, accept_compression_encodings: self.accept_compression_encodings, send_compression_encodings: self.send_compression_encodings, max_decoding_message_size: self.max_decoding_message_size, max_encoding_message_size: self.max_encoding_message_size, } } } /// Generated gRPC service name pub const SERVICE_NAME: &str = "$service_name$"; impl tonic::server::NamedService for $server_service$ { const NAME: &'static str = SERVICE_NAME; } } )rs"); } } // namespace server void GenerateService(protobuf::io::Printer &printer, const ServiceDescriptor *service_desc, const GrpcOpts &opts) { Service service = Service(service_desc); client::GenerateClient(service, printer, opts); printer.Print("\n"); server::GenerateServer(service, printer, opts); } std::string GetRsGrpcFile(const protobuf::FileDescriptor &file) { absl::string_view basename = absl::StripSuffix(file.name(), ".proto"); return absl::StrCat(basename, "_grpc.pb.rs"); } } // namespace rust_grpc_generator ================================================ FILE: protoc-gen-rust-grpc/src/grpc_rust_generator.h ================================================ // Copyright 2025 gRPC authors. // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. #ifndef PROTOC_GEN_RUST_GRPC_GRPC_RUST_GENERATOR_H_ #define PROTOC_GEN_RUST_GRPC_GRPC_RUST_GENERATOR_H_ #include "absl/log/absl_log.h" #include "google/protobuf/descriptor.h" namespace rust_grpc_generator { class GrpcOpts { public: void SetMessageModulePath(const std::string path) { message_module_path_ = std::move(path); } const std::string &GetMessageModulePath() const { return message_module_path_; } void SetImportPathToCrateName( const absl::flat_hash_map mapping) { import_path_to_crate_name_ = std::move(mapping); } void SetFilesInCurrentCrate( const std::vector files) { files_in_current_crate_ = std::move(files); } absl::string_view GetCrateName(absl::string_view import_path) const { auto it = import_path_to_crate_name_.find(import_path); if (it == import_path_to_crate_name_.end()) { ABSL_LOG(ERROR) << "Path " << import_path << " not found in crate mapping. Crate mapping contains " << import_path_to_crate_name_.size() << " entries:"; for (const auto &entry : import_path_to_crate_name_) { ABSL_LOG(ERROR) << " " << entry.first << " : " << entry.second << "\n"; } ABSL_LOG(FATAL) << "Cannot continue with missing crate mapping."; } return it->second; } bool IsFileInCurrentCrate(const google::protobuf::FileDescriptor &f) const { return std::find(files_in_current_crate_.begin(), files_in_current_crate_.end(), &f) != files_in_current_crate_.end(); } private: // Path of the module containing the generated message code. Defaults to // "self", i.e. the message code and service code are present in the same // module. std::string message_module_path_ = "self"; absl::flat_hash_map import_path_to_crate_name_ = {}; std::vector files_in_current_crate_ = {}; }; // Writes the generated service interface into the given ZeroCopyOutputStream void GenerateService(google::protobuf::io::Printer &printer, const google::protobuf::ServiceDescriptor *service, const GrpcOpts &opts); std::string GetRsGrpcFile(const google::protobuf::FileDescriptor &file); // Returns a map from import path of a .proto file to the name of the crate // covering that file. // // This function parses a .rust_crate_mapping file generated by a build system. // The file contains: // // \n // \n // > GetImportPathToCrateNameMap(const absl::string_view mapping_file_path); } // namespace rust_grpc_generator #endif // PROTOC_GEN_RUST_GRPC_GRPC_RUST_GENERATOR_H_ ================================================ FILE: protoc-gen-rust-grpc/src/grpc_rust_plugin.cc ================================================ // Copyright 2025 gRPC authors. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. #include #include "google/protobuf/compiler/code_generator.h" #include "google/protobuf/compiler/plugin.h" #include "google/protobuf/io/printer.h" #include "grpc_rust_generator.h" namespace protobuf = google::protobuf; class RustGrpcGenerator : public protobuf::compiler::CodeGenerator { public: // Protobuf 5.27 released edition 2023. #if GOOGLE_PROTOBUF_VERSION >= 5027000 uint64_t GetSupportedFeatures() const override { return Feature::FEATURE_PROTO3_OPTIONAL | Feature::FEATURE_SUPPORTS_EDITIONS; } protobuf::Edition GetMinimumEdition() const override { return protobuf::Edition::EDITION_PROTO2; } protobuf::Edition GetMaximumEdition() const override { return protobuf::Edition::EDITION_2023; } #else uint64_t GetSupportedFeatures() const override { return Feature::FEATURE_PROTO3_OPTIONAL; } #endif bool Generate(const protobuf::FileDescriptor *file, const std::string ¶meter, protobuf::compiler::GeneratorContext *context, std::string *error) const override { // Return early to avoid creating an empty output file. if (file->service_count() == 0) { return true; } std::vector> options; protobuf::compiler::ParseGeneratorParameter(parameter, &options); rust_grpc_generator::GrpcOpts grpc_opts; for (auto opt : options) { if (opt.first == "message_module_path") { grpc_opts.SetMessageModulePath(opt.second); } else if (opt.first == "crate_mapping") { absl::StatusOr> crate_map = rust_grpc_generator::GetImportPathToCrateNameMap(opt.second); if (crate_map.ok()) { grpc_opts.SetImportPathToCrateName(std::move(*crate_map)); } else { *error = std::string(crate_map.status().message()); return false; } } } std::vector files; context->ListParsedFiles(&files); grpc_opts.SetFilesInCurrentCrate(std::move(files)); auto outfile = absl::WrapUnique( context->Open(rust_grpc_generator::GetRsGrpcFile(*file))); protobuf::io::Printer printer(outfile.get()); for (int i = 0; i < file->service_count(); ++i) { const protobuf::ServiceDescriptor *service = file->service(i); rust_grpc_generator::GenerateService(printer, service, grpc_opts); } return true; } }; int main(int argc, char *argv[]) { RustGrpcGenerator generator; return protobuf::compiler::PluginMain(argc, argv, &generator); } ================================================ FILE: publish-release.sh ================================================ #!/usr/bin/env bash # Script which automates publishing a crates.io release of the prost crates. set -ex if [ "$#" -ne 0 ] then echo "Usage: $0" exit 1 fi DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" CRATES=( \ "tonic" \ "tonic-build" \ "tonic-prost" \ "tonic-prost-build" \ "tonic-types" \ "tonic-reflection" \ "tonic-health" \ "tonic-web" \ ) for CRATE in "${CRATES[@]}"; do pushd "$DIR/$CRATE" echo "Publishing $CRATE" cargo publish popd done ================================================ FILE: tests/compile/Cargo.toml ================================================ [package] name = "test-compile" edition = "2021" license = "MIT" [dependencies] prost = "0.14" [dev-dependencies] static_assertions = "1" trybuild = "1" tonic = {path = "../../tonic"} tonic-prost = {path = "../../tonic-prost"} [build-dependencies] tonic-prost-build = {path = "../../tonic-prost-build"} ================================================ FILE: tests/compile/build.rs ================================================ fn main() { tonic_prost_build::compile_protos("proto/result.proto").unwrap(); tonic_prost_build::compile_protos("proto/service.proto").unwrap(); tonic_prost_build::compile_protos("proto/stream.proto").unwrap(); tonic_prost_build::compile_protos("proto/same_name.proto").unwrap(); tonic_prost_build::compile_protos("proto/ambiguous_methods.proto").unwrap(); tonic_prost_build::compile_protos("proto/includer.proto").unwrap(); tonic_prost_build::configure() .extern_path(".root_crate_path.Animal", "crate::Animal") .compile_protos(&["proto/root_crate_path.proto"], &["."]) .unwrap(); tonic_prost_build::configure() .skip_debug(["skip_debug.Test"]) .skip_debug(["skip_debug.Output"]) .build_client(true) .build_server(true) .compile_protos(&["proto/skip_debug.proto"], &["proto"]) .unwrap(); tonic_prost_build::configure() .use_arc_self(true) .compile_protos(&["proto/use_arc_self.proto"], &["proto"]) .unwrap(); } ================================================ FILE: tests/compile/proto/ambiguous_methods.proto ================================================ syntax = "proto3"; package ambiguous_methods; message DropReq {} message DropResp {} // The generated stubs can confuse drop and clone // with the same method names from Arc, // resulting in a compile error. service HelloService { rpc Drop (DropReq) returns (DropResp); rpc Clone (DropReq) returns (DropResp); } service HelloStreamingService { rpc Drop (DropReq) returns (stream DropResp); rpc Clone (DropReq) returns (stream DropResp); } ================================================ FILE: tests/compile/proto/includee.proto ================================================ syntax = "proto3"; package includee; service Included { rpc SomeMethod(SomeRequest) returns (SomeResponse) {} } message SomeRequest {} message SomeResponse {} ================================================ FILE: tests/compile/proto/includer.proto ================================================ syntax = "proto3"; package includer; message TopMessage {} service TopService { rpc TopMethod(TopMessage) returns (TopMessage) {} } import "includee.proto"; ================================================ FILE: tests/compile/proto/result.proto ================================================ syntax = "proto3"; import "google/protobuf/empty.proto"; package result; service Result { rpc Listen(google.protobuf.Empty) returns (Reply) {} } message Reply { int32 step = 1; } ================================================ FILE: tests/compile/proto/root_crate_path.proto ================================================ syntax = "proto2"; package root_crate_path; message Animal { optional string name = 1; } service Zoo { rpc process_animal(Animal) returns (Animal) {}; } ================================================ FILE: tests/compile/proto/same_name.proto ================================================ syntax = "proto3"; package same_name; service Foo { rpc Foo(stream FooRequest) returns (stream FooResponse) {} } message FooRequest {} message FooResponse {} ================================================ FILE: tests/compile/proto/service.proto ================================================ syntax = "proto3"; package foo; service Service { rpc Foo(stream FooRequest) returns (stream FooResponse) {} } message FooRequest {} message FooResponse {} ================================================ FILE: tests/compile/proto/skip_debug.proto ================================================ syntax = "proto3"; package skip_debug; service Test { rpc Rpc(Input) returns (Output); } message Input {} message Output {} ================================================ FILE: tests/compile/proto/stream.proto ================================================ syntax = "proto3"; package stream; message Message { bool ok = 1; } service Stream { rpc RunStream(Message) returns (stream Message); } ================================================ FILE: tests/compile/proto/use_arc_self.proto ================================================ syntax = "proto3"; package use_arc_self; service Test { rpc TestRequest(SomeData) returns (SomeData); } message SomeData { // include a bunch of data so there actually is something to compress bytes data = 1; } ================================================ FILE: tests/compile/src/lib.rs ================================================ ================================================ FILE: tests/compile/tests/ui/ambiguous_methods.rs ================================================ tonic::include_proto!("ambiguous_methods"); fn main() {} ================================================ FILE: tests/compile/tests/ui/includer.rs ================================================ mod pb { tonic::include_proto!("includer"); } // Ensure that an RPC service, defined before including a file that defines // another service in a different protocol buffer package, is not incorrectly // cleared from the context of its package. type _Test = dyn pb::top_service_server::TopService; fn main() {} ================================================ FILE: tests/compile/tests/ui/result.rs ================================================ tonic::include_proto!("result"); fn main() {} ================================================ FILE: tests/compile/tests/ui/root_file_path.rs ================================================ #[derive(Clone, PartialEq, ::prost::Message)] struct Animal { #[prost(string, optional, tag = "1")] pub name: ::core::option::Option<::prost::alloc::string::String>, } mod pb { tonic::include_proto!("root_crate_path"); } fn main() {} ================================================ FILE: tests/compile/tests/ui/same_name.rs ================================================ tonic::include_proto!("foo"); fn main() {} ================================================ FILE: tests/compile/tests/ui/service.rs ================================================ tonic::include_proto!("foo"); fn main() {} ================================================ FILE: tests/compile/tests/ui/skip_debug.rs ================================================ mod pb { tonic::include_proto!("skip_debug"); } static_assertions::assert_not_impl_all!(pb::Output: std::fmt::Debug); fn main() {} ================================================ FILE: tests/compile/tests/ui/stream.rs ================================================ tonic::include_proto!("stream"); fn main() {} ================================================ FILE: tests/compile/tests/ui/use_arc_self.rs ================================================ use std::sync::Arc; use tonic::{Request, Response, Status}; tonic::include_proto!("use_arc_self"); struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn test_request( self: Arc, req: Request, ) -> Result, Status> { Ok(Response::new(req.into_inner())) } } fn main() {} ================================================ FILE: tests/compile/tests/ui.rs ================================================ #[test] fn ui() { let t = trybuild::TestCases::new(); t.pass("tests/ui/*.rs"); } ================================================ FILE: tests/compression/Cargo.toml ================================================ [package] authors = ["Lucio Franco "] edition = "2021" license = "MIT" name = "compression" [dependencies] bytes = "1" http = "1" http-body = "1" http-body-util = "0.1" hyper-util = "0.1" paste = "1.0.12" pin-project = "1.0" prost = "0.14" tokio = {version = "1.0", features = ["macros", "rt-multi-thread", "net"]} tokio-stream = "0.1" tonic = {path = "../../tonic", features = ["gzip", "deflate", "zstd"]} tonic-prost = {path = "../../tonic-prost"} tower = "0.5" tower-http = {version = "0.6", features = ["map-response-body", "map-request-body"]} [build-dependencies] tonic-prost-build = {path = "../../tonic-prost-build" } ================================================ FILE: tests/compression/build.rs ================================================ fn main() { tonic_prost_build::compile_protos("proto/test.proto").unwrap(); } ================================================ FILE: tests/compression/proto/test.proto ================================================ syntax = "proto3"; package test; import "google/protobuf/empty.proto"; service Test { rpc CompressOutputUnary(google.protobuf.Empty) returns (SomeData); rpc CompressInputUnary(SomeData) returns (google.protobuf.Empty); rpc CompressOutputServerStream(google.protobuf.Empty) returns (stream SomeData); rpc CompressInputClientStream(stream SomeData) returns (google.protobuf.Empty); rpc CompressOutputClientStream(stream SomeData) returns (SomeData); rpc CompressInputOutputBidirectionalStream(stream SomeData) returns (stream SomeData); } message SomeData { // include a bunch of data so there actually is something to compress bytes data = 1; } ================================================ FILE: tests/compression/src/bidirectional_stream.rs ================================================ use super::*; use http_body::Body; use tonic::codec::CompressionEncoding; util::parametrized_tests! { client_enabled_server_enabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_enabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()) .accept_compressed(encoding) .send_compressed(encoding); let request_bytes_counter = Arc::new(AtomicUsize::new(0)); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); #[derive(Clone)] pub struct AssertRightEncoding { encoding: CompressionEncoding, } #[allow(dead_code)] impl AssertRightEncoding { pub fn new(encoding: CompressionEncoding) -> Self { Self { encoding } } pub fn call(self, req: http::Request) -> http::Request { let expected = match self.encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {:?}", self.encoding), }; assert_eq!(req.headers().get("grpc-encoding").unwrap(), expected); req } } tokio::spawn({ let request_bytes_counter = request_bytes_counter.clone(); let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .map_request(move |req| { AssertRightEncoding::new(encoding).clone().call(req) }) .layer(measure_request_body_size_layer( request_bytes_counter.clone(), )) .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await) .send_compressed(encoding) .accept_compressed(encoding); let data = [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(); let stream = tokio_stream::iter(vec![SomeData { data: data.clone() }, SomeData { data }]); let req = Request::new(stream); let res = client .compress_input_output_bidirectional_stream(req) .await .unwrap(); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; assert_eq!(res.metadata().get("grpc-encoding").unwrap(), expected); let mut stream: Streaming = res.into_inner(); stream .next() .await .expect("stream empty") .expect("item was error"); stream .next() .await .expect("stream empty") .expect("item was error"); assert!(request_bytes_counter.load(SeqCst) < UNCOMPRESSED_MIN_BODY_SIZE); assert!(response_bytes_counter.load(SeqCst) < UNCOMPRESSED_MIN_BODY_SIZE); } ================================================ FILE: tests/compression/src/client_stream.rs ================================================ use super::*; use http_body::Body; use tonic::codec::CompressionEncoding; util::parametrized_tests! { client_enabled_server_enabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_enabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()).accept_compressed(encoding); let request_bytes_counter = Arc::new(AtomicUsize::new(0)); #[derive(Clone)] pub struct AssertRightEncoding { encoding: CompressionEncoding, } #[allow(dead_code)] impl AssertRightEncoding { pub fn new(encoding: CompressionEncoding) -> Self { Self { encoding } } pub fn call(self, req: http::Request) -> http::Request { let expected = match self.encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {:?}", self.encoding), }; assert_eq!(req.headers().get("grpc-encoding").unwrap(), expected); req } } tokio::spawn({ let request_bytes_counter = request_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .map_request(move |req| { AssertRightEncoding::new(encoding).clone().call(req) }) .layer(measure_request_body_size_layer( request_bytes_counter.clone(), )) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).send_compressed(encoding); let data = [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(); let stream = tokio_stream::iter(vec![SomeData { data: data.clone() }, SomeData { data }]); let req = Request::new(Box::pin(stream)); client.compress_input_client_stream(req).await.unwrap(); let bytes_sent = request_bytes_counter.load(SeqCst); assert!(bytes_sent < UNCOMPRESSED_MIN_BODY_SIZE); } util::parametrized_tests! { client_disabled_server_enabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_disabled_server_enabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()).accept_compressed(encoding); let request_bytes_counter = Arc::new(AtomicUsize::new(0)); fn assert_right_encoding(req: http::Request) -> http::Request { assert!(req.headers().get("grpc-encoding").is_none()); req } tokio::spawn({ let request_bytes_counter = request_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .map_request(assert_right_encoding) .layer(measure_request_body_size_layer( request_bytes_counter.clone(), )) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await); let data = [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(); let stream = tokio_stream::iter(vec![SomeData { data: data.clone() }, SomeData { data }]); let req = Request::new(Box::pin(stream)); client.compress_input_client_stream(req).await.unwrap(); let bytes_sent = request_bytes_counter.load(SeqCst); assert!(bytes_sent > UNCOMPRESSED_MIN_BODY_SIZE); } util::parametrized_tests! { client_enabled_server_disabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_disabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()); tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).send_compressed(encoding); let data = [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(); let stream = tokio_stream::iter(vec![SomeData { data: data.clone() }, SomeData { data }]); let req = Request::new(Box::pin(stream)); let status = client.compress_input_client_stream(req).await.unwrap_err(); assert_eq!(status.code(), tonic::Code::Unimplemented); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; assert_eq!( status.message(), format!("Content is compressed with `{expected}` which isn't supported") ); } util::parametrized_tests! { compressing_response_from_client_stream, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn compressing_response_from_client_stream(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()).send_compressed(encoding); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).accept_compressed(encoding); let req = Request::new(Box::pin(tokio_stream::empty())); let res = client.compress_output_client_stream(req).await.unwrap(); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; assert_eq!(res.metadata().get("grpc-encoding").unwrap(), expected); let bytes_sent = response_bytes_counter.load(SeqCst); assert!(bytes_sent < UNCOMPRESSED_MIN_BODY_SIZE); } ================================================ FILE: tests/compression/src/compressing_request.rs ================================================ use super::*; use http_body::Body; use tonic::codec::CompressionEncoding; util::parametrized_tests! { client_enabled_server_enabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_enabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()).accept_compressed(encoding); let request_bytes_counter = Arc::new(AtomicUsize::new(0)); #[derive(Clone)] pub struct AssertRightEncoding { encoding: CompressionEncoding, } #[allow(dead_code)] impl AssertRightEncoding { pub fn new(encoding: CompressionEncoding) -> Self { Self { encoding } } pub fn call(self, req: http::Request) -> http::Request { let expected = match self.encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {:?}", self.encoding), }; assert_eq!(req.headers().get("grpc-encoding").unwrap(), expected); req } } tokio::spawn({ let request_bytes_counter = request_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer( ServiceBuilder::new() .map_request(move |req| { AssertRightEncoding::new(encoding).clone().call(req) }) .layer(measure_request_body_size_layer(request_bytes_counter)) .into_inner(), ) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).send_compressed(encoding); for _ in 0..3 { client .compress_input_unary(SomeData { data: [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(), }) .await .unwrap(); let bytes_sent = request_bytes_counter.load(SeqCst); assert!(bytes_sent < UNCOMPRESSED_MIN_BODY_SIZE); } } util::parametrized_tests! { client_enabled_server_enabled_multi_encoding, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_enabled_multi_encoding(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()) .accept_compressed(CompressionEncoding::Gzip) .accept_compressed(CompressionEncoding::Zstd) .accept_compressed(CompressionEncoding::Deflate); let request_bytes_counter = Arc::new(AtomicUsize::new(0)); fn assert_right_encoding(req: http::Request) -> http::Request { let supported_encodings = ["gzip", "zstd", "deflate"]; let req_encoding = req.headers().get("grpc-encoding").unwrap(); assert!(supported_encodings.iter().any(|e| e == req_encoding)); req } tokio::spawn({ let request_bytes_counter = request_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer( ServiceBuilder::new() .map_request(assert_right_encoding) .layer(measure_request_body_size_layer(request_bytes_counter)) .into_inner(), ) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).send_compressed(encoding); for _ in 0..3 { client .compress_input_unary(SomeData { data: [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(), }) .await .unwrap(); let bytes_sent = request_bytes_counter.load(SeqCst); assert!(bytes_sent < UNCOMPRESSED_MIN_BODY_SIZE); } } parametrized_tests! { client_enabled_server_disabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_disabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()); tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).send_compressed(encoding); let status = client .compress_input_unary(SomeData { data: [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(), }) .await .unwrap_err(); assert_eq!(status.code(), tonic::Code::Unimplemented); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; assert_eq!( status.message(), format!("Content is compressed with `{expected}` which isn't supported") ); assert_eq!( status.metadata().get("grpc-accept-encoding").unwrap(), "identity" ); } parametrized_tests! { client_mark_compressed_without_header_server_enabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_mark_compressed_without_header_server_enabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()).accept_compressed(encoding); tokio::spawn({ async move { Server::builder() .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::with_interceptor( mock_io_channel(client).await, move |mut req: Request<()>| { req.metadata_mut().remove("grpc-encoding"); Ok(req) }, ) .send_compressed(CompressionEncoding::Gzip); let status = client .compress_input_unary(SomeData { data: [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(), }) .await .unwrap_err(); assert_eq!(status.code(), tonic::Code::Internal); assert_eq!( status.message(), "protocol error: received message with compressed-flag but no grpc-encoding was specified" ); } util::parametrized_tests! { limit_decoded_message_size, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[cfg(test)] async fn limit_decoded_message_size(encoding: CompressionEncoding) { use prost::Message; let under_limit_request = SomeData { data: [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(), }; let limit = under_limit_request.encoded_len(); let over_limit_request = SomeData { data: [0_u8; 1 + UNCOMPRESSED_MIN_BODY_SIZE].to_vec(), }; let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()) .accept_compressed(encoding) .max_decoding_message_size(limit); let request_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let request_bytes_counter = request_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer( ServiceBuilder::new() .layer(measure_request_body_size_layer(request_bytes_counter)) .into_inner(), ) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).send_compressed(encoding); for _ in 0..3 { // compressed messages that are under or exactly at the limit are successful. client .compress_input_unary(under_limit_request.clone()) .await .unwrap(); let bytes_sent = request_bytes_counter.load(SeqCst); assert!(bytes_sent < UNCOMPRESSED_MIN_BODY_SIZE); // compressed messages that are over the limit are fail with resource exhausted let status = client .compress_input_unary(over_limit_request.clone()) .await .unwrap_err(); assert_eq!(status.code(), tonic::Code::ResourceExhausted); } } ================================================ FILE: tests/compression/src/compressing_response.rs ================================================ use super::*; use tonic::codec::CompressionEncoding; util::parametrized_tests! { client_enabled_server_enabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_enabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); #[derive(Clone, Copy)] struct AssertCorrectAcceptEncoding { service: S, encoding: CompressionEncoding, } impl Service> for AssertCorrectAcceptEncoding where S: Service>, { type Response = S::Response; type Error = S::Error; type Future = S::Future; fn poll_ready( &mut self, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { self.service.poll_ready(cx) } fn call(&mut self, req: http::Request) -> Self::Future { let expected = match self.encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {:?}", self.encoding), }; assert_eq!( req.headers() .get("grpc-accept-encoding") .unwrap() .to_str() .unwrap(), format!("{expected},identity") ); self.service.call(req) } } let svc = test_server::TestServer::new(Svc::default()).send_compressed(encoding); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(layer_fn(|service| AssertCorrectAcceptEncoding { service, encoding, })) .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).accept_compressed(encoding); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; for _ in 0..3 { let res = client.compress_output_unary(()).await.unwrap(); assert_eq!(res.metadata().get("grpc-encoding").unwrap(), expected); let bytes_sent = response_bytes_counter.load(SeqCst); assert!(bytes_sent < UNCOMPRESSED_MIN_BODY_SIZE); } } util::parametrized_tests! { client_enabled_server_disabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_disabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() // no compression enable on the server so responses should not be compressed .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).accept_compressed(encoding); let res = client.compress_output_unary(()).await.unwrap(); assert!(res.metadata().get("grpc-encoding").is_none()); let bytes_sent = response_bytes_counter.load(SeqCst); assert!(bytes_sent > UNCOMPRESSED_MIN_BODY_SIZE); } #[tokio::test(flavor = "multi_thread")] async fn client_enabled_server_disabled_multi_encoding() { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() // no compression enable on the server so responses should not be compressed .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await) .accept_compressed(CompressionEncoding::Gzip) .accept_compressed(CompressionEncoding::Zstd) .accept_compressed(CompressionEncoding::Deflate); let res = client.compress_output_unary(()).await.unwrap(); assert!(res.metadata().get("grpc-encoding").is_none()); let bytes_sent = response_bytes_counter.load(SeqCst); assert!(bytes_sent > UNCOMPRESSED_MIN_BODY_SIZE); } util::parametrized_tests! { client_disabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_disabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); #[derive(Clone, Copy)] struct AssertCorrectAcceptEncoding(S); impl Service> for AssertCorrectAcceptEncoding where S: Service>, { type Response = S::Response; type Error = S::Error; type Future = S::Future; fn poll_ready( &mut self, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { self.0.poll_ready(cx) } fn call(&mut self, req: http::Request) -> Self::Future { assert!(req.headers().get("grpc-accept-encoding").is_none()); self.0.call(req) } } let svc = test_server::TestServer::new(Svc::default()).send_compressed(encoding); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(layer_fn(AssertCorrectAcceptEncoding)) .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await); let res = client.compress_output_unary(()).await.unwrap(); assert!(res.metadata().get("grpc-encoding").is_none()); let bytes_sent = response_bytes_counter.load(SeqCst); assert!(bytes_sent > UNCOMPRESSED_MIN_BODY_SIZE); } util::parametrized_tests! { server_replying_with_unsupported_encoding, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn server_replying_with_unsupported_encoding(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()).send_compressed(encoding); fn add_weird_content_encoding(mut response: http::Response) -> http::Response { response .headers_mut() .insert("grpc-encoding", "br".parse().unwrap()); response } tokio::spawn(async move { Server::builder() .layer( ServiceBuilder::new() .map_response(add_weird_content_encoding) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).accept_compressed(encoding); let status: Status = client.compress_output_unary(()).await.unwrap_err(); assert_eq!(status.code(), tonic::Code::Unimplemented); assert_eq!( status.message(), "Content is compressed with `br` which isn't supported" ); } util::parametrized_tests! { disabling_compression_on_single_response, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn disabling_compression_on_single_response(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc { disable_compressing_on_response: true, }) .send_compressed(encoding); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).accept_compressed(encoding); let res = client.compress_output_unary(()).await.unwrap(); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; assert_eq!(res.metadata().get("grpc-encoding").unwrap(), expected); let bytes_sent = response_bytes_counter.load(SeqCst); assert!(bytes_sent > UNCOMPRESSED_MIN_BODY_SIZE); } util::parametrized_tests! { disabling_compression_on_response_but_keeping_compression_on_stream, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn disabling_compression_on_response_but_keeping_compression_on_stream( encoding: CompressionEncoding, ) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc { disable_compressing_on_response: true, }) .send_compressed(encoding); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).accept_compressed(encoding); let res = client.compress_output_server_stream(()).await.unwrap(); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; assert_eq!(res.metadata().get("grpc-encoding").unwrap(), expected); let mut stream: Streaming = res.into_inner(); stream .next() .await .expect("stream empty") .expect("item was error"); assert!(response_bytes_counter.load(SeqCst) < UNCOMPRESSED_MIN_BODY_SIZE); stream .next() .await .expect("stream empty") .expect("item was error"); assert!(response_bytes_counter.load(SeqCst) < UNCOMPRESSED_MIN_BODY_SIZE); } util::parametrized_tests! { disabling_compression_on_response_from_client_stream, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn disabling_compression_on_response_from_client_stream(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc { disable_compressing_on_response: true, }) .send_compressed(encoding); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).accept_compressed(encoding); let req = Request::new(Box::pin(tokio_stream::empty())); let res = client.compress_output_client_stream(req).await.unwrap(); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; assert_eq!(res.metadata().get("grpc-encoding").unwrap(), expected); let bytes_sent = response_bytes_counter.load(SeqCst); assert!(bytes_sent > UNCOMPRESSED_MIN_BODY_SIZE); } util::parametrized_tests! { limit_decoded_message_size, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[cfg(test)] async fn limit_decoded_message_size(encoding: CompressionEncoding) { use prost::Message; let under_limit_request = SomeData { data: [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(), }; let limit = under_limit_request.encoded_len(); let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()).send_compressed(encoding); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; // compressed messages that are under or exactly at the limit are successful. let mut under_limit_client = test_client::TestClient::new(mock_io_channel(client).await) .accept_compressed(encoding) .max_decoding_message_size(limit); for _ in 0..3 { let res = under_limit_client.compress_output_unary(()).await.unwrap(); assert_eq!(res.metadata().get("grpc-encoding").unwrap(), expected); let bytes_sent = response_bytes_counter.load(SeqCst); assert!(bytes_sent < UNCOMPRESSED_MIN_BODY_SIZE); } // compressed messages that are over the limit are fail with resource exhausted let mut over_limit_client = under_limit_client.max_decoding_message_size(limit - 1); for _ in 0..3 { let status = over_limit_client .compress_output_unary(()) .await .unwrap_err(); assert_eq!(status.code(), tonic::Code::ResourceExhausted); } } ================================================ FILE: tests/compression/src/lib.rs ================================================ #![allow(unused_imports)] use self::util::*; use crate::util::mock_io_channel; use std::{ pin::Pin, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, }; use tokio::net::TcpListener; use tokio_stream::{Stream, StreamExt}; use tonic::{ transport::{Channel, Endpoint, Server, Uri}, Request, Response, Status, Streaming, }; use tower::{layer::layer_fn, service_fn, Service, ServiceBuilder}; use tower_http::{map_request_body::MapRequestBodyLayer, map_response_body::MapResponseBodyLayer}; mod bidirectional_stream; mod client_stream; mod compressing_request; mod compressing_response; mod server_stream; mod util; tonic::include_proto!("test"); #[derive(Debug, Default)] struct Svc { disable_compressing_on_response: bool, } const UNCOMPRESSED_MIN_BODY_SIZE: usize = 1024; impl Svc { fn prepare_response(&self, mut res: Response) -> Response { if self.disable_compressing_on_response { res.disable_compression(); } res } } #[tonic::async_trait] impl test_server::Test for Svc { async fn compress_output_unary(&self, _req: Request<()>) -> Result, Status> { let data = [0_u8; UNCOMPRESSED_MIN_BODY_SIZE]; Ok(self.prepare_response(Response::new(SomeData { data: data.to_vec(), }))) } async fn compress_input_unary(&self, req: Request) -> Result, Status> { assert_eq!(req.into_inner().data.len(), UNCOMPRESSED_MIN_BODY_SIZE); Ok(Response::new(())) } type CompressOutputServerStreamStream = Pin> + Send + 'static>>; async fn compress_output_server_stream( &self, _req: Request<()>, ) -> Result, Status> { let data = [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(); let stream = tokio_stream::iter(std::iter::repeat(SomeData { data })) .take(2) .map(Ok::<_, Status>); Ok(self.prepare_response(Response::new(Box::pin(stream)))) } async fn compress_input_client_stream( &self, req: Request>, ) -> Result, Status> { let mut stream = req.into_inner(); while let Some(item) = stream.next().await { item.unwrap(); } Ok(self.prepare_response(Response::new(()))) } async fn compress_output_client_stream( &self, req: Request>, ) -> Result, Status> { let mut stream = req.into_inner(); while let Some(item) = stream.next().await { item.unwrap(); } let data = [0_u8; UNCOMPRESSED_MIN_BODY_SIZE]; Ok(self.prepare_response(Response::new(SomeData { data: data.to_vec(), }))) } type CompressInputOutputBidirectionalStreamStream = Pin> + Send + 'static>>; async fn compress_input_output_bidirectional_stream( &self, req: Request>, ) -> Result, Status> { let mut stream = req.into_inner(); while let Some(item) = stream.next().await { item.unwrap(); } let data = [0_u8; UNCOMPRESSED_MIN_BODY_SIZE].to_vec(); let stream = tokio_stream::iter(std::iter::repeat(SomeData { data })) .take(2) .map(Ok::<_, Status>); Ok(self.prepare_response(Response::new(Box::pin(stream)))) } } ================================================ FILE: tests/compression/src/server_stream.rs ================================================ use super::*; use tonic::codec::CompressionEncoding; use tonic::Streaming; util::parametrized_tests! { client_enabled_server_enabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_enabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()).send_compressed(encoding); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).accept_compressed(encoding); let res = client.compress_output_server_stream(()).await.unwrap(); let expected = match encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {encoding:?}"), }; assert_eq!(res.metadata().get("grpc-encoding").unwrap(), expected); let mut stream: Streaming = res.into_inner(); stream .next() .await .expect("stream empty") .expect("item was error"); assert!(response_bytes_counter.load(SeqCst) < UNCOMPRESSED_MIN_BODY_SIZE); stream .next() .await .expect("stream empty") .expect("item was error"); assert!(response_bytes_counter.load(SeqCst) < UNCOMPRESSED_MIN_BODY_SIZE); } util::parametrized_tests! { client_disabled_server_enabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, } #[allow(dead_code)] async fn client_disabled_server_enabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()).send_compressed(encoding); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await); let res = client.compress_output_server_stream(()).await.unwrap(); assert!(res.metadata().get("grpc-encoding").is_none()); let mut stream: Streaming = res.into_inner(); stream .next() .await .expect("stream empty") .expect("item was error"); assert!(response_bytes_counter.load(SeqCst) > UNCOMPRESSED_MIN_BODY_SIZE); } util::parametrized_tests! { client_enabled_server_disabled, zstd: CompressionEncoding::Zstd, gzip: CompressionEncoding::Gzip, deflate: CompressionEncoding::Deflate, } #[allow(dead_code)] async fn client_enabled_server_disabled(encoding: CompressionEncoding) { let (client, server) = tokio::io::duplex(UNCOMPRESSED_MIN_BODY_SIZE * 10); let svc = test_server::TestServer::new(Svc::default()); let response_bytes_counter = Arc::new(AtomicUsize::new(0)); tokio::spawn({ let response_bytes_counter = response_bytes_counter.clone(); async move { Server::builder() .layer( ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| { util::CountBytesBody { inner: body, counter: response_bytes_counter.clone(), } })) .into_inner(), ) .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); } }); let mut client = test_client::TestClient::new(mock_io_channel(client).await).accept_compressed(encoding); let res = client.compress_output_server_stream(()).await.unwrap(); assert!(res.metadata().get("grpc-encoding").is_none()); let mut stream: Streaming = res.into_inner(); stream .next() .await .expect("stream empty") .expect("item was error"); assert!(response_bytes_counter.load(SeqCst) > UNCOMPRESSED_MIN_BODY_SIZE); } ================================================ FILE: tests/compression/src/util.rs ================================================ use super::*; use bytes::{Buf, Bytes}; use http_body::{Body as HttpBody, Frame}; use http_body_util::BodyExt as _; use hyper_util::rt::TokioIo; use pin_project::pin_project; use std::{ pin::Pin, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, task::{ready, Context, Poll}, }; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tonic::body::Body; use tonic::codec::CompressionEncoding; use tonic::transport::{server::Connected, Channel}; use tower_http::map_request_body::MapRequestBodyLayer; macro_rules! parametrized_tests { ($fn_name:ident, $($test_name:ident: $input:expr),+ $(,)?) => { paste::paste! { $( #[tokio::test(flavor = "multi_thread")] async fn [<$fn_name _ $test_name>]() { let input = $input; $fn_name(input).await; } )+ } } } pub(crate) use parametrized_tests; /// A body that tracks how many bytes passes through it #[pin_project] pub struct CountBytesBody { #[pin] pub inner: B, pub counter: Arc, } impl HttpBody for CountBytesBody where B: HttpBody, { type Data = B::Data; type Error = B::Error; fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { let this = self.project(); let counter: Arc = this.counter.clone(); match ready!(this.inner.poll_frame(cx)) { Some(Ok(chunk)) => { println!("response body chunk size = {}", frame_data_length(&chunk)); counter.fetch_add(frame_data_length(&chunk), SeqCst); Poll::Ready(Some(Ok(chunk))) } x => Poll::Ready(x), } } fn is_end_stream(&self) -> bool { self.inner.is_end_stream() } fn size_hint(&self) -> http_body::SizeHint { self.inner.size_hint() } } fn frame_data_length(frame: &http_body::Frame) -> usize { if let Some(data) = frame.data_ref() { data.len() } else { 0 } } #[pin_project] struct ChannelBody { #[pin] rx: tokio::sync::mpsc::Receiver>, } impl ChannelBody { pub fn new() -> (tokio::sync::mpsc::Sender>, Self) { let (tx, rx) = tokio::sync::mpsc::channel(32); (tx, Self { rx }) } } impl HttpBody for ChannelBody where T: Buf, { type Data = T; type Error = tonic::Status; fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { let frame = ready!(self.project().rx.poll_recv(cx)); Poll::Ready(frame.map(Ok)) } } #[allow(dead_code)] pub fn measure_request_body_size_layer( bytes_sent_counter: Arc, ) -> MapRequestBodyLayer Body + Clone> { MapRequestBodyLayer::new(move |mut body: Body| { let (tx, new_body) = ChannelBody::new(); let bytes_sent_counter = bytes_sent_counter.clone(); tokio::spawn(async move { while let Some(chunk) = body.frame().await { let chunk = chunk.unwrap(); println!("request body chunk size = {}", frame_data_length(&chunk)); bytes_sent_counter.fetch_add(frame_data_length(&chunk), SeqCst); tx.send(chunk).await.unwrap(); } }); Body::new(new_body) }) } #[allow(dead_code)] pub async fn mock_io_channel(client: tokio::io::DuplexStream) -> Channel { let mut client = Some(client); Endpoint::try_from("http://[::]:50051") .unwrap() .connect_with_connector(service_fn(move |_: Uri| { let client = TokioIo::new(client.take().unwrap()); async move { Ok::<_, std::io::Error>(client) } })) .await .unwrap() } #[derive(Clone)] pub struct AssertRightEncoding { encoding: CompressionEncoding, } #[allow(dead_code)] impl AssertRightEncoding { pub fn new(encoding: CompressionEncoding) -> Self { Self { encoding } } pub fn call(self, req: http::Request) -> http::Request { let expected = match self.encoding { CompressionEncoding::Gzip => "gzip", CompressionEncoding::Zstd => "zstd", CompressionEncoding::Deflate => "deflate", _ => panic!("unexpected encoding {:?}", self.encoding), }; assert_eq!(req.headers().get("grpc-encoding").unwrap(), expected); req } } ================================================ FILE: tests/default_stubs/Cargo.toml ================================================ [package] authors = ["Jordan Singh "] edition = "2021" license = "MIT" name = "default_stubs" [dependencies] tokio = {version = "1.0", features = ["macros", "rt-multi-thread", "net"]} tokio-stream = {version = "0.1", features = ["net"]} tonic = {path = "../../tonic"} tonic-prost = {path = "../../tonic-prost"} [dev-dependencies] tempfile = "3.20" [build-dependencies] tonic-prost-build = {path = "../../tonic-prost-build" } ================================================ FILE: tests/default_stubs/build.rs ================================================ fn main() { tonic_prost_build::configure() .compile_protos(&["proto/test.proto"], &["proto"]) .unwrap(); tonic_prost_build::configure() .generate_default_stubs(true) .compile_protos(&["proto/test_default.proto"], &["proto"]) .unwrap(); } ================================================ FILE: tests/default_stubs/proto/test.proto ================================================ syntax = "proto3"; package test; import "google/protobuf/empty.proto"; service Test { rpc Unary(google.protobuf.Empty) returns (google.protobuf.Empty); rpc ServerStream(google.protobuf.Empty) returns (stream google.protobuf.Empty); rpc ClientStream(stream google.protobuf.Empty) returns (google.protobuf.Empty); rpc BidirectionalStream(stream google.protobuf.Empty) returns (stream google.protobuf.Empty); } ================================================ FILE: tests/default_stubs/proto/test_default.proto ================================================ syntax = "proto3"; package test_default; import "google/protobuf/empty.proto"; service TestDefault { rpc Unary(google.protobuf.Empty) returns (google.protobuf.Empty); rpc ServerStream(google.protobuf.Empty) returns (stream google.protobuf.Empty); rpc ClientStream(stream google.protobuf.Empty) returns (google.protobuf.Empty); rpc BidirectionalStream(stream google.protobuf.Empty) returns (stream google.protobuf.Empty); } ================================================ FILE: tests/default_stubs/src/lib.rs ================================================ use std::pin::Pin; use tokio_stream::Stream; use tonic::{Request, Response, Status, Streaming}; tonic::include_proto!("test"); tonic::include_proto!("test_default"); #[derive(Debug, Default)] pub struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { type ServerStreamStream = Pin> + Send + 'static>>; type BidirectionalStreamStream = Pin> + Send + 'static>>; async fn unary(&self, _: Request<()>) -> Result, Status> { Err(Status::permission_denied("")) } async fn server_stream( &self, _: Request<()>, ) -> Result, Status> { Err(Status::permission_denied("")) } async fn client_stream(&self, _: Request>) -> Result, Status> { Err(Status::permission_denied("")) } async fn bidirectional_stream( &self, _: Request>, ) -> Result, Status> { Err(Status::permission_denied("")) } } #[tonic::async_trait] impl test_default_server::TestDefault for Svc { // Default unimplemented stubs provided here. } ================================================ FILE: tests/default_stubs/tests/default.rs ================================================ use default_stubs::test_client::TestClient; use default_stubs::*; use std::net::SocketAddr; use tokio::net::TcpListener; use tokio_stream::{Stream, StreamExt}; use tonic::transport::Channel; use tonic::transport::Server; fn echo_requests_iter() -> impl Stream { tokio_stream::iter(1..usize::MAX).map(|_| ()) } async fn test_default_stubs( mut client: TestClient, mut client_default_stubs: TestClient, ) { use tonic::Code; // First validate pre-existing functionality (trait has no default implementation, we explicitly return PermissionDenied in lib.rs). assert_eq!( client.unary(()).await.unwrap_err().code(), Code::PermissionDenied ); assert_eq!( client.server_stream(()).await.unwrap_err().code(), Code::PermissionDenied ); assert_eq!( client .client_stream(echo_requests_iter().take(5)) .await .unwrap_err() .code(), Code::PermissionDenied ); assert_eq!( client .bidirectional_stream(echo_requests_iter().take(5)) .await .unwrap_err() .code(), Code::PermissionDenied ); // Then validate opt-in new functionality (trait has default implementation of returning Unimplemented). assert_eq!( client_default_stubs.unary(()).await.unwrap_err().code(), Code::Unimplemented ); assert_eq!( client_default_stubs .server_stream(()) .await .unwrap_err() .code(), Code::Unimplemented ); assert_eq!( client_default_stubs .client_stream(echo_requests_iter().take(5)) .await .unwrap_err() .code(), Code::Unimplemented ); assert_eq!( client_default_stubs .bidirectional_stream(echo_requests_iter().take(5)) .await .unwrap_err() .code(), Code::Unimplemented ); } #[tokio::test()] async fn test_default_stubs_tcp() { let addrs = run_services_in_background().await; let client = test_client::TestClient::connect(format!("http://{}", addrs.0)) .await .unwrap(); let client_default_stubs = test_client::TestClient::connect(format!("http://{}", addrs.1)) .await .unwrap(); test_default_stubs(client, client_default_stubs).await; } #[tokio::test()] #[cfg(not(target_os = "windows"))] async fn test_default_stubs_uds() { let addrs = run_services_in_background_uds().await; let client = test_client::TestClient::connect(addrs.0).await.unwrap(); let client_default_stubs = test_client::TestClient::connect(addrs.1).await.unwrap(); test_default_stubs(client, client_default_stubs).await; } async fn run_services_in_background() -> (SocketAddr, SocketAddr) { let svc = test_server::TestServer::new(Svc {}); let svc_default_stubs = test_default_server::TestDefaultServer::new(Svc {}); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let listener_default_stubs = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr_default_stubs = listener_default_stubs.local_addr().unwrap(); tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)) .await .unwrap(); }); tokio::spawn(async move { Server::builder() .add_service(svc_default_stubs) .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new( listener_default_stubs, )) .await .unwrap(); }); (addr, addr_default_stubs) } #[cfg(not(target_os = "windows"))] async fn run_services_in_background_uds() -> (String, String) { use tokio::net::UnixListener; let svc = test_server::TestServer::new(Svc {}); let svc_default_stubs = test_default_server::TestDefaultServer::new(Svc {}); let tmpdir = tempfile::Builder::new() .prefix("tonic-test-") .tempdir() .unwrap() .keep(); let uds_filepath = tmpdir.join("impl.sock").to_str().unwrap().to_string(); let listener = UnixListener::bind(uds_filepath.as_str()).unwrap(); let uds_addr = format!("unix://{uds_filepath}"); let uds_default_stubs_filepath = tmpdir.join("stub.sock").to_str().unwrap().to_string(); let listener_default_stubs = UnixListener::bind(uds_default_stubs_filepath.as_str()).unwrap(); let uds_default_stubs_addr = format!("unix://{uds_default_stubs_filepath}"); tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming(tokio_stream::wrappers::UnixListenerStream::new(listener)) .await .unwrap(); }); tokio::spawn(async move { Server::builder() .add_service(svc_default_stubs) .serve_with_incoming(tokio_stream::wrappers::UnixListenerStream::new( listener_default_stubs, )) .await .unwrap(); }); (uds_addr, uds_default_stubs_addr) } ================================================ FILE: tests/deprecated_methods/Cargo.toml ================================================ [package] name = "deprecated_methods" edition = "2021" license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] prost = "0.14" tonic = { path = "../../tonic" } tonic-prost = { path = "../../tonic-prost" } [build-dependencies] tonic-prost-build = { path = "../../tonic-prost-build" } ================================================ FILE: tests/deprecated_methods/build.rs ================================================ fn main() { tonic_prost_build::compile_protos("proto/test.proto").unwrap(); } ================================================ FILE: tests/deprecated_methods/proto/test.proto ================================================ syntax = "proto3"; package test; service Service1 { rpc Deprecated(Request) returns (Response) { option deprecated = true; } rpc NotDeprecated(Request) returns (Response); } message Request {} message Response {} ================================================ FILE: tests/deprecated_methods/src/lib.rs ================================================ pub mod pb { tonic::include_proto!("test"); } ================================================ FILE: tests/deprecated_methods/tests/deprecated_methods.rs ================================================ use std::{fs, path::PathBuf}; #[test] fn test() { let path = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("test.rs"); let s = fs::read_to_string(path) .unwrap() .split_whitespace() .collect::>() .join(" "); assert!(s.contains("#[deprecated] pub async fn deprecated(")); assert!(!s.contains("#[deprecated] pub async fn not_deprecated(")); } ================================================ FILE: tests/disable_comments/Cargo.toml ================================================ [package] authors = ["bouzuya "] edition = "2021" license = "MIT" name = "disable-comments" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] prost = "0.14" tonic = { path = "../../tonic" } tonic-prost = { path = "../../tonic-prost" } [build-dependencies] tonic-prost-build = { path = "../../tonic-prost-build" } ================================================ FILE: tests/disable_comments/build.rs ================================================ fn main() { let mut config = tonic_prost_build::Config::default(); config.disable_comments(["test.Input1", "test.Output1"]); tonic_prost_build::configure() .disable_comments(["test.Service1"]) .disable_comments(["test.Service1.Rpc1"]) .build_client(true) .build_server(true) .compile_with_config(config, &["proto/test.proto"], &["proto"]) .unwrap(); } ================================================ FILE: tests/disable_comments/proto/test.proto ================================================ syntax = "proto3"; package test; // This comment will be removed. service Service1 { // This comment will be removed. rpc Rpc1(Input1) returns (Output1); // This comment will not be removed. rpc Rpc2(Input2) returns (Output2); } // This comment will not be removed. service Service2 { // This comment will not be removed. rpc Rpc(Input1) returns (Output1); } // This comment will be removed. message Input1 {} // This comment will not be removed. message Input2 {} // This comment will be removed. message Output1 {} // This comment will not be removed. message Output2 {} ================================================ FILE: tests/disable_comments/src/lib.rs ================================================ pub mod pb { tonic::include_proto!("test"); } ================================================ FILE: tests/disable_comments/tests/disable_comments.rs ================================================ use std::{fs, path::PathBuf}; #[test] fn test() { let path = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("test.rs"); let s = fs::read_to_string(path).unwrap(); assert!(!s.contains("This comment will be removed.")); let mut count = 0_usize; let mut index = 0_usize; while let Some(found) = s[index..].find("This comment will not be removed.") { index += found + 1; count += 1; } assert_eq!(count, 2 + 3 + 3); // message: 2, client: 3, server: 3 } ================================================ FILE: tests/extern_path/my_application/Cargo.toml ================================================ [package] authors = ["Danny Hua "] edition = "2021" license = "MIT" name = "my_application" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] prost = "0.14" tonic = {path = "../../../tonic"} tonic-prost = {path = "../../../tonic-prost"} uuid = {package = "uuid1", path = "../uuid"} [build-dependencies] tonic-prost-build = {path = "../../../tonic-prost-build"} ================================================ FILE: tests/extern_path/my_application/build.rs ================================================ fn main() -> Result<(), std::io::Error> { tonic_prost_build::configure() .build_server(false) .build_client(true) .extern_path(".uuid", "::uuid") .compile_protos( &["service.proto", "uuid.proto"], &["../proto/my_application", "../proto/uuid"], )?; Ok(()) } ================================================ FILE: tests/extern_path/my_application/src/main.rs ================================================ use uuid::DoSomething; mod pb { tonic::include_proto!("my_application"); } fn main() { // verify that extern_path to replace proto's with impl's from other crates works. let message = pb::MyMessage { message_id: Some(::uuid::Uuid { uuid_str: "".to_string(), }), some_payload: "".to_string(), }; dbg!(message.message_id.unwrap().do_it()); } #[cfg(test)] #[test] fn service_types_have_extern_types() { // verify that extern_path to replace proto's with impl's from other crates works. let message = pb::MyMessage { message_id: Some(::uuid::Uuid { uuid_str: "not really a uuid".to_string(), }), some_payload: "payload".to_string(), }; assert_eq!(message.message_id.unwrap().do_it(), "Done"); } ================================================ FILE: tests/extern_path/proto/my_application/service.proto ================================================ syntax = "proto3"; package my_application; option go_package = "my_applicationpb"; option java_multiple_files = true; option java_outer_classname = "ServiceProto"; option java_package = "com.my_application"; import "uuid.proto"; message MyMessage { uuid.Uuid message_id = 1; string some_payload = 2; } service MyService { rpc GetUuid(MyMessage) returns (uuid.Uuid){ } rpc GetMyMessage(uuid.Uuid) returns (MyMessage){ } } ================================================ FILE: tests/extern_path/proto/uuid/uuid.proto ================================================ syntax = "proto3"; package uuid; option go_package = "uuidpb"; option java_multiple_files = true; option java_outer_classname = "UuidProto"; option java_package = "com.uuid"; message Uuid { string uuid_str = 1; } ================================================ FILE: tests/extern_path/uuid/Cargo.toml ================================================ [package] authors = ["Danny Hua "] edition = "2021" license = "MIT" name = "uuid1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] prost = "0.14" [build-dependencies] prost-build = "0.14" ================================================ FILE: tests/extern_path/uuid/build.rs ================================================ fn main() { prost_build::compile_protos(&["uuid/uuid.proto"], &["../proto/"]).unwrap(); } ================================================ FILE: tests/extern_path/uuid/src/lib.rs ================================================ include!(concat!(env!("OUT_DIR"), "/uuid.rs")); pub trait DoSomething { fn do_it(&self) -> String; } impl DoSomething for Uuid { fn do_it(&self) -> String { "Done".to_string() } } ================================================ FILE: tests/integration_tests/Cargo.toml ================================================ [package] authors = ["Lucio Franco "] edition = "2021" license = "MIT" name = "integration-tests" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] bytes = "1.0" prost = "0.14" tokio = {version = "1.0", features = ["macros", "rt-multi-thread", "net", "sync"]} tonic = {path = "../../tonic"} tonic-prost = {path = "../../tonic-prost"} tracing-subscriber = {version = "0.3"} [dev-dependencies] http = "1" http-body = "1" hyper-util = "0.1" rustls = {version = "0.23", features = ["ring"]} tokio-stream = {version = "0.1.5", features = ["net"]} tower = "0.5" tower-http = { version = "0.6", features = ["set-header", "trace"] } tower-service = "0.3" [build-dependencies] tonic-prost-build = {path = "../../tonic-prost-build"} ================================================ FILE: tests/integration_tests/build.rs ================================================ fn main() { tonic_prost_build::compile_protos("proto/test.proto").unwrap(); tonic_prost_build::compile_protos("proto/stream.proto").unwrap(); } ================================================ FILE: tests/integration_tests/proto/stream.proto ================================================ syntax = "proto3"; package stream; service TestStream { rpc StreamCall(InputStream) returns (stream OutputStream); } message InputStream {} message OutputStream {} ================================================ FILE: tests/integration_tests/proto/test.proto ================================================ syntax = "proto3"; package test; service Test { rpc UnaryCall(Input) returns (Output); } message Input {} message Output {} service Test1 { rpc UnaryCall(Input1) returns (Output1); rpc StreamCall(Input1) returns (stream Output1); } message Input1 { bytes buf = 1; } message Output1 { bytes buf = 1; } ================================================ FILE: tests/integration_tests/src/lib.rs ================================================ pub mod pb { tonic::include_proto!("test"); tonic::include_proto!("stream"); } pub mod mock { use std::{ io::IoSlice, pin::Pin, task::{Context, Poll}, }; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tonic::transport::server::Connected; #[derive(Debug)] pub struct MockStream(pub tokio::io::DuplexStream); impl Connected for MockStream { type ConnectInfo = (); /// Create type holding information about the connection. fn connect_info(&self) -> Self::ConnectInfo {} } impl AsyncRead for MockStream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { Pin::new(&mut self.0).poll_read(cx, buf) } } impl AsyncWrite for MockStream { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { Pin::new(&mut self.0).poll_write(cx, buf) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.0).poll_flush(cx) } fn poll_shutdown( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { Pin::new(&mut self.0).poll_shutdown(cx) } fn poll_write_vectored( mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[IoSlice<'_>], ) -> Poll> { Pin::new(&mut self.0).poll_write_vectored(cx, bufs) } fn is_write_vectored(&self) -> bool { self.0.is_write_vectored() } } } pub fn trace_init() { let _ = tracing_subscriber::fmt::try_init(); } pub type BoxFuture<'a, T> = std::pin::Pin + Send + 'a>>; ================================================ FILE: tests/integration_tests/tests/client_layer.rs ================================================ use http::{header::HeaderName, HeaderValue}; use integration_tests::pb::{test_client::TestClient, test_server, Input, Output}; use std::time::Duration; use tokio::{net::TcpListener, sync::oneshot}; use tonic::{ transport::{server::TcpIncoming, Endpoint, Server}, Request, Response, Status, }; use tower::ServiceBuilder; use tower_http::{set_header::SetRequestHeaderLayer, trace::TraceLayer}; #[tokio::test] async fn connect_supports_standard_tower_layers() { struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, req: Request) -> Result, Status> { match req.metadata().get("x-test") { Some(_) => Ok(Response::new(Output {})), None => Err(Status::internal("user-agent header is missing")), } } } let (tx, rx) = oneshot::channel(); let svc = test_server::TestServer::new(Svc); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); // Start the server now, second call should succeed let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); let channel = Endpoint::from_shared(format!("http://{addr}")) .unwrap() .connect_lazy(); // prior to https://github.com/hyperium/tonic/pull/974 // this would not compile. (specifically the `TraceLayer`) let mut client = TestClient::new( ServiceBuilder::new() .layer(SetRequestHeaderLayer::overriding( HeaderName::from_static("x-test"), HeaderValue::from_static("test-header"), )) .layer(TraceLayer::new_for_grpc()) .service(channel), ); tokio::time::sleep(Duration::from_millis(100)).await; client.unary_call(Request::new(Input {})).await.unwrap(); tx.send(()).unwrap(); jh.await.unwrap(); } ================================================ FILE: tests/integration_tests/tests/complex_tower_middleware.rs ================================================ #![allow(unused_variables, dead_code)] use http_body::Body; use integration_tests::pb::{test_server, Input, Output}; use std::{ future::Future, pin::Pin, task::{Context, Poll}, }; use tonic::{transport::Server, Request, Response, Status}; use tower::{layer::Layer, BoxError, Service}; // all we care about is that this compiles async fn complex_tower_layers_work() { struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, req: Request) -> Result, Status> { unimplemented!() } } let svc = test_server::TestServer::new(Svc); Server::builder() .layer(MyServiceLayer::new()) .add_service(svc) .serve("127.0.0.1:0".parse().unwrap()) .await .unwrap(); } #[derive(Debug, Clone)] struct MyServiceLayer {} impl MyServiceLayer { fn new() -> Self { unimplemented!() } } impl Layer for MyServiceLayer { type Service = MyService; fn layer(&self, inner: S) -> Self::Service { unimplemented!() } } #[derive(Debug, Clone)] struct MyService { inner: S, } impl Service for MyService where S: Service>, { type Response = http::Response>; type Error = BoxError; type Future = MyFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { unimplemented!() } fn call(&mut self, req: R) -> Self::Future { unimplemented!() } } struct MyFuture { inner: F, body: B, } impl Future for MyFuture where F: Future, E>>, { type Output = Result>, BoxError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { unimplemented!() } } struct MyBody { inner: B, } impl Body for MyBody where B: Body, { type Data = B::Data; type Error = BoxError; fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { unimplemented!() } } ================================================ FILE: tests/integration_tests/tests/connect_info.rs ================================================ use integration_tests::pb::{test_client, test_server, Input, Output}; use std::time::Duration; use tokio::{net::TcpListener, sync::oneshot}; use tonic::{ transport::{ server::{TcpConnectInfo, TcpIncoming}, Endpoint, Server, }, Request, Response, Status, }; #[tokio::test] async fn getting_connect_info() { struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, req: Request) -> Result, Status> { assert!(req.local_addr().is_some()); assert!(req.remote_addr().is_some()); assert!(req.extensions().get::().is_some()); Ok(Response::new(Output {})) } } let svc = test_server::TestServer::new(Svc); let (tx, rx) = oneshot::channel::<()>(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let channel = Endpoint::from_shared(format!("http://{addr}")) .unwrap() .connect() .await .unwrap(); let mut client = test_client::TestClient::new(channel); client.unary_call(Input {}).await.unwrap(); tx.send(()).unwrap(); jh.await.unwrap(); } #[cfg(unix)] pub mod unix { use std::io; use hyper_util::rt::TokioIo; use tokio::{ net::{UnixListener, UnixStream}, sync::oneshot, }; use tokio_stream::wrappers::UnixListenerStream; use tonic::{ transport::{server::UdsConnectInfo, Endpoint, Server, Uri}, Request, Response, Status, }; use tower::service_fn; use integration_tests::pb::{test_client, test_server, Input, Output}; struct Svc {} #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, req: Request) -> Result, Status> { let conn_info = req.extensions().get::().unwrap(); // Client-side unix sockets are unnamed. assert!(req.local_addr().is_none()); assert!(req.remote_addr().is_none()); assert!(conn_info.peer_addr.as_ref().unwrap().is_unnamed()); // This should contain process credentials for the client socket. assert!(conn_info.peer_cred.as_ref().is_some()); Ok(Response::new(Output {})) } } #[tokio::test] async fn getting_connect_info() { let mut unix_socket_path = std::env::temp_dir(); unix_socket_path.push("uds-integration-test"); let uds = UnixListener::bind(&unix_socket_path).unwrap(); let uds_stream = UnixListenerStream::new(uds); let service = test_server::TestServer::new(Svc {}); let (tx, rx) = oneshot::channel::<()>(); let jh = tokio::spawn(async move { Server::builder() .add_service(service) .serve_with_incoming_shutdown(uds_stream, async { drop(rx.await) }) .await .unwrap(); }); // Take a copy before moving into the `service_fn` closure so that the closure // can implement `FnMut`. let path = unix_socket_path.clone(); let channel = Endpoint::try_from("http://[::]:50051") .unwrap() .connect_with_connector(service_fn(move |_: Uri| { let path = path.clone(); async move { Ok::<_, io::Error>(TokioIo::new(UnixStream::connect(path).await?)) } })) .await .unwrap(); let mut client = test_client::TestClient::new(channel); client.unary_call(Input {}).await.unwrap(); tx.send(()).unwrap(); jh.await.unwrap(); std::fs::remove_file(unix_socket_path).unwrap(); } } ================================================ FILE: tests/integration_tests/tests/connection.rs ================================================ use integration_tests::pb::{test_client::TestClient, test_server, Input, Output}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::{net::TcpListener, sync::oneshot}; use tonic::{ transport::{server::TcpIncoming, Endpoint, Server}, Code, Request, Response, Status, }; struct Svc(Arc>>>); #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, _: Request) -> Result, Status> { let mut l = self.0.lock().unwrap(); l.take().unwrap().send(()).unwrap(); Ok(Response::new(Output {})) } } #[tokio::test] async fn connect_returns_err() { let res = TestClient::connect("http://thisdoesntexist.test").await; assert!(res.is_err()); } #[tokio::test] async fn connect_handles_tls() { rustls::crypto::ring::default_provider() .install_default() .unwrap(); TestClient::connect("https://github.com").await.unwrap(); } #[tokio::test] async fn connect_returns_err_via_call_after_connected() { let (tx, rx) = oneshot::channel(); let sender = Arc::new(Mutex::new(Some(tx))); let svc = test_server::TestServer::new(Svc(sender)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let mut client = TestClient::connect(format!("http://{addr}")).await.unwrap(); // First call should pass, then shutdown the server client.unary_call(Request::new(Input {})).await.unwrap(); tokio::time::sleep(Duration::from_millis(100)).await; let res = client.unary_call(Request::new(Input {})).await; let err = res.unwrap_err(); assert_eq!(err.code(), Code::Unavailable); jh.await.unwrap(); } #[tokio::test] async fn connect_lazy_reconnects_after_first_failure() { let (tx, rx) = oneshot::channel(); let sender = Arc::new(Mutex::new(Some(tx))); let svc = test_server::TestServer::new(Svc(sender)); { let channel = Endpoint::from_static("http://127.0.0.1:0").connect_lazy(); let mut client = TestClient::new(channel); // First call should fail, the server is not running client.unary_call(Request::new(Input {})).await.unwrap_err(); } let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); // Start the server now, second call should succeed let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); let channel = Endpoint::from_shared(format!("http://{addr}")) .unwrap() .connect_lazy(); let mut client = TestClient::new(channel); tokio::time::sleep(Duration::from_millis(100)).await; client.unary_call(Request::new(Input {})).await.unwrap(); // The server shut down, third call should fail tokio::time::sleep(Duration::from_millis(100)).await; let err = client.unary_call(Request::new(Input {})).await.unwrap_err(); assert_eq!(err.code(), Code::Unavailable); jh.await.unwrap(); } ================================================ FILE: tests/integration_tests/tests/extensions.rs ================================================ use integration_tests::{ pb::{test_client, test_server, Input, Output}, BoxFuture, }; use std::{ task::{Context, Poll}, time::Duration, }; use tokio::{net::TcpListener, sync::oneshot}; use tonic::{ body::Body, server::NamedService, transport::{server::TcpIncoming, Endpoint, Server}, Request, Response, Status, }; use tower_service::Service; #[derive(Clone)] struct ExtensionValue(i32); #[tokio::test] async fn setting_extension_from_interceptor() { struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, req: Request) -> Result, Status> { let value = req.extensions().get::().unwrap(); assert_eq!(value.0, 42); Ok(Response::new(Output {})) } } let svc = test_server::TestServer::with_interceptor(Svc, |mut req: Request<()>| { req.extensions_mut().insert(ExtensionValue(42)); Ok(req) }); let (tx, rx) = oneshot::channel::<()>(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let channel = Endpoint::from_shared(format!("http://{addr}")) .unwrap() .connect() .await .unwrap(); let mut client = test_client::TestClient::new(channel); client.unary_call(Input {}).await.unwrap(); tx.send(()).unwrap(); jh.await.unwrap(); } #[tokio::test] async fn setting_extension_from_tower() { struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, req: Request) -> Result, Status> { let value = req.extensions().get::().unwrap(); assert_eq!(value.0, 42); Ok(Response::new(Output {})) } } let svc = InterceptedService { inner: test_server::TestServer::new(Svc), }; let (tx, rx) = oneshot::channel::<()>(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let channel = Endpoint::from_shared(format!("http://{addr}")) .unwrap() .connect() .await .unwrap(); let mut client = test_client::TestClient::new(channel); client.unary_call(Input {}).await.unwrap(); tx.send(()).unwrap(); jh.await.unwrap(); } #[derive(Debug, Clone)] struct InterceptedService { inner: S, } impl Service> for InterceptedService where S: Service, Response = http::Response> + NamedService + Clone + Send + 'static, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, mut req: http::Request) -> Self::Future { let clone = self.inner.clone(); let mut inner = std::mem::replace(&mut self.inner, clone); req.extensions_mut().insert(ExtensionValue(42)); Box::pin(async move { let response = inner.call(req).await?; Ok(response) }) } } impl NamedService for InterceptedService { const NAME: &'static str = S::NAME; } ================================================ FILE: tests/integration_tests/tests/http2_keep_alive.rs ================================================ use std::time::Duration; use tokio::{net::TcpListener, sync::oneshot}; use integration_tests::pb::{test_client::TestClient, test_server, Input, Output}; use tonic::transport::{server::TcpIncoming, Channel, Server}; use tonic::{Request, Response, Status}; struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, _: Request) -> Result, Status> { Ok(Response::new(Output {})) } } #[tokio::test] async fn http2_keepalive_does_not_cause_panics() { let svc = test_server::TestServer::new(Svc {}); let (tx, rx) = oneshot::channel::<()>(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .http2_keepalive_interval(Some(Duration::from_secs(10))) .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let mut client = TestClient::connect(format!("http://{addr}")).await.unwrap(); let res = client.unary_call(Request::new(Input {})).await; assert!(res.is_ok()); tx.send(()).unwrap(); jh.await.unwrap(); } #[tokio::test] async fn http2_keepalive_does_not_cause_panics_on_client_side() { let svc = test_server::TestServer::new(Svc {}); let (tx, rx) = oneshot::channel::<()>(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .http2_keepalive_interval(Some(Duration::from_secs(5))) .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let channel = Channel::from_shared(format!("http://{addr}")) .unwrap() .http2_keep_alive_interval(Duration::from_secs(5)) .connect() .await .unwrap(); let mut client = TestClient::new(channel); let res = client.unary_call(Request::new(Input {})).await; assert!(res.is_ok()); tx.send(()).unwrap(); jh.await.unwrap(); } ================================================ FILE: tests/integration_tests/tests/http2_max_header_list_size.rs ================================================ use std::time::Duration; use integration_tests::pb::{test_client, test_server, Input, Output}; use tokio::sync::oneshot; use tonic::{ transport::{Endpoint, Server}, Request, Response, Status, }; /// This test checks that the max header list size is respected, and that /// it allows for error messages up to that size. #[tokio::test] async fn test_http_max_header_list_size_and_long_errors() { struct Svc; // The default value is 16k. const N: usize = 20_000; fn long_message() -> String { "a".repeat(N) } #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, _: Request) -> Result, Status> { Err(Status::internal(long_message())) } } let svc = test_server::TestServer::new(Svc); let (tx, rx) = oneshot::channel::<()>(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = format!("http://{}", listener.local_addr().unwrap()); let jh = tokio::spawn(async move { let (nodelay, keepalive) = (Some(true), Some(Duration::from_secs(1))); let listener = tonic::transport::server::TcpIncoming::from(listener) .with_nodelay(nodelay) .with_keepalive(keepalive); Server::builder() .http2_max_pending_accept_reset_streams(Some(0)) .add_service(svc) .serve_with_incoming_shutdown(listener, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let channel = Endpoint::from_shared(addr) .unwrap() // Increase the max header list size to 2x the default value. If this is // not set, this test will hang. See // . .http2_max_header_list_size(u32::try_from(N * 2).unwrap()) .connect() .await .unwrap(); let mut client = test_client::TestClient::new(channel); let err = client.unary_call(Request::new(Input {})).await.unwrap_err(); assert_eq!(err.message(), long_message()); tx.send(()).unwrap(); jh.await.unwrap(); } ================================================ FILE: tests/integration_tests/tests/interceptor.rs ================================================ use integration_tests::pb::{test_client::TestClient, test_server, Input, Output}; use std::time::Duration; use tokio::{net::TcpListener, sync::oneshot}; use tonic::{ transport::{server::TcpIncoming, Endpoint, Server}, GrpcMethod, Request, Response, Status, }; #[tokio::test] async fn interceptor_retrieves_grpc_method() { use test_server::Test; struct Svc; #[tonic::async_trait] impl Test for Svc { async fn unary_call(&self, _: Request) -> Result, Status> { Ok(Response::new(Output {})) } } let svc = test_server::TestServer::new(Svc); let (tx, rx) = oneshot::channel(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); // Start the server now, second call should succeed let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); let channel = Endpoint::from_shared(format!("http://{addr}")) .unwrap() .connect_lazy(); fn client_intercept(req: Request<()>) -> Result, Status> { println!("Intercepting client request: {req:?}"); let gm = req.extensions().get::().unwrap(); assert_eq!(gm.service(), "test.Test"); assert_eq!(gm.method(), "UnaryCall"); Ok(req) } let mut client = TestClient::with_interceptor(channel, client_intercept); tokio::time::sleep(Duration::from_millis(100)).await; client.unary_call(Request::new(Input {})).await.unwrap(); tx.send(()).unwrap(); jh.await.unwrap(); } ================================================ FILE: tests/integration_tests/tests/load_shed.rs ================================================ use integration_tests::pb::{test_client, test_server, Input, Output}; use std::net::SocketAddr; use tokio::net::TcpListener; use tonic::{transport::Server, Code, Request, Response, Status}; #[tokio::test] async fn service_resource_exhausted() { let addr = run_service_in_background(0).await; let mut client = test_client::TestClient::connect(format!("http://{addr}")) .await .unwrap(); let req = Request::new(Input {}); let res = client.unary_call(req).await; let err = res.unwrap_err(); assert_eq!(err.code(), Code::ResourceExhausted); } #[tokio::test] async fn service_resource_not_exhausted() { let addr = run_service_in_background(1).await; let mut client = test_client::TestClient::connect(format!("http://{addr}")) .await .unwrap(); let req = Request::new(Input {}); let res = client.unary_call(req).await; assert!(res.is_ok()); } async fn run_service_in_background(concurrency_limit: usize) -> SocketAddr { struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, _req: Request) -> Result, Status> { Ok(Response::new(Output {})) } } let svc = test_server::TestServer::new(Svc {}); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { Server::builder() .concurrency_limit_per_connection(concurrency_limit) .load_shed(true) .add_service(svc) .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)) .await .unwrap(); }); addr } ================================================ FILE: tests/integration_tests/tests/max_message_size.rs ================================================ use std::pin::Pin; use hyper_util::rt::TokioIo; use integration_tests::{ pb::{test1_client, test1_server, Input1, Output1}, trace_init, }; use tokio_stream::Stream; use tonic::{ transport::{Endpoint, Server}, Code, Request, Response, Status, }; #[test] fn max_message_recv_size() { trace_init(); // Server recv assert_server_recv_max_success(128); // 5 is the size of the gRPC header assert_server_recv_max_success((4 * 1024 * 1024) - 5); // 4mb is the max recv size assert_server_recv_max_failure(4 * 1024 * 1024); assert_server_recv_max_failure(4 * 1024 * 1024 + 1); assert_server_recv_max_failure(8 * 1024 * 1024); // Client recv assert_client_recv_max_success(128); // 5 is the size of the gRPC header assert_client_recv_max_success((4 * 1024 * 1024) - 5); // 4mb is the max recv size assert_client_recv_max_failure(4 * 1024 * 1024); assert_client_recv_max_failure(4 * 1024 * 1024 + 1); assert_client_recv_max_failure(8 * 1024 * 1024); // Custom limit settings assert_test_case(TestCase { // 5 is the size of the gRPC header server_blob_size: 1024 - 5, client_recv_max: Some(1024), ..Default::default() }); assert_test_case(TestCase { server_blob_size: 1024, client_recv_max: Some(1024), expected_code: Some(Code::OutOfRange), ..Default::default() }); assert_test_case(TestCase { // 5 is the size of the gRPC header client_blob_size: 1024 - 5, server_recv_max: Some(1024), ..Default::default() }); assert_test_case(TestCase { client_blob_size: 1024, server_recv_max: Some(1024), expected_code: Some(Code::OutOfRange), ..Default::default() }); } #[test] fn max_message_send_size() { trace_init(); // Check client send limit works assert_test_case(TestCase { client_blob_size: 4 * 1024 * 1024, server_recv_max: Some(usize::MAX), ..Default::default() }); assert_test_case(TestCase { // 5 is the size of the gRPC header client_blob_size: 1024 - 5, server_recv_max: Some(usize::MAX), client_send_max: Some(1024), ..Default::default() }); assert_test_case(TestCase { // 5 is the size of the gRPC header client_blob_size: 4 * 1024 * 1024, server_recv_max: Some(usize::MAX), // Set client send limit to 1024 client_send_max: Some(1024), // TODO: This should return OutOfRange // https://github.com/hyperium/tonic/issues/1334 expected_code: Some(Code::Internal), ..Default::default() }); // Check server send limit works assert_test_case(TestCase { server_blob_size: 4 * 1024 * 1024, client_recv_max: Some(usize::MAX), ..Default::default() }); assert_test_case(TestCase { // 5 is the gRPC header size server_blob_size: 1024 - 5, client_recv_max: Some(usize::MAX), // Set server send limit to 1024 server_send_max: Some(1024), ..Default::default() }); assert_test_case(TestCase { server_blob_size: 4 * 1024 * 1024, client_recv_max: Some(usize::MAX), // Set server send limit to 1024 server_send_max: Some(1024), expected_code: Some(Code::OutOfRange), ..Default::default() }); } #[tokio::test] async fn response_stream_limit() { let client_blob = vec![0; 1]; let (client, server) = tokio::io::duplex(1024); struct Svc; #[tonic::async_trait] impl test1_server::Test1 for Svc { async fn unary_call(&self, _req: Request) -> Result, Status> { unimplemented!() } type StreamCallStream = Pin> + Send + 'static>>; async fn stream_call( &self, _req: Request, ) -> Result, Status> { let blob = Output1 { buf: vec![0; 6877902], }; let stream = tokio_stream::iter(vec![Ok(blob.clone()), Ok(blob.clone())]); Ok(Response::new(Box::pin(stream))) } } let svc = test1_server::Test1Server::new(Svc); tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); }); // Move client to an option so we can _move_ the inner value // on the first attempt to connect. All other attempts will fail. let mut client = Some(client); let channel = Endpoint::try_from("http://[::]:50051") .unwrap() .connect_with_connector(tower::service_fn(move |_| { let client = client.take(); async move { if let Some(client) = client { Ok(TokioIo::new(client)) } else { Err(std::io::Error::other("Client already taken")) } } })) .await .unwrap(); let client = test1_client::Test1Client::new(channel); let mut client = client.max_decoding_message_size(6877902 + 5); let req = Request::new(Input1 { buf: client_blob.clone(), }); let mut stream = client.stream_call(req).await.unwrap().into_inner(); while let Some(_b) = stream.message().await.unwrap() {} } // Track caller doesn't work on async fn so we extract the async part // into a sync version and assert the response there using track track_caller // so that when this does panic it tells us which line in the test failed not // where we placed the panic call. #[track_caller] fn assert_server_recv_max_success(size: usize) { let case = TestCase { client_blob_size: size, server_blob_size: 0, ..Default::default() }; assert_test_case(case); } #[track_caller] fn assert_server_recv_max_failure(size: usize) { let case = TestCase { client_blob_size: size, server_blob_size: 0, expected_code: Some(Code::OutOfRange), ..Default::default() }; assert_test_case(case); } #[track_caller] fn assert_client_recv_max_success(size: usize) { let case = TestCase { client_blob_size: 0, server_blob_size: size, ..Default::default() }; assert_test_case(case); } #[track_caller] fn assert_client_recv_max_failure(size: usize) { let case = TestCase { client_blob_size: 0, server_blob_size: size, expected_code: Some(Code::OutOfRange), ..Default::default() }; assert_test_case(case); } #[track_caller] fn assert_test_case(case: TestCase) { let res = max_message_run(&case); match (case.expected_code, res) { (Some(_), Ok(())) => panic!("Expected failure, but got success"), (Some(code), Err(status)) => { if status.code() != code { panic!("Expected failure, got failure but wrong code, got: {status:?}") } } (None, Err(status)) => panic!("Expected success, but got failure, got: {status:?}"), _ => (), } } #[derive(Default)] struct TestCase { client_blob_size: usize, server_blob_size: usize, client_recv_max: Option, server_recv_max: Option, client_send_max: Option, server_send_max: Option, expected_code: Option, } #[tokio::main] async fn max_message_run(case: &TestCase) -> Result<(), Status> { let client_blob = vec![0; case.client_blob_size]; let server_blob = vec![0; case.server_blob_size]; let (client, server) = tokio::io::duplex(1024); struct Svc(Vec); #[tonic::async_trait] impl test1_server::Test1 for Svc { async fn unary_call(&self, _req: Request) -> Result, Status> { Ok(Response::new(Output1 { buf: self.0.clone(), })) } type StreamCallStream = Pin> + Send + 'static>>; async fn stream_call( &self, _req: Request, ) -> Result, Status> { unimplemented!() } } let svc = test1_server::Test1Server::new(Svc(server_blob)); let svc = if let Some(size) = case.server_recv_max { svc.max_decoding_message_size(size) } else { svc }; let svc = if let Some(size) = case.server_send_max { svc.max_encoding_message_size(size) } else { svc }; tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming(tokio_stream::once(Ok::<_, std::io::Error>(server))) .await .unwrap(); }); // Move client to an option so we can _move_ the inner value // on the first attempt to connect. All other attempts will fail. let mut client = Some(client); let channel = Endpoint::try_from("http://[::]:50051") .unwrap() .connect_with_connector(tower::service_fn(move |_| { let client = client.take(); async move { if let Some(client) = client { Ok(TokioIo::new(client)) } else { Err(std::io::Error::other("Client already taken")) } } })) .await .unwrap(); let client = test1_client::Test1Client::new(channel); let client = if let Some(size) = case.client_recv_max { client.max_decoding_message_size(size) } else { client }; let mut client = if let Some(size) = case.client_send_max { client.max_encoding_message_size(size) } else { client }; let req = Request::new(Input1 { buf: client_blob.clone(), }); client.unary_call(req).await.map(|_| ()) } ================================================ FILE: tests/integration_tests/tests/origin.rs ================================================ use integration_tests::pb::test_client; use integration_tests::pb::{test_server, Input, Output}; use integration_tests::BoxFuture; use std::task::Context; use std::task::Poll; use std::time::Duration; use tokio::{net::TcpListener, sync::oneshot}; use tonic::codegen::http::Request; use tonic::{ transport::{server::TcpIncoming, Endpoint, Server}, Response, Status, }; use tower::Layer; use tower::Service; #[tokio::test] async fn writes_origin_header() { struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call( &self, _req: tonic::Request, ) -> Result, Status> { Ok(Response::new(Output {})) } } let svc = test_server::TestServer::new(Svc); let (tx, rx) = oneshot::channel::<()>(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .layer(OriginLayer {}) .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let channel = Endpoint::from_shared(format!("http://{addr}")) .unwrap() .origin("https://docs.rs".parse().expect("valid uri")) .connect() .await .unwrap(); let mut client = test_client::TestClient::new(channel); match client.unary_call(Input {}).await { Ok(_) => {} Err(status) => panic!("{}", status.message()), } tx.send(()).unwrap(); jh.await.unwrap(); } #[derive(Clone)] struct OriginLayer {} impl Layer for OriginLayer { type Service = OriginService; fn layer(&self, inner: S) -> Self::Service { OriginService { inner } } } #[derive(Clone)] struct OriginService { inner: S, } impl Service> for OriginService where T: Service>, T::Future: Send + 'static, T::Error: Into>, { type Response = T::Response; type Error = Box; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, req: Request) -> Self::Future { assert_eq!(req.uri().host(), Some("docs.rs")); let fut = self.inner.call(req); Box::pin(async move { fut.await.map_err(Into::into) }) } } ================================================ FILE: tests/integration_tests/tests/routes_builder.rs ================================================ use std::time::Duration; use tokio::{net::TcpListener, sync::oneshot}; use tokio_stream::StreamExt; use integration_tests::pb::{ test1_client, test1_server, test_client, test_server, Input, Input1, Output, Output1, }; use tonic::codegen::BoxStream; use tonic::service::RoutesBuilder; use tonic::transport::server::TcpIncoming; use tonic::{ transport::{Endpoint, Server}, Request, Response, Status, }; #[tokio::test] async fn multiple_service_using_routes_builder() { struct Svc1; #[tonic::async_trait] impl test_server::Test for Svc1 { async fn unary_call(&self, _req: Request) -> Result, Status> { Ok(Response::new(Output {})) } } struct Svc2; #[tonic::async_trait] impl test1_server::Test1 for Svc2 { async fn unary_call(&self, request: Request) -> Result, Status> { Ok(Response::new(Output1 { buf: request.into_inner().buf, })) } type StreamCallStream = BoxStream; async fn stream_call( &self, request: Request, ) -> Result, Status> { let output = Output1 { buf: request.into_inner().buf, }; let stream = tokio_stream::once(Ok(output)); Ok(Response::new(Box::pin(stream))) } } let svc1 = test_server::TestServer::new(Svc1); let svc2 = test1_server::Test1Server::new(Svc2); let (tx, rx) = oneshot::channel::<()>(); let mut routes_builder = RoutesBuilder::default(); routes_builder.add_service(svc1).add_service(svc2); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .add_routes(routes_builder.routes()) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let channel = Endpoint::from_shared(format!("http://{addr}")) .unwrap() .connect() .await .unwrap(); let mut client1 = test_client::TestClient::new(channel.clone()); let mut client2 = test1_client::Test1Client::new(channel); client1.unary_call(Input {}).await.unwrap(); let resp2 = client2 .unary_call(Input1 { buf: b"hello".to_vec(), }) .await .unwrap() .into_inner(); assert_eq!(&resp2.buf, b"hello"); let mut stream_response = client2 .stream_call(Input1 { buf: b"world".to_vec(), }) .await .unwrap() .into_inner(); let first = match stream_response.next().await { Some(Ok(first)) => first, _ => panic!("expected one non-error item in the stream call response"), }; assert_eq!(&first.buf, b"world"); assert!(stream_response.next().await.is_none()); tx.send(()).unwrap(); jh.await.unwrap(); } ================================================ FILE: tests/integration_tests/tests/status.rs ================================================ use bytes::Bytes; use http::Uri; use hyper_util::rt::TokioIo; use integration_tests::mock::MockStream; use integration_tests::pb::{ test_client, test_server, test_stream_client, test_stream_server, Input, InputStream, Output, OutputStream, }; use integration_tests::BoxFuture; use std::error::Error; use std::task::{Context, Poll}; use std::time::Duration; use tokio::{net::TcpListener, sync::oneshot}; use tonic::body::Body; use tonic::metadata::{MetadataMap, MetadataValue}; use tonic::{ transport::{server::TcpIncoming, Endpoint, Server}, Code, Request, Response, Status, }; #[tokio::test] async fn status_with_details() { struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, _: Request) -> Result, Status> { Err(Status::with_details( Code::ResourceExhausted, "Too many requests", Bytes::from_static(&[1]), )) } } let svc = test_server::TestServer::new(Svc); let (tx, rx) = oneshot::channel::<()>(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let mut channel = test_client::TestClient::connect(format!("http://{addr}")) .await .unwrap(); let err = channel .unary_call(Request::new(Input {})) .await .unwrap_err(); assert_eq!(err.message(), "Too many requests"); assert_eq!(err.details(), &[1]); tx.send(()).unwrap(); jh.await.unwrap(); } #[tokio::test] async fn status_with_metadata() { const MESSAGE: &str = "Internal error, see metadata for details"; const ASCII_NAME: &str = "x-host-ip"; const ASCII_VALUE: &str = "127.0.0.1"; const BINARY_NAME: &str = "x-host-name-bin"; const BINARY_VALUE: &[u8] = b"localhost"; struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, _: Request) -> Result, Status> { let mut metadata = MetadataMap::new(); metadata.insert(ASCII_NAME, ASCII_VALUE.parse().unwrap()); metadata.insert_bin(BINARY_NAME, MetadataValue::from_bytes(BINARY_VALUE)); Err(Status::with_metadata(Code::Internal, MESSAGE, metadata)) } } let svc = test_server::TestServer::new(Svc); let (tx, rx) = oneshot::channel::<()>(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let mut channel = test_client::TestClient::connect(format!("http://{addr}")) .await .unwrap(); let err = channel .unary_call(Request::new(Input {})) .await .unwrap_err(); assert_eq!(err.code(), Code::Internal); assert_eq!(err.message(), MESSAGE); let metadata = err.metadata(); assert_eq!( metadata.get(ASCII_NAME).unwrap().to_str().unwrap(), ASCII_VALUE ); assert_eq!( metadata.get_bin(BINARY_NAME).unwrap().to_bytes().unwrap(), BINARY_VALUE ); tx.send(()).unwrap(); jh.await.unwrap(); } type Stream = std::pin::Pin< Box> + Send + 'static>, >; #[tokio::test] async fn status_from_server_stream() { integration_tests::trace_init(); struct Svc; #[tonic::async_trait] impl test_stream_server::TestStream for Svc { type StreamCallStream = Stream; async fn stream_call( &self, _: Request, ) -> Result, Status> { let s = tokio_stream::iter(vec![ Err::(Status::unavailable("foo")), Err::(Status::unavailable("bar")), ]); Ok(Response::new(Box::pin(s) as Self::StreamCallStream)) } } let svc = test_stream_server::TestStreamServer::new(Svc); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming(incoming) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let mut client = test_stream_client::TestStreamClient::connect(format!("http://{addr}")) .await .unwrap(); let mut stream = client .stream_call(InputStream {}) .await .unwrap() .into_inner(); assert_eq!(stream.message().await.unwrap_err().message(), "foo"); assert_eq!(stream.message().await.unwrap(), None); } #[tokio::test] async fn status_from_server_stream_with_source() { integration_tests::trace_init(); let channel = Endpoint::try_from("http://[::]:50051") .unwrap() .connect_with_connector_lazy(tower::service_fn(move |_: Uri| async move { Err::, _>(std::io::Error::other("WTF")) })); let mut client = test_stream_client::TestStreamClient::new(channel); let error = client.stream_call(InputStream {}).await.unwrap_err(); let source = error.source().unwrap(); source.downcast_ref::().unwrap(); } #[tokio::test] async fn status_from_server_stream_with_inferred_status() { integration_tests::trace_init(); struct Svc; #[tonic::async_trait] impl test_stream_server::TestStream for Svc { type StreamCallStream = Stream; async fn stream_call( &self, _: Request, ) -> Result, Status> { let s = tokio_stream::once(Ok(OutputStream {})); Ok(Response::new(Box::pin(s) as Self::StreamCallStream)) } } #[derive(Clone)] struct TestLayer; impl tower::Layer for TestLayer { type Service = TestService; fn layer(&self, _: S) -> Self::Service { TestService } } #[derive(Clone)] struct TestService; impl tower::Service> for TestService { type Response = http::Response; type Error = std::convert::Infallible; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, _: http::Request) -> Self::Future { Box::pin(async { Ok(http::Response::builder() .status(http::StatusCode::BAD_GATEWAY) .body(Body::empty()) .unwrap()) }) } } let svc = test_stream_server::TestStreamServer::new(Svc); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming: TcpIncoming = TcpIncoming::from(listener).with_nodelay(Some(true)); tokio::spawn(async move { Server::builder() .layer(TestLayer) .add_service(svc) .serve_with_incoming(incoming) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let mut client = test_stream_client::TestStreamClient::connect(format!("http://{addr}")) .await .unwrap(); let mut stream = client .stream_call(InputStream {}) .await .unwrap() .into_inner(); assert_eq!( stream.message().await.unwrap_err().code(), Code::Unavailable ); assert_eq!(stream.message().await.unwrap(), None); } #[tokio::test] async fn message_and_then_status_from_server_stream() { integration_tests::trace_init(); struct Svc; #[tonic::async_trait] impl test_stream_server::TestStream for Svc { type StreamCallStream = Stream; async fn stream_call( &self, _: Request, ) -> Result, Status> { let s = tokio_stream::iter(vec![ Ok(OutputStream {}), Err::(Status::unavailable("foo")), ]); Ok(Response::new(Box::pin(s) as Self::StreamCallStream)) } } let svc = test_stream_server::TestStreamServer::new(Svc); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming(incoming) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let mut client = test_stream_client::TestStreamClient::connect(format!("http://{addr}")) .await .unwrap(); let mut stream = client .stream_call(InputStream {}) .await .unwrap() .into_inner(); assert_eq!(stream.message().await.unwrap(), Some(OutputStream {})); assert_eq!(stream.message().await.unwrap_err().message(), "foo"); assert_eq!(stream.message().await.unwrap(), None); } ================================================ FILE: tests/integration_tests/tests/streams.rs ================================================ use integration_tests::pb::{test_stream_server, InputStream, OutputStream}; use tokio::sync::oneshot; use tonic::{transport::Server, Request, Response, Status}; type Stream = std::pin::Pin< Box> + Send + 'static>, >; #[tokio::test] async fn status_from_server_stream_with_source() { struct Svc; #[tonic::async_trait] impl test_stream_server::TestStream for Svc { type StreamCallStream = Stream; async fn stream_call( &self, _: Request, ) -> Result, Status> { let s = Unsync(std::ptr::null_mut::<()>()); Ok(Response::new(Box::pin(s) as Self::StreamCallStream)) } } let svc = test_stream_server::TestStreamServer::new(Svc); let (tx, rx) = oneshot::channel::<()>(); let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_shutdown("127.0.0.1:0".parse().unwrap(), async { drop(rx.await) }) .await .unwrap(); }); tx.send(()).unwrap(); jh.await.unwrap(); } #[allow(dead_code)] struct Unsync(*mut ()); unsafe impl Send for Unsync {} impl tokio_stream::Stream for Unsync { type Item = Result; fn poll_next( self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { unimplemented!() } } ================================================ FILE: tests/integration_tests/tests/timeout.rs ================================================ use integration_tests::pb::{test_client, test_server, Input, Output}; use std::{net::SocketAddr, time::Duration}; use tokio::net::TcpListener; use tonic::{transport::Server, Code, Request, Response, Status}; #[tokio::test] async fn cancelation_on_timeout() { let addr = run_service_in_background(Duration::from_secs(1), Duration::from_secs(100)).await; let mut client = test_client::TestClient::connect(format!("http://{addr}")) .await .unwrap(); let mut req = Request::new(Input {}); req.metadata_mut() // 500 ms .insert("grpc-timeout", "500m".parse().unwrap()); let res = client.unary_call(req).await; let err = res.unwrap_err(); assert!(err.message().contains("Timeout expired")); assert_eq!(err.code(), Code::Cancelled); } #[tokio::test] async fn picks_server_timeout_if_thats_sorter() { let addr = run_service_in_background(Duration::from_secs(1), Duration::from_millis(100)).await; let mut client = test_client::TestClient::connect(format!("http://{addr}")) .await .unwrap(); let mut req = Request::new(Input {}); req.metadata_mut() // 10 hours .insert("grpc-timeout", "10H".parse().unwrap()); let res = client.unary_call(req).await; let err = res.unwrap_err(); assert!(err.message().contains("Timeout expired")); assert_eq!(err.code(), Code::Cancelled); } #[tokio::test] async fn picks_client_timeout_if_thats_sorter() { let addr = run_service_in_background(Duration::from_secs(1), Duration::from_secs(100)).await; let mut client = test_client::TestClient::connect(format!("http://{addr}")) .await .unwrap(); let mut req = Request::new(Input {}); req.metadata_mut() // 100 ms .insert("grpc-timeout", "100m".parse().unwrap()); let res = client.unary_call(req).await; let err = res.unwrap_err(); assert!(err.message().contains("Timeout expired")); assert_eq!(err.code(), Code::Cancelled); } async fn run_service_in_background(latency: Duration, server_timeout: Duration) -> SocketAddr { struct Svc { latency: Duration, } #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, _req: Request) -> Result, Status> { tokio::time::sleep(self.latency).await; Ok(Response::new(Output {})) } } let svc = test_server::TestServer::new(Svc { latency }); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { Server::builder() .timeout(server_timeout) .add_service(svc) .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)) .await .unwrap(); }); addr } ================================================ FILE: tests/integration_tests/tests/user_agent.rs ================================================ use integration_tests::pb::{test_client, test_server, Input, Output}; use std::time::Duration; use tokio::{net::TcpListener, sync::oneshot}; use tonic::{ transport::{server::TcpIncoming, Endpoint, Server}, Request, Response, Status, }; #[tokio::test] async fn writes_user_agent_header() { struct Svc; #[tonic::async_trait] impl test_server::Test for Svc { async fn unary_call(&self, req: Request) -> Result, Status> { match req.metadata().get("user-agent") { Some(_) => Ok(Response::new(Output {})), None => Err(Status::internal("user-agent header is missing")), } } } let svc = test_server::TestServer::new(Svc); let (tx, rx) = oneshot::channel::<()>(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let incoming = TcpIncoming::from(listener).with_nodelay(Some(true)); let jh = tokio::spawn(async move { Server::builder() .add_service(svc) .serve_with_incoming_shutdown(incoming, async { drop(rx.await) }) .await .unwrap(); }); tokio::time::sleep(Duration::from_millis(100)).await; let channel = Endpoint::from_shared(format!("http://{addr}")) .unwrap() .user_agent("my-client") .expect("valid user agent") .connect() .await .unwrap(); let mut client = test_client::TestClient::new(channel); match client.unary_call(Input {}).await { Ok(_) => {} Err(status) => panic!("{}", status.message()), } tx.send(()).unwrap(); jh.await.unwrap(); } ================================================ FILE: tests/web/Cargo.toml ================================================ [package] authors = ["Juan Alvarez "] edition = "2021" name = "test_web" license = "MIT" [dependencies] base64 = "0.22" bytes = "1.0" http-body-util = "0.1" hyper = "1" hyper-util = "0.1" prost = "0.14" tokio = { version = "1", features = ["macros", "rt", "net"] } tokio-stream = { version = "0.1", features = ["net"] } tonic = { path = "../../tonic" } tonic-prost = { path = "../../tonic-prost" } [dev-dependencies] tonic-web = { path = "../../tonic-web" } [build-dependencies] tonic-prost-build = { path = "../../tonic-prost-build" } ================================================ FILE: tests/web/build.rs ================================================ fn main() { let protos = &["proto/test.proto"]; tonic_prost_build::configure() .compile_protos(protos, &["proto"]) .unwrap(); protos .iter() .for_each(|file| println!("cargo:rerun-if-changed={file}")); } ================================================ FILE: tests/web/proto/test.proto ================================================ syntax = "proto3"; package test; service Test { rpc UnaryCall(Input) returns (Output); rpc ServerStream(Input) returns (stream Output); rpc ClientStream(stream Input) returns (Output); } message Input { int32 id = 1; string desc = 2; } message Output { int32 id = 1; string desc = 2; } ================================================ FILE: tests/web/src/lib.rs ================================================ use std::pin::Pin; use tokio_stream::{self as stream, Stream, StreamExt}; use tonic::{Request, Response, Status, Streaming}; use pb::{test_server::Test, Input, Output}; pub mod pb { tonic::include_proto!("test"); } type BoxStream = Pin> + Send + 'static>>; pub struct Svc; #[tonic::async_trait] impl Test for Svc { async fn unary_call(&self, req: Request) -> Result, Status> { let req = req.into_inner(); if &req.desc == "boom" { Err(Status::invalid_argument("invalid boom")) } else { Ok(Response::new(Output { id: req.id, desc: req.desc, })) } } type ServerStreamStream = BoxStream; async fn server_stream( &self, req: Request, ) -> Result, Status> { let req = req.into_inner(); Ok(Response::new(Box::pin(stream::iter(vec![1, 2]).map( move |n| { Ok(Output { id: req.id, desc: format!("{}-{}", n, req.desc), }) }, )))) } async fn client_stream( &self, req: Request>, ) -> Result, Status> { let out = Output { id: 0, desc: "".into(), }; Ok(Response::new( req.into_inner() .fold(out, |mut acc, input| { let input = input.unwrap(); acc.id += input.id; acc.desc += &input.desc; acc }) .await, )) } } pub mod util { pub mod base64 { use base64::{ alphabet, engine::{ general_purpose::{GeneralPurpose, GeneralPurposeConfig}, DecodePaddingMode, }, }; pub const STANDARD: GeneralPurpose = GeneralPurpose::new( &alphabet::STANDARD, GeneralPurposeConfig::new() .with_encode_padding(true) .with_decode_padding_mode(DecodePaddingMode::Indifferent), ); } } ================================================ FILE: tests/web/tests/grpc.rs ================================================ use std::future::Future; use std::net::SocketAddr; use tokio::net::TcpListener; use tokio::time::Duration; use tokio::{join, try_join}; use tokio_stream::wrappers::TcpListenerStream; use tokio_stream::{self as stream, StreamExt}; use tonic::transport::{Channel, Error, Server}; use tonic::{Response, Streaming}; use test_web::pb::{test_client::TestClient, test_server::TestServer, Input}; use test_web::Svc; use tonic_web::GrpcWebLayer; #[tokio::test] async fn smoke_unary() { let (mut c1, mut c2, mut c3, mut c4) = spawn().await.expect("clients"); let (r1, r2, r3, r4) = try_join!( c1.unary_call(input()), c2.unary_call(input()), c3.unary_call(input()), c4.unary_call(input()), ) .expect("responses"); assert!(meta(&r1) == meta(&r2) && meta(&r2) == meta(&r3) && meta(&r3) == meta(&r4)); assert!(data(&r1) == data(&r2) && data(&r2) == data(&r3) && data(&r3) == data(&r4)); } #[tokio::test] async fn smoke_client_stream() { let (mut c1, mut c2, mut c3, mut c4) = spawn().await.expect("clients"); let input_stream = || stream::iter(vec![input(), input()]); let (r1, r2, r3, r4) = try_join!( c1.client_stream(input_stream()), c2.client_stream(input_stream()), c3.client_stream(input_stream()), c4.client_stream(input_stream()), ) .expect("responses"); assert!(meta(&r1) == meta(&r2) && meta(&r2) == meta(&r3) && meta(&r3) == meta(&r4)); assert!(data(&r1) == data(&r2) && data(&r2) == data(&r3) && data(&r3) == data(&r4)); } #[tokio::test] async fn smoke_server_stream() { let (mut c1, mut c2, mut c3, mut c4) = spawn().await.expect("clients"); let (r1, r2, r3, r4) = try_join!( c1.server_stream(input()), c2.server_stream(input()), c3.server_stream(input()), c4.server_stream(input()), ) .expect("responses"); assert!(meta(&r1) == meta(&r2) && meta(&r2) == meta(&r3) && meta(&r3) == meta(&r4)); let r1 = stream(r1).await; let r2 = stream(r2).await; let r3 = stream(r3).await; let r4 = stream(r4).await; assert!(r1 == r2 && r2 == r3 && r3 == r4); } #[tokio::test] async fn smoke_error() { let (mut c1, mut c2, mut c3, mut c4) = spawn().await.expect("clients"); let boom = Input { id: 1, desc: "boom".to_owned(), }; let (r1, r2, r3, r4) = join!( c1.unary_call(boom.clone()), c2.unary_call(boom.clone()), c3.unary_call(boom.clone()), c4.unary_call(boom.clone()), ); let s1 = r1.unwrap_err(); let s2 = r2.unwrap_err(); let s3 = r3.unwrap_err(); let s4 = r4.unwrap_err(); assert!(status(&s1) == status(&s2) && status(&s2) == status(&s3) && status(&s3) == status(&s4)) } async fn bind() -> (TcpListener, String) { let addr = SocketAddr::from(([127, 0, 0, 1], 0)); let lis = TcpListener::bind(addr).await.expect("listener"); let url = format!("http://{}", lis.local_addr().unwrap()); (lis, url) } async fn grpc(accept_h1: bool) -> (impl Future>, String) { let (listener, url) = bind().await; let fut = Server::builder() .accept_http1(accept_h1) .add_service(TestServer::new(Svc)) .serve_with_incoming(TcpListenerStream::new(listener)); (fut, url) } async fn grpc_web(accept_h1: bool) -> (impl Future>, String) { let (listener, url) = bind().await; let fut = Server::builder() .accept_http1(accept_h1) .layer(GrpcWebLayer::new()) .add_service(TestServer::new(Svc)) .serve_with_incoming(TcpListenerStream::new(listener)); (fut, url) } type Client = TestClient; async fn spawn() -> Result<(Client, Client, Client, Client), Error> { let ((s1, u1), (s2, u2), (s3, u3), (s4, u4)) = join!(grpc(true), grpc(false), grpc_web(true), grpc_web(false)); drop(tokio::spawn(async move { join!(s1, s2, s3, s4) })); tokio::time::sleep(Duration::from_millis(30)).await; try_join!( TestClient::connect(u1), TestClient::connect(u2), TestClient::connect(u3), TestClient::connect(u4) ) } fn input() -> Input { Input { id: 1, desc: "one".to_owned(), } } fn meta(r: &Response) -> String { format!("{:?}", r.metadata()) } fn data(r: &Response) -> &T { r.get_ref() } async fn stream(r: Response>) -> Vec { r.into_inner().collect::, _>>().await.unwrap() } fn status(s: &tonic::Status) -> (String, tonic::Code) { (format!("{:?}", s.metadata()), s.code()) } ================================================ FILE: tests/web/tests/grpc_web.rs ================================================ use std::net::SocketAddr; use base64::Engine as _; use bytes::{Buf, BufMut, Bytes, BytesMut}; use http_body_util::{BodyExt as _, Full}; use hyper::body::Incoming; use hyper::http::{header, StatusCode}; use hyper::{Method, Request, Uri}; use hyper_util::client::legacy::Client; use hyper_util::rt::TokioExecutor; use prost::Message; use tokio::net::TcpListener; use tokio_stream::wrappers::TcpListenerStream; use tonic::body::Body; use tonic::transport::Server; use test_web::pb::{test_server::TestServer, Input, Output}; use test_web::Svc; use tonic::Status; use tonic_web::GrpcWebLayer; #[tokio::test] async fn binary_request() { let server_url = spawn().await; let client = Client::builder(TokioExecutor::new()).build_http(); let req = build_request(server_url, "grpc-web", "grpc-web"); let res = client.request(req).await.unwrap(); let content_type = res.headers().get(header::CONTENT_TYPE).unwrap().clone(); let content_type = content_type.to_str().unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(content_type, "application/grpc-web+proto"); let (message, trailers) = decode_body(res.into_body(), content_type).await; let expected = Output { id: 1, desc: "one".to_owned(), }; assert_eq!(message, expected); assert_eq!(&trailers[..], b"grpc-status:0\r\n"); } #[tokio::test] async fn text_request() { let server_url = spawn().await; let client = Client::builder(TokioExecutor::new()).build_http(); let req = build_request(server_url, "grpc-web-text", "grpc-web-text"); let res = client.request(req).await.unwrap(); let content_type = res.headers().get(header::CONTENT_TYPE).unwrap().clone(); let content_type = content_type.to_str().unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!(content_type, "application/grpc-web-text+proto"); let (message, trailers) = decode_body(res.into_body(), content_type).await; let expected = Output { id: 1, desc: "one".to_owned(), }; assert_eq!(message, expected); assert_eq!(&trailers[..], b"grpc-status:0\r\n"); } async fn spawn() -> String { let addr = SocketAddr::from(([127, 0, 0, 1], 0)); let listener = TcpListener::bind(addr).await.expect("listener"); let url = format!("http://{}", listener.local_addr().unwrap()); let listener_stream = TcpListenerStream::new(listener); drop(tokio::spawn(async move { Server::builder() .accept_http1(true) .layer(GrpcWebLayer::new()) .add_service(TestServer::new(Svc)) .serve_with_incoming(listener_stream) .await .unwrap() })); url } fn encode_body() -> Bytes { let input = Input { id: 1, desc: "one".to_owned(), }; let mut buf = BytesMut::with_capacity(1024); buf.reserve(5); unsafe { buf.advance_mut(5); } input.encode(&mut buf).unwrap(); let len = buf.len() - 5; { let mut buf = &mut buf[..5]; buf.put_u8(0); buf.put_u32(len as u32); } buf.split_to(len + 5).freeze() } fn build_request(base_uri: String, content_type: &str, accept: &str) -> Request { use header::{ACCEPT, CONTENT_TYPE, ORIGIN}; let request_uri = format!("{}/{}/{}", base_uri, "test.Test", "UnaryCall") .parse::() .unwrap(); let bytes = match content_type { "grpc-web" => encode_body(), "grpc-web-text" => test_web::util::base64::STANDARD .encode(encode_body()) .into(), _ => panic!("invalid content type {content_type}"), }; Request::builder() .method(Method::POST) .header(CONTENT_TYPE, format!("application/{content_type}")) .header(ORIGIN, "http://example.com") .header(ACCEPT, format!("application/{accept}")) .uri(request_uri) .body(Body::new( Full::new(bytes).map_err(|err| Status::internal(err.to_string())), )) .unwrap() } async fn decode_body(body: Incoming, content_type: &str) -> (Output, Bytes) { let mut body = body.collect().await.unwrap().to_bytes(); if content_type == "application/grpc-web-text+proto" { body = test_web::util::base64::STANDARD .decode(body) .unwrap() .into() } body.advance(1); let len = body.get_u32(); let msg = Output::decode(&mut body.split_to(len as usize)).expect("decode"); body.advance(5); (msg, body) } ================================================ FILE: tests/wellknown/Cargo.toml ================================================ [package] authors = ["Lucio Franco "] edition = "2021" license = "MIT" name = "wellknown" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] prost = "0.14" prost-types = "0.14" tonic = {path = "../../tonic"} tonic-prost = {path = "../../tonic-prost"} [build-dependencies] tonic-prost-build = {path = "../../tonic-prost-build"} ================================================ FILE: tests/wellknown/build.rs ================================================ fn main() { tonic_prost_build::compile_protos("proto/wellknown.proto").unwrap(); } ================================================ FILE: tests/wellknown/proto/wellknown.proto ================================================ syntax = "proto3"; package wellknown; import "google/protobuf/empty.proto"; import "google/protobuf/wrappers.proto"; import "google/protobuf/any.proto"; service Admin { rpc EmptyCall(google.protobuf.Empty) returns (google.protobuf.Empty); rpc StringCall(google.protobuf.StringValue) returns (google.protobuf.Empty); rpc AnyCall(google.protobuf.Any) returns (google.protobuf.Empty); } ================================================ FILE: tests/wellknown/src/lib.rs ================================================ pub mod pb { tonic::include_proto!("wellknown"); } ================================================ FILE: tests/wellknown-compiled/Cargo.toml ================================================ [package] authors = ["Lucio Franco "] edition = "2021" license = "MIT" name = "wellknown-compiled" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] doctest = false [dependencies] prost = "0.14" tonic = {path = "../../tonic"} tonic-prost = {path = "../../tonic-prost"} [build-dependencies] tonic-prost-build = {path = "../../tonic-prost-build"} ================================================ FILE: tests/wellknown-compiled/build.rs ================================================ fn main() { tonic_prost_build::configure() .extern_path(".google.protobuf.Empty", "()") .compile_well_known_types(true) .compile_protos(&["proto/google.proto", "proto/test.proto"], &["proto"]) .unwrap(); } ================================================ FILE: tests/wellknown-compiled/proto/google.proto ================================================ syntax = "proto3"; package google.protobuf; import "google/protobuf/any.proto"; import "google/protobuf/api.proto"; import "google/protobuf/descriptor.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/field_mask.proto"; import "google/protobuf/source_context.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/type.proto"; import "google/protobuf/wrappers.proto"; ================================================ FILE: tests/wellknown-compiled/proto/test.proto ================================================ syntax = "proto3"; package test; import "google/protobuf/empty.proto"; message Input { } service Service { rpc Call(Input) returns (google.protobuf.Empty); } ================================================ FILE: tests/wellknown-compiled/src/lib.rs ================================================ pub mod gen { pub mod google { pub mod protobuf { #![allow(clippy::doc_overindented_list_items)] tonic::include_proto!("google.protobuf"); } } pub mod test { tonic::include_proto!("test"); } } pub fn grok() { let _any = crate::gen::google::protobuf::Any { type_url: "foo".to_owned(), value: Vec::new(), }; } ================================================ FILE: tonic/Cargo.toml ================================================ [package] name = "tonic" # When releasing to crates.io: # - Remove path dependencies # - Update CHANGELOG.md. # - Create "v0.11.x" git tag. authors = ["Lucio Franco "] categories = ["web-programming", "network-programming", "asynchronous"] description = """ A gRPC over HTTP/2 implementation focused on high performance, interoperability, and flexibility. """ edition = "2024" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "async", "futures", "protobuf"] license = "MIT" readme = "../README.md" repository = "https://github.com/hyperium/tonic" version = "0.14.5" rust-version = {workspace = true} exclude = ["benches-disabled"] [features] codegen = ["dep:async-trait"] gzip = ["dep:flate2"] deflate = ["dep:flate2"] zstd = ["dep:zstd"] default = ["router", "transport", "codegen"] _tls-any = ["dep:tokio", "tokio?/rt", "tokio?/macros", "tls-connect-info"] # Internal. Please choose one of `tls-ring` or `tls-aws-lc` tls-ring = ["_tls-any", "tokio-rustls/ring"] tls-aws-lc = ["_tls-any", "tokio-rustls/aws-lc-rs"] tls-native-roots = ["_tls-any", "channel", "dep:rustls-native-certs"] tls-webpki-roots = ["_tls-any","channel", "dep:webpki-roots"] tls-connect-info = ["dep:tokio-rustls"] router = ["dep:axum", "dep:tower", "tower?/util"] server = [ "dep:h2", "dep:hyper", "hyper?/server", "dep:hyper-util", "hyper-util?/service", "hyper-util?/server-auto", "dep:socket2", "dep:tokio", "tokio?/macros", "tokio?/net", "tokio?/time", "tokio-stream/net", "dep:tower", "tower?/util", "tower?/limit", "tower?/load-shed", ] channel = [ "dep:hyper", "hyper?/client", "dep:hyper-util", "hyper-util?/client-legacy", "dep:tower", "tower?/balance", "tower?/buffer", "tower?/discover", "tower?/limit", "tower?/load-shed", "tower?/util", "dep:tokio", "tokio?/time", "dep:hyper-timeout", ] transport = ["server", "channel"] # [[bench]] # name = "bench_main" # harness = false [dependencies] base64 = "0.22" bytes = "1.0" http = "1.1.0" tracing = "0.1" http-body = "1" http-body-util = "0.1" percent-encoding = "2.1" pin-project = "1.0.11" tower-layer = "0.3" tower-service = "0.3" tokio-stream = {version = "0.1.16", default-features = false} # codegen async-trait = {version = "0.1.13", optional = true} # transport h2 = {version = "0.4", optional = true} hyper = {version = "1", features = ["http1", "http2"], optional = true} hyper-util = { version = "0.1.11", features = ["tokio"], optional = true } socket2 = { version = "0.6", optional = true, features = ["all"] } tokio = {version = "1", default-features = false, optional = true} tower = {version = "0.5", default-features = false, optional = true} axum = {version = "0.8", default-features = false, optional = true} # rustls rustls-native-certs = { version = "0.8", optional = true } tokio-rustls = { version = "0.26.1", default-features = false, features = ["logging", "tls12"], optional = true } webpki-roots = { version = "1", optional = true } # compression flate2 = {version = "1.0", optional = true} zstd = { version = "0.13.0", optional = true } # channel hyper-timeout = {version = "0.5", optional = true} sync_wrapper = "1.0.2" [dev-dependencies] bencher = "0.1.5" quickcheck = "1.0" quickcheck_macros = "1.0" static_assertions = "1.0" tokio = {version = "1.0", features = ["rt-multi-thread", "macros", "test-util"]} tower = {version = "0.5", features = ["load-shed", "timeout"]} [lints] workspace = true [package.metadata.docs.rs] all-features = true [package.metadata.cargo_check_external_types] allowed_external_types = [ # major released "bytes::*", "tokio::*", "http::*", "http_body::*", "hyper::*", "rustls_pki_types::*", # not major released "prost::*", "tracing::*", "async_trait::async_trait", "axum_core::body::Body", "axum_core::response::into_response::IntoResponse", "axum::routing::Router", "futures_core::stream::Stream", "h2::error::Error", "tower_service::Service", "tower_layer::Layer", "tower_layer::stack::Stack", "tower_layer::identity::Identity", ] [[bench]] harness = false name = "decode" ================================================ FILE: tonic/benches/decode.rs ================================================ #![allow(missing_docs)] use bencher::{Bencher, benchmark_group, benchmark_main}; use bytes::{Buf, BufMut, Bytes, BytesMut}; use http_body::{Body, Frame, SizeHint}; use std::{ fmt::{Error, Formatter}, pin::Pin, task::{Context, Poll}, }; use tonic::{Status, Streaming, codec::DecodeBuf, codec::Decoder}; macro_rules! bench { ($name:ident, $message_size:expr, $chunk_size:expr, $message_count:expr) => { fn $name(b: &mut Bencher) { let rt = tokio::runtime::Builder::new_multi_thread() .build() .expect("runtime"); let payload = make_payload($message_size, $message_count); let body = MockBody::new(payload, $chunk_size); b.bytes = body.len() as u64; b.iter(|| { rt.block_on(async { let decoder = MockDecoder::new($message_size); let mut stream = Streaming::new_request(decoder, body.clone(), None, None); let mut count = 0; while let Some(msg) = stream.message().await.unwrap() { assert_eq!($message_size, msg.len()); count += 1; } assert_eq!(count, $message_count); assert!(stream.trailers().await.unwrap().is_none()); }) }) } }; } #[derive(Clone)] struct MockBody { data: Bytes, chunk_size: usize, } impl MockBody { fn new(data: Bytes, chunk_size: usize) -> Self { MockBody { data, chunk_size } } fn len(&self) -> usize { self.data.len() } } impl Body for MockBody { type Data = Bytes; type Error = Status; fn poll_frame( mut self: Pin<&mut Self>, _cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { if self.data.has_remaining() { let split = std::cmp::min(self.chunk_size, self.data.remaining()); Poll::Ready(Some(Ok(Frame::data(self.data.split_to(split))))) } else { Poll::Ready(None) } } fn is_end_stream(&self) -> bool { self.data.is_empty() } fn size_hint(&self) -> SizeHint { SizeHint::with_exact(self.data.len() as u64) } } impl std::fmt::Debug for MockBody { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { let sample = self.data.iter().take(10).collect::>(); write!(f, "{:?}...({})", sample, self.data.len()) } } #[derive(Debug, Clone)] struct MockDecoder { message_size: usize, } impl MockDecoder { fn new(message_size: usize) -> Self { MockDecoder { message_size } } } impl Decoder for MockDecoder { type Item = Vec; type Error = Status; fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error> { let out = Vec::from(buf.chunk()); buf.advance(self.message_size); Ok(Some(out)) } } fn make_payload(message_length: usize, message_count: usize) -> Bytes { let mut buf = BytesMut::new(); for _ in 0..message_count { let msg = vec![97u8; message_length]; buf.reserve(msg.len() + 5); buf.put_u8(0); buf.put_u32(msg.len() as u32); buf.put(&msg[..]); } buf.freeze() } // change body chunk size only bench!(chunk_size_100, 1_000, 100, 1); bench!(chunk_size_500, 1_000, 500, 1); bench!(chunk_size_1005, 1_000, 1_005, 1); // change message size only bench!(message_size_1k, 1_000, 1_005, 2); bench!(message_size_5k, 5_000, 1_005, 2); bench!(message_size_10k, 10_000, 1_005, 2); // change message count only bench!(message_count_1, 500, 505, 1); bench!(message_count_10, 500, 505, 10); bench!(message_count_20, 500, 505, 20); benchmark_group!(chunk_size, chunk_size_100, chunk_size_500, chunk_size_1005); benchmark_group!( message_size, message_size_1k, message_size_5k, message_size_10k ); benchmark_group!( message_count, message_count_1, message_count_10, message_count_20 ); benchmark_main!(chunk_size, message_size, message_count); ================================================ FILE: tonic/benches-disabled/README.md ================================================ ## Criterion benchmarks for Tonic ### Running the benchmarks From the root Tonic directory, `cargo bench` After running, the reports can be found in `tonic/target/criterion/report/index.html` [Gnuplot](http://www.gnuplot.info/) is required for graph generation. If gnuplot is not installed, Criterion will display: `Gnuplot not found, disabling plotting` at the console. ### Notes 1) Currently, these benchmarks only test the performance of constructing Tonic Requests and Responses, not over-the-wire throughput. 2) The `thrpt` value generated by Criterion is simply a measure of bytes consumed by the target function. 3) As we are not testing tonic-build compile time, the tests reference pre-compiled .rs files in 'benchmarks/compiled_protos'. 4) The original proto files are in the `proto` directory for reference. 5) This used the Criterion 3.0 `Criterion Group` functionality. Details here: https://docs.rs/criterion/0.3.0/criterion/ ### Interpreting Results Criterion is particularly useful for establishing a first-run baseline and then comparing after code-changes - e.g. `Performance has regressed` below. ```bash Request_Response/request/100000 time: [2.7231 us 2.7588 us 2.7969 us] thrpt: [33.298 GiB/s 33.758 GiB/s 34.200 GiB/s] change: time: [+16.073% +17.871% +19.980%] (p = 0.00 < 0.05) thrpt: [-16.653% -15.162% -13.847%] Performance has regressed. Found 3 outliers among 100 measurements (3.00%) 1 (1.00%) high mild 2 (2.00%) high severe ``` ================================================ FILE: tonic/benches-disabled/bench_main.rs ================================================ #![cfg(feature = "broken")] use criterion::*; mod benchmarks; criterion_group!( benches, benchmarks::request_response::bench_throughput, benchmarks::request_response_diverse_types::bench_throughput, ); criterion_main!(benches); ================================================ FILE: tonic/benches-disabled/benchmarks/compiled_protos/diverse_types.rs ================================================ #[derive(Clone, PartialEq, ::prost::Message)] pub struct GoogleMessage1 { #[prost(string, tag = "1")] pub field1: std::string::String, #[prost(string, tag = "9")] pub field9: std::string::String, #[prost(string, tag = "18")] pub field18: std::string::String, #[prost(bool, tag = "80")] pub field80: bool, #[prost(bool, tag = "81")] pub field81: bool, #[prost(int32, tag = "2")] pub field2: i32, #[prost(int32, tag = "3")] pub field3: i32, #[prost(int32, tag = "280")] pub field280: i32, #[prost(int32, tag = "6")] pub field6: i32, #[prost(int64, tag = "22")] pub field22: i64, #[prost(string, tag = "4")] pub field4: std::string::String, #[prost(fixed64, repeated, tag = "5")] pub field5: ::std::vec::Vec, #[prost(bool, tag = "59")] pub field59: bool, #[prost(string, tag = "7")] pub field7: std::string::String, #[prost(int32, tag = "16")] pub field16: i32, #[prost(int32, tag = "130")] pub field130: i32, #[prost(bool, tag = "12")] pub field12: bool, #[prost(bool, tag = "17")] pub field17: bool, #[prost(bool, tag = "13")] pub field13: bool, #[prost(bool, tag = "14")] pub field14: bool, #[prost(int32, tag = "104")] pub field104: i32, #[prost(int32, tag = "100")] pub field100: i32, #[prost(int32, tag = "101")] pub field101: i32, #[prost(string, tag = "102")] pub field102: std::string::String, #[prost(string, tag = "103")] pub field103: std::string::String, #[prost(int32, tag = "29")] pub field29: i32, #[prost(bool, tag = "30")] pub field30: bool, #[prost(int32, tag = "60")] pub field60: i32, #[prost(int32, tag = "271")] pub field271: i32, #[prost(int32, tag = "272")] pub field272: i32, #[prost(int32, tag = "150")] pub field150: i32, #[prost(int32, tag = "23")] pub field23: i32, #[prost(bool, tag = "24")] pub field24: bool, #[prost(int32, tag = "25")] pub field25: i32, #[prost(message, optional, tag = "15")] pub field15: ::std::option::Option, #[prost(bool, tag = "78")] pub field78: bool, #[prost(int32, tag = "67")] pub field67: i32, #[prost(int32, tag = "68")] pub field68: i32, #[prost(int32, tag = "128")] pub field128: i32, #[prost(string, tag = "129")] pub field129: std::string::String, #[prost(int32, tag = "131")] pub field131: i32, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct GoogleMessage1SubMessage { #[prost(int32, tag = "1")] pub field1: i32, #[prost(int32, tag = "2")] pub field2: i32, #[prost(int32, tag = "3")] pub field3: i32, #[prost(string, tag = "15")] pub field15: std::string::String, #[prost(bool, tag = "12")] pub field12: bool, #[prost(int64, tag = "13")] pub field13: i64, #[prost(int64, tag = "14")] pub field14: i64, #[prost(int32, tag = "16")] pub field16: i32, #[prost(int32, tag = "19")] pub field19: i32, #[prost(bool, tag = "20")] pub field20: bool, #[prost(bool, tag = "28")] pub field28: bool, #[prost(fixed64, tag = "21")] pub field21: u64, #[prost(int32, tag = "22")] pub field22: i32, #[prost(bool, tag = "23")] pub field23: bool, #[prost(bool, tag = "206")] pub field206: bool, #[prost(fixed32, tag = "203")] pub field203: u32, #[prost(int32, tag = "204")] pub field204: i32, #[prost(string, tag = "205")] pub field205: std::string::String, #[prost(uint64, tag = "207")] pub field207: u64, #[prost(uint64, tag = "300")] pub field300: u64, } ================================================ FILE: tonic/benches-disabled/benchmarks/compiled_protos/helloworld.rs ================================================ /// The request message containing the user's name. #[derive(Clone, PartialEq, ::prost::Message)] pub struct HelloRequest { #[prost(string, tag = "1")] pub name: std::string::String, } /// The response message containing the greetings #[derive(Clone, PartialEq, ::prost::Message)] pub struct HelloReply { #[prost(string, tag = "1")] pub message: std::string::String, } #[doc = r" Generated client implementations."] pub mod client { #![allow(unused_variables, dead_code, missing_docs)] use tonic::codegen::*; #[doc = " The greeting service definition."] pub struct GreeterClient { inner: tonic::client::Grpc, } impl GreeterClient { #[doc = r" Attempt to create a new client by connecting to a given endpoint."] pub async fn connect(dst: D) -> Result where D: TryInto, D::Error: Into, { let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; Ok(Self::new(conn)) } } impl GreeterClient where T: tonic::client::GrpcService, T::ResponseBody: Body + Send + 'static, T::Error: Into, ::Error: Into + Send, ::Data: Into + Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } #[doc = r" Check if the service is ready."] pub async fn ready(&mut self) -> Result<(), tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::new( tonic::Code::Unknown, format!("Service was not ready: {}", e.into()), ) }) } #[doc = " Sends a greeting"] pub async fn say_hello( &mut self, request: tonic::Request, ) -> Result, tonic::Status> { self.ready().await?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/helloworld.Greeter/SayHello"); self.inner.unary(request, path, codec).await } } impl Clone for GreeterClient { fn clone(&self) -> Self { Self { inner: self.inner.clone(), } } } } #[doc = r" Generated server implementations."] pub mod server { #![allow(unused_variables, dead_code, missing_docs)] use tonic::codegen::*; #[doc = "Generated trait containing gRPC methods that should be implemented for use with GreeterServer."] #[async_trait] pub trait Greeter: Send + Sync + 'static { #[doc = " Sends a greeting"] async fn say_hello( &self, request: tonic::Request, ) -> Result, tonic::Status> { Err(tonic::Status::unimplemented("Not yet implemented")) } } #[doc = " The greeting service definition."] #[derive(Clone, Debug)] pub struct GreeterServer { inner: Arc, } #[derive(Clone, Debug)] #[doc(hidden)] pub struct GreeterServerSvc { inner: Arc, } impl GreeterServer { #[doc = "Create a new GreeterServer from a type that implements Greeter."] pub fn new(inner: T) -> Self { let inner = Arc::new(inner); Self::from_shared(inner) } pub fn from_shared(inner: Arc) -> Self { Self { inner } } } impl GreeterServerSvc { pub fn new(inner: Arc) -> Self { Self { inner } } } impl Service for GreeterServer { type Response = GreeterServerSvc; type Error = Never; type Future = Ready>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, _: R) -> Self::Future { ok(GreeterServerSvc::new(self.inner.clone())) } } impl Service> for GreeterServerSvc { type Response = http::Response; type Error = Never; type Future = BoxFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request) -> Self::Future { let inner = self.inner.clone(); match req.uri().path() { "/helloworld.Greeter/SayHello" => { struct SayHello(pub Arc); impl tonic::server::UnaryService for SayHello { type Response = super::HelloReply; type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { inner.say_hello(request).await }; Box::pin(fut) } } let inner = self.inner.clone(); let fut = async move { let method = SayHello(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec); let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) } _ => Box::pin(async move { Ok(http::Response::builder() .status(200) .header("grpc-status", "12") .body(empty_body()) .unwrap()) }), } } } } ================================================ FILE: tonic/benches-disabled/benchmarks/compiled_protos/mod.rs ================================================ pub mod diverse_types; pub mod helloworld; ================================================ FILE: tonic/benches-disabled/benchmarks/mod.rs ================================================ pub mod request_response; pub mod request_response_diverse_types; pub mod compiled_protos; mod utils; ================================================ FILE: tonic/benches-disabled/benchmarks/request_response.rs ================================================ use criterion::*; use crate::benchmarks::compiled_protos::helloworld::{HelloReply, HelloRequest}; use crate::benchmarks::utils; fn build_request(_name: String) { let _request = tonic::Request::new(HelloRequest { name: _name }); } fn build_response(_message: String) { let _response = tonic::Request::new(HelloReply { message: _message }); } pub fn bench_throughput(c: &mut Criterion) { let mut group = c.benchmark_group("Request_Response"); let plot_config = PlotConfiguration::default().summary_scale(AxisScale::Logarithmic); group.plot_config(plot_config); let tiny_string = utils::generate_rnd_string(100).unwrap(); let short_string = utils::generate_rnd_string(1_000).unwrap(); let medium_string = utils::generate_rnd_string(10_000).unwrap(); let big_string = utils::generate_rnd_string(100_000).unwrap(); let huge_string = utils::generate_rnd_string(1_000_000).unwrap(); let massive_string = utils::generate_rnd_string(10_000_000).unwrap(); for size in [ tiny_string, short_string, medium_string, big_string, huge_string, massive_string, ] .iter() { group.throughput(Throughput::Bytes(size.len() as u64)); group.bench_with_input(BenchmarkId::new("request", size.len()), size, |b, i| { b.iter(|| build_request(i.to_string())) }); group.bench_with_input(BenchmarkId::new("response", size.len()), size, |b, i| { b.iter(|| build_response(i.to_string())) }); } group.finish(); } ================================================ FILE: tonic/benches-disabled/benchmarks/request_response_diverse_types.rs ================================================ use criterion::*; use crate::benchmarks::compiled_protos::diverse_types::{GoogleMessage1, GoogleMessage1SubMessage}; use crate::benchmarks::utils; fn build_request(_name: String) { let sub_message = GoogleMessage1SubMessage { field1: 10, field2: 20, field3: 30, field15: _name, field12: false, field13: 70, field14: 80, field16: 90, field19: 100, field20: true, field28: false, field21: 110, field22: 120, field23: false, field206: true, field203: 233, field204: 333, field205: String::from("idiopathic"), field207: 4000, field300: 4000, }; let _request = tonic::Request::new(GoogleMessage1 { field1: String::from("foo"), field9: String::from("red"), field18: String::from("red"), field80: true, field81: true, field2: 10, field3: 30, field280: 28, field6: 60, field22: 220, field4: String::from("red"), field5: Vec::new(), field59: true, field7: String::from("blue"), field16: 160, field130: 13, field17: false, field12: true, field13: true, field14: false, field104: 1040, field100: 50, field101: 1010, field102: String::from("green"), field103: String::from("pink"), field29: 290, field30: true, field60: 601, field271: 27, field272: 200, field150: 15, field23: 230, field24: false, field25: 250, field15: Some(sub_message), field78: true, field67: 670, field68: 680, field128: 1280, field129: String::from("red"), field131: 300, }); } pub fn bench_throughput(c: &mut Criterion) { let mut group = c.benchmark_group("Request_Response_Diverse_Types"); //log plot to get everything on the graph let plot_config = PlotConfiguration::default().summary_scale(AxisScale::Logarithmic); group.plot_config(plot_config); let tiny_string = utils::generate_rnd_string(100).unwrap(); let short_string = utils::generate_rnd_string(1_000).unwrap(); let medium_string = utils::generate_rnd_string(10_000).unwrap(); for size in [tiny_string, short_string, medium_string].iter() { group.throughput(Throughput::Bytes(size.len() as u64)); group.bench_with_input(BenchmarkId::new("request", size.len()), size, |b, i| { b.iter(|| build_request(i.to_string())) }); } group.finish(); } ================================================ FILE: tonic/benches-disabled/benchmarks/utils.rs ================================================ use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; pub fn generate_rnd_string(string_size: usize) -> Result> { let rand_name: String = thread_rng() .sample_iter(&Alphanumeric) .take(string_size) .collect(); Ok(rand_name) } ================================================ FILE: tonic/benches-disabled/proto/diverse_types/diverse_types.proto ================================================ // Benchmark messages for proto3. // Pinched from the protobuf benchmarks syntax = "proto3"; package benchmarks.proto3; option java_package = "com.google.protobuf.benchmarks"; // This is the default, but we specify it here explicitly. option optimize_for = SPEED; option cc_enable_arenas = true; message GoogleMessage1 { string field1 = 1; string field9 = 9; string field18 = 18; bool field80 = 80; bool field81 = 81; int32 field2 = 2; int32 field3 = 3; int32 field280 = 280; int32 field6 = 6; int64 field22 = 22; string field4 = 4; repeated fixed64 field5 = 5; bool field59 = 59; string field7 = 7; int32 field16 = 16; int32 field130 = 130; bool field12 = 12; bool field17 = 17; bool field13 = 13; bool field14 = 14; int32 field104 = 104; int32 field100 = 100; int32 field101 = 101; string field102 = 102; string field103 = 103; int32 field29 = 29; bool field30 = 30; int32 field60 = 60; int32 field271 = 271; int32 field272 = 272; int32 field150 = 150; int32 field23 = 23; bool field24 = 24; int32 field25 = 25; GoogleMessage1SubMessage field15 = 15; bool field78 = 78; int32 field67 = 67; int32 field68 = 68; int32 field128 = 128; string field129 = 129; int32 field131 = 131; } message GoogleMessage1SubMessage { int32 field1 = 1; int32 field2 = 2; int32 field3 = 3; string field15 = 15; bool field12 = 12; int64 field13 = 13; int64 field14 = 14; int32 field16 = 16; int32 field19 = 19; bool field20 = 20; bool field28 = 28; fixed64 field21 = 21; int32 field22 = 22; bool field23 = 23; bool field206 = 206; fixed32 field203 = 203; int32 field204 = 204; string field205 = 205; uint64 field207 = 207; uint64 field300 = 300; } ================================================ FILE: tonic/benches-disabled/proto/helloworld/helloworld.proto ================================================ // Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; package helloworld; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } ================================================ FILE: tonic/src/body.rs ================================================ //! HTTP specific body utilities. use std::{pin::Pin, task::Poll}; use http_body_util::BodyExt as _; // A type erased HTTP body. type BoxBody = http_body_util::combinators::UnsyncBoxBody; /// A body type used in `tonic`. #[derive(Debug)] pub struct Body { kind: Kind, } #[derive(Debug)] enum Kind { Empty, Wrap(BoxBody), } impl Body { fn from_kind(kind: Kind) -> Self { Self { kind } } /// Create a new empty `Body`. pub const fn empty() -> Self { Self { kind: Kind::Empty } } /// Create a new `Body` from an existing `Body`. pub fn new(body: B) -> Self where B: http_body::Body + Send + 'static, B::Error: Into, { if body.is_end_stream() { return Self::empty(); } let mut body = Some(body); if let Some(body) = ::downcast_mut::>(&mut body) { return body.take().unwrap(); } if let Some(body) = ::downcast_mut::>(&mut body) { return Self::from_kind(Kind::Wrap(body.take().unwrap())); } let body = body .unwrap() .map_err(crate::Status::map_error) .boxed_unsync(); Self::from_kind(Kind::Wrap(body)) } } impl Default for Body { fn default() -> Self { Self::empty() } } impl http_body::Body for Body { type Data = bytes::Bytes; type Error = crate::Status; fn poll_frame( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll, Self::Error>>> { match &mut self.kind { Kind::Empty => Poll::Ready(None), Kind::Wrap(body) => Pin::new(body).poll_frame(cx), } } fn size_hint(&self) -> http_body::SizeHint { match &self.kind { Kind::Empty => http_body::SizeHint::with_exact(0), Kind::Wrap(body) => body.size_hint(), } } fn is_end_stream(&self) -> bool { match &self.kind { Kind::Empty => true, Kind::Wrap(body) => body.is_end_stream(), } } } ================================================ FILE: tonic/src/client/grpc.rs ================================================ use crate::codec::EncodeBody; use crate::codec::{CompressionEncoding, EnabledCompressionEncodings}; use crate::metadata::GRPC_CONTENT_TYPE; use crate::{ Code, Request, Response, Status, body::Body, client::GrpcService, codec::{Codec, Decoder, Streaming}, request::SanitizeHeaders, }; use http::{ header::{CONTENT_TYPE, HeaderValue, TE}, uri::{PathAndQuery, Uri}, }; use http_body::Body as HttpBody; use std::{fmt, future, pin::pin}; use tokio_stream::{Stream, StreamExt}; /// A gRPC client dispatcher. /// /// This will wrap some inner [`GrpcService`] and will encode/decode /// messages via the provided codec. /// /// Each request method takes a [`Request`], a [`PathAndQuery`], and a /// [`Codec`]. The request contains the message to send via the /// [`Codec::encoder`]. The path determines the fully qualified path /// that will be append to the outgoing uri. The path must follow /// the conventions explained in the [gRPC protocol definition] under `Path →`. An /// example of this path could look like `/greeter.Greeter/SayHello`. /// /// [gRPC protocol definition]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests pub struct Grpc { inner: T, config: GrpcConfig, } struct GrpcConfig { origin: Uri, /// Which compression encodings does the client accept? accept_compression_encodings: EnabledCompressionEncodings, /// The compression encoding that will be applied to requests. send_compression_encodings: Option, /// Limits the maximum size of a decoded message. max_decoding_message_size: Option, /// Limits the maximum size of an encoded message. max_encoding_message_size: Option, } impl Grpc { /// Creates a new gRPC client with the provided [`GrpcService`]. pub fn new(inner: T) -> Self { Self::with_origin(inner, Uri::default()) } /// Creates a new gRPC client with the provided [`GrpcService`] and `Uri`. /// /// The provided Uri will use only the scheme and authority parts as the /// path_and_query portion will be set for each method. pub fn with_origin(inner: T, origin: Uri) -> Self { Self { inner, config: GrpcConfig { origin, send_compression_encodings: None, accept_compression_encodings: EnabledCompressionEncodings::default(), max_decoding_message_size: None, max_encoding_message_size: None, }, } } /// Compress requests with the provided encoding. /// /// Requires the server to accept the specified encoding, otherwise it might return an error. /// /// # Example /// /// The most common way of using this is through a client generated by tonic-build: /// /// ```rust /// use tonic::transport::Channel; /// # enum CompressionEncoding { Gzip } /// # struct TestClient(T); /// # impl TestClient { /// # fn new(channel: T) -> Self { Self(channel) } /// # fn send_compressed(self, _: CompressionEncoding) -> Self { self } /// # } /// /// # async { /// let channel = Channel::builder("127.0.0.1:3000".parse().unwrap()) /// .connect() /// .await /// .unwrap(); /// /// let client = TestClient::new(channel).send_compressed(CompressionEncoding::Gzip); /// # }; /// ``` pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.config.send_compression_encodings = Some(encoding); self } /// Enable accepting compressed responses. /// /// Requires the server to also support sending compressed responses. /// /// # Example /// /// The most common way of using this is through a client generated by tonic-build: /// /// ```rust /// use tonic::transport::Channel; /// # enum CompressionEncoding { Gzip } /// # struct TestClient(T); /// # impl TestClient { /// # fn new(channel: T) -> Self { Self(channel) } /// # fn accept_compressed(self, _: CompressionEncoding) -> Self { self } /// # } /// /// # async { /// let channel = Channel::builder("127.0.0.1:3000".parse().unwrap()) /// .connect() /// .await /// .unwrap(); /// /// let client = TestClient::new(channel).accept_compressed(CompressionEncoding::Gzip); /// # }; /// ``` pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.config.accept_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. /// /// # Example /// /// The most common way of using this is through a client generated by tonic-build: /// /// ```rust /// use tonic::transport::Channel; /// # struct TestClient(T); /// # impl TestClient { /// # fn new(channel: T) -> Self { Self(channel) } /// # fn max_decoding_message_size(self, _: usize) -> Self { self } /// # } /// /// # async { /// let channel = Channel::builder("127.0.0.1:3000".parse().unwrap()) /// .connect() /// .await /// .unwrap(); /// /// // Set the limit to 2MB, Defaults to 4MB. /// let limit = 2 * 1024 * 1024; /// let client = TestClient::new(channel).max_decoding_message_size(limit); /// # }; /// ``` pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.config.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. /// /// # Example /// /// The most common way of using this is through a client generated by tonic-build: /// /// ```rust /// use tonic::transport::Channel; /// # struct TestClient(T); /// # impl TestClient { /// # fn new(channel: T) -> Self { Self(channel) } /// # fn max_encoding_message_size(self, _: usize) -> Self { self } /// # } /// /// # async { /// let channel = Channel::builder("127.0.0.1:3000".parse().unwrap()) /// .connect() /// .await /// .unwrap(); /// /// // Set the limit to 2MB, Defaults to `usize::MAX`. /// let limit = 2 * 1024 * 1024; /// let client = TestClient::new(channel).max_encoding_message_size(limit); /// # }; /// ``` pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.config.max_encoding_message_size = Some(limit); self } /// Check if the inner [`GrpcService`] is able to accept a new request. /// /// This will call [`GrpcService::poll_ready`] until it returns ready or /// an error. If this returns ready the inner [`GrpcService`] is ready to /// accept one more request. pub async fn ready(&mut self) -> Result<(), T::Error> where T: GrpcService, { future::poll_fn(|cx| self.inner.poll_ready(cx)).await } /// Send a single unary gRPC request. pub async fn unary( &mut self, request: Request, path: PathAndQuery, codec: C, ) -> Result, Status> where T: GrpcService, T::ResponseBody: HttpBody + Send + 'static, ::Error: Into, C: Codec, M1: Send + Sync + 'static, M2: Send + Sync + 'static, { let request = request.map(|m| tokio_stream::once(m)); self.client_streaming(request, path, codec).await } /// Send a client side streaming gRPC request. pub async fn client_streaming( &mut self, request: Request, path: PathAndQuery, codec: C, ) -> Result, Status> where T: GrpcService, T::ResponseBody: HttpBody + Send + 'static, ::Error: Into, S: Stream + Send + 'static, C: Codec, M1: Send + Sync + 'static, M2: Send + Sync + 'static, { let (mut parts, body, extensions) = self.streaming(request, path, codec).await?.into_parts(); let mut body = pin!(body); let message = body .try_next() .await .map_err(|mut status| { status.metadata_mut().merge(parts.clone()); status })? .ok_or_else(|| Status::internal("Missing response message."))?; if let Some(trailers) = body.trailers().await? { parts.merge(trailers); } Ok(Response::from_parts(parts, message, extensions)) } /// Send a server side streaming gRPC request. pub async fn server_streaming( &mut self, request: Request, path: PathAndQuery, codec: C, ) -> Result>, Status> where T: GrpcService, T::ResponseBody: HttpBody + Send + 'static, ::Error: Into, C: Codec, M1: Send + Sync + 'static, M2: Send + Sync + 'static, { let request = request.map(|m| tokio_stream::once(m)); self.streaming(request, path, codec).await } /// Send a bi-directional streaming gRPC request. pub async fn streaming( &mut self, request: Request, path: PathAndQuery, mut codec: C, ) -> Result>, Status> where T: GrpcService, T::ResponseBody: HttpBody + Send + 'static, ::Error: Into, S: Stream + Send + 'static, C: Codec, M1: Send + Sync + 'static, M2: Send + Sync + 'static, { let request = request .map(|s| { EncodeBody::new_client( codec.encoder(), s.map(Ok), self.config.send_compression_encodings, self.config.max_encoding_message_size, ) }) .map(Body::new); let request = self.config.prepare_request(request, path); let response = self .inner .call(request) .await .map_err(Status::from_error_generic)?; let decoder = codec.decoder(); self.create_response(decoder, response) } // Keeping this code in a separate function from Self::streaming lets functions that return the // same output share the generated binary code fn create_response( &self, decoder: impl Decoder + Send + 'static, response: http::Response, ) -> Result>, Status> where T: GrpcService, T::ResponseBody: HttpBody + Send + 'static, ::Error: Into, { let encoding = CompressionEncoding::from_encoding_header( response.headers(), self.config.accept_compression_encodings, )?; let status_code = response.status(); let trailers_only_status = Status::from_header_map(response.headers()); // We do not need to check for trailers if the `grpc-status` header is present // with a valid code. let expect_additional_trailers = if let Some(status) = trailers_only_status { if status.code() != Code::Ok { return Err(status); } false } else { true }; let response = response.map(|body| { if expect_additional_trailers { Streaming::new_response( decoder, body, status_code, encoding, self.config.max_decoding_message_size, ) } else { Streaming::new_empty(decoder, body) } }); Ok(Response::from_http(response)) } } impl GrpcConfig { fn prepare_request(&self, request: Request, path: PathAndQuery) -> http::Request { let mut parts = self.origin.clone().into_parts(); match &parts.path_and_query { Some(pnq) if pnq != "/" => { parts.path_and_query = Some( format!("{}{}", pnq.path(), path) .parse() .expect("must form valid path_and_query"), ) } _ => { parts.path_and_query = Some(path); } } let uri = Uri::from_parts(parts).expect("path_and_query only is valid Uri"); let mut request = request.into_http( uri, http::Method::POST, http::Version::HTTP_2, SanitizeHeaders::Yes, ); // Add the gRPC related HTTP headers request .headers_mut() .insert(TE, HeaderValue::from_static("trailers")); // Set the content type request .headers_mut() .insert(CONTENT_TYPE, GRPC_CONTENT_TYPE); #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] if let Some(encoding) = self.send_compression_encodings { request.headers_mut().insert( crate::codec::compression::ENCODING_HEADER, encoding.into_header_value(), ); } if let Some(header_value) = self .accept_compression_encodings .into_accept_encoding_header_value() { request.headers_mut().insert( crate::codec::compression::ACCEPT_ENCODING_HEADER, header_value, ); } request } } impl Clone for Grpc { fn clone(&self) -> Self { Self { inner: self.inner.clone(), config: GrpcConfig { origin: self.config.origin.clone(), send_compression_encodings: self.config.send_compression_encodings, accept_compression_encodings: self.config.accept_compression_encodings, max_encoding_message_size: self.config.max_encoding_message_size, max_decoding_message_size: self.config.max_decoding_message_size, }, } } } impl fmt::Debug for Grpc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Grpc") .field("inner", &self.inner) .field("origin", &self.config.origin) .field( "compression_encoding", &self.config.send_compression_encodings, ) .field( "accept_compression_encodings", &self.config.accept_compression_encodings, ) .field( "max_decoding_message_size", &self.config.max_decoding_message_size, ) .field( "max_encoding_message_size", &self.config.max_encoding_message_size, ) .finish() } } ================================================ FILE: tonic/src/client/mod.rs ================================================ //! Generic client implementation. //! //! This module contains the low level components to build a gRPC client. It //! provides a codec agnostic gRPC client dispatcher and a decorated tower //! service trait. //! //! This client is generally used by some code generation tool to provide stubs //! for the gRPC service. Thusly, they are a bit cumbersome to use by hand. //! //! ## Concurrent usage //! //! Upon using the your generated client, you will discover all the functions //! corresponding to your rpc methods take `&mut self`, making concurrent //! usage of the client difficult. The answer is simply to clone the client, //! which is cheap as all client instances will share the same channel for //! communication. For more details, see //! [transport::Channel](../transport/struct.Channel.html#multiplexing-requests). mod grpc; mod service; pub use self::grpc::Grpc; pub use self::service::GrpcService; ================================================ FILE: tonic/src/client/service.rs ================================================ use http_body::Body; use std::future::Future; use std::task::{Context, Poll}; use tower_service::Service; /// Definition of the gRPC trait alias for [`tower_service`]. /// /// This trait enforces that all tower services provided to [`Grpc`] implements /// the correct traits. /// /// [`Grpc`]: ../client/struct.Grpc.html /// [`tower_service`]: https://docs.rs/tower-service pub trait GrpcService { /// Responses body given by the service. type ResponseBody: Body; /// Errors produced by the service. type Error: Into; /// The future response value. type Future: Future, Self::Error>>; /// Returns `Ready` when the service is able to process requests. /// /// Reference [`Service::poll_ready`]. fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll>; /// Process the request and return the response asynchronously. /// /// Reference [`Service::call`]. fn call(&mut self, request: http::Request) -> Self::Future; } impl GrpcService for T where T: Service, Response = http::Response>, T::Error: Into, ResBody: Body, ::Error: Into, { type ResponseBody = ResBody; type Error = T::Error; type Future = T::Future; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { Service::poll_ready(self, cx) } fn call(&mut self, request: http::Request) -> Self::Future { Service::call(self, request) } } ================================================ FILE: tonic/src/codec/buffer.rs ================================================ use bytes::buf::UninitSlice; use bytes::{Buf, BufMut, Bytes, BytesMut}; /// A specialized buffer to decode gRPC messages from. #[derive(Debug)] pub struct DecodeBuf<'a> { buf: &'a mut BytesMut, len: usize, } /// A specialized buffer to encode gRPC messages into. #[derive(Debug)] pub struct EncodeBuf<'a> { buf: &'a mut BytesMut, } impl<'a> DecodeBuf<'a> { pub(crate) fn new(buf: &'a mut BytesMut, len: usize) -> Self { DecodeBuf { buf, len } } } impl Buf for DecodeBuf<'_> { #[inline] fn remaining(&self) -> usize { self.len } #[inline] fn chunk(&self) -> &[u8] { let ret = self.buf.chunk(); if ret.len() > self.len { &ret[..self.len] } else { ret } } #[inline] fn advance(&mut self, cnt: usize) { assert!(cnt <= self.len); self.buf.advance(cnt); self.len -= cnt; } #[inline] fn copy_to_bytes(&mut self, len: usize) -> Bytes { assert!(len <= self.len); self.len -= len; self.buf.copy_to_bytes(len) } } impl<'a> EncodeBuf<'a> { pub(crate) fn new(buf: &'a mut BytesMut) -> Self { EncodeBuf { buf } } } impl EncodeBuf<'_> { /// Reserves capacity for at least `additional` more bytes to be inserted /// into the buffer. /// /// More than `additional` bytes may be reserved in order to avoid frequent /// reallocations. A call to `reserve` may result in an allocation. #[inline] pub fn reserve(&mut self, additional: usize) { self.buf.reserve(additional); } } unsafe impl BufMut for EncodeBuf<'_> { #[inline] fn remaining_mut(&self) -> usize { self.buf.remaining_mut() } #[inline] unsafe fn advance_mut(&mut self, cnt: usize) { unsafe { self.buf.advance_mut(cnt) } } #[inline] fn chunk_mut(&mut self) -> &mut UninitSlice { self.buf.chunk_mut() } #[inline] fn put(&mut self, src: T) where Self: Sized, { self.buf.put(src) } #[inline] fn put_slice(&mut self, src: &[u8]) { self.buf.put_slice(src) } #[inline] fn put_bytes(&mut self, val: u8, cnt: usize) { self.buf.put_bytes(val, cnt); } } #[cfg(test)] mod tests { use super::*; #[test] fn decode_buf() { let mut payload = BytesMut::with_capacity(100); payload.put(&vec![0u8; 50][..]); let mut buf = DecodeBuf::new(&mut payload, 20); assert_eq!(buf.len, 20); assert_eq!(buf.remaining(), 20); assert_eq!(buf.chunk().len(), 20); buf.advance(10); assert_eq!(buf.remaining(), 10); let mut out = [0; 5]; buf.copy_to_slice(&mut out); assert_eq!(buf.remaining(), 5); assert_eq!(buf.chunk().len(), 5); assert_eq!(buf.copy_to_bytes(5).len(), 5); assert!(!buf.has_remaining()); } #[test] fn encode_buf() { let mut bytes = BytesMut::with_capacity(100); let mut buf = EncodeBuf::new(&mut bytes); let initial = buf.remaining_mut(); unsafe { buf.advance_mut(20) }; assert_eq!(buf.remaining_mut(), initial - 20); buf.put_u8(b'a'); assert_eq!(buf.remaining_mut(), initial - 20 - 1); } } ================================================ FILE: tonic/src/codec/compression.rs ================================================ use crate::{Status, metadata::MetadataValue}; use bytes::{Buf, BufMut, BytesMut}; #[cfg(feature = "gzip")] use flate2::read::{GzDecoder, GzEncoder}; #[cfg(feature = "deflate")] use flate2::read::{ZlibDecoder, ZlibEncoder}; use std::{borrow::Cow, fmt}; #[cfg(feature = "zstd")] use zstd::stream::read::{Decoder, Encoder}; pub(crate) const ENCODING_HEADER: &str = "grpc-encoding"; pub(crate) const ACCEPT_ENCODING_HEADER: &str = "grpc-accept-encoding"; /// Struct used to configure which encodings are enabled on a server or channel. /// /// Represents an ordered list of compression encodings that are enabled. #[derive(Debug, Default, Clone, Copy)] pub struct EnabledCompressionEncodings { inner: [Option; 3], } impl EnabledCompressionEncodings { /// Enable a [`CompressionEncoding`]. /// /// Adds the new encoding to the end of the encoding list. pub fn enable(&mut self, encoding: CompressionEncoding) { for e in self.inner.iter_mut() { match e { Some(e) if *e == encoding => return, None => { *e = Some(encoding); return; } _ => continue, } } } /// Remove the last [`CompressionEncoding`]. pub fn pop(&mut self) -> Option { self.inner .iter_mut() .rev() .find(|entry| entry.is_some())? .take() } pub(crate) fn into_accept_encoding_header_value(self) -> Option { let mut value = BytesMut::new(); for encoding in self.inner.into_iter().flatten() { value.put_slice(encoding.as_str().as_bytes()); value.put_u8(b','); } if value.is_empty() { return None; } value.put_slice(b"identity"); Some(http::HeaderValue::from_maybe_shared(value).unwrap()) } /// Check if a [`CompressionEncoding`] is enabled. pub fn is_enabled(&self, encoding: CompressionEncoding) -> bool { self.inner.contains(&Some(encoding)) } /// Check if any [`CompressionEncoding`]s are enabled. pub fn is_empty(&self) -> bool { self.inner.iter().all(|e| e.is_none()) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct CompressionSettings { pub(crate) encoding: CompressionEncoding, /// buffer_growth_interval controls memory growth for internal buffers to balance resizing cost against memory waste. /// The default buffer growth interval is 8 kilobytes. pub(crate) buffer_growth_interval: usize, } /// The compression encodings Tonic supports. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum CompressionEncoding { #[allow(missing_docs)] #[cfg(feature = "gzip")] Gzip, #[allow(missing_docs)] #[cfg(feature = "deflate")] Deflate, #[allow(missing_docs)] #[cfg(feature = "zstd")] Zstd, } impl CompressionEncoding { pub(crate) const ENCODINGS: &'static [CompressionEncoding] = &[ #[cfg(feature = "gzip")] CompressionEncoding::Gzip, #[cfg(feature = "deflate")] CompressionEncoding::Deflate, #[cfg(feature = "zstd")] CompressionEncoding::Zstd, ]; /// Based on the `grpc-accept-encoding` header, pick an encoding to use. pub(crate) fn from_accept_encoding_header( map: &http::HeaderMap, enabled_encodings: EnabledCompressionEncodings, ) -> Option { if enabled_encodings.is_empty() { return None; } let header_value = map.get(ACCEPT_ENCODING_HEADER)?; let header_value_str = header_value.to_str().ok()?; split_by_comma(header_value_str).find_map(|value| match value { #[cfg(feature = "gzip")] "gzip" => Some(CompressionEncoding::Gzip), #[cfg(feature = "deflate")] "deflate" => Some(CompressionEncoding::Deflate), #[cfg(feature = "zstd")] "zstd" => Some(CompressionEncoding::Zstd), _ => None, }) } /// Get the value of `grpc-encoding` header. Returns an error if the encoding isn't supported. pub(crate) fn from_encoding_header( map: &http::HeaderMap, enabled_encodings: EnabledCompressionEncodings, ) -> Result, Status> { let Some(header_value) = map.get(ENCODING_HEADER) else { return Ok(None); }; match header_value.as_bytes() { #[cfg(feature = "gzip")] b"gzip" if enabled_encodings.is_enabled(CompressionEncoding::Gzip) => { Ok(Some(CompressionEncoding::Gzip)) } #[cfg(feature = "deflate")] b"deflate" if enabled_encodings.is_enabled(CompressionEncoding::Deflate) => { Ok(Some(CompressionEncoding::Deflate)) } #[cfg(feature = "zstd")] b"zstd" if enabled_encodings.is_enabled(CompressionEncoding::Zstd) => { Ok(Some(CompressionEncoding::Zstd)) } b"identity" => Ok(None), other => { let other = match std::str::from_utf8(other) { Ok(s) => Cow::Borrowed(s), Err(_) => Cow::Owned(format!("{other:?}")), }; let mut status = Status::unimplemented(format!( "Content is compressed with `{other}` which isn't supported" )); let header_value = enabled_encodings .into_accept_encoding_header_value() .map(MetadataValue::unchecked_from_header_value) .unwrap_or_else(|| MetadataValue::from_static("identity")); status .metadata_mut() .insert(ACCEPT_ENCODING_HEADER, header_value); Err(status) } } } pub(crate) fn as_str(self) -> &'static str { match self { #[cfg(feature = "gzip")] CompressionEncoding::Gzip => "gzip", #[cfg(feature = "deflate")] CompressionEncoding::Deflate => "deflate", #[cfg(feature = "zstd")] CompressionEncoding::Zstd => "zstd", } } #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] pub(crate) fn into_header_value(self) -> http::HeaderValue { http::HeaderValue::from_static(self.as_str()) } } impl fmt::Display for CompressionEncoding { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } fn split_by_comma(s: &str) -> impl Iterator { s.split(',').map(|s| s.trim()) } /// Compress `len` bytes from `decompressed_buf` into `out_buf`. /// buffer_size_increment is a hint to control the growth of out_buf versus the cost of resizing it. #[allow(unused_variables, unreachable_code)] pub(crate) fn compress( settings: CompressionSettings, decompressed_buf: &mut BytesMut, out_buf: &mut BytesMut, len: usize, ) -> Result<(), std::io::Error> { let buffer_growth_interval = settings.buffer_growth_interval; let capacity = ((len / buffer_growth_interval) + 1) * buffer_growth_interval; out_buf.reserve(capacity); #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] let mut out_writer = out_buf.writer(); match settings.encoding { #[cfg(feature = "gzip")] CompressionEncoding::Gzip => { let mut gzip_encoder = GzEncoder::new( &decompressed_buf[0..len], // FIXME: support customizing the compression level flate2::Compression::new(6), ); std::io::copy(&mut gzip_encoder, &mut out_writer)?; } #[cfg(feature = "deflate")] CompressionEncoding::Deflate => { let mut deflate_encoder = ZlibEncoder::new( &decompressed_buf[0..len], // FIXME: support customizing the compression level flate2::Compression::new(6), ); std::io::copy(&mut deflate_encoder, &mut out_writer)?; } #[cfg(feature = "zstd")] CompressionEncoding::Zstd => { let mut zstd_encoder = Encoder::new( &decompressed_buf[0..len], // FIXME: support customizing the compression level zstd::DEFAULT_COMPRESSION_LEVEL, )?; std::io::copy(&mut zstd_encoder, &mut out_writer)?; } } decompressed_buf.advance(len); Ok(()) } /// Decompress `len` bytes from `compressed_buf` into `out_buf`. #[allow(unused_variables, unreachable_code)] pub(crate) fn decompress( settings: CompressionSettings, compressed_buf: &mut BytesMut, mut out_buf: bytes::buf::Limit<&mut BytesMut>, len: usize, ) -> Result<(), std::io::Error> { let buffer_growth_interval = settings.buffer_growth_interval; let estimate_decompressed_len = len * 2; let capacity = std::cmp::min( bytes::buf::Limit::limit(&out_buf), ((estimate_decompressed_len / buffer_growth_interval) + 1) * buffer_growth_interval, ); out_buf.get_mut().reserve(capacity); #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] let mut out_writer = out_buf.writer(); match settings.encoding { #[cfg(feature = "gzip")] CompressionEncoding::Gzip => { let mut gzip_decoder = GzDecoder::new(&compressed_buf[0..len]); std::io::copy(&mut gzip_decoder, &mut out_writer)?; } #[cfg(feature = "deflate")] CompressionEncoding::Deflate => { let mut deflate_decoder = ZlibDecoder::new(&compressed_buf[0..len]); std::io::copy(&mut deflate_decoder, &mut out_writer)?; } #[cfg(feature = "zstd")] CompressionEncoding::Zstd => { let mut zstd_decoder = Decoder::new(&compressed_buf[0..len])?; std::io::copy(&mut zstd_decoder, &mut out_writer)?; } } compressed_buf.advance(len); Ok(()) } /// Controls compression behavior for individual messages within a stream. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum SingleMessageCompressionOverride { /// Inherit whatever compression is already configured. If the stream is compressed this /// message will also be configured. /// /// This is the default. #[default] Inherit, /// Don't compress this message, even if compression is enabled on the stream. Disable, } #[cfg(test)] mod tests { #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] use http::HeaderValue; use super::*; #[test] fn convert_none_into_header_value() { let encodings = EnabledCompressionEncodings::default(); assert!(encodings.into_accept_encoding_header_value().is_none()); } #[test] #[cfg(feature = "gzip")] fn convert_gzip_into_header_value() { const GZIP: HeaderValue = HeaderValue::from_static("gzip,identity"); let encodings = EnabledCompressionEncodings { inner: [Some(CompressionEncoding::Gzip), None, None], }; assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), GZIP); let encodings = EnabledCompressionEncodings { inner: [None, None, Some(CompressionEncoding::Gzip)], }; assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), GZIP); } #[test] #[cfg(feature = "zstd")] fn convert_zstd_into_header_value() { const ZSTD: HeaderValue = HeaderValue::from_static("zstd,identity"); let encodings = EnabledCompressionEncodings { inner: [Some(CompressionEncoding::Zstd), None, None], }; assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), ZSTD); let encodings = EnabledCompressionEncodings { inner: [None, None, Some(CompressionEncoding::Zstd)], }; assert_eq!(encodings.into_accept_encoding_header_value().unwrap(), ZSTD); } #[test] #[cfg(all(feature = "gzip", feature = "deflate", feature = "zstd"))] fn convert_compression_encodings_into_header_value() { let encodings = EnabledCompressionEncodings { inner: [ Some(CompressionEncoding::Gzip), Some(CompressionEncoding::Deflate), Some(CompressionEncoding::Zstd), ], }; assert_eq!( encodings.into_accept_encoding_header_value().unwrap(), HeaderValue::from_static("gzip,deflate,zstd,identity"), ); let encodings = EnabledCompressionEncodings { inner: [ Some(CompressionEncoding::Zstd), Some(CompressionEncoding::Deflate), Some(CompressionEncoding::Gzip), ], }; assert_eq!( encodings.into_accept_encoding_header_value().unwrap(), HeaderValue::from_static("zstd,deflate,gzip,identity"), ); } } ================================================ FILE: tonic/src/codec/decode.rs ================================================ use super::compression::{CompressionEncoding, CompressionSettings, decompress}; use super::{BufferSettings, DEFAULT_MAX_RECV_MESSAGE_SIZE, DecodeBuf, Decoder, HEADER_SIZE}; use crate::{Code, Status, body::Body, metadata::MetadataMap}; use bytes::{Buf, BufMut, BytesMut}; use http::{HeaderMap, StatusCode}; use http_body::Body as HttpBody; use http_body_util::BodyExt; use std::{ fmt, future, pin::Pin, task::ready, task::{Context, Poll}, }; use sync_wrapper::SyncWrapper; use tokio_stream::Stream; use tracing::{debug, trace}; /// Streaming requests and responses. /// /// This will wrap some inner [`Body`] and [`Decoder`] and provide an interface /// to fetch the message stream and trailing metadata pub struct Streaming { decoder: SyncWrapper + Send + 'static>>, inner: StreamingInner, } struct StreamingInner { body: SyncWrapper, state: State, direction: Direction, buf: BytesMut, trailers: Option, decompress_buf: BytesMut, encoding: Option, max_message_size: Option, } impl Unpin for Streaming {} #[derive(Debug, Clone)] enum State { ReadHeader, ReadBody { compression: Option, len: usize, }, Error(Option), } #[derive(Debug, PartialEq, Eq)] enum Direction { Request, Response(StatusCode), EmptyResponse, } impl Streaming { /// Create a new streaming response in the grpc response format for decoding a response [Body] /// into message of type T pub fn new_response( decoder: D, body: B, status_code: StatusCode, encoding: Option, max_message_size: Option, ) -> Self where B: HttpBody + Send + 'static, B::Error: Into, D: Decoder + Send + 'static, { Self::new( decoder, body, Direction::Response(status_code), encoding, max_message_size, ) } /// Create empty response. For creating responses that have no content (headers + trailers only) pub fn new_empty(decoder: D, body: B) -> Self where B: HttpBody + Send + 'static, B::Error: Into, D: Decoder + Send + 'static, { Self::new(decoder, body, Direction::EmptyResponse, None, None) } /// Create a new streaming request in the grpc response format for decoding a request [Body] /// into message of type T pub fn new_request( decoder: D, body: B, encoding: Option, max_message_size: Option, ) -> Self where B: HttpBody + Send + 'static, B::Error: Into, D: Decoder + Send + 'static, { Self::new( decoder, body, Direction::Request, encoding, max_message_size, ) } fn new( decoder: D, body: B, direction: Direction, encoding: Option, max_message_size: Option, ) -> Self where B: HttpBody + Send + 'static, B::Error: Into, D: Decoder + Send + 'static, { let buffer_size = decoder.buffer_settings().buffer_size; Self { decoder: SyncWrapper::new(Box::new(decoder)), inner: StreamingInner { body: SyncWrapper::new(Body::new( body.map_frame(|frame| { frame.map_data(|mut buf| buf.copy_to_bytes(buf.remaining())) }) .map_err(|err| Status::map_error(err.into())), )), state: State::ReadHeader, direction, buf: BytesMut::with_capacity(buffer_size), trailers: None, decompress_buf: BytesMut::new(), encoding, max_message_size, }, } } } impl StreamingInner { fn decode_chunk( &mut self, buffer_settings: BufferSettings, ) -> Result>, Status> { if let State::ReadHeader = self.state { if self.buf.remaining() < HEADER_SIZE { return Ok(None); } let compression_encoding = match self.buf.get_u8() { 0 => None, 1 => { { if self.encoding.is_some() { self.encoding } else { // https://grpc.github.io/grpc/core/md_doc_compression.html // An ill-constructed message with its Compressed-Flag bit set but lacking a grpc-encoding // entry different from identity in its metadata MUST fail with INTERNAL status, // its associated description indicating the invalid Compressed-Flag condition. return Err(Status::internal( "protocol error: received message with compressed-flag but no grpc-encoding was specified", )); } } } f => { trace!("unexpected compression flag"); let message = if let Direction::Response(status) = self.direction { format!( "protocol error: received message with invalid compression flag: {f} (valid flags are 0 and 1) while receiving response with status: {status}" ) } else { format!( "protocol error: received message with invalid compression flag: {f} (valid flags are 0 and 1), while sending request" ) }; return Err(Status::internal(message)); } }; let len = self.buf.get_u32() as usize; let limit = self .max_message_size .unwrap_or(DEFAULT_MAX_RECV_MESSAGE_SIZE); if len > limit { return Err(Status::out_of_range(format!( "Error, decoded message length too large: found {len} bytes, the limit is: {limit} bytes" ))); } self.buf.reserve(len); self.state = State::ReadBody { compression: compression_encoding, len, } } if let State::ReadBody { len, compression } = self.state { // if we haven't read enough of the message then return and keep // reading if self.buf.remaining() < len || self.buf.len() < len { return Ok(None); } let decode_buf = if let Some(encoding) = compression { self.decompress_buf.clear(); let limit = self .max_message_size .unwrap_or(DEFAULT_MAX_RECV_MESSAGE_SIZE); let limited_out_buf = (&mut self.decompress_buf).limit(limit); if let Err(err) = decompress( CompressionSettings { encoding, buffer_growth_interval: buffer_settings.buffer_size, }, &mut self.buf, limited_out_buf, len, ) { if matches!(err.kind(), std::io::ErrorKind::WriteZero) { return Err(Status::resource_exhausted(format!( "Error decompressing: size limit, of {limit} bytes, exceeded while decompressing message" ))); } let message = if let Direction::Response(status) = self.direction { format!( "Error decompressing: {err}, while receiving response with status: {status}" ) } else { format!("Error decompressing: {err}, while sending request") }; return Err(Status::internal(message)); } let decompressed_len = self.decompress_buf.len(); DecodeBuf::new(&mut self.decompress_buf, decompressed_len) } else { DecodeBuf::new(&mut self.buf, len) }; return Ok(Some(decode_buf)); } Ok(None) } // Returns Some(()) if data was found or None if the loop in `poll_next` should break fn poll_frame(&mut self, cx: &mut Context<'_>) -> Poll, Status>> { let frame = match ready!(Pin::new(self.body.get_mut()).poll_frame(cx)) { Some(Ok(frame)) => frame, Some(Err(status)) => { if self.direction == Direction::Request && status.code() == Code::Cancelled { return Poll::Ready(Ok(None)); } let _ = std::mem::replace(&mut self.state, State::Error(Some(status.clone()))); debug!("decoder inner stream error: {:?}", status); return Poll::Ready(Err(status)); } None => { // FIXME: improve buf usage. return Poll::Ready(if self.buf.has_remaining() { trace!("unexpected EOF decoding stream, state: {:?}", self.state); Err(Status::internal("Unexpected EOF decoding stream.")) } else { Ok(None) }); } }; Poll::Ready(if frame.is_data() { self.buf.put(frame.into_data().unwrap()); Ok(Some(())) } else if frame.is_trailers() { if let Some(trailers) = &mut self.trailers { trailers.extend(frame.into_trailers().unwrap()); } else { self.trailers = Some(frame.into_trailers().unwrap()); } Ok(None) } else { panic!("unexpected frame: {frame:?}"); }) } fn response(&mut self) -> Result<(), Status> { if let Direction::Response(status) = self.direction { if let Err(Some(e)) = crate::status::infer_grpc_status(self.trailers.as_ref(), status) { // If the trailers contain a grpc-status, then we should return that as the error // and otherwise stop the stream (by taking the error state) self.trailers.take(); return Err(e); } } Ok(()) } } impl Streaming { /// Fetch the next message from this stream. /// /// # Return value /// /// - `Result::Err(val)` means a gRPC error was sent by the sender instead /// of a valid response message. Refer to [`Status::code`] and /// [`Status::message`] to examine possible error causes. /// /// - `Result::Ok(None)` means the stream was closed by the sender and no /// more messages will be delivered. Further attempts to call /// [`Streaming::message`] will result in the same return value. /// /// - `Result::Ok(Some(val))` means the sender streamed a valid response /// message `val`. /// /// ```rust /// # use tonic::{Streaming, Status, codec::Decoder}; /// # use std::fmt::Debug; /// # async fn next_message_ex(mut request: Streaming) -> Result<(), Status> /// # where T: Debug, /// # D: Decoder + Send + 'static, /// # { /// if let Some(next_message) = request.message().await? { /// println!("{:?}", next_message); /// } /// # Ok(()) /// # } /// ``` pub async fn message(&mut self) -> Result, Status> { match future::poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await { Some(Ok(m)) => Ok(Some(m)), Some(Err(e)) => Err(e), None => Ok(None), } } /// Fetch the trailing metadata. /// /// This will drain the stream of all its messages to receive the trailing /// metadata. If [`Streaming::message`] returns `None` then this function /// will not need to poll for trailers since the body was totally consumed. /// /// ```rust /// # use tonic::{Streaming, Status}; /// # async fn trailers_ex(mut request: Streaming) -> Result<(), Status> { /// if let Some(metadata) = request.trailers().await? { /// println!("{:?}", metadata); /// } /// # Ok(()) /// # } /// ``` pub async fn trailers(&mut self) -> Result, Status> { // Shortcut to see if we already pulled the trailers in the stream step // we need to do that so that the stream can error on trailing grpc-status if let Some(trailers) = self.inner.trailers.take() { return Ok(Some(MetadataMap::from_headers(trailers))); } // To fetch the trailers we must clear the body and drop it. while self.message().await?.is_some() {} // Since we call poll_trailers internally on poll_next we need to // check if it got cached again. if let Some(trailers) = self.inner.trailers.take() { return Ok(Some(MetadataMap::from_headers(trailers))); } // We've polled through all the frames, and still no trailers, return None Ok(None) } fn decode_chunk(&mut self) -> Result, Status> { match self .inner .decode_chunk(self.decoder.get_mut().buffer_settings())? { Some(mut decode_buf) => match self.decoder.get_mut().decode(&mut decode_buf)? { Some(msg) => { self.inner.state = State::ReadHeader; Ok(Some(msg)) } None => Ok(None), }, None => Ok(None), } } } impl Stream for Streaming { type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { loop { // When the stream encounters an error yield that error once and then on subsequent // calls to poll_next return Poll::Ready(None) indicating that the stream has been // fully exhausted. if let State::Error(status) = &mut self.inner.state { return Poll::Ready(status.take().map(Err)); } if let Some(item) = self.decode_chunk()? { return Poll::Ready(Some(Ok(item))); } if ready!(self.inner.poll_frame(cx))?.is_none() { match self.inner.response() { Ok(()) => return Poll::Ready(None), Err(err) => self.inner.state = State::Error(Some(err)), } } } } } impl fmt::Debug for Streaming { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Streaming").finish() } } #[cfg(test)] static_assertions::assert_impl_all!(Streaming<()>: Send, Sync); ================================================ FILE: tonic/src/codec/encode.rs ================================================ use super::compression::{ CompressionEncoding, CompressionSettings, SingleMessageCompressionOverride, compress, }; use super::{BufferSettings, DEFAULT_MAX_SEND_MESSAGE_SIZE, EncodeBuf, Encoder, HEADER_SIZE}; use crate::Status; use bytes::{BufMut, Bytes, BytesMut}; use http::HeaderMap; use http_body::{Body, Frame}; use pin_project::pin_project; use std::{ pin::Pin, task::{Context, Poll, ready}, }; use tokio_stream::{Stream, StreamExt, adapters::Fuse}; /// Combinator for efficient encoding of messages into reasonably sized buffers. /// EncodedBytes encodes ready messages from its delegate stream into a BytesMut, /// splitting off and yielding a buffer when either: /// * The delegate stream polls as not ready, or /// * The encoded buffer surpasses YIELD_THRESHOLD. #[pin_project(project = EncodedBytesProj)] #[derive(Debug)] struct EncodedBytes { #[pin] source: Fuse, encoder: T, compression_encoding: Option, max_message_size: Option, buf: BytesMut, uncompression_buf: BytesMut, error: Option, } impl EncodedBytes { fn new( encoder: T, source: U, compression_encoding: Option, compression_override: SingleMessageCompressionOverride, max_message_size: Option, ) -> Self { let buffer_settings = encoder.buffer_settings(); let buf = BytesMut::with_capacity(buffer_settings.buffer_size); let compression_encoding = if compression_override == SingleMessageCompressionOverride::Disable { None } else { compression_encoding }; let uncompression_buf = if compression_encoding.is_some() { BytesMut::with_capacity(buffer_settings.buffer_size) } else { BytesMut::new() }; Self { source: source.fuse(), encoder, compression_encoding, max_message_size, buf, uncompression_buf, error: None, } } } impl Stream for EncodedBytes where T: Encoder, U: Stream>, { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let EncodedBytesProj { mut source, encoder, compression_encoding, max_message_size, buf, uncompression_buf, error, } = self.project(); let buffer_settings = encoder.buffer_settings(); if let Some(status) = error.take() { return Poll::Ready(Some(Err(status))); } loop { match source.as_mut().poll_next(cx) { Poll::Pending if buf.is_empty() => { return Poll::Pending; } Poll::Ready(None) if buf.is_empty() => { return Poll::Ready(None); } Poll::Pending | Poll::Ready(None) => { return Poll::Ready(Some(Ok(buf.split_to(buf.len()).freeze()))); } Poll::Ready(Some(Ok(item))) => { if let Err(status) = encode_item( encoder, buf, uncompression_buf, *compression_encoding, *max_message_size, buffer_settings, item, ) { return Poll::Ready(Some(Err(status))); } if buf.len() >= buffer_settings.yield_threshold { return Poll::Ready(Some(Ok(buf.split_to(buf.len()).freeze()))); } } Poll::Ready(Some(Err(status))) => { if buf.is_empty() { return Poll::Ready(Some(Err(status))); } *error = Some(status); return Poll::Ready(Some(Ok(buf.split_to(buf.len()).freeze()))); } } } } } fn encode_item( encoder: &mut T, buf: &mut BytesMut, uncompression_buf: &mut BytesMut, compression_encoding: Option, max_message_size: Option, buffer_settings: BufferSettings, item: T::Item, ) -> Result<(), Status> where T: Encoder, { let offset = buf.len(); buf.reserve(HEADER_SIZE); unsafe { buf.advance_mut(HEADER_SIZE); } if let Some(encoding) = compression_encoding { uncompression_buf.clear(); encoder .encode(item, &mut EncodeBuf::new(uncompression_buf)) .map_err(|err| Status::internal(format!("Error encoding: {err}")))?; let uncompressed_len = uncompression_buf.len(); compress( CompressionSettings { encoding, buffer_growth_interval: buffer_settings.buffer_size, }, uncompression_buf, buf, uncompressed_len, ) .map_err(|err| Status::internal(format!("Error compressing: {err}")))?; } else { encoder .encode(item, &mut EncodeBuf::new(buf)) .map_err(|err| Status::internal(format!("Error encoding: {err}")))?; } // now that we know length, we can write the header finish_encoding(compression_encoding, max_message_size, &mut buf[offset..]) } fn finish_encoding( compression_encoding: Option, max_message_size: Option, buf: &mut [u8], ) -> Result<(), Status> { let len = buf.len() - HEADER_SIZE; let limit = max_message_size.unwrap_or(DEFAULT_MAX_SEND_MESSAGE_SIZE); if len > limit { return Err(Status::out_of_range(format!( "Error, encoded message length too large: found {len} bytes, the limit is: {limit} bytes" ))); } if len > u32::MAX as usize { return Err(Status::resource_exhausted(format!( "Cannot return body with more than 4GB of data but got {len} bytes" ))); } { let mut buf = &mut buf[..HEADER_SIZE]; buf.put_u8(compression_encoding.is_some() as u8); buf.put_u32(len as u32); } Ok(()) } #[derive(Debug)] enum Role { Client, Server, } /// A specialized implementation of [Body] for encoding [Result]. #[pin_project] #[derive(Debug)] pub struct EncodeBody { #[pin] inner: EncodedBytes, state: EncodeState, } #[derive(Debug)] struct EncodeState { error: Option, role: Role, is_end_stream: bool, } impl EncodeBody { /// Turns a stream of grpc messages into [EncodeBody] which is used by grpc clients for /// turning the messages into http frames for sending over the network. pub fn new_client( encoder: T, source: U, compression_encoding: Option, max_message_size: Option, ) -> Self { Self { inner: EncodedBytes::new( encoder, source, compression_encoding, SingleMessageCompressionOverride::default(), max_message_size, ), state: EncodeState { error: None, role: Role::Client, is_end_stream: false, }, } } /// Turns a stream of grpc results (message or error status) into [EncodeBody] which is used by grpc /// servers for turning the messages into http frames for sending over the network. pub fn new_server( encoder: T, source: U, compression_encoding: Option, compression_override: SingleMessageCompressionOverride, max_message_size: Option, ) -> Self { Self { inner: EncodedBytes::new( encoder, source, compression_encoding, compression_override, max_message_size, ), state: EncodeState { error: None, role: Role::Server, is_end_stream: false, }, } } } impl EncodeState { fn trailers(&mut self) -> Option> { match self.role { Role::Client => None, Role::Server => { if self.is_end_stream { return None; } self.is_end_stream = true; let status = if let Some(status) = self.error.take() { status } else { Status::ok("") }; Some(status.to_header_map()) } } } } impl Body for EncodeBody where T: Encoder, U: Stream>, { type Data = Bytes; type Error = Status; fn is_end_stream(&self) -> bool { self.state.is_end_stream } fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { let self_proj = self.project(); match ready!(self_proj.inner.poll_next(cx)) { Some(Ok(d)) => Some(Ok(Frame::data(d))).into(), Some(Err(status)) => match self_proj.state.role { Role::Client => Some(Err(status)).into(), Role::Server => { self_proj.state.is_end_stream = true; Some(Ok(Frame::trailers(status.to_header_map()?))).into() } }, None => self_proj .state .trailers() .map(|t| t.map(Frame::trailers)) .into(), } } } ================================================ FILE: tonic/src/codec/mod.rs ================================================ //! Generic encoding and decoding. //! //! This module contains the generic `Codec`, `Encoder` and `Decoder` traits. mod buffer; pub(crate) mod compression; mod decode; mod encode; use crate::Status; use std::io; pub use self::buffer::{DecodeBuf, EncodeBuf}; pub use self::compression::{CompressionEncoding, EnabledCompressionEncodings}; pub use self::decode::Streaming; pub use self::encode::EncodeBody; // Doc hidden since this is used in a test in another crate, we can expose this publically later // if we need it. #[doc(hidden)] pub use self::compression::SingleMessageCompressionOverride; /// Unless overridden, this is the buffer size used for encoding requests. /// This is spent per-rpc, so you may wish to adjust it. The default is /// pretty good for most uses, but if you have a ton of concurrent rpcs /// you may find it too expensive. const DEFAULT_CODEC_BUFFER_SIZE: usize = 8 * 1024; const DEFAULT_YIELD_THRESHOLD: usize = 32 * 1024; /// Settings for how tonic allocates and grows buffers. /// /// Tonic eagerly allocates the buffer_size per RPC, and grows /// the buffer by buffer_size increments to handle larger messages. /// Buffer size defaults to 8KiB. /// /// Example: /// ```ignore /// Buffer start: | 8kb | /// Message received: | 24612 bytes | /// Buffer grows: | 8kb | 8kb | 8kb | 8kb | /// ``` /// /// The buffer grows to the next largest buffer_size increment of /// 32768 to hold 24612 bytes, which is just slightly too large for /// the previous buffer increment of 24576. /// /// If you use a smaller buffer size you will waste less memory, but /// you will allocate more frequently. If one way or the other matters /// more to you, you may wish to customize your tonic Codec (see /// codec_buffers example). /// /// Yield threshold is an optimization for streaming rpcs. Sometimes /// you may have many small messages ready to send. When they are ready, /// it is a much more efficient use of system resources to batch them /// together into one larger send(). The yield threshold controls how /// much you want to bulk up such a batch of ready-to-send messages. /// The larger your yield threshold the more you will batch - and /// consequently allocate contiguous memory, which might be relevant /// if you're considering large numbers here. /// If your server streaming rpc does not reach the yield threshold /// before it reaches Poll::Pending (meaning, it's waiting for more /// data from wherever you're streaming from) then Tonic will just send /// along a smaller batch. Yield threshold is an upper-bound, it will /// not affect the responsiveness of your streaming rpc (for reasonable /// sizes of yield threshold). /// Yield threshold defaults to 32 KiB. #[derive(Clone, Copy, Debug)] pub struct BufferSettings { buffer_size: usize, yield_threshold: usize, } impl BufferSettings { /// Create a new `BufferSettings` pub fn new(buffer_size: usize, yield_threshold: usize) -> Self { Self { buffer_size, yield_threshold, } } } impl Default for BufferSettings { fn default() -> Self { Self { buffer_size: DEFAULT_CODEC_BUFFER_SIZE, yield_threshold: DEFAULT_YIELD_THRESHOLD, } } } // Doc hidden because it's used in tests in another crate but not part of the // public api. #[doc(hidden)] pub const HEADER_SIZE: usize = // compression flag std::mem::size_of::() + // data length std::mem::size_of::(); // The default maximum uncompressed size in bytes for a message. Defaults to 4MB. const DEFAULT_MAX_RECV_MESSAGE_SIZE: usize = 4 * 1024 * 1024; const DEFAULT_MAX_SEND_MESSAGE_SIZE: usize = usize::MAX; /// Trait that knows how to encode and decode gRPC messages. pub trait Codec { /// The encodable message. type Encode: Send + 'static; /// The decodable message. type Decode: Send + 'static; /// The encoder that can encode a message. type Encoder: Encoder + Send + 'static; /// The decoder that can decode a message. type Decoder: Decoder + Send + 'static; /// Fetch the encoder. fn encoder(&mut self) -> Self::Encoder; /// Fetch the decoder. fn decoder(&mut self) -> Self::Decoder; } /// Encodes gRPC message types pub trait Encoder { /// The type that is encoded. type Item; /// The type of encoding errors. /// /// The type of unrecoverable frame encoding errors. type Error: From; /// Encodes a message into the provided buffer. fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error>; /// Controls how tonic creates and expands encode buffers. fn buffer_settings(&self) -> BufferSettings { BufferSettings::default() } } /// Decodes gRPC message types pub trait Decoder { /// The type that is decoded. type Item; /// The type of unrecoverable frame decoding errors. type Error: From; /// Decode a message from the buffer. /// /// The buffer will contain exactly the bytes of a full message. There /// is no need to get the length from the bytes, gRPC framing is handled /// for you. fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result, Self::Error>; /// Controls how tonic creates and expands decode buffers. fn buffer_settings(&self) -> BufferSettings { BufferSettings::default() } } ================================================ FILE: tonic/src/codegen.rs ================================================ //! Codegen exports used by `tonic-build`. pub use async_trait::async_trait; pub use tokio_stream; pub use std::future::Future; pub use std::pin::Pin; pub use std::sync::Arc; pub use std::task::{Context, Poll}; pub use tower_service::Service; pub type StdError = Box; pub use crate::codec::{CompressionEncoding, EnabledCompressionEncodings}; pub use crate::extensions::GrpcMethod; pub use crate::service::interceptor::InterceptedService; pub use bytes::Bytes; pub use http; pub use http_body::Body; pub type BoxFuture = self::Pin> + Send + 'static>>; pub type BoxStream = self::Pin> + Send + 'static>>; ================================================ FILE: tonic/src/extensions.rs ================================================ /// A gRPC Method info extension. #[derive(Debug, Clone)] pub struct GrpcMethod<'a> { service: &'a str, method: &'a str, } impl<'a> GrpcMethod<'a> { /// Create a new `GrpcMethod` extension. #[doc(hidden)] pub fn new(service: &'a str, method: &'a str) -> Self { Self { service, method } } /// gRPC service name pub fn service(&self) -> &str { self.service } /// gRPC method name pub fn method(&self) -> &str { self.method } } ================================================ FILE: tonic/src/lib.rs ================================================ //! A Rust implementation of [gRPC], a high performance, open source, general //! RPC framework that puts mobile and HTTP/2 first. //! //! [`tonic`] is a gRPC over HTTP/2 implementation focused on **high //! performance**, **interoperability**, and **flexibility**. This library was //! created to have first class support of async/await and to act as a core building //! block for production systems written in Rust. //! //! # Examples //! //! Examples can be found in the [`tonic-examples`] crate. //! //! # Getting Started //! //! Follow the instructions in the [`tonic-build`] crate documentation. //! //! # Feature Flags //! //! - `transport`: Enables the fully featured, batteries included client and server //! implementation based on [`hyper`], [`tower`] and [`tokio`]. This enables `server` //! and `channel` features. Enabled by default. //! - `server`: Enables just the full featured server portion of the `transport` feature. //! - `channel`: Enables just the full featured channel portion of the `transport` feature. //! - `router`: Enables the [`axum`] based service router. Enabled by default. //! - `codegen`: Enables all the required exports and optional dependencies required //! for [`tonic-build`]. Enabled by default. //! - `tls-ring`: Enables the [`rustls`] based TLS options for the `transport` feature using //! the [`ring`] libcrypto provider. Not enabled by default. //! - `tls-aws-lc`: Enables the [`rustls`] based TLS options for the `transport` feature using //! the [`aws-lc-rs`] libcrypto provider. Not enabled by default. //! - `tls-native-roots`: Adds system trust roots to [`rustls`]-based gRPC clients using the //! [`rustls-native-certs`] crate. Not enabled by default. //! - `tls-webpki-roots`: Add the standard trust roots from the [`webpki-roots`] crate to //! `rustls`-based gRPC clients. Not enabled by default. //! - `tls-connect-info`: Adds additional implementations of [`Connected`] //! on common TLS connectors. Not enabled by default, unless any of the other `tls-*` //! features are enabled. This feature is useful for when trying to use a custom //! TLS connector with `connect_with_connector` without enabling any `tls-*` features. //! - `gzip`: Enables compressing requests, responses, and streams. Depends on [`flate2`]. //! Not enabled by default. //! - `deflate`: Enables compressing requests, responses, and streams. Depends on [`flate2`]. //! Not enabled by default. //! - `zstd`: Enables compressing requests, responses, and streams. Depends on [`zstd`]. //! Not enabled by default. //! //! # Structure //! //! ## Generic implementation //! //! The main goal of [`tonic`] is to provide a generic gRPC implementation over HTTP/2 //! framing. This means at the lowest level this library provides the ability to use //! a generic HTTP/2 implementation with different types of gRPC encodings formats. Generally, //! some form of codegen should be used instead of interacting directly with the items in //! [`client`] and [`server`]. //! //! ## Transport //! //! The [`transport`] module contains a fully featured HTTP/2.0 [`Channel`] (gRPC terminology) //! and [`Server`]. These implementations are built on top of [`tokio`], [`hyper`] and [`tower`]. //! It also provides many of the features that the core gRPC libraries provide such as load balancing, //! tls, timeouts, and many more. This implementation can also be used as a reference implementation //! to build even more feature rich clients and servers. This module also provides the ability to //! enable TLS using [`rustls`], via the `tls` feature flag. //! //! # Code generated client/server configuration //! //! ## Max Message Size //! //! Currently, both servers and clients can be configured to set the max message encoding and //! decoding size. This will ensure that an incoming gRPC message will not exhaust the systems //! memory. By default, the decoding message limit is `4MB` and the encoding limit is `usize::MAX`. //! //! [gRPC]: https://grpc.io //! [`tonic`]: https://github.com/hyperium/tonic //! [`tokio`]: https://docs.rs/tokio //! [`hyper`]: https://docs.rs/hyper //! [`tower`]: https://docs.rs/tower //! [`tonic-build`]: https://docs.rs/tonic-build //! [`ring`]: https://docs.rs/ring //! [`aws-lc-rs`]: https://docs.rs/aws-lc-rs //! [`tonic-examples`]: https://github.com/hyperium/tonic/tree/master/examples //! [`Codec`]: codec/trait.Codec.html //! [`Channel`]: transport/struct.Channel.html //! [`Server`]: transport/struct.Server.html //! [`Connected`]: transport/server/trait.Connected.html //! [`rustls`]: https://docs.rs/rustls //! [`client`]: client/index.html //! [`transport`]: transport/index.html //! [`rustls-native-certs`]: https://docs.rs/rustls-native-certs //! [`webpki-roots`]: https://docs.rs/webpki-roots //! [`flate2`]: https://docs.rs/flate2 //! [`zstd`]: https://docs.rs/zstd #![recursion_limit = "256"] #![doc( html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" )] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] #![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))] #![cfg_attr(docsrs, feature(doc_cfg))] pub mod body; pub mod client; pub mod codec; pub mod metadata; pub mod server; pub mod service; #[cfg(any(feature = "server", feature = "channel"))] pub mod transport; mod extensions; mod macros; mod request; mod response; mod status; mod util; /// A re-export of [`async-trait`](https://docs.rs/async-trait) for use with codegen. #[cfg(feature = "codegen")] pub use async_trait::async_trait; #[doc(inline)] pub use codec::Streaming; pub use extensions::GrpcMethod; pub use http::Extensions; pub use request::{IntoRequest, IntoStreamingRequest, Request}; pub use response::Response; pub use status::{Code, ConnectError, Status, TimeoutExpired}; pub(crate) type BoxError = Box; #[doc(hidden)] #[cfg(feature = "codegen")] pub mod codegen; /// `Result` is a type that represents either success ([`Ok`]) or failure ([`Err`]). /// By default, the Err value is of type [`Status`] but this can be overridden if desired. pub type Result = std::result::Result; ================================================ FILE: tonic/src/macros.rs ================================================ /// Include generated proto server and client items. /// /// You must specify the gRPC package name. /// /// ```rust,ignore /// mod pb { /// tonic::include_proto!("helloworld"); /// } /// ``` /// /// # Note: /// **This only works if the tonic-build output directory has been unmodified**. /// The default output directory is set to the [`OUT_DIR`] environment variable. /// If the output directory has been modified, the following pattern may be used /// instead of this macro. /// /// ```rust,ignore /// mod pb { /// include!("/relative/protobuf/directory/helloworld.rs"); /// } /// ``` /// You can also use a custom environment variable using the following pattern. /// ```rust,ignore /// mod pb { /// include!(concat!(env!("PROTOBUFS"), "/helloworld.rs")); /// } /// ``` /// /// [`OUT_DIR`]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts #[macro_export] macro_rules! include_proto { ($package: tt) => { include!(concat!(env!("OUT_DIR"), concat!("/", $package, ".rs"))); }; } /// Include an encoded `prost_types::FileDescriptorSet` as a `&'static [u8]`. The parameter must be /// the stem of the filename passed to `file_descriptor_set_path` for the `tonic-build::Builder`, /// excluding the `.bin` extension. /// /// For example, a file descriptor set compiled like this in `build.rs`: /// /// ```rust,ignore /// let descriptor_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("my_descriptor.bin") /// tonic_build::configure() /// .file_descriptor_set_path(&descriptor_path) /// .compile_protos(&["proto/reflection.proto"], &["proto/"])?; /// ``` /// /// Can be included like this: /// /// ```rust,ignore /// mod pb { /// pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("my_descriptor"); /// } /// ``` /// /// # Note: /// **This only works if the tonic-build output directory has been unmodified**. /// The default output directory is set to the [`OUT_DIR`] environment variable. /// If the output directory has been modified, the following pattern may be used /// instead of this macro. /// /// ```rust,ignore /// mod pb { /// pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("/relative/protobuf/directory/descriptor_name.bin"); /// } /// ``` /// /// [`OUT_DIR`]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts #[macro_export] macro_rules! include_file_descriptor_set { ($package: tt) => { include_bytes!(concat!(env!("OUT_DIR"), concat!("/", $package, ".bin"))) }; } ================================================ FILE: tonic/src/metadata/encoding.rs ================================================ use base64::Engine as _; use bytes::Bytes; use http::header::HeaderValue; use std::error::Error; use std::fmt; use std::hash::Hash; /// A possible error when converting a `MetadataValue` from a string or byte /// slice. #[derive(Debug, Hash)] pub struct InvalidMetadataValue { _priv: (), } mod value_encoding { use super::InvalidMetadataValueBytes; use bytes::Bytes; use http::header::HeaderValue; use std::fmt; pub trait Sealed { #[doc(hidden)] fn is_empty(value: &[u8]) -> bool; #[doc(hidden)] fn from_bytes(value: &[u8]) -> Result; #[doc(hidden)] fn from_shared(value: Bytes) -> Result; #[doc(hidden)] fn from_static(value: &'static str) -> HeaderValue; #[doc(hidden)] fn decode(value: &[u8]) -> Result; #[doc(hidden)] fn equals(a: &HeaderValue, b: &[u8]) -> bool; #[doc(hidden)] fn values_equal(a: &HeaderValue, b: &HeaderValue) -> bool; #[doc(hidden)] fn fmt(value: &HeaderValue, f: &mut fmt::Formatter<'_>) -> fmt::Result; } } pub trait ValueEncoding: Clone + Eq + PartialEq + Hash + self::value_encoding::Sealed { /// Returns true if the provided key is valid for this ValueEncoding type. /// For example, `Ascii::is_valid_key("a") == true`, /// `Ascii::is_valid_key("a-bin") == false`. fn is_valid_key(key: &str) -> bool; } /// gRPC metadata values can be either ASCII strings or binary. Note that only /// visible ASCII characters (32-127) are permitted. /// This type should never be instantiated -- in fact, it's impossible /// to, because there's no variants to instantiate. Instead, it's just used as /// a type parameter for [`MetadataKey`] and [`MetadataValue`]. /// /// [`MetadataKey`]: struct.MetadataKey.html /// [`MetadataValue`]: struct.MetadataValue.html #[derive(Clone, Debug, Eq, PartialEq, Hash)] #[non_exhaustive] pub enum Ascii {} /// gRPC metadata values can be either ASCII strings or binary. /// This type should never be instantiated -- in fact, it's impossible /// to, because there's no variants to instantiate. Instead, it's just used as /// a type parameter for [`MetadataKey`] and [`MetadataValue`]. /// /// [`MetadataKey`]: struct.MetadataKey.html /// [`MetadataValue`]: struct.MetadataValue.html #[derive(Clone, Debug, Eq, PartialEq, Hash)] #[non_exhaustive] pub enum Binary {} // ===== impl ValueEncoding ===== impl self::value_encoding::Sealed for Ascii { fn is_empty(value: &[u8]) -> bool { value.is_empty() } fn from_bytes(value: &[u8]) -> Result { HeaderValue::from_bytes(value).map_err(|_| InvalidMetadataValueBytes::new()) } fn from_shared(value: Bytes) -> Result { HeaderValue::from_maybe_shared(value).map_err(|_| InvalidMetadataValueBytes::new()) } fn from_static(value: &'static str) -> HeaderValue { HeaderValue::from_static(value) } fn decode(value: &[u8]) -> Result { Ok(Bytes::copy_from_slice(value)) } fn equals(a: &HeaderValue, b: &[u8]) -> bool { a.as_bytes() == b } fn values_equal(a: &HeaderValue, b: &HeaderValue) -> bool { a == b } fn fmt(value: &HeaderValue, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(value, f) } } impl ValueEncoding for Ascii { fn is_valid_key(key: &str) -> bool { !Binary::is_valid_key(key) } } impl self::value_encoding::Sealed for Binary { fn is_empty(value: &[u8]) -> bool { for c in value { if *c != b'=' { return false; } } true } fn from_bytes(value: &[u8]) -> Result { let encoded_value: String = crate::util::base64::STANDARD_NO_PAD.encode(value); HeaderValue::from_maybe_shared(Bytes::from(encoded_value)) .map_err(|_| InvalidMetadataValueBytes::new()) } fn from_shared(value: Bytes) -> Result { Self::from_bytes(value.as_ref()) } fn from_static(value: &'static str) -> HeaderValue { if crate::util::base64::STANDARD.decode(value).is_err() { panic!("Invalid base64 passed to from_static: {value}"); } unsafe { // Because this is valid base64 this must be a valid HTTP header value, // no need to check again by calling from_shared. HeaderValue::from_maybe_shared_unchecked(Bytes::from_static(value.as_ref())) } } fn decode(value: &[u8]) -> Result { crate::util::base64::STANDARD .decode(value) .map(|bytes_vec| bytes_vec.into()) .map_err(|_| InvalidMetadataValueBytes::new()) } fn equals(a: &HeaderValue, b: &[u8]) -> bool { if let Ok(decoded) = crate::util::base64::STANDARD.decode(a.as_bytes()) { decoded == b } else { a.as_bytes() == b } } fn values_equal(a: &HeaderValue, b: &HeaderValue) -> bool { match (Self::decode(a.as_bytes()), Self::decode(b.as_bytes())) { (Ok(a), Ok(b)) => a == b, (Err(_), Err(_)) => true, _ => false, } } fn fmt(value: &HeaderValue, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Ok(decoded) = Self::decode(value.as_bytes()) { write!(f, "{decoded:?}") } else { write!(f, "b[invalid]{value:?}") } } } impl ValueEncoding for Binary { fn is_valid_key(key: &str) -> bool { key.ends_with("-bin") } } // ===== impl InvalidMetadataValue ===== impl InvalidMetadataValue { pub(crate) fn new() -> Self { InvalidMetadataValue { _priv: () } } } impl fmt::Display for InvalidMetadataValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("failed to parse metadata value") } } impl Error for InvalidMetadataValue {} /// A possible error when converting a `MetadataValue` from a string or byte /// slice. #[derive(Debug, Hash)] pub struct InvalidMetadataValueBytes(InvalidMetadataValue); // ===== impl InvalidMetadataValueBytes ===== impl InvalidMetadataValueBytes { pub(crate) fn new() -> Self { InvalidMetadataValueBytes(InvalidMetadataValue::new()) } } impl fmt::Display for InvalidMetadataValueBytes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl Error for InvalidMetadataValueBytes {} ================================================ FILE: tonic/src/metadata/key.rs ================================================ use bytes::Bytes; use http::header::HeaderName; use std::borrow::Borrow; use std::error::Error; use std::fmt; use std::marker::PhantomData; use std::str::FromStr; use super::encoding::{Ascii, Binary, ValueEncoding}; /// Represents a custom metadata field name. /// /// `MetadataKey` is used as the [`MetadataMap`] key. /// /// [`HeaderMap`]: struct.HeaderMap.html /// [`MetadataMap`]: struct.MetadataMap.html #[derive(Clone, Eq, PartialEq, Hash)] #[repr(transparent)] pub struct MetadataKey { // Note: There are unsafe transmutes that assume that the memory layout // of MetadataValue is identical to HeaderName pub(crate) inner: http::header::HeaderName, phantom: PhantomData, } /// A possible error when converting a `MetadataKey` from another type. #[derive(Debug, Default)] pub struct InvalidMetadataKey { _priv: (), } /// An ascii metadata key. pub type AsciiMetadataKey = MetadataKey; /// A binary metadata key. pub type BinaryMetadataKey = MetadataKey; impl MetadataKey { /// Converts a slice of bytes to a `MetadataKey`. /// /// This function normalizes the input. pub fn from_bytes(src: &[u8]) -> Result { match HeaderName::from_bytes(src) { Ok(name) => { if !VE::is_valid_key(name.as_str()) { return Err(InvalidMetadataKey::new()); } Ok(MetadataKey { inner: name, phantom: PhantomData, }) } Err(_) => Err(InvalidMetadataKey::new()), } } /// Converts a static string to a `MetadataKey`. /// /// This function panics when the static string is a invalid metadata key. /// /// This function requires the static string to only contain lowercase /// characters, numerals and symbols, as per the HTTP/2.0 specification /// and header names internal representation within this library. /// /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// // Parsing a metadata key /// let CUSTOM_KEY: &'static str = "custom-key"; /// /// let a = AsciiMetadataKey::from_bytes(b"custom-key").unwrap(); /// let b = AsciiMetadataKey::from_static(CUSTOM_KEY); /// assert_eq!(a, b); /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// // Parsing a metadata key that contains invalid symbols(s): /// AsciiMetadataKey::from_static("content{}{}length"); // This line panics! /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// // Parsing a metadata key that contains invalid uppercase characters. /// let a = AsciiMetadataKey::from_static("foobar"); /// let b = AsciiMetadataKey::from_static("FOOBAR"); // This line panics! /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// // Parsing a -bin metadata key as an Ascii key. /// let b = AsciiMetadataKey::from_static("hello-bin"); // This line panics! /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// // Parsing a non-bin metadata key as an Binary key. /// let b = BinaryMetadataKey::from_static("hello"); // This line panics! /// ``` pub fn from_static(src: &'static str) -> Self { let name = HeaderName::from_static(src); if !VE::is_valid_key(name.as_str()) { panic!("invalid metadata key") } MetadataKey { inner: name, phantom: PhantomData, } } /// Returns a `str` representation of the metadata key. /// /// The returned string will always be lower case. #[inline] pub fn as_str(&self) -> &str { self.inner.as_str() } /// Converts a HeaderName reference to a MetadataKey. This method assumes /// that the caller has made sure that the header name has the correct /// "-bin" or non-"-bin" suffix, it does not validate its input. #[inline] pub(crate) fn unchecked_from_header_name_ref(header_name: &HeaderName) -> &Self { unsafe { &*(header_name as *const HeaderName as *const Self) } } /// Converts a HeaderName reference to a MetadataKey. This method assumes /// that the caller has made sure that the header name has the correct /// "-bin" or non-"-bin" suffix, it does not validate its input. #[inline] pub(crate) fn unchecked_from_header_name(name: HeaderName) -> Self { MetadataKey { inner: name, phantom: PhantomData, } } } impl FromStr for MetadataKey { type Err = InvalidMetadataKey; fn from_str(s: &str) -> Result { MetadataKey::from_bytes(s.as_bytes()).map_err(|_| InvalidMetadataKey::new()) } } impl AsRef for MetadataKey { fn as_ref(&self) -> &str { self.as_str() } } impl AsRef<[u8]> for MetadataKey { fn as_ref(&self) -> &[u8] { self.as_str().as_bytes() } } impl Borrow for MetadataKey { fn borrow(&self) -> &str { self.as_str() } } impl fmt::Debug for MetadataKey { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(self.as_str(), fmt) } } impl fmt::Display for MetadataKey { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self.as_str(), fmt) } } impl InvalidMetadataKey { #[doc(hidden)] pub fn new() -> InvalidMetadataKey { Self::default() } } impl<'a, VE: ValueEncoding> From<&'a MetadataKey> for MetadataKey { fn from(src: &'a MetadataKey) -> MetadataKey { src.clone() } } impl From> for Bytes { #[inline] fn from(name: MetadataKey) -> Bytes { Bytes::copy_from_slice(name.inner.as_ref()) } } impl<'a, VE: ValueEncoding> PartialEq<&'a MetadataKey> for MetadataKey { #[inline] fn eq(&self, other: &&'a MetadataKey) -> bool { *self == **other } } impl PartialEq> for &MetadataKey { #[inline] fn eq(&self, other: &MetadataKey) -> bool { *other == *self } } impl PartialEq for MetadataKey { /// Performs a case-insensitive comparison of the string against the header /// name /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let content_length = AsciiMetadataKey::from_static("content-length"); /// /// assert_eq!(content_length, "content-length"); /// assert_eq!(content_length, "Content-Length"); /// assert_ne!(content_length, "content length"); /// ``` #[inline] fn eq(&self, other: &str) -> bool { self.inner.eq(other) } } impl PartialEq> for str { /// Performs a case-insensitive comparison of the string against the header /// name /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let content_length = AsciiMetadataKey::from_static("content-length"); /// /// assert_eq!(content_length, "content-length"); /// assert_eq!(content_length, "Content-Length"); /// assert_ne!(content_length, "content length"); /// ``` #[inline] fn eq(&self, other: &MetadataKey) -> bool { other.inner == *self } } impl<'a, VE: ValueEncoding> PartialEq<&'a str> for MetadataKey { /// Performs a case-insensitive comparison of the string against the header /// name #[inline] fn eq(&self, other: &&'a str) -> bool { *self == **other } } impl PartialEq> for &str { /// Performs a case-insensitive comparison of the string against the header /// name #[inline] fn eq(&self, other: &MetadataKey) -> bool { *other == *self } } impl fmt::Display for InvalidMetadataKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("invalid gRPC metadata key name") } } impl Error for InvalidMetadataKey {} #[cfg(test)] mod tests { use super::{AsciiMetadataKey, BinaryMetadataKey}; #[test] fn test_from_bytes_binary() { assert!(BinaryMetadataKey::from_bytes(b"").is_err()); assert!(BinaryMetadataKey::from_bytes(b"\xFF").is_err()); assert!(BinaryMetadataKey::from_bytes(b"abc").is_err()); assert_eq!( BinaryMetadataKey::from_bytes(b"abc-bin").unwrap().as_str(), "abc-bin" ); } #[test] fn test_from_bytes_ascii() { assert!(AsciiMetadataKey::from_bytes(b"").is_err()); assert!(AsciiMetadataKey::from_bytes(b"\xFF").is_err()); assert_eq!( AsciiMetadataKey::from_bytes(b"abc").unwrap().as_str(), "abc" ); assert!(AsciiMetadataKey::from_bytes(b"abc-bin").is_err()); } } ================================================ FILE: tonic/src/metadata/map.rs ================================================ use http::HeaderName; pub(crate) use self::as_encoding_agnostic_metadata_key::AsEncodingAgnosticMetadataKey; pub(crate) use self::as_metadata_key::AsMetadataKey; pub(crate) use self::into_metadata_key::IntoMetadataKey; use super::encoding::{Ascii, Binary, ValueEncoding}; use super::key::{InvalidMetadataKey, MetadataKey}; use super::value::MetadataValue; use std::marker::PhantomData; /// A set of gRPC custom metadata entries. /// /// # Examples /// /// Basic usage /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// map.insert("x-host", "example.com".parse().unwrap()); /// map.insert("x-number", "123".parse().unwrap()); /// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"[binary data]")); /// /// assert!(map.contains_key("x-host")); /// assert!(!map.contains_key("x-location")); /// /// assert_eq!(map.get("x-host").unwrap(), "example.com"); /// /// map.remove("x-host"); /// /// assert!(!map.contains_key("x-host")); /// ``` #[derive(Clone, Debug, Default)] pub struct MetadataMap { headers: http::HeaderMap, } impl AsRef for MetadataMap { fn as_ref(&self) -> &http::HeaderMap { &self.headers } } impl AsMut for MetadataMap { fn as_mut(&mut self) -> &mut http::HeaderMap { &mut self.headers } } /// `MetadataMap` entry iterator. /// /// Yields `KeyAndValueRef` values. The same header name may be yielded /// more than once if it has more than one associated value. #[derive(Debug)] pub struct Iter<'a> { inner: http::header::Iter<'a, http::header::HeaderValue>, } /// Reference to a key and an associated value in a `MetadataMap`. It can point /// to either an ascii or a binary ("*-bin") key. #[derive(Debug)] pub enum KeyAndValueRef<'a> { /// An ascii metadata key and value. Ascii(&'a MetadataKey, &'a MetadataValue), /// A binary metadata key and value. Binary(&'a MetadataKey, &'a MetadataValue), } /// Reference to a key and an associated value in a `MetadataMap`. It can point /// to either an ascii or a binary ("*-bin") key. #[derive(Debug)] pub enum KeyAndMutValueRef<'a> { /// An ascii metadata key and value. Ascii(&'a MetadataKey, &'a mut MetadataValue), /// A binary metadata key and value. Binary(&'a MetadataKey, &'a mut MetadataValue), } /// `MetadataMap` entry iterator. /// /// Yields `(&MetadataKey, &mut value)` tuples. The same header name may be yielded /// more than once if it has more than one associated value. #[derive(Debug)] pub struct IterMut<'a> { inner: http::header::IterMut<'a, http::header::HeaderValue>, } /// A drain iterator of all values associated with a single metadata key. #[derive(Debug)] pub struct ValueDrain<'a, VE: ValueEncoding> { inner: http::header::ValueDrain<'a, http::header::HeaderValue>, phantom: PhantomData, } /// An iterator over `MetadataMap` keys. /// /// Yields `KeyRef` values. Each header name is yielded only once, even if it /// has more than one associated value. #[derive(Debug)] pub struct Keys<'a> { inner: http::header::Keys<'a, http::header::HeaderValue>, } /// Reference to a key in a `MetadataMap`. It can point /// to either an ascii or a binary ("*-bin") key. #[derive(Debug)] pub enum KeyRef<'a> { /// An ascii metadata key and value. Ascii(&'a MetadataKey), /// A binary metadata key and value. Binary(&'a MetadataKey), } /// `MetadataMap` value iterator. /// /// Yields `ValueRef` values. Each value contained in the `MetadataMap` will be /// yielded. #[derive(Debug)] pub struct Values<'a> { // Need to use http::header::Iter and not http::header::Values to be able // to know if a value is binary or not. inner: http::header::Iter<'a, http::header::HeaderValue>, } /// Reference to a value in a `MetadataMap`. It can point /// to either an ascii or a binary ("*-bin" key) value. #[derive(Debug)] pub enum ValueRef<'a> { /// An ascii metadata key and value. Ascii(&'a MetadataValue), /// A binary metadata key and value. Binary(&'a MetadataValue), } /// `MetadataMap` value iterator. /// /// Each value contained in the `MetadataMap` will be yielded. #[derive(Debug)] pub struct ValuesMut<'a> { // Need to use http::header::IterMut and not http::header::ValuesMut to be // able to know if a value is binary or not. inner: http::header::IterMut<'a, http::header::HeaderValue>, } /// Reference to a value in a `MetadataMap`. It can point /// to either an ascii or a binary ("*-bin" key) value. #[derive(Debug)] pub enum ValueRefMut<'a> { /// An ascii metadata key and value. Ascii(&'a mut MetadataValue), /// A binary metadata key and value. Binary(&'a mut MetadataValue), } /// An iterator of all values associated with a single metadata key. #[derive(Debug)] pub struct ValueIter<'a, VE: ValueEncoding> { inner: Option>, phantom: PhantomData, } /// An iterator of all values associated with a single metadata key. #[derive(Debug)] pub struct ValueIterMut<'a, VE: ValueEncoding> { inner: http::header::ValueIterMut<'a, http::header::HeaderValue>, phantom: PhantomData, } /// A view to all values stored in a single entry. /// /// This struct is returned by `MetadataMap::get_all` and /// `MetadataMap::get_all_bin`. #[derive(Debug)] pub struct GetAll<'a, VE: ValueEncoding> { inner: Option>, phantom: PhantomData, } /// A view into a single location in a `MetadataMap`, which may be vacant or /// occupied. #[derive(Debug)] pub enum Entry<'a, VE: ValueEncoding> { /// An occupied entry Occupied(OccupiedEntry<'a, VE>), /// A vacant entry Vacant(VacantEntry<'a, VE>), } /// A view into a single empty location in a `MetadataMap`. /// /// This struct is returned as part of the `Entry` enum. #[derive(Debug)] pub struct VacantEntry<'a, VE: ValueEncoding> { inner: http::header::VacantEntry<'a, http::header::HeaderValue>, phantom: PhantomData, } /// A view into a single occupied location in a `MetadataMap`. /// /// This struct is returned as part of the `Entry` enum. #[derive(Debug)] pub struct OccupiedEntry<'a, VE: ValueEncoding> { inner: http::header::OccupiedEntry<'a, http::header::HeaderValue>, phantom: PhantomData, } pub(crate) const GRPC_TIMEOUT_HEADER: &str = "grpc-timeout"; // ===== impl MetadataMap ===== impl MetadataMap { // Headers reserved by the gRPC protocol. pub(crate) const GRPC_RESERVED_HEADERS: [HeaderName; 5] = [ HeaderName::from_static("te"), HeaderName::from_static("content-type"), HeaderName::from_static("grpc-message"), HeaderName::from_static("grpc-message-type"), HeaderName::from_static("grpc-status"), ]; /// Create an empty `MetadataMap`. /// /// The map will be created without any capacity. This function will not /// allocate. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let map = MetadataMap::new(); /// /// assert!(map.is_empty()); /// assert_eq!(0, map.capacity()); /// ``` pub fn new() -> Self { MetadataMap::with_capacity(0) } /// Convert an HTTP HeaderMap to a MetadataMap pub fn from_headers(headers: http::HeaderMap) -> Self { MetadataMap { headers } } /// Convert a MetadataMap into a HTTP HeaderMap /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("x-host", "example.com".parse().unwrap()); /// /// let http_map = map.into_headers(); /// /// assert_eq!(http_map.get("x-host").unwrap(), "example.com"); /// ``` pub fn into_headers(self) -> http::HeaderMap { self.headers } pub(crate) fn into_sanitized_headers(mut self) -> http::HeaderMap { for r in &Self::GRPC_RESERVED_HEADERS { self.headers.remove(r); } self.headers } /// Create an empty `MetadataMap` with the specified capacity. /// /// The returned map will allocate internal storage in order to hold about /// `capacity` elements without reallocating. However, this is a "best /// effort" as there are usage patterns that could cause additional /// allocations before `capacity` metadata entries are stored in the map. /// /// More capacity than requested may be allocated. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let map: MetadataMap = MetadataMap::with_capacity(10); /// /// assert!(map.is_empty()); /// assert!(map.capacity() >= 10); /// ``` pub fn with_capacity(capacity: usize) -> MetadataMap { MetadataMap { headers: http::HeaderMap::with_capacity(capacity), } } /// Returns the number of metadata entries (ascii and binary) stored in the /// map. /// /// This number represents the total number of **values** stored in the map. /// This number can be greater than or equal to the number of **keys** /// stored given that a single key may have more than one associated value. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// assert_eq!(0, map.len()); /// /// map.insert("x-host-ip", "127.0.0.1".parse().unwrap()); /// map.insert_bin("x-host-name-bin", MetadataValue::from_bytes(b"localhost")); /// /// assert_eq!(2, map.len()); /// /// map.append("x-host-ip", "text/html".parse().unwrap()); /// /// assert_eq!(3, map.len()); /// ``` pub fn len(&self) -> usize { self.headers.len() } /// Returns the number of keys (ascii and binary) stored in the map. /// /// This number will be less than or equal to `len()` as each key may have /// more than one associated value. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// assert_eq!(0, map.keys_len()); /// /// map.insert("x-host-ip", "127.0.0.1".parse().unwrap()); /// map.insert_bin("x-host-name-bin", MetadataValue::from_bytes(b"localhost")); /// /// assert_eq!(2, map.keys_len()); /// /// map.append("x-host-ip", "text/html".parse().unwrap()); /// /// assert_eq!(2, map.keys_len()); /// ``` pub fn keys_len(&self) -> usize { self.headers.keys_len() } /// Returns true if the map contains no elements. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// assert!(map.is_empty()); /// /// map.insert("x-host", "hello.world".parse().unwrap()); /// /// assert!(!map.is_empty()); /// ``` pub fn is_empty(&self) -> bool { self.headers.is_empty() } /// Clears the map, removing all key-value pairs. Keeps the allocated memory /// for reuse. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("x-host", "hello.world".parse().unwrap()); /// /// map.clear(); /// assert!(map.is_empty()); /// assert!(map.capacity() > 0); /// ``` pub fn clear(&mut self) { self.headers.clear(); } /// Returns the number of custom metadata entries the map can hold without /// reallocating. /// /// This number is an approximation as certain usage patterns could cause /// additional allocations before the returned capacity is filled. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// assert_eq!(0, map.capacity()); /// /// map.insert("x-host", "hello.world".parse().unwrap()); /// assert_eq!(6, map.capacity()); /// ``` pub fn capacity(&self) -> usize { self.headers.capacity() } /// Reserves capacity for at least `additional` more custom metadata to be /// inserted into the `MetadataMap`. /// /// The metadata map may reserve more space to avoid frequent reallocations. /// Like with `with_capacity`, this will be a "best effort" to avoid /// allocations until `additional` more custom metadata is inserted. Certain /// usage patterns could cause additional allocations before the number is /// reached. /// /// # Panics /// /// Panics if the new allocation size overflows `usize`. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.reserve(10); /// # map.insert("x-host", "bar".parse().unwrap()); /// ``` pub fn reserve(&mut self, additional: usize) { self.headers.reserve(additional); } /// Returns a reference to the value associated with the key. This method /// is for ascii metadata entries (those whose names don't end with /// "-bin"). For binary entries, use get_bin. /// /// If there are multiple values associated with the key, then the first one /// is returned. Use `get_all` to get all values associated with a given /// key. Returns `None` if there are no values associated with the key. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// assert!(map.get("x-host").is_none()); /// /// map.insert("x-host", "hello".parse().unwrap()); /// assert_eq!(map.get("x-host").unwrap(), &"hello"); /// assert_eq!(map.get("x-host").unwrap(), &"hello"); /// /// map.append("x-host", "world".parse().unwrap()); /// assert_eq!(map.get("x-host").unwrap(), &"hello"); /// /// // Attempting to read a key of the wrong type fails by not /// // finding anything. /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); /// assert!(map.get("host-bin").is_none()); /// assert!(map.get("host-bin".to_string()).is_none()); /// assert!(map.get(&("host-bin".to_string())).is_none()); /// /// // Attempting to read an invalid key string fails by not /// // finding anything. /// assert!(map.get("host{}bin").is_none()); /// assert!(map.get("host{}bin".to_string()).is_none()); /// assert!(map.get(&("host{}bin".to_string())).is_none()); /// ``` pub fn get(&self, key: K) -> Option<&MetadataValue> where K: AsMetadataKey, { key.get(self) } /// Like get, but for Binary keys (for example "trace-proto-bin"). /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// assert!(map.get_bin("trace-proto-bin").is_none()); /// /// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"hello")); /// assert_eq!(map.get_bin("trace-proto-bin").unwrap(), &"hello"); /// assert_eq!(map.get_bin("trace-proto-bin").unwrap(), &"hello"); /// /// map.append_bin("trace-proto-bin", MetadataValue::from_bytes(b"world")); /// assert_eq!(map.get_bin("trace-proto-bin").unwrap(), &"hello"); /// /// // Attempting to read a key of the wrong type fails by not /// // finding anything. /// map.append("host", "world".parse().unwrap()); /// assert!(map.get_bin("host").is_none()); /// assert!(map.get_bin("host".to_string()).is_none()); /// assert!(map.get_bin(&("host".to_string())).is_none()); /// /// // Attempting to read an invalid key string fails by not /// // finding anything. /// assert!(map.get_bin("host{}-bin").is_none()); /// assert!(map.get_bin("host{}-bin".to_string()).is_none()); /// assert!(map.get_bin(&("host{}-bin".to_string())).is_none()); /// ``` pub fn get_bin(&self, key: K) -> Option<&MetadataValue> where K: AsMetadataKey, { key.get(self) } /// Returns a mutable reference to the value associated with the key. This /// method is for ascii metadata entries (those whose names don't end with /// "-bin"). For binary entries, use get_mut_bin. /// /// If there are multiple values associated with the key, then the first one /// is returned. Use `entry` to get all values associated with a given /// key. Returns `None` if there are no values associated with the key. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::default(); /// map.insert("x-host", "hello".parse().unwrap()); /// map.get_mut("x-host").unwrap().set_sensitive(true); /// /// assert!(map.get("x-host").unwrap().is_sensitive()); /// /// // Attempting to read a key of the wrong type fails by not /// // finding anything. /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); /// assert!(map.get_mut("host-bin").is_none()); /// assert!(map.get_mut("host-bin".to_string()).is_none()); /// assert!(map.get_mut(&("host-bin".to_string())).is_none()); /// /// // Attempting to read an invalid key string fails by not /// // finding anything. /// assert!(map.get_mut("host{}").is_none()); /// assert!(map.get_mut("host{}".to_string()).is_none()); /// assert!(map.get_mut(&("host{}".to_string())).is_none()); /// ``` pub fn get_mut(&mut self, key: K) -> Option<&mut MetadataValue> where K: AsMetadataKey, { key.get_mut(self) } /// Like get_mut, but for Binary keys (for example "trace-proto-bin"). /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::default(); /// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"hello")); /// map.get_bin_mut("trace-proto-bin").unwrap().set_sensitive(true); /// /// assert!(map.get_bin("trace-proto-bin").unwrap().is_sensitive()); /// /// // Attempting to read a key of the wrong type fails by not /// // finding anything. /// map.append("host", "world".parse().unwrap()); /// assert!(map.get_bin_mut("host").is_none()); /// assert!(map.get_bin_mut("host".to_string()).is_none()); /// assert!(map.get_bin_mut(&("host".to_string())).is_none()); /// /// // Attempting to read an invalid key string fails by not /// // finding anything. /// assert!(map.get_bin_mut("host{}-bin").is_none()); /// assert!(map.get_bin_mut("host{}-bin".to_string()).is_none()); /// assert!(map.get_bin_mut(&("host{}-bin".to_string())).is_none()); /// ``` pub fn get_bin_mut(&mut self, key: K) -> Option<&mut MetadataValue> where K: AsMetadataKey, { key.get_mut(self) } /// Returns a view of all values associated with a key. This method is for /// ascii metadata entries (those whose names don't end with "-bin"). For /// binary entries, use get_all_bin. /// /// The returned view does not incur any allocations and allows iterating /// the values associated with the key. See [`GetAll`] for more details. /// Returns `None` if there are no values associated with the key. /// /// [`GetAll`]: struct.GetAll.html /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// map.insert("x-host", "hello".parse().unwrap()); /// map.append("x-host", "goodbye".parse().unwrap()); /// /// { /// let view = map.get_all("x-host"); /// /// let mut iter = view.iter(); /// assert_eq!(&"hello", iter.next().unwrap()); /// assert_eq!(&"goodbye", iter.next().unwrap()); /// assert!(iter.next().is_none()); /// } /// /// // Attempting to read a key of the wrong type fails by not /// // finding anything. /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); /// assert!(map.get_all("host-bin").iter().next().is_none()); /// assert!(map.get_all("host-bin".to_string()).iter().next().is_none()); /// assert!(map.get_all(&("host-bin".to_string())).iter().next().is_none()); /// /// // Attempting to read an invalid key string fails by not /// // finding anything. /// assert!(map.get_all("host{}").iter().next().is_none()); /// assert!(map.get_all("host{}".to_string()).iter().next().is_none()); /// assert!(map.get_all(&("host{}".to_string())).iter().next().is_none()); /// ``` pub fn get_all(&self, key: K) -> GetAll<'_, Ascii> where K: AsMetadataKey, { GetAll { inner: key.get_all(self), phantom: PhantomData, } } /// Like get_all, but for Binary keys (for example "trace-proto-bin"). /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"hello")); /// map.append_bin("trace-proto-bin", MetadataValue::from_bytes(b"goodbye")); /// /// { /// let view = map.get_all_bin("trace-proto-bin"); /// /// let mut iter = view.iter(); /// assert_eq!(&"hello", iter.next().unwrap()); /// assert_eq!(&"goodbye", iter.next().unwrap()); /// assert!(iter.next().is_none()); /// } /// /// // Attempting to read a key of the wrong type fails by not /// // finding anything. /// map.append("host", "world".parse().unwrap()); /// assert!(map.get_all_bin("host").iter().next().is_none()); /// assert!(map.get_all_bin("host".to_string()).iter().next().is_none()); /// assert!(map.get_all_bin(&("host".to_string())).iter().next().is_none()); /// /// // Attempting to read an invalid key string fails by not /// // finding anything. /// assert!(map.get_all_bin("host{}-bin").iter().next().is_none()); /// assert!(map.get_all_bin("host{}-bin".to_string()).iter().next().is_none()); /// assert!(map.get_all_bin(&("host{}-bin".to_string())).iter().next().is_none()); /// ``` pub fn get_all_bin(&self, key: K) -> GetAll<'_, Binary> where K: AsMetadataKey, { GetAll { inner: key.get_all(self), phantom: PhantomData, } } /// Returns true if the map contains a value for the specified key. This /// method works for both ascii and binary entries. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// assert!(!map.contains_key("x-host")); /// /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); /// map.insert("x-host", "world".parse().unwrap()); /// /// // contains_key works for both Binary and Ascii keys: /// assert!(map.contains_key("x-host")); /// assert!(map.contains_key("host-bin")); /// /// // contains_key returns false for invalid keys: /// assert!(!map.contains_key("x{}host")); /// ``` pub fn contains_key(&self, key: K) -> bool where K: AsEncodingAgnosticMetadataKey, { key.contains_key(self) } /// An iterator visiting all key-value pairs (both ascii and binary). /// /// The iteration order is arbitrary, but consistent across platforms for /// the same crate version. Each key will be yielded once per associated /// value. So, if a key has 3 associated values, it will be yielded 3 times. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// map.insert("x-word", "hello".parse().unwrap()); /// map.append("x-word", "goodbye".parse().unwrap()); /// map.insert("x-number", "123".parse().unwrap()); /// /// for key_and_value in map.iter() { /// match key_and_value { /// KeyAndValueRef::Ascii(ref key, ref value) => /// println!("Ascii: {:?}: {:?}", key, value), /// KeyAndValueRef::Binary(ref key, ref value) => /// println!("Binary: {:?}: {:?}", key, value), /// } /// } /// ``` pub fn iter(&self) -> Iter<'_> { Iter { inner: self.headers.iter(), } } /// An iterator visiting all key-value pairs, with mutable value references. /// /// The iterator order is arbitrary, but consistent across platforms for the /// same crate version. Each key will be yielded once per associated value, /// so if a key has 3 associated values, it will be yielded 3 times. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// map.insert("x-word", "hello".parse().unwrap()); /// map.append("x-word", "goodbye".parse().unwrap()); /// map.insert("x-number", "123".parse().unwrap()); /// /// for key_and_value in map.iter_mut() { /// match key_and_value { /// KeyAndMutValueRef::Ascii(key, mut value) => /// value.set_sensitive(true), /// KeyAndMutValueRef::Binary(key, mut value) => /// value.set_sensitive(false), /// } /// } /// ``` pub fn iter_mut(&mut self) -> IterMut<'_> { IterMut { inner: self.headers.iter_mut(), } } /// An iterator visiting all keys. /// /// The iteration order is arbitrary, but consistent across platforms for /// the same crate version. Each key will be yielded only once even if it /// has multiple associated values. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// map.insert("x-word", "hello".parse().unwrap()); /// map.append("x-word", "goodbye".parse().unwrap()); /// map.insert_bin("x-number-bin", MetadataValue::from_bytes(b"123")); /// /// for key in map.keys() { /// match key { /// KeyRef::Ascii(ref key) => /// println!("Ascii key: {:?}", key), /// KeyRef::Binary(ref key) => /// println!("Binary key: {:?}", key), /// } /// println!("{:?}", key); /// } /// ``` pub fn keys(&self) -> Keys<'_> { Keys { inner: self.headers.keys(), } } /// An iterator visiting all values (both ascii and binary). /// /// The iteration order is arbitrary, but consistent across platforms for /// the same crate version. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// map.insert("x-word", "hello".parse().unwrap()); /// map.append("x-word", "goodbye".parse().unwrap()); /// map.insert_bin("x-number-bin", MetadataValue::from_bytes(b"123")); /// /// for value in map.values() { /// match value { /// ValueRef::Ascii(ref value) => /// println!("Ascii value: {:?}", value), /// ValueRef::Binary(ref value) => /// println!("Binary value: {:?}", value), /// } /// println!("{:?}", value); /// } /// ``` pub fn values(&self) -> Values<'_> { Values { inner: self.headers.iter(), } } /// An iterator visiting all values mutably. /// /// The iteration order is arbitrary, but consistent across platforms for /// the same crate version. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::default(); /// /// map.insert("x-word", "hello".parse().unwrap()); /// map.append("x-word", "goodbye".parse().unwrap()); /// map.insert("x-number", "123".parse().unwrap()); /// /// for value in map.values_mut() { /// match value { /// ValueRefMut::Ascii(mut value) => /// value.set_sensitive(true), /// ValueRefMut::Binary(mut value) => /// value.set_sensitive(false), /// } /// } /// ``` pub fn values_mut(&mut self) -> ValuesMut<'_> { ValuesMut { inner: self.headers.iter_mut(), } } /// Gets the given ascii key's corresponding entry in the map for in-place /// manipulation. For binary keys, use `entry_bin`. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::default(); /// /// let headers = &[ /// "content-length", /// "x-hello", /// "Content-Length", /// "x-world", /// ]; /// /// for &header in headers { /// let counter = map.entry(header).unwrap().or_insert("".parse().unwrap()); /// *counter = format!("{}{}", counter.to_str().unwrap(), "1").parse().unwrap(); /// } /// /// assert_eq!(map.get("content-length").unwrap(), "11"); /// assert_eq!(map.get("x-hello").unwrap(), "1"); /// /// // Gracefully handles parting invalid key strings /// assert!(!map.entry("a{}b").is_ok()); /// /// // Attempting to read a key of the wrong type fails by not /// // finding anything. /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); /// assert!(!map.entry("host-bin").is_ok()); /// assert!(!map.entry("host-bin".to_string()).is_ok()); /// assert!(!map.entry(&("host-bin".to_string())).is_ok()); /// /// // Attempting to read an invalid key string fails by not /// // finding anything. /// assert!(!map.entry("host{}").is_ok()); /// assert!(!map.entry("host{}".to_string()).is_ok()); /// assert!(!map.entry(&("host{}".to_string())).is_ok()); /// ``` pub fn entry(&mut self, key: K) -> Result, InvalidMetadataKey> where K: AsMetadataKey, { self.generic_entry::(key) } /// Gets the given Binary key's corresponding entry in the map for in-place /// manipulation. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// # use std::str; /// let mut map = MetadataMap::default(); /// /// let headers = &[ /// "content-length-bin", /// "x-hello-bin", /// "Content-Length-bin", /// "x-world-bin", /// ]; /// /// for &header in headers { /// let counter = map.entry_bin(header).unwrap().or_insert(MetadataValue::from_bytes(b"")); /// *counter = MetadataValue::from_bytes(format!("{}{}", str::from_utf8(counter.to_bytes().unwrap().as_ref()).unwrap(), "1").as_bytes()); /// } /// /// assert_eq!(map.get_bin("content-length-bin").unwrap(), "11"); /// assert_eq!(map.get_bin("x-hello-bin").unwrap(), "1"); /// /// // Attempting to read a key of the wrong type fails by not /// // finding anything. /// map.append("host", "world".parse().unwrap()); /// assert!(!map.entry_bin("host").is_ok()); /// assert!(!map.entry_bin("host".to_string()).is_ok()); /// assert!(!map.entry_bin(&("host".to_string())).is_ok()); /// /// // Attempting to read an invalid key string fails by not /// // finding anything. /// assert!(!map.entry_bin("host{}-bin").is_ok()); /// assert!(!map.entry_bin("host{}-bin".to_string()).is_ok()); /// assert!(!map.entry_bin(&("host{}-bin".to_string())).is_ok()); /// ``` pub fn entry_bin(&mut self, key: K) -> Result, InvalidMetadataKey> where K: AsMetadataKey, { self.generic_entry::(key) } fn generic_entry( &mut self, key: K, ) -> Result, InvalidMetadataKey> where K: AsMetadataKey, { match key.entry(self) { Ok(entry) => Ok(match entry { http::header::Entry::Occupied(e) => Entry::Occupied(OccupiedEntry { inner: e, phantom: PhantomData, }), http::header::Entry::Vacant(e) => Entry::Vacant(VacantEntry { inner: e, phantom: PhantomData, }), }), Err(err) => Err(err), } } /// Inserts an ascii key-value pair into the map. To insert a binary entry, /// use `insert_bin`. /// /// This method panics when the given key is a string and it cannot be /// converted to a `MetadataKey`. /// /// If the map did not previously have this key present, then `None` is /// returned. /// /// If the map did have this key present, the new value is associated with /// the key and all previous values are removed. **Note** that only a single /// one of the previous values is returned. If there are multiple values /// that have been previously associated with the key, then the first one is /// returned. See `insert_mult` on `OccupiedEntry` for an API that returns /// all values. /// /// The key is not updated, though; this matters for types that can be `==` /// without being identical. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// assert!(map.insert("x-host", "world".parse().unwrap()).is_none()); /// assert!(!map.is_empty()); /// /// let mut prev = map.insert("x-host", "earth".parse().unwrap()).unwrap(); /// assert_eq!("world", prev); /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// // Trying to insert a key that is not valid panics. /// map.insert("x{}host", "world".parse().unwrap()); /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// // Trying to insert a key that is binary panics (use insert_bin). /// map.insert("x-host-bin", "world".parse().unwrap()); /// ``` pub fn insert(&mut self, key: K, val: MetadataValue) -> Option> where K: IntoMetadataKey, { key.insert(self, val) } /// Like insert, but for Binary keys (for example "trace-proto-bin"). /// /// This method panics when the given key is a string and it cannot be /// converted to a `MetadataKey`. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// assert!(map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"world")).is_none()); /// assert!(!map.is_empty()); /// /// let mut prev = map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"earth")).unwrap(); /// assert_eq!("world", prev); /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// let mut map = MetadataMap::default(); /// // Attempting to add a binary metadata entry with an invalid name /// map.insert_bin("trace-proto", MetadataValue::from_bytes(b"hello")); // This line panics! /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// // Trying to insert a key that is not valid panics. /// map.insert_bin("x{}host-bin", MetadataValue::from_bytes(b"world")); // This line panics! /// ``` pub fn insert_bin( &mut self, key: K, val: MetadataValue, ) -> Option> where K: IntoMetadataKey, { key.insert(self, val) } /// Inserts an ascii key-value pair into the map. To insert a binary entry, /// use `append_bin`. /// /// This method panics when the given key is a string and it cannot be /// converted to a `MetadataKey`. /// /// If the map did not previously have this key present, then `false` is /// returned. /// /// If the map did have this key present, the new value is pushed to the end /// of the list of values currently associated with the key. The key is not /// updated, though; this matters for types that can be `==` without being /// identical. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// assert!(map.insert("x-host", "world".parse().unwrap()).is_none()); /// assert!(!map.is_empty()); /// /// map.append("x-host", "earth".parse().unwrap()); /// /// let values = map.get_all("x-host"); /// let mut i = values.iter(); /// assert_eq!("world", *i.next().unwrap()); /// assert_eq!("earth", *i.next().unwrap()); /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// // Trying to append a key that is not valid panics. /// map.append("x{}host", "world".parse().unwrap()); // This line panics! /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// // Trying to append a key that is binary panics (use append_bin). /// map.append("x-host-bin", "world".parse().unwrap()); // This line panics! /// ``` pub fn append(&mut self, key: K, value: MetadataValue) -> bool where K: IntoMetadataKey, { key.append(self, value) } /// Like append, but for binary keys (for example "trace-proto-bin"). /// /// This method panics when the given key is a string and it cannot be /// converted to a `MetadataKey`. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// assert!(map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"world")).is_none()); /// assert!(!map.is_empty()); /// /// map.append_bin("trace-proto-bin", MetadataValue::from_bytes(b"earth")); /// /// let values = map.get_all_bin("trace-proto-bin"); /// let mut i = values.iter(); /// assert_eq!("world", *i.next().unwrap()); /// assert_eq!("earth", *i.next().unwrap()); /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// // Trying to append a key that is not valid panics. /// map.append_bin("x{}host-bin", MetadataValue::from_bytes(b"world")); // This line panics! /// ``` /// /// ```should_panic /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// // Trying to append a key that is ascii panics (use append). /// map.append_bin("x-host", MetadataValue::from_bytes(b"world")); // This line panics! /// ``` pub fn append_bin(&mut self, key: K, value: MetadataValue) -> bool where K: IntoMetadataKey, { key.append(self, value) } /// Removes an ascii key from the map, returning the value associated with /// the key. To remove a binary key, use `remove_bin`. /// /// Returns `None` if the map does not contain the key. If there are /// multiple values associated with the key, then the first one is returned. /// See `remove_entry_mult` on `OccupiedEntry` for an API that yields all /// values. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("x-host", "hello.world".parse().unwrap()); /// /// let prev = map.remove("x-host").unwrap(); /// assert_eq!("hello.world", prev); /// /// assert!(map.remove("x-host").is_none()); /// /// // Attempting to remove a key of the wrong type fails by not /// // finding anything. /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); /// assert!(map.remove("host-bin").is_none()); /// assert!(map.remove("host-bin".to_string()).is_none()); /// assert!(map.remove(&("host-bin".to_string())).is_none()); /// /// // Attempting to remove an invalid key string fails by not /// // finding anything. /// assert!(map.remove("host{}").is_none()); /// assert!(map.remove("host{}".to_string()).is_none()); /// assert!(map.remove(&("host{}".to_string())).is_none()); /// ``` pub fn remove(&mut self, key: K) -> Option> where K: AsMetadataKey, { key.remove(self) } /// Like remove, but for Binary keys (for example "trace-proto-bin"). /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"hello.world")); /// /// let prev = map.remove_bin("trace-proto-bin").unwrap(); /// assert_eq!("hello.world", prev); /// /// assert!(map.remove_bin("trace-proto-bin").is_none()); /// /// // Attempting to remove a key of the wrong type fails by not /// // finding anything. /// map.append("host", "world".parse().unwrap()); /// assert!(map.remove_bin("host").is_none()); /// assert!(map.remove_bin("host".to_string()).is_none()); /// assert!(map.remove_bin(&("host".to_string())).is_none()); /// /// // Attempting to remove an invalid key string fails by not /// // finding anything. /// assert!(map.remove_bin("host{}-bin").is_none()); /// assert!(map.remove_bin("host{}-bin".to_string()).is_none()); /// assert!(map.remove_bin(&("host{}-bin".to_string())).is_none()); /// ``` pub fn remove_bin(&mut self, key: K) -> Option> where K: AsMetadataKey, { key.remove(self) } pub(crate) fn merge(&mut self, other: MetadataMap) { self.headers.extend(other.headers); } } // ===== impl Iter ===== impl<'a> Iterator for Iter<'a> { type Item = KeyAndValueRef<'a>; fn next(&mut self) -> Option { self.inner.next().map(|item| { let (name, value) = item; if Ascii::is_valid_key(name.as_str()) { KeyAndValueRef::Ascii( MetadataKey::unchecked_from_header_name_ref(name), MetadataValue::unchecked_from_header_value_ref(value), ) } else { KeyAndValueRef::Binary( MetadataKey::unchecked_from_header_name_ref(name), MetadataValue::unchecked_from_header_value_ref(value), ) } }) } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } // ===== impl IterMut ===== impl<'a> Iterator for IterMut<'a> { type Item = KeyAndMutValueRef<'a>; fn next(&mut self) -> Option { self.inner.next().map(|item| { let (name, value) = item; if Ascii::is_valid_key(name.as_str()) { KeyAndMutValueRef::Ascii( MetadataKey::unchecked_from_header_name_ref(name), MetadataValue::unchecked_from_mut_header_value_ref(value), ) } else { KeyAndMutValueRef::Binary( MetadataKey::unchecked_from_header_name_ref(name), MetadataValue::unchecked_from_mut_header_value_ref(value), ) } }) } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } // ===== impl ValueDrain ===== impl Iterator for ValueDrain<'_, VE> { type Item = MetadataValue; fn next(&mut self) -> Option { self.inner .next() .map(MetadataValue::unchecked_from_header_value) } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } // ===== impl Keys ===== impl<'a> Iterator for Keys<'a> { type Item = KeyRef<'a>; fn next(&mut self) -> Option { self.inner.next().map(|key| { if Ascii::is_valid_key(key.as_str()) { KeyRef::Ascii(MetadataKey::unchecked_from_header_name_ref(key)) } else { KeyRef::Binary(MetadataKey::unchecked_from_header_name_ref(key)) } }) } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } impl ExactSizeIterator for Keys<'_> {} // ===== impl Values ==== impl<'a> Iterator for Values<'a> { type Item = ValueRef<'a>; fn next(&mut self) -> Option { self.inner.next().map(|item| { let (name, value) = item; if Ascii::is_valid_key(name.as_str()) { ValueRef::Ascii(MetadataValue::unchecked_from_header_value_ref(value)) } else { ValueRef::Binary(MetadataValue::unchecked_from_header_value_ref(value)) } }) } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } // ===== impl Values ==== impl<'a> Iterator for ValuesMut<'a> { type Item = ValueRefMut<'a>; fn next(&mut self) -> Option { self.inner.next().map(|item| { let (name, value) = item; if Ascii::is_valid_key(name.as_str()) { ValueRefMut::Ascii(MetadataValue::unchecked_from_mut_header_value_ref(value)) } else { ValueRefMut::Binary(MetadataValue::unchecked_from_mut_header_value_ref(value)) } }) } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } // ===== impl ValueIter ===== impl<'a, VE: ValueEncoding> Iterator for ValueIter<'a, VE> where VE: 'a, { type Item = &'a MetadataValue; fn next(&mut self) -> Option { match self.inner { Some(ref mut inner) => inner .next() .map(MetadataValue::unchecked_from_header_value_ref), None => None, } } fn size_hint(&self) -> (usize, Option) { match self.inner { Some(ref inner) => inner.size_hint(), None => (0, Some(0)), } } } impl<'a, VE: ValueEncoding> DoubleEndedIterator for ValueIter<'a, VE> where VE: 'a, { fn next_back(&mut self) -> Option { match self.inner { Some(ref mut inner) => inner .next_back() .map(MetadataValue::unchecked_from_header_value_ref), None => None, } } } // ===== impl ValueIterMut ===== impl<'a, VE: ValueEncoding> Iterator for ValueIterMut<'a, VE> where VE: 'a, { type Item = &'a mut MetadataValue; fn next(&mut self) -> Option { self.inner .next() .map(MetadataValue::unchecked_from_mut_header_value_ref) } } impl<'a, VE: ValueEncoding> DoubleEndedIterator for ValueIterMut<'a, VE> where VE: 'a, { fn next_back(&mut self) -> Option { self.inner .next_back() .map(MetadataValue::unchecked_from_mut_header_value_ref) } } // ===== impl Entry ===== impl<'a, VE: ValueEncoding> Entry<'a, VE> { /// Ensures a value is in the entry by inserting the default if empty. /// /// Returns a mutable reference to the **first** value in the entry. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map: MetadataMap = MetadataMap::default(); /// /// let keys = &[ /// "content-length", /// "x-hello", /// "Content-Length", /// "x-world", /// ]; /// /// for &key in keys { /// let counter = map.entry(key) /// .expect("valid key names") /// .or_insert("".parse().unwrap()); /// *counter = format!("{}{}", counter.to_str().unwrap(), "1").parse().unwrap(); /// } /// /// assert_eq!(map.get("content-length").unwrap(), "11"); /// assert_eq!(map.get("x-hello").unwrap(), "1"); /// ``` pub fn or_insert(self, default: MetadataValue) -> &'a mut MetadataValue { use self::Entry::*; match self { Occupied(e) => e.into_mut(), Vacant(e) => e.insert(default), } } /// Ensures a value is in the entry by inserting the result of the default /// function if empty. /// /// The default function is not called if the entry exists in the map. /// Returns a mutable reference to the **first** value in the entry. /// /// # Examples /// /// Basic usage. /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// let res = map.entry("x-hello").unwrap() /// .or_insert_with(|| "world".parse().unwrap()); /// /// assert_eq!(res, "world"); /// ``` /// /// The default function is not called if the entry exists in the map. /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("host", "world".parse().unwrap()); /// /// let res = map.entry("host") /// .expect("host is a valid string") /// .or_insert_with(|| unreachable!()); /// /// /// assert_eq!(res, "world"); /// ``` pub fn or_insert_with MetadataValue>( self, default: F, ) -> &'a mut MetadataValue { use self::Entry::*; match self { Occupied(e) => e.into_mut(), Vacant(e) => e.insert(default()), } } /// Returns a reference to the entry's key /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// assert_eq!(map.entry("x-hello").unwrap().key(), "x-hello"); /// ``` pub fn key(&self) -> &MetadataKey { use self::Entry::*; MetadataKey::unchecked_from_header_name_ref(match *self { Vacant(ref e) => e.inner.key(), Occupied(ref e) => e.inner.key(), }) } } // ===== impl VacantEntry ===== impl<'a, VE: ValueEncoding> VacantEntry<'a, VE> { /// Returns a reference to the entry's key /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// assert_eq!(map.entry("x-hello").unwrap().key(), "x-hello"); /// ``` pub fn key(&self) -> &MetadataKey { MetadataKey::unchecked_from_header_name_ref(self.inner.key()) } /// Take ownership of the key /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// if let Entry::Vacant(v) = map.entry("x-hello").unwrap() { /// assert_eq!(v.into_key().as_str(), "x-hello"); /// } /// ``` pub fn into_key(self) -> MetadataKey { MetadataKey::unchecked_from_header_name(self.inner.into_key()) } /// Insert the value into the entry. /// /// The value will be associated with this entry's key. A mutable reference /// to the inserted value will be returned. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// if let Entry::Vacant(v) = map.entry("x-hello").unwrap() { /// v.insert("world".parse().unwrap()); /// } /// /// assert_eq!(map.get("x-hello").unwrap(), "world"); /// ``` pub fn insert(self, value: MetadataValue) -> &'a mut MetadataValue { MetadataValue::unchecked_from_mut_header_value_ref(self.inner.insert(value.inner)) } /// Insert the value into the entry. /// /// The value will be associated with this entry's key. The new /// `OccupiedEntry` is returned, allowing for further manipulation. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// /// if let Entry::Vacant(v) = map.entry("x-hello").unwrap() { /// let mut e = v.insert_entry("world".parse().unwrap()); /// e.insert("world2".parse().unwrap()); /// } /// /// assert_eq!(map.get("x-hello").unwrap(), "world2"); /// ``` pub fn insert_entry(self, value: MetadataValue) -> OccupiedEntry<'a, Ascii> { OccupiedEntry { inner: self.inner.insert_entry(value.inner), phantom: PhantomData, } } } // ===== impl OccupiedEntry ===== impl<'a, VE: ValueEncoding> OccupiedEntry<'a, VE> { /// Returns a reference to the entry's key. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("host", "world".parse().unwrap()); /// /// if let Entry::Occupied(e) = map.entry("host").unwrap() { /// assert_eq!("host", e.key()); /// } /// ``` pub fn key(&self) -> &MetadataKey { MetadataKey::unchecked_from_header_name_ref(self.inner.key()) } /// Get a reference to the first value in the entry. /// /// Values are stored in insertion order. /// /// # Panics /// /// `get` panics if there are no values associated with the entry. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("host", "hello.world".parse().unwrap()); /// /// if let Entry::Occupied(mut e) = map.entry("host").unwrap() { /// assert_eq!(e.get(), &"hello.world"); /// /// e.append("hello.earth".parse().unwrap()); /// /// assert_eq!(e.get(), &"hello.world"); /// } /// ``` pub fn get(&self) -> &MetadataValue { MetadataValue::unchecked_from_header_value_ref(self.inner.get()) } /// Get a mutable reference to the first value in the entry. /// /// Values are stored in insertion order. /// /// # Panics /// /// `get_mut` panics if there are no values associated with the entry. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::default(); /// map.insert("host", "hello.world".parse().unwrap()); /// /// if let Entry::Occupied(mut e) = map.entry("host").unwrap() { /// e.get_mut().set_sensitive(true); /// assert_eq!(e.get(), &"hello.world"); /// assert!(e.get().is_sensitive()); /// } /// ``` pub fn get_mut(&mut self) -> &mut MetadataValue { MetadataValue::unchecked_from_mut_header_value_ref(self.inner.get_mut()) } /// Converts the `OccupiedEntry` into a mutable reference to the **first** /// value. /// /// The lifetime of the returned reference is bound to the original map. /// /// # Panics /// /// `into_mut` panics if there are no values associated with the entry. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::default(); /// map.insert("host", "hello.world".parse().unwrap()); /// map.append("host", "hello.earth".parse().unwrap()); /// /// if let Entry::Occupied(e) = map.entry("host").unwrap() { /// e.into_mut().set_sensitive(true); /// } /// /// assert!(map.get("host").unwrap().is_sensitive()); /// ``` pub fn into_mut(self) -> &'a mut MetadataValue { MetadataValue::unchecked_from_mut_header_value_ref(self.inner.into_mut()) } /// Sets the value of the entry. /// /// All previous values associated with the entry are removed and the first /// one is returned. See `insert_mult` for an API that returns all values. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("host", "hello.world".parse().unwrap()); /// /// if let Entry::Occupied(mut e) = map.entry("host").unwrap() { /// let mut prev = e.insert("earth".parse().unwrap()); /// assert_eq!("hello.world", prev); /// } /// /// assert_eq!("earth", map.get("host").unwrap()); /// ``` pub fn insert(&mut self, value: MetadataValue) -> MetadataValue { let header_value = self.inner.insert(value.inner); MetadataValue::unchecked_from_header_value(header_value) } /// Sets the value of the entry. /// /// This function does the same as `insert` except it returns an iterator /// that yields all values previously associated with the key. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("host", "world".parse().unwrap()); /// map.append("host", "world2".parse().unwrap()); /// /// if let Entry::Occupied(mut e) = map.entry("host").unwrap() { /// let mut prev = e.insert_mult("earth".parse().unwrap()); /// assert_eq!("world", prev.next().unwrap()); /// assert_eq!("world2", prev.next().unwrap()); /// assert!(prev.next().is_none()); /// } /// /// assert_eq!("earth", map.get("host").unwrap()); /// ``` pub fn insert_mult(&mut self, value: MetadataValue) -> ValueDrain<'_, VE> { ValueDrain { inner: self.inner.insert_mult(value.inner), phantom: PhantomData, } } /// Insert the value into the entry. /// /// The new value is appended to the end of the entry's value list. All /// previous values associated with the entry are retained. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("host", "world".parse().unwrap()); /// /// if let Entry::Occupied(mut e) = map.entry("host").unwrap() { /// e.append("earth".parse().unwrap()); /// } /// /// let values = map.get_all("host"); /// let mut i = values.iter(); /// assert_eq!("world", *i.next().unwrap()); /// assert_eq!("earth", *i.next().unwrap()); /// ``` pub fn append(&mut self, value: MetadataValue) { self.inner.append(value.inner) } /// Remove the entry from the map. /// /// All values associated with the entry are removed and the first one is /// returned. See `remove_entry_mult` for an API that returns all values. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("host", "world".parse().unwrap()); /// /// if let Entry::Occupied(e) = map.entry("host").unwrap() { /// let mut prev = e.remove(); /// assert_eq!("world", prev); /// } /// /// assert!(!map.contains_key("host")); /// ``` pub fn remove(self) -> MetadataValue { let value = self.inner.remove(); MetadataValue::unchecked_from_header_value(value) } /// Remove the entry from the map. /// /// The key and all values associated with the entry are removed and the /// first one is returned. See `remove_entry_mult` for an API that returns /// all values. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("host", "world".parse().unwrap()); /// /// if let Entry::Occupied(e) = map.entry("host").unwrap() { /// let (key, mut prev) = e.remove_entry(); /// assert_eq!("host", key.as_str()); /// assert_eq!("world", prev); /// } /// /// assert!(!map.contains_key("host")); /// ``` pub fn remove_entry(self) -> (MetadataKey, MetadataValue) { let (name, value) = self.inner.remove_entry(); ( MetadataKey::unchecked_from_header_name(name), MetadataValue::unchecked_from_header_value(value), ) } /// Remove the entry from the map. /// /// The key and all values associated with the entry are removed and /// returned. pub fn remove_entry_mult(self) -> (MetadataKey, ValueDrain<'a, VE>) { let (name, value_drain) = self.inner.remove_entry_mult(); ( MetadataKey::unchecked_from_header_name(name), ValueDrain { inner: value_drain, phantom: PhantomData, }, ) } /// Returns an iterator visiting all values associated with the entry. /// /// Values are iterated in insertion order. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("host", "world".parse().unwrap()); /// map.append("host", "earth".parse().unwrap()); /// /// if let Entry::Occupied(e) = map.entry("host").unwrap() { /// let mut iter = e.iter(); /// assert_eq!(&"world", iter.next().unwrap()); /// assert_eq!(&"earth", iter.next().unwrap()); /// assert!(iter.next().is_none()); /// } /// ``` pub fn iter(&self) -> ValueIter<'_, VE> { ValueIter { inner: Some(self.inner.iter()), phantom: PhantomData, } } /// Returns an iterator mutably visiting all values associated with the /// entry. /// /// Values are iterated in insertion order. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::default(); /// map.insert("host", "world".parse().unwrap()); /// map.append("host", "earth".parse().unwrap()); /// /// if let Entry::Occupied(mut e) = map.entry("host").unwrap() { /// for e in e.iter_mut() { /// e.set_sensitive(true); /// } /// } /// /// let mut values = map.get_all("host"); /// let mut i = values.iter(); /// assert!(i.next().unwrap().is_sensitive()); /// assert!(i.next().unwrap().is_sensitive()); /// ``` pub fn iter_mut(&mut self) -> ValueIterMut<'_, VE> { ValueIterMut { inner: self.inner.iter_mut(), phantom: PhantomData, } } } impl<'a, VE: ValueEncoding> IntoIterator for OccupiedEntry<'a, VE> where VE: 'a, { type Item = &'a mut MetadataValue; type IntoIter = ValueIterMut<'a, VE>; fn into_iter(self) -> ValueIterMut<'a, VE> { ValueIterMut { inner: self.inner.into_iter(), phantom: PhantomData, } } } impl<'a, 'b: 'a, VE: ValueEncoding> IntoIterator for &'b OccupiedEntry<'a, VE> { type Item = &'a MetadataValue; type IntoIter = ValueIter<'a, VE>; fn into_iter(self) -> ValueIter<'a, VE> { self.iter() } } impl<'a, 'b: 'a, VE: ValueEncoding> IntoIterator for &'b mut OccupiedEntry<'a, VE> { type Item = &'a mut MetadataValue; type IntoIter = ValueIterMut<'a, VE>; fn into_iter(self) -> ValueIterMut<'a, VE> { self.iter_mut() } } // ===== impl GetAll ===== impl<'a, VE: ValueEncoding> GetAll<'a, VE> { /// Returns an iterator visiting all values associated with the entry. /// /// Values are iterated in insertion order. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut map = MetadataMap::new(); /// map.insert("x-host", "hello.world".parse().unwrap()); /// map.append("x-host", "hello.earth".parse().unwrap()); /// /// let values = map.get_all("x-host"); /// let mut iter = values.iter(); /// assert_eq!(&"hello.world", iter.next().unwrap()); /// assert_eq!(&"hello.earth", iter.next().unwrap()); /// assert!(iter.next().is_none()); /// ``` pub fn iter(&self) -> ValueIter<'a, VE> { ValueIter { inner: self.inner.as_ref().map(|inner| inner.iter()), phantom: PhantomData, } } } impl PartialEq for GetAll<'_, VE> { fn eq(&self, other: &Self) -> bool { self.inner.iter().eq(other.inner.iter()) } } impl<'a, VE: ValueEncoding> IntoIterator for GetAll<'a, VE> where VE: 'a, { type Item = &'a MetadataValue; type IntoIter = ValueIter<'a, VE>; fn into_iter(self) -> ValueIter<'a, VE> { ValueIter { inner: self.inner.map(|inner| inner.into_iter()), phantom: PhantomData, } } } impl<'a, 'b: 'a, VE: ValueEncoding> IntoIterator for &'b GetAll<'a, VE> { type Item = &'a MetadataValue; type IntoIter = ValueIter<'a, VE>; fn into_iter(self) -> ValueIter<'a, VE> { ValueIter { inner: self.inner.as_ref().map(|inner| inner.into_iter()), phantom: PhantomData, } } } // ===== impl IntoMetadataKey / AsMetadataKey ===== mod into_metadata_key { use super::{MetadataMap, MetadataValue, ValueEncoding}; use crate::metadata::key::MetadataKey; /// A marker trait used to identify values that can be used as insert keys /// to a `MetadataMap`. pub trait IntoMetadataKey: Sealed {} // All methods are on this pub(super) trait, instead of `IntoMetadataKey`, // so that they aren't publicly exposed to the world. // // Being on the `IntoMetadataKey` trait would mean users could call // `"host".insert(&mut map, "localhost")`. // // Ultimately, this allows us to adjust the signatures of these methods // without breaking any external crate. pub trait Sealed { #[doc(hidden)] fn insert(self, map: &mut MetadataMap, val: MetadataValue) -> Option>; #[doc(hidden)] fn append(self, map: &mut MetadataMap, val: MetadataValue) -> bool; } // ==== impls ==== impl Sealed for MetadataKey { #[doc(hidden)] #[inline] fn insert( self, map: &mut MetadataMap, val: MetadataValue, ) -> Option> { map.headers .insert(self.inner, val.inner) .map(MetadataValue::unchecked_from_header_value) } #[doc(hidden)] #[inline] fn append(self, map: &mut MetadataMap, val: MetadataValue) -> bool { map.headers.append(self.inner, val.inner) } } impl IntoMetadataKey for MetadataKey {} impl Sealed for &MetadataKey { #[doc(hidden)] #[inline] fn insert( self, map: &mut MetadataMap, val: MetadataValue, ) -> Option> { map.headers .insert(&self.inner, val.inner) .map(MetadataValue::unchecked_from_header_value) } #[doc(hidden)] #[inline] fn append(self, map: &mut MetadataMap, val: MetadataValue) -> bool { map.headers.append(&self.inner, val.inner) } } impl IntoMetadataKey for &MetadataKey {} impl Sealed for &'static str { #[doc(hidden)] #[inline] fn insert( self, map: &mut MetadataMap, val: MetadataValue, ) -> Option> { // Perform name validation let key = MetadataKey::::from_static(self); map.headers .insert(key.inner, val.inner) .map(MetadataValue::unchecked_from_header_value) } #[doc(hidden)] #[inline] fn append(self, map: &mut MetadataMap, val: MetadataValue) -> bool { // Perform name validation let key = MetadataKey::::from_static(self); map.headers.append(key.inner, val.inner) } } impl IntoMetadataKey for &'static str {} } mod as_metadata_key { use super::{MetadataMap, MetadataValue, ValueEncoding}; use crate::metadata::key::{InvalidMetadataKey, MetadataKey}; use http::header::{Entry, GetAll, HeaderValue}; /// A marker trait used to identify values that can be used as search keys /// to a `MetadataMap`. pub trait AsMetadataKey: Sealed {} // All methods are on this pub(super) trait, instead of `AsMetadataKey`, // so that they aren't publicly exposed to the world. // // Being on the `AsMetadataKey` trait would mean users could call // `"host".find(&map)`. // // Ultimately, this allows us to adjust the signatures of these methods // without breaking any external crate. pub trait Sealed { #[doc(hidden)] fn get(self, map: &MetadataMap) -> Option<&MetadataValue>; #[doc(hidden)] fn get_mut(self, map: &mut MetadataMap) -> Option<&mut MetadataValue>; #[doc(hidden)] fn get_all(self, map: &MetadataMap) -> Option>; #[doc(hidden)] fn entry(self, map: &mut MetadataMap) -> Result, InvalidMetadataKey>; #[doc(hidden)] fn remove(self, map: &mut MetadataMap) -> Option>; } // ==== impls ==== impl Sealed for MetadataKey { #[doc(hidden)] #[inline] fn get(self, map: &MetadataMap) -> Option<&MetadataValue> { map.headers .get(self.inner) .map(MetadataValue::unchecked_from_header_value_ref) } #[doc(hidden)] #[inline] fn get_mut(self, map: &mut MetadataMap) -> Option<&mut MetadataValue> { map.headers .get_mut(self.inner) .map(MetadataValue::unchecked_from_mut_header_value_ref) } #[doc(hidden)] #[inline] fn get_all(self, map: &MetadataMap) -> Option> { Some(map.headers.get_all(self.inner)) } #[doc(hidden)] #[inline] fn entry( self, map: &mut MetadataMap, ) -> Result, InvalidMetadataKey> { Ok(map.headers.entry(self.inner)) } #[doc(hidden)] #[inline] fn remove(self, map: &mut MetadataMap) -> Option> { map.headers .remove(self.inner) .map(MetadataValue::unchecked_from_header_value) } } impl AsMetadataKey for MetadataKey {} impl Sealed for &MetadataKey { #[doc(hidden)] #[inline] fn get(self, map: &MetadataMap) -> Option<&MetadataValue> { map.headers .get(&self.inner) .map(MetadataValue::unchecked_from_header_value_ref) } #[doc(hidden)] #[inline] fn get_mut(self, map: &mut MetadataMap) -> Option<&mut MetadataValue> { map.headers .get_mut(&self.inner) .map(MetadataValue::unchecked_from_mut_header_value_ref) } #[doc(hidden)] #[inline] fn get_all(self, map: &MetadataMap) -> Option> { Some(map.headers.get_all(&self.inner)) } #[doc(hidden)] #[inline] fn entry( self, map: &mut MetadataMap, ) -> Result, InvalidMetadataKey> { Ok(map.headers.entry(&self.inner)) } #[doc(hidden)] #[inline] fn remove(self, map: &mut MetadataMap) -> Option> { map.headers .remove(&self.inner) .map(MetadataValue::unchecked_from_header_value) } } impl AsMetadataKey for &MetadataKey {} impl Sealed for &str { #[doc(hidden)] #[inline] fn get(self, map: &MetadataMap) -> Option<&MetadataValue> { if !VE::is_valid_key(self) { return None; } map.headers .get(self) .map(MetadataValue::unchecked_from_header_value_ref) } #[doc(hidden)] #[inline] fn get_mut(self, map: &mut MetadataMap) -> Option<&mut MetadataValue> { if !VE::is_valid_key(self) { return None; } map.headers .get_mut(self) .map(MetadataValue::unchecked_from_mut_header_value_ref) } #[doc(hidden)] #[inline] fn get_all(self, map: &MetadataMap) -> Option> { if !VE::is_valid_key(self) { return None; } Some(map.headers.get_all(self)) } #[doc(hidden)] #[inline] fn entry( self, map: &mut MetadataMap, ) -> Result, InvalidMetadataKey> { if !VE::is_valid_key(self) { return Err(InvalidMetadataKey::new()); } let key = http::header::HeaderName::from_bytes(self.as_bytes()) .map_err(|_| InvalidMetadataKey::new())?; let entry = map.headers.entry(key); Ok(entry) } #[doc(hidden)] #[inline] fn remove(self, map: &mut MetadataMap) -> Option> { if !VE::is_valid_key(self) { return None; } map.headers .remove(self) .map(MetadataValue::unchecked_from_header_value) } } impl AsMetadataKey for &str {} impl Sealed for String { #[doc(hidden)] #[inline] fn get(self, map: &MetadataMap) -> Option<&MetadataValue> { if !VE::is_valid_key(self.as_str()) { return None; } map.headers .get(self.as_str()) .map(MetadataValue::unchecked_from_header_value_ref) } #[doc(hidden)] #[inline] fn get_mut(self, map: &mut MetadataMap) -> Option<&mut MetadataValue> { if !VE::is_valid_key(self.as_str()) { return None; } map.headers .get_mut(self.as_str()) .map(MetadataValue::unchecked_from_mut_header_value_ref) } #[doc(hidden)] #[inline] fn get_all(self, map: &MetadataMap) -> Option> { if !VE::is_valid_key(self.as_str()) { return None; } Some(map.headers.get_all(self.as_str())) } #[doc(hidden)] #[inline] fn entry( self, map: &mut MetadataMap, ) -> Result, InvalidMetadataKey> { if !VE::is_valid_key(self.as_str()) { return Err(InvalidMetadataKey::new()); } let key = http::header::HeaderName::from_bytes(self.as_bytes()) .map_err(|_| InvalidMetadataKey::new())?; Ok(map.headers.entry(key)) } #[doc(hidden)] #[inline] fn remove(self, map: &mut MetadataMap) -> Option> { if !VE::is_valid_key(self.as_str()) { return None; } map.headers .remove(self.as_str()) .map(MetadataValue::unchecked_from_header_value) } } impl AsMetadataKey for String {} impl Sealed for &String { #[doc(hidden)] #[inline] fn get(self, map: &MetadataMap) -> Option<&MetadataValue> { if !VE::is_valid_key(self) { return None; } map.headers .get(self.as_str()) .map(MetadataValue::unchecked_from_header_value_ref) } #[doc(hidden)] #[inline] fn get_mut(self, map: &mut MetadataMap) -> Option<&mut MetadataValue> { if !VE::is_valid_key(self) { return None; } map.headers .get_mut(self.as_str()) .map(MetadataValue::unchecked_from_mut_header_value_ref) } #[doc(hidden)] #[inline] fn get_all(self, map: &MetadataMap) -> Option> { if !VE::is_valid_key(self) { return None; } Some(map.headers.get_all(self.as_str())) } #[doc(hidden)] #[inline] fn entry( self, map: &mut MetadataMap, ) -> Result, InvalidMetadataKey> { if !VE::is_valid_key(self) { return Err(InvalidMetadataKey::new()); } let key = http::header::HeaderName::from_bytes(self.as_bytes()) .map_err(|_| InvalidMetadataKey::new())?; Ok(map.headers.entry(key)) } #[doc(hidden)] #[inline] fn remove(self, map: &mut MetadataMap) -> Option> { if !VE::is_valid_key(self) { return None; } map.headers .remove(self.as_str()) .map(MetadataValue::unchecked_from_header_value) } } impl AsMetadataKey for &String {} } mod as_encoding_agnostic_metadata_key { use super::{MetadataMap, ValueEncoding}; use crate::metadata::key::MetadataKey; /// A marker trait used to identify values that can be used as search keys /// to a `MetadataMap`, for operations that don't expose the actual value. pub trait AsEncodingAgnosticMetadataKey: Sealed {} // All methods are on this pub(super) trait, instead of // `AsEncodingAgnosticMetadataKey`, so that they aren't publicly exposed to // the world. // // Being on the `AsEncodingAgnosticMetadataKey` trait would mean users could // call `"host".contains_key(&map)`. // // Ultimately, this allows us to adjust the signatures of these methods // without breaking any external crate. pub trait Sealed { #[doc(hidden)] fn contains_key(&self, map: &MetadataMap) -> bool; } // ==== impls ==== impl Sealed for MetadataKey { #[doc(hidden)] #[inline] fn contains_key(&self, map: &MetadataMap) -> bool { map.headers.contains_key(&self.inner) } } impl AsEncodingAgnosticMetadataKey for MetadataKey {} impl Sealed for &MetadataKey { #[doc(hidden)] #[inline] fn contains_key(&self, map: &MetadataMap) -> bool { map.headers.contains_key(&self.inner) } } impl AsEncodingAgnosticMetadataKey for &MetadataKey {} impl Sealed for &str { #[doc(hidden)] #[inline] fn contains_key(&self, map: &MetadataMap) -> bool { map.headers.contains_key(*self) } } impl AsEncodingAgnosticMetadataKey for &str {} impl Sealed for String { #[doc(hidden)] #[inline] fn contains_key(&self, map: &MetadataMap) -> bool { map.headers.contains_key(self.as_str()) } } impl AsEncodingAgnosticMetadataKey for String {} impl Sealed for &String { #[doc(hidden)] #[inline] fn contains_key(&self, map: &MetadataMap) -> bool { map.headers.contains_key(self.as_str()) } } impl AsEncodingAgnosticMetadataKey for &String {} } #[cfg(test)] mod tests { use super::*; #[test] fn test_from_headers_takes_http_headers() { let mut http_map = http::HeaderMap::new(); http_map.insert("x-host", "example.com".parse().unwrap()); let map = MetadataMap::from_headers(http_map); assert_eq!(map.get("x-host").unwrap(), "example.com"); } #[test] fn test_to_headers_encoding() { use crate::Status; let special_char_message = "Beyond 100% ascii \t\n\r🌶️💉💧🐮🍺"; let s1 = Status::unknown(special_char_message); assert_eq!(s1.message(), special_char_message); let s1_map = s1.to_header_map().unwrap(); let s2 = Status::from_header_map(&s1_map).unwrap(); assert_eq!(s1.message(), s2.message()); assert!( s1_map .get("grpc-message") .unwrap() .to_str() .unwrap() .starts_with("Beyond%20100%25%20ascii"), "Percent sign or other character isn't encoded as desired: {:?}", s1_map.get("grpc-message") ); } #[test] fn test_iter_categorizes_ascii_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); map.insert_bin("x-number-bin", MetadataValue::from_bytes(b"123")); let mut found_x_word = false; for key_and_value in map.iter() { if let KeyAndValueRef::Ascii(key, _value) = key_and_value { if key.as_str() == "x-word" { found_x_word = true; } else { panic!("Unexpected key"); } } } assert!(found_x_word); } #[test] fn test_iter_categorizes_binary_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); let mut found_x_word_bin = false; for key_and_value in map.iter() { if let KeyAndValueRef::Binary(key, _value) = key_and_value { if key.as_str() == "x-word-bin" { found_x_word_bin = true; } else { panic!("Unexpected key"); } } } assert!(found_x_word_bin); } #[test] fn test_iter_mut_categorizes_ascii_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); map.insert_bin("x-number-bin", MetadataValue::from_bytes(b"123")); let mut found_x_word = false; for key_and_value in map.iter_mut() { if let KeyAndMutValueRef::Ascii(key, _value) = key_and_value { if key.as_str() == "x-word" { found_x_word = true; } else { panic!("Unexpected key"); } } } assert!(found_x_word); } #[test] fn test_iter_mut_categorizes_binary_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); let mut found_x_word_bin = false; for key_and_value in map.iter_mut() { if let KeyAndMutValueRef::Binary(key, _value) = key_and_value { if key.as_str() == "x-word-bin" { found_x_word_bin = true; } else { panic!("Unexpected key"); } } } assert!(found_x_word_bin); } #[test] fn test_keys_categorizes_ascii_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); map.insert_bin("x-number-bin", MetadataValue::from_bytes(b"123")); let mut found_x_word = false; for key in map.keys() { if let KeyRef::Ascii(key) = key { if key.as_str() == "x-word" { found_x_word = true; } else { panic!("Unexpected key"); } } } assert!(found_x_word); } #[test] fn test_keys_categorizes_binary_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.insert_bin("x-number-bin", MetadataValue::from_bytes(b"123")); let mut found_x_number_bin = false; for key in map.keys() { if let KeyRef::Binary(key) = key { if key.as_str() == "x-number-bin" { found_x_number_bin = true; } else { panic!("Unexpected key"); } } } assert!(found_x_number_bin); } #[test] fn test_values_categorizes_ascii_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); map.insert_bin("x-number-bin", MetadataValue::from_bytes(b"123")); let mut found_x_word = false; for value in map.values() { if let ValueRef::Ascii(value) = value { if *value == "hello" { found_x_word = true; } else { panic!("Unexpected key"); } } } assert!(found_x_word); } #[test] fn test_values_categorizes_binary_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); let mut found_x_word_bin = false; for value_ref in map.values() { if let ValueRef::Binary(value) = value_ref { assert_eq!(*value, "goodbye"); found_x_word_bin = true; } } assert!(found_x_word_bin); } #[test] fn test_values_mut_categorizes_ascii_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); map.insert_bin("x-number-bin", MetadataValue::from_bytes(b"123")); let mut found_x_word = false; for value_ref in map.values_mut() { if let ValueRefMut::Ascii(value) = value_ref { assert_eq!(*value, "hello"); found_x_word = true; } } assert!(found_x_word); } #[test] fn test_values_mut_categorizes_binary_entries() { let mut map = MetadataMap::new(); map.insert("x-word", "hello".parse().unwrap()); map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); let mut found_x_word_bin = false; for value in map.values_mut() { if let ValueRefMut::Binary(value) = value { assert_eq!(*value, "goodbye"); found_x_word_bin = true; } } assert!(found_x_word_bin); } #[allow(dead_code)] fn value_drain_is_send_sync() { fn is_send_sync() {} is_send_sync::>(); is_send_sync::>(); is_send_sync::>(); is_send_sync::>(); is_send_sync::>(); is_send_sync::>(); } } ================================================ FILE: tonic/src/metadata/mod.rs ================================================ //! Contains data structures and utilities for handling gRPC custom metadata. mod encoding; mod key; mod map; mod value; pub use self::encoding::Ascii; pub use self::encoding::Binary; pub use self::key::AsciiMetadataKey; pub use self::key::BinaryMetadataKey; pub use self::key::MetadataKey; pub use self::map::Entry; pub use self::map::GetAll; pub use self::map::Iter; pub use self::map::IterMut; pub use self::map::KeyAndMutValueRef; pub use self::map::KeyAndValueRef; pub use self::map::KeyRef; pub use self::map::Keys; pub use self::map::MetadataMap; pub use self::map::OccupiedEntry; pub use self::map::VacantEntry; pub use self::map::ValueDrain; pub use self::map::ValueIter; pub use self::map::ValueRef; pub use self::map::ValueRefMut; pub use self::map::Values; pub use self::map::ValuesMut; pub use self::value::AsciiMetadataValue; pub use self::value::BinaryMetadataValue; pub use self::value::MetadataValue; use http::HeaderValue; pub(crate) use self::map::GRPC_TIMEOUT_HEADER; /// HTTP Header `content-type` value for gRPC calls. pub const GRPC_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("application/grpc"); /// The metadata::errors module contains types for errors that can occur /// while handling gRPC custom metadata. pub mod errors { pub use super::encoding::InvalidMetadataValue; pub use super::encoding::InvalidMetadataValueBytes; pub use super::key::InvalidMetadataKey; pub use super::value::ToStrError; } ================================================ FILE: tonic/src/metadata/value.rs ================================================ use super::encoding::{ Ascii, Binary, InvalidMetadataValue, InvalidMetadataValueBytes, ValueEncoding, }; use super::key::MetadataKey; use bytes::Bytes; use http::header::HeaderValue; use std::error::Error; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use std::str::FromStr; use std::{cmp, fmt}; /// Represents a custom metadata field value. /// /// `MetadataValue` is used as the [`MetadataMap`] value. /// /// [`HeaderMap`]: struct.HeaderMap.html /// [`MetadataMap`]: struct.MetadataMap.html #[derive(Clone)] #[repr(transparent)] pub struct MetadataValue { // Note: There are unsafe transmutes that assume that the memory layout // of MetadataValue is identical to HeaderValue pub(crate) inner: HeaderValue, phantom: PhantomData, } /// A possible error when converting a `MetadataValue` to a string representation. /// /// Metadata field values may contain opaque bytes, in which case it is not /// possible to represent the value as a string. #[derive(Debug)] pub struct ToStrError { _priv: (), } /// An ascii metadata value. pub type AsciiMetadataValue = MetadataValue; /// A binary metadata value. pub type BinaryMetadataValue = MetadataValue; impl MetadataValue { /// Convert a static string to a `MetadataValue`. /// /// This function will not perform any copying, however the string is /// checked to ensure that no invalid characters are present. /// /// For Ascii values, only visible ASCII characters (32-127) are permitted. /// For Binary values, the string must be valid base64. /// /// # Panics /// /// This function panics if the argument contains invalid metadata value /// characters. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::from_static("hello"); /// assert_eq!(val, "hello"); /// ``` /// /// ``` /// # use tonic::metadata::*; /// let val = BinaryMetadataValue::from_static("SGVsbG8hIQ=="); /// assert_eq!(val, "Hello!!"); /// ``` #[inline] pub fn from_static(src: &'static str) -> Self { MetadataValue { inner: VE::from_static(src), phantom: PhantomData, } } /// Convert a `Bytes` directly into a `MetadataValue` without validating. /// For `MetadataValue` the provided parameter must be base64 /// encoded without padding bytes at the end. /// /// # Safety /// /// This function does NOT validate that illegal bytes are not contained /// within the buffer. #[inline] pub unsafe fn from_shared_unchecked(src: Bytes) -> Self { unsafe { MetadataValue { inner: HeaderValue::from_maybe_shared_unchecked(src), phantom: PhantomData, } } } /// Returns true if the `MetadataValue` has a length of zero bytes. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::from_static(""); /// assert!(val.is_empty()); /// /// let val = AsciiMetadataValue::from_static("hello"); /// assert!(!val.is_empty()); /// ``` #[inline] pub fn is_empty(&self) -> bool { VE::is_empty(self.inner.as_bytes()) } /// Converts a `MetadataValue` to a Bytes buffer. This method cannot /// fail for Ascii values. For Ascii values, `as_bytes` is more convenient /// to use. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::from_static("hello"); /// assert_eq!(val.to_bytes().unwrap().as_ref(), b"hello"); /// ``` /// /// ``` /// # use tonic::metadata::*; /// let val = BinaryMetadataValue::from_bytes(b"hello"); /// assert_eq!(val.to_bytes().unwrap().as_ref(), b"hello"); /// ``` #[inline] pub fn to_bytes(&self) -> Result { VE::decode(self.inner.as_bytes()) } /// Mark that the metadata value represents sensitive information. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut val = AsciiMetadataValue::from_static("my secret"); /// /// val.set_sensitive(true); /// assert!(val.is_sensitive()); /// /// val.set_sensitive(false); /// assert!(!val.is_sensitive()); /// ``` #[inline] pub fn set_sensitive(&mut self, val: bool) { self.inner.set_sensitive(val); } /// Returns `true` if the value represents sensitive data. /// /// Sensitive data could represent passwords or other data that should not /// be stored on disk or in memory. This setting can be used by components /// like caches to avoid storing the value. HPACK encoders must set the /// metadata field to never index when `is_sensitive` returns true. /// /// Note that sensitivity is not factored into equality or ordering. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let mut val = AsciiMetadataValue::from_static("my secret"); /// /// val.set_sensitive(true); /// assert!(val.is_sensitive()); /// /// val.set_sensitive(false); /// assert!(!val.is_sensitive()); /// ``` #[inline] pub fn is_sensitive(&self) -> bool { self.inner.is_sensitive() } /// Converts a `MetadataValue` to a byte slice. For Binary values, the /// return value is base64 encoded. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::from_static("hello"); /// assert_eq!(val.as_encoded_bytes(), b"hello"); /// ``` /// /// ``` /// # use tonic::metadata::*; /// let val = BinaryMetadataValue::from_bytes(b"Hello!"); /// assert_eq!(val.as_encoded_bytes(), b"SGVsbG8h"); /// ``` #[inline] pub fn as_encoded_bytes(&self) -> &[u8] { self.inner.as_bytes() } /// Converts a HeaderValue to a MetadataValue. This method assumes that the /// caller has made sure that the value is of the correct Ascii or Binary /// value encoding. #[inline] pub(crate) fn unchecked_from_header_value(value: HeaderValue) -> Self { MetadataValue { inner: value, phantom: PhantomData, } } /// Converts a HeaderValue reference to a MetadataValue. This method assumes /// that the caller has made sure that the value is of the correct Ascii or /// Binary value encoding. #[inline] pub(crate) fn unchecked_from_header_value_ref(header_value: &HeaderValue) -> &Self { unsafe { &*(header_value as *const HeaderValue as *const Self) } } /// Converts a HeaderValue reference to a MetadataValue. This method assumes /// that the caller has made sure that the value is of the correct Ascii or /// Binary value encoding. #[inline] pub(crate) fn unchecked_from_mut_header_value_ref(header_value: &mut HeaderValue) -> &mut Self { unsafe { &mut *(header_value as *mut HeaderValue as *mut Self) } } } /// Attempt to convert a byte slice to a `MetadataValue`. /// /// For Ascii metadata values, If the argument contains invalid metadata /// value bytes, an error is returned. Only byte values between 32 and 255 /// (inclusive) are permitted, excluding byte 127 (DEL). /// /// For Binary metadata values this method cannot fail. See also the Binary /// only version of this method `from_bytes`. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::try_from(b"hello\xfa").unwrap(); /// assert_eq!(val, &b"hello\xfa"[..]); /// ``` /// /// An invalid value /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::try_from(b"\n"); /// assert!(val.is_err()); /// ``` impl TryFrom<&[u8]> for MetadataValue { type Error = InvalidMetadataValueBytes; #[inline] fn try_from(src: &[u8]) -> Result { VE::from_bytes(src).map(|value| MetadataValue { inner: value, phantom: PhantomData, }) } } /// Attempt to convert a byte slice to a `MetadataValue`. /// /// For Ascii metadata values, If the argument contains invalid metadata /// value bytes, an error is returned. Only byte values between 32 and 255 /// (inclusive) are permitted, excluding byte 127 (DEL). /// /// For Binary metadata values this method cannot fail. See also the Binary /// only version of this method `from_bytes`. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::try_from(b"hello\xfa").unwrap(); /// assert_eq!(val, &b"hello\xfa"[..]); /// ``` /// /// An invalid value /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::try_from(b"\n"); /// assert!(val.is_err()); /// ``` impl TryFrom<&[u8; N]> for MetadataValue { type Error = InvalidMetadataValueBytes; #[inline] fn try_from(src: &[u8; N]) -> Result { Self::try_from(src.as_ref()) } } /// Attempt to convert a `Bytes` buffer to a `MetadataValue`. /// /// For `MetadataValue`, if the argument contains invalid metadata /// value bytes, an error is returned. Only byte values between 32 and 255 /// (inclusive) are permitted, excluding byte 127 (DEL). /// /// For `MetadataValue`, if the argument is not valid base64, an /// error is returned. In use cases where the input is not base64 encoded, /// use `from_bytes`; if the value has to be encoded it's not possible to /// share the memory anyways. impl TryFrom for MetadataValue { type Error = InvalidMetadataValueBytes; #[inline] fn try_from(src: Bytes) -> Result { VE::from_shared(src).map(|value| MetadataValue { inner: value, phantom: PhantomData, }) } } /// Attempt to convert a Vec of bytes to a `MetadataValue`. /// /// For `MetadataValue`, if the argument contains invalid metadata /// value bytes, an error is returned. Only byte values between 32 and 255 /// (inclusive) are permitted, excluding byte 127 (DEL). /// /// For `MetadataValue`, if the argument is not valid base64, an /// error is returned. In use cases where the input is not base64 encoded, /// use `from_bytes`; if the value has to be encoded it's not possible to /// share the memory anyways. impl TryFrom> for MetadataValue { type Error = InvalidMetadataValueBytes; #[inline] fn try_from(src: Vec) -> Result { Self::try_from(src.as_slice()) } } /// Attempt to convert a string to a `MetadataValue`. /// /// If the argument contains invalid metadata value characters, an error is /// returned. Only visible ASCII characters (32-127) are permitted. Use /// `from_bytes` to create a `MetadataValue` that includes opaque octets /// (128-255). impl<'a> TryFrom<&'a str> for MetadataValue { type Error = InvalidMetadataValue; #[inline] fn try_from(s: &'a str) -> Result { s.parse() } } /// Attempt to convert a string to a `MetadataValue`. /// /// If the argument contains invalid metadata value characters, an error is /// returned. Only visible ASCII characters (32-127) are permitted. Use /// `from_bytes` to create a `MetadataValue` that includes opaque octets /// (128-255). impl<'a> TryFrom<&'a String> for MetadataValue { type Error = InvalidMetadataValue; #[inline] fn try_from(s: &'a String) -> Result { s.parse() } } /// Attempt to convert a string to a `MetadataValue`. /// /// If the argument contains invalid metadata value characters, an error is /// returned. Only visible ASCII characters (32-127) are permitted. Use /// `from_bytes` to create a `MetadataValue` that includes opaque octets /// (128-255). impl TryFrom for MetadataValue { type Error = InvalidMetadataValue; #[inline] fn try_from(s: String) -> Result { s.parse() } } impl MetadataValue { /// Converts a MetadataKey into a `MetadataValue`. /// /// Since every valid MetadataKey is a valid MetadataValue this is done /// infallibly. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::from_key::("accept".parse().unwrap()); /// assert_eq!(val, AsciiMetadataValue::try_from(b"accept").unwrap()); /// ``` #[inline] pub fn from_key(key: MetadataKey) -> Self { key.into() } /// Returns the length of `self`, in bytes. /// /// This method is not available for `MetadataValue` because that /// cannot be implemented in constant time, which most people would probably /// expect. To get the length of `MetadataValue`, convert it to a /// Bytes value and measure its length. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::from_static("hello"); /// assert_eq!(val.len(), 5); /// ``` #[inline] pub fn len(&self) -> usize { self.inner.len() } /// Yields a `&str` slice if the `MetadataValue` only contains visible ASCII /// chars. /// /// This function will perform a scan of the metadata value, checking all the /// characters. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::from_static("hello"); /// assert_eq!(val.to_str().unwrap(), "hello"); /// ``` pub fn to_str(&self) -> Result<&str, ToStrError> { self.inner.to_str().map_err(|_| ToStrError::new()) } /// Converts a `MetadataValue` to a byte slice. For Binary values, use /// `to_bytes`. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = AsciiMetadataValue::from_static("hello"); /// assert_eq!(val.as_bytes(), b"hello"); /// ``` #[inline] pub fn as_bytes(&self) -> &[u8] { self.inner.as_bytes() } } impl MetadataValue { /// Convert a byte slice to a `MetadataValue`. /// /// # Examples /// /// ``` /// # use tonic::metadata::*; /// let val = BinaryMetadataValue::from_bytes(b"hello\xfa"); /// assert_eq!(val, &b"hello\xfa"[..]); /// ``` #[inline] pub fn from_bytes(src: &[u8]) -> Self { // Only the Ascii version of try_from can fail. Self::try_from(src).unwrap() } } impl AsRef<[u8]> for MetadataValue { #[inline] fn as_ref(&self) -> &[u8] { self.inner.as_ref() } } impl fmt::Debug for MetadataValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { VE::fmt(&self.inner, f) } } impl From> for MetadataValue { #[inline] fn from(h: MetadataKey) -> MetadataValue { MetadataValue { inner: h.inner.into(), phantom: PhantomData, } } } macro_rules! from_integers { ($($name:ident: $t:ident => $max_len:expr),*) => {$( impl From<$t> for MetadataValue { fn from(num: $t) -> MetadataValue { MetadataValue { inner: HeaderValue::from(num), phantom: PhantomData, } } } #[test] fn $name() { let n: $t = 55; let val = AsciiMetadataValue::from(n); assert_eq!(val, &n.to_string()); let n = $t::MAX; let val = AsciiMetadataValue::from(n); assert_eq!(val, &n.to_string()); } )*}; } from_integers! { // integer type => maximum decimal length // u8 purposely left off... AsciiMetadataValue::from(b'3') could be confusing from_u16: u16 => 5, from_i16: i16 => 6, from_u32: u32 => 10, from_i32: i32 => 11, from_u64: u64 => 20, from_i64: i64 => 20 } #[cfg(target_pointer_width = "16")] from_integers! { from_usize: usize => 5, from_isize: isize => 6 } #[cfg(target_pointer_width = "32")] from_integers! { from_usize: usize => 10, from_isize: isize => 11 } #[cfg(target_pointer_width = "64")] from_integers! { from_usize: usize => 20, from_isize: isize => 20 } #[cfg(test)] mod from_metadata_value_tests { use super::*; use crate::metadata::map::MetadataMap; #[test] fn it_can_insert_metadata_key_as_metadata_value() { let mut map = MetadataMap::new(); map.insert( "accept", MetadataKey::::from_bytes(b"hello-world") .unwrap() .into(), ); assert_eq!( map.get("accept").unwrap(), AsciiMetadataValue::try_from(b"hello-world").unwrap() ); } } impl FromStr for MetadataValue { type Err = InvalidMetadataValue; #[inline] fn from_str(s: &str) -> Result, Self::Err> { HeaderValue::from_str(s) .map(|value| MetadataValue { inner: value, phantom: PhantomData, }) .map_err(|_| InvalidMetadataValue::new()) } } impl From> for Bytes { #[inline] fn from(value: MetadataValue) -> Bytes { Bytes::copy_from_slice(value.inner.as_bytes()) } } impl<'a, VE: ValueEncoding> From<&'a MetadataValue> for MetadataValue { #[inline] fn from(t: &'a MetadataValue) -> Self { t.clone() } } // ===== ToStrError ===== impl ToStrError { pub(crate) fn new() -> Self { ToStrError { _priv: () } } } impl fmt::Display for ToStrError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("failed to convert metadata to a str") } } impl Error for ToStrError {} impl Hash for MetadataValue { fn hash(&self, state: &mut H) { self.inner.hash(state) } } impl Hash for MetadataValue { fn hash(&self, state: &mut H) { match self.to_bytes() { Ok(b) => b.hash(state), Err(e) => e.hash(state), } } } // ===== PartialEq / PartialOrd ===== impl PartialEq for MetadataValue { #[inline] fn eq(&self, other: &MetadataValue) -> bool { // Note: Different binary strings that after base64 decoding // will count as the same value for Binary values. Also, // different invalid base64 values count as equal for Binary // values. VE::values_equal(&self.inner, &other.inner) } } impl Eq for MetadataValue {} impl PartialOrd for MetadataValue { #[inline] fn partial_cmp(&self, other: &MetadataValue) -> Option { Some(self.cmp(other)) } } impl Ord for MetadataValue { #[inline] fn cmp(&self, other: &Self) -> cmp::Ordering { self.inner.cmp(&other.inner) } } impl PartialEq for MetadataValue { #[inline] fn eq(&self, other: &str) -> bool { VE::equals(&self.inner, other.as_bytes()) } } impl PartialEq<[u8]> for MetadataValue { #[inline] fn eq(&self, other: &[u8]) -> bool { VE::equals(&self.inner, other) } } impl PartialOrd for MetadataValue { #[inline] fn partial_cmp(&self, other: &str) -> Option { self.inner.partial_cmp(other.as_bytes()) } } impl PartialOrd<[u8]> for MetadataValue { #[inline] fn partial_cmp(&self, other: &[u8]) -> Option { self.inner.partial_cmp(other) } } impl PartialEq> for str { #[inline] fn eq(&self, other: &MetadataValue) -> bool { *other == *self } } impl PartialEq> for [u8] { #[inline] fn eq(&self, other: &MetadataValue) -> bool { *other == *self } } impl PartialOrd> for str { #[inline] fn partial_cmp(&self, other: &MetadataValue) -> Option { self.as_bytes().partial_cmp(other.inner.as_bytes()) } } impl PartialOrd> for [u8] { #[inline] fn partial_cmp(&self, other: &MetadataValue) -> Option { self.partial_cmp(other.inner.as_bytes()) } } impl PartialEq for MetadataValue { #[inline] fn eq(&self, other: &String) -> bool { *self == other[..] } } impl PartialOrd for MetadataValue { #[inline] fn partial_cmp(&self, other: &String) -> Option { self.inner.partial_cmp(other.as_bytes()) } } impl PartialEq> for String { #[inline] fn eq(&self, other: &MetadataValue) -> bool { *other == *self } } impl PartialOrd> for String { #[inline] fn partial_cmp(&self, other: &MetadataValue) -> Option { self.as_bytes().partial_cmp(other.inner.as_bytes()) } } impl PartialEq> for &MetadataValue { #[inline] fn eq(&self, other: &MetadataValue) -> bool { **self == *other } } impl PartialOrd> for &MetadataValue { #[inline] fn partial_cmp(&self, other: &MetadataValue) -> Option { (**self).partial_cmp(other) } } impl<'a, VE: ValueEncoding, T: ?Sized> PartialEq<&'a T> for MetadataValue where MetadataValue: PartialEq, { #[inline] fn eq(&self, other: &&'a T) -> bool { *self == **other } } impl<'a, VE: ValueEncoding, T: ?Sized> PartialOrd<&'a T> for MetadataValue where MetadataValue: PartialOrd, { #[inline] fn partial_cmp(&self, other: &&'a T) -> Option { self.partial_cmp(*other) } } impl PartialEq> for &str { #[inline] fn eq(&self, other: &MetadataValue) -> bool { *other == *self } } impl PartialOrd> for &str { #[inline] fn partial_cmp(&self, other: &MetadataValue) -> Option { self.as_bytes().partial_cmp(other.inner.as_bytes()) } } #[test] fn test_debug() { let cases = &[ ("hello", "\"hello\""), ("hello \"world\"", "\"hello \\\"world\\\"\""), ("\u{7FFF}hello", "\"\\xe7\\xbf\\xbfhello\""), ]; for &(value, expected) in cases { let val = AsciiMetadataValue::try_from(value.as_bytes()).unwrap(); let actual = format!("{val:?}"); assert_eq!(expected, actual); } let mut sensitive = AsciiMetadataValue::from_static("password"); sensitive.set_sensitive(true); assert_eq!("Sensitive", format!("{sensitive:?}")); } #[test] fn test_is_empty() { fn from_str(s: &str) -> MetadataValue { MetadataValue::::unchecked_from_header_value(s.parse().unwrap()) } assert!(from_str::("").is_empty()); assert!(from_str::("").is_empty()); assert!(!from_str::("a").is_empty()); assert!(!from_str::("a").is_empty()); assert!(!from_str::("=").is_empty()); assert!(from_str::("=").is_empty()); assert!(!from_str::("===").is_empty()); assert!(from_str::("===").is_empty()); assert!(!from_str::("=====").is_empty()); assert!(from_str::("=====").is_empty()); } #[test] fn test_from_shared_base64_encodes() { let value = BinaryMetadataValue::try_from(Bytes::from_static(b"Hello")).unwrap(); assert_eq!(value.as_encoded_bytes(), b"SGVsbG8"); } #[test] fn test_value_eq_value() { type Bmv = BinaryMetadataValue; type Amv = AsciiMetadataValue; assert_eq!(Amv::from_static("abc"), Amv::from_static("abc")); assert_ne!(Amv::from_static("abc"), Amv::from_static("ABC")); assert_eq!(Bmv::from_bytes(b"abc"), Bmv::from_bytes(b"abc")); assert_ne!(Bmv::from_bytes(b"abc"), Bmv::from_bytes(b"ABC")); // Padding is ignored. assert_eq!( Bmv::from_static("SGVsbG8hIQ=="), Bmv::from_static("SGVsbG8hIQ") ); // Invalid values are all just invalid from this point of view. unsafe { assert_eq!( Bmv::from_shared_unchecked(Bytes::from_static(b"..{}")), Bmv::from_shared_unchecked(Bytes::from_static(b"{}..")) ); } } #[test] fn test_value_eq_str() { type Bmv = BinaryMetadataValue; type Amv = AsciiMetadataValue; assert_eq!(Amv::from_static("abc"), "abc"); assert_ne!(Amv::from_static("abc"), "ABC"); assert_eq!("abc", Amv::from_static("abc")); assert_ne!("ABC", Amv::from_static("abc")); assert_eq!(Bmv::from_bytes(b"abc"), "abc"); assert_ne!(Bmv::from_bytes(b"abc"), "ABC"); assert_eq!("abc", Bmv::from_bytes(b"abc")); assert_ne!("ABC", Bmv::from_bytes(b"abc")); // Padding is ignored. assert_eq!(Bmv::from_static("SGVsbG8hIQ=="), "Hello!!"); assert_eq!("Hello!!", Bmv::from_static("SGVsbG8hIQ==")); } #[test] fn test_value_eq_bytes() { type Bmv = BinaryMetadataValue; type Amv = AsciiMetadataValue; assert_eq!(Amv::from_static("abc"), "abc".as_bytes()); assert_ne!(Amv::from_static("abc"), "ABC".as_bytes()); assert_eq!(*"abc".as_bytes(), Amv::from_static("abc")); assert_ne!(*"ABC".as_bytes(), Amv::from_static("abc")); assert_eq!(*"abc".as_bytes(), Bmv::from_bytes(b"abc")); assert_ne!(*"ABC".as_bytes(), Bmv::from_bytes(b"abc")); // Padding is ignored. assert_eq!(Bmv::from_static("SGVsbG8hIQ=="), "Hello!!".as_bytes()); assert_eq!(*"Hello!!".as_bytes(), Bmv::from_static("SGVsbG8hIQ==")); } #[test] fn test_ascii_value_hash() { use std::collections::hash_map::DefaultHasher; type Amv = AsciiMetadataValue; fn hash(value: Amv) -> u64 { let mut hasher = DefaultHasher::new(); value.hash(&mut hasher); hasher.finish() } let value1 = Amv::from_static("abc"); let value2 = Amv::from_static("abc"); assert_eq!(value1, value2); assert_eq!(hash(value1), hash(value2)); let value1 = Amv::from_static("abc"); let value2 = Amv::from_static("xyz"); assert_ne!(value1, value2); assert_ne!(hash(value1), hash(value2)); } #[test] fn test_valid_binary_value_hash() { use std::collections::hash_map::DefaultHasher; type Bmv = BinaryMetadataValue; fn hash(value: Bmv) -> u64 { let mut hasher = DefaultHasher::new(); value.hash(&mut hasher); hasher.finish() } let value1 = Bmv::from_bytes(b"abc"); let value2 = Bmv::from_bytes(b"abc"); assert_eq!(value1, value2); assert_eq!(hash(value1), hash(value2)); let value1 = Bmv::from_bytes(b"abc"); let value2 = Bmv::from_bytes(b"xyz"); assert_ne!(value1, value2); assert_ne!(hash(value1), hash(value2)); } #[test] fn test_invalid_binary_value_hash() { use std::collections::hash_map::DefaultHasher; type Bmv = BinaryMetadataValue; fn hash(value: Bmv) -> u64 { let mut hasher = DefaultHasher::new(); value.hash(&mut hasher); hasher.finish() } unsafe { let value1 = Bmv::from_shared_unchecked(Bytes::from_static(b"..{}")); let value2 = Bmv::from_shared_unchecked(Bytes::from_static(b"{}..")); assert_eq!(value1, value2); assert_eq!(hash(value1), hash(value2)); } unsafe { let valid = Bmv::from_bytes(b"abc"); let invalid = Bmv::from_shared_unchecked(Bytes::from_static(b"{}..")); assert_ne!(valid, invalid); assert_ne!(hash(valid), hash(invalid)); } } ================================================ FILE: tonic/src/request.rs ================================================ use crate::metadata::{MetadataMap, MetadataValue}; #[cfg(feature = "server")] use crate::transport::server::TcpConnectInfo; #[cfg(all(feature = "server", feature = "_tls-any"))] use crate::transport::server::TlsConnectInfo; use http::Extensions; #[cfg(feature = "server")] use std::net::SocketAddr; #[cfg(all(feature = "server", feature = "_tls-any"))] use std::sync::Arc; use std::time::Duration; #[cfg(all(feature = "server", feature = "_tls-any"))] use tokio_rustls::rustls::pki_types::CertificateDer; use tokio_stream::Stream; /// A gRPC request and metadata from an RPC call. #[derive(Debug)] pub struct Request { metadata: MetadataMap, message: T, extensions: Extensions, } /// Trait implemented by RPC request types. /// /// Types implementing this trait can be used as arguments to client RPC /// methods without explicitly wrapping them into `tonic::Request`s. The purpose /// is to make client calls slightly more convenient to write. /// /// Tonic's code generation and blanket implementations handle this for you, /// so it is not necessary to implement this trait directly. /// /// # Example /// /// Given the following gRPC method definition: /// ```proto /// rpc GetFeature(Point) returns (Feature) {} /// ``` /// /// we can call `get_feature` in two equivalent ways: /// ```rust /// # pub struct Point {} /// # pub struct Client {} /// # impl Client { /// # fn get_feature(&self, r: impl tonic::IntoRequest) {} /// # } /// # let client = Client {}; /// use tonic::Request; /// /// client.get_feature(Point {}); /// client.get_feature(Request::new(Point {})); /// ``` pub trait IntoRequest: sealed::Sealed { /// Wrap the input message `T` in a `tonic::Request` fn into_request(self) -> Request; } /// Trait implemented by RPC streaming request types. /// /// Types implementing this trait can be used as arguments to client streaming /// RPC methods without explicitly wrapping them into `tonic::Request`s. The /// purpose is to make client calls slightly more convenient to write. /// /// Tonic's code generation and blanket implementations handle this for you, /// so it is not necessary to implement this trait directly. /// /// # Example /// /// Given the following gRPC service method definition: /// ```proto /// rpc RecordRoute(stream Point) returns (RouteSummary) {} /// ``` /// we can call `record_route` in two equivalent ways: /// /// ```rust /// # #[derive(Clone)] /// # pub struct Point {}; /// # pub struct Client {}; /// # impl Client { /// # fn record_route(&self, r: impl tonic::IntoStreamingRequest) {} /// # } /// # let client = Client {}; /// use tonic::Request; /// /// let messages = vec![Point {}, Point {}]; /// /// client.record_route(Request::new(tokio_stream::iter(messages.clone()))); /// client.record_route(tokio_stream::iter(messages)); /// ``` pub trait IntoStreamingRequest: sealed::Sealed { /// The RPC request stream type type Stream: Stream + Send + 'static; /// The RPC request type type Message; /// Wrap the stream of messages in a `tonic::Request` fn into_streaming_request(self) -> Request; } impl Request { /// Create a new gRPC request. /// /// ```rust /// # use tonic::Request; /// # pub struct HelloRequest { /// # pub name: String, /// # } /// Request::new(HelloRequest { /// name: "Bob".into(), /// }); /// ``` pub fn new(message: T) -> Self { Request { metadata: MetadataMap::new(), message, extensions: Extensions::new(), } } /// Get a reference to the message pub fn get_ref(&self) -> &T { &self.message } /// Get a mutable reference to the message pub fn get_mut(&mut self) -> &mut T { &mut self.message } /// Get a reference to the custom request metadata. pub fn metadata(&self) -> &MetadataMap { &self.metadata } /// Get a mutable reference to the request metadata. pub fn metadata_mut(&mut self) -> &mut MetadataMap { &mut self.metadata } /// Consumes `self`, returning the message pub fn into_inner(self) -> T { self.message } /// Consumes `self` returning the parts of the request. pub fn into_parts(self) -> (MetadataMap, Extensions, T) { (self.metadata, self.extensions, self.message) } /// Create a new gRPC request from metadata, extensions and message. pub fn from_parts(metadata: MetadataMap, extensions: Extensions, message: T) -> Self { Self { metadata, extensions, message, } } pub(crate) fn from_http_parts(parts: http::request::Parts, message: T) -> Self { Request { metadata: MetadataMap::from_headers(parts.headers), message, extensions: parts.extensions, } } /// Convert an HTTP request to a gRPC request pub fn from_http(http: http::Request) -> Self { let (parts, message) = http.into_parts(); Request::from_http_parts(parts, message) } pub(crate) fn into_http( self, uri: http::Uri, method: http::Method, version: http::Version, sanitize_headers: SanitizeHeaders, ) -> http::Request { let mut request = http::Request::new(self.message); *request.version_mut() = version; *request.method_mut() = method; *request.uri_mut() = uri; *request.headers_mut() = match sanitize_headers { SanitizeHeaders::Yes => self.metadata.into_sanitized_headers(), SanitizeHeaders::No => self.metadata.into_headers(), }; *request.extensions_mut() = self.extensions; request } #[doc(hidden)] pub fn map(self, f: F) -> Request where F: FnOnce(T) -> U, { let message = f(self.message); Request { metadata: self.metadata, message, extensions: self.extensions, } } /// Get the local address of this connection. /// /// This will return `None` if the `IO` type used /// does not implement `Connected` or when using a unix domain socket. /// This currently only works on the server side. #[cfg(feature = "server")] pub fn local_addr(&self) -> Option { let addr = self .extensions() .get::() .and_then(|i| i.local_addr()); #[cfg(feature = "_tls-any")] let addr = addr.or_else(|| { self.extensions() .get::>() .and_then(|i| i.get_ref().local_addr()) }); addr } /// Get the remote address of this connection. /// /// This will return `None` if the `IO` type used /// does not implement `Connected` or when using a unix domain socket. /// This currently only works on the server side. #[cfg(feature = "server")] pub fn remote_addr(&self) -> Option { let addr = self .extensions() .get::() .and_then(|i| i.remote_addr()); #[cfg(feature = "_tls-any")] let addr = addr.or_else(|| { self.extensions() .get::>() .and_then(|i| i.get_ref().remote_addr()) }); addr } /// Get the peer certificates of the connected client. /// /// This is used to fetch the certificates from the TLS session /// and is mostly used for mTLS. This currently only returns /// `Some` on the server side of the `transport` server with /// TLS enabled connections. #[cfg(all(feature = "server", feature = "_tls-any"))] pub fn peer_certs(&self) -> Option>>> { self.extensions() .get::>() .and_then(|i| i.peer_certs()) } /// Set the max duration the request is allowed to take. /// /// Requires the server to support the `grpc-timeout` metadata, which Tonic does. /// /// The duration will be formatted according to [the spec] and use the most precise unit /// possible. /// /// Example: /// /// ```rust /// use std::time::Duration; /// use tonic::Request; /// /// let mut request = Request::new(()); /// /// request.set_timeout(Duration::from_secs(30)); /// /// let value = request.metadata().get("grpc-timeout").unwrap(); /// /// assert_eq!( /// value, /// // equivalent to 30 seconds /// "30000000u" /// ); /// ``` /// /// [the spec]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md pub fn set_timeout(&mut self, deadline: Duration) { let value: MetadataValue<_> = duration_to_grpc_timeout(deadline).parse().unwrap(); self.metadata_mut() .insert(crate::metadata::GRPC_TIMEOUT_HEADER, value); } /// Returns a reference to the associated extensions. pub fn extensions(&self) -> &Extensions { &self.extensions } /// Returns a mutable reference to the associated extensions. /// /// # Example /// /// Extensions can be set in interceptors: /// /// ```no_run /// use tonic::{Request, Status}; /// /// #[derive(Clone)] // Extensions must be Clone /// struct MyExtension { /// some_piece_of_data: String, /// } /// /// fn intercept(mut request: Request<()>) -> Result, Status> { /// request.extensions_mut().insert(MyExtension { /// some_piece_of_data: "foo".to_string(), /// }); /// /// Ok(request) /// } /// ``` /// /// And picked up by RPCs: /// /// ```no_run /// use tonic::{async_trait, Status, Request, Response}; /// # /// # struct Output {} /// # struct Input; /// # struct MyService; /// # struct MyExtension; /// # #[async_trait] /// # trait TestService { /// # async fn handler(&self, req: Request) -> Result, Status>; /// # } /// /// #[async_trait] /// impl TestService for MyService { /// async fn handler(&self, req: Request) -> Result, Status> { /// let value: &MyExtension = req.extensions().get::().unwrap(); /// /// Ok(Response::new(Output {})) /// } /// } /// ``` pub fn extensions_mut(&mut self) -> &mut Extensions { &mut self.extensions } } impl IntoRequest for T { fn into_request(self) -> Request { Request::new(self) } } impl IntoRequest for Request { fn into_request(self) -> Request { self } } impl IntoStreamingRequest for T where T: Stream + Send + 'static, { type Stream = T; type Message = T::Item; fn into_streaming_request(self) -> Request { Request::new(self) } } impl IntoStreamingRequest for Request where T: Stream + Send + 'static, { type Stream = T; type Message = T::Item; fn into_streaming_request(self) -> Self { self } } impl sealed::Sealed for T {} mod sealed { pub trait Sealed {} } fn duration_to_grpc_timeout(duration: Duration) -> String { fn try_format>( duration: Duration, unit: char, convert: impl FnOnce(Duration) -> T, ) -> Option { // The gRPC spec specifies that the timeout most be at most 8 digits. So this is the largest a // value can be before we need to use a bigger unit. let max_size: u128 = 99_999_999; // exactly 8 digits let value = convert(duration).into(); if value > max_size { None } else { Some(format!("{value}{unit}")) } } // pick the most precise unit that is less than or equal to 8 digits as per the gRPC spec try_format(duration, 'n', |d| d.as_nanos()) .or_else(|| try_format(duration, 'u', |d| d.as_micros())) .or_else(|| try_format(duration, 'm', |d| d.as_millis())) .or_else(|| try_format(duration, 'S', |d| d.as_secs())) .or_else(|| try_format(duration, 'M', |d| d.as_secs() / 60)) .or_else(|| { try_format(duration, 'H', |d| { let minutes = d.as_secs() / 60; minutes / 60 }) }) // duration has to be more than 11_415 years for this to happen .expect("duration is unrealistically large") } /// When converting a `tonic::Request` into a `http::Request` should reserved /// headers be removed? pub(crate) enum SanitizeHeaders { Yes, No, } #[cfg(test)] mod tests { use super::*; use crate::metadata::{MetadataKey, MetadataValue}; use http::Uri; #[test] fn reserved_headers_are_excluded() { let mut r = Request::new(1); for header in &MetadataMap::GRPC_RESERVED_HEADERS { r.metadata_mut().insert( MetadataKey::unchecked_from_header_name(header.clone()), MetadataValue::from_static("invalid"), ); } let http_request = r.into_http( Uri::default(), http::Method::POST, http::Version::HTTP_2, SanitizeHeaders::Yes, ); assert!(http_request.headers().is_empty()); } #[test] fn preserves_user_agent() { let mut r = Request::new(1); r.metadata_mut().insert( MetadataKey::from_static("user-agent"), MetadataValue::from_static("Custom/1.2.3"), ); let http_request = r.into_http( Uri::default(), http::Method::POST, http::Version::HTTP_2, SanitizeHeaders::Yes, ); let user_agent = http_request.headers().get("user-agent").unwrap(); assert_eq!(user_agent, "Custom/1.2.3"); } #[test] fn duration_to_grpc_timeout_less_than_second() { let timeout = Duration::from_millis(500); let value = duration_to_grpc_timeout(timeout); assert_eq!(value, format!("{}u", timeout.as_micros())); } #[test] fn duration_to_grpc_timeout_more_than_second() { let timeout = Duration::from_secs(30); let value = duration_to_grpc_timeout(timeout); assert_eq!(value, format!("{}u", timeout.as_micros())); } #[test] fn duration_to_grpc_timeout_a_very_long_time() { let one_hour = Duration::from_secs(60 * 60); let value = duration_to_grpc_timeout(one_hour); assert_eq!(value, format!("{}m", one_hour.as_millis())); } } ================================================ FILE: tonic/src/response.rs ================================================ use http::Extensions; use crate::metadata::MetadataMap; /// A gRPC response and metadata from an RPC call. #[derive(Debug)] pub struct Response { metadata: MetadataMap, message: T, extensions: Extensions, } impl Response { /// Create a new gRPC response. /// /// ```rust /// # use tonic::Response; /// # pub struct HelloReply { /// # pub message: String, /// # } /// # let name = ""; /// Response::new(HelloReply { /// message: format!("Hello, {}!", name).into(), /// }); /// ``` pub fn new(message: T) -> Self { Response { metadata: MetadataMap::new(), message, extensions: Extensions::new(), } } /// Get a immutable reference to `T`. pub fn get_ref(&self) -> &T { &self.message } /// Get a mutable reference to the message pub fn get_mut(&mut self) -> &mut T { &mut self.message } /// Get a reference to the custom response metadata. pub fn metadata(&self) -> &MetadataMap { &self.metadata } /// Get a mutable reference to the response metadata. pub fn metadata_mut(&mut self) -> &mut MetadataMap { &mut self.metadata } /// Consumes `self`, returning the message pub fn into_inner(self) -> T { self.message } /// Consumes `self` returning the parts of the response. pub fn into_parts(self) -> (MetadataMap, T, Extensions) { (self.metadata, self.message, self.extensions) } /// Create a new gRPC response from metadata, message and extensions. pub fn from_parts(metadata: MetadataMap, message: T, extensions: Extensions) -> Self { Self { metadata, message, extensions, } } pub(crate) fn from_http(res: http::Response) -> Self { let (head, message) = res.into_parts(); Response { metadata: MetadataMap::from_headers(head.headers), message, extensions: head.extensions, } } pub(crate) fn into_http(self) -> http::Response { let mut res = http::Response::new(self.message); *res.version_mut() = http::Version::HTTP_2; *res.headers_mut() = self.metadata.into_sanitized_headers(); *res.extensions_mut() = self.extensions; res } #[doc(hidden)] pub fn map(self, f: F) -> Response where F: FnOnce(T) -> U, { let message = f(self.message); Response { metadata: self.metadata, message, extensions: self.extensions, } } /// Returns a reference to the associated extensions. pub fn extensions(&self) -> &Extensions { &self.extensions } /// Returns a mutable reference to the associated extensions. pub fn extensions_mut(&mut self) -> &mut Extensions { &mut self.extensions } /// Disable compression of the response body. /// /// This disables compression of the body of this response, even if compression is enabled on /// the server. /// /// **Note**: This only has effect on responses to unary requests and responses to client to /// server streams. Response streams (server to client stream and bidirectional streams) will /// still be compressed according to the configuration of the server. #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] pub fn disable_compression(&mut self) { self.extensions_mut() .insert(crate::codec::compression::SingleMessageCompressionOverride::Disable); } } impl From for Response { fn from(inner: T) -> Self { Response::new(inner) } } #[cfg(test)] mod tests { use super::*; use crate::metadata::{MetadataKey, MetadataValue}; #[test] fn reserved_headers_are_excluded() { let mut r = Response::new(1); for header in &MetadataMap::GRPC_RESERVED_HEADERS { r.metadata_mut().insert( MetadataKey::unchecked_from_header_name(header.clone()), MetadataValue::from_static("invalid"), ); } let http_response = r.into_http(); assert!(http_response.headers().is_empty()); } } ================================================ FILE: tonic/src/server/grpc.rs ================================================ use crate::codec::EncodeBody; use crate::codec::compression::{ CompressionEncoding, EnabledCompressionEncodings, SingleMessageCompressionOverride, }; use crate::metadata::GRPC_CONTENT_TYPE; use crate::{ Request, Status, body::Body, codec::{Codec, Streaming}, server::{ClientStreamingService, ServerStreamingService, StreamingService, UnaryService}, }; use http_body::Body as HttpBody; use std::{fmt, pin::pin}; use tokio_stream::{Stream, StreamExt}; macro_rules! t { ($result:expr) => { match $result { Ok(value) => value, Err(status) => return status.into_http(), } }; } /// A gRPC Server handler. /// /// This will wrap some inner [`Codec`] and provide utilities to handle /// inbound unary, client side streaming, server side streaming, and /// bi-directional streaming. /// /// Each request handler method accepts some service that implements the /// corresponding service trait and a http request that contains some body that /// implements some [`Body`]. pub struct Grpc { codec: T, /// Which compression encodings does the server accept for requests? accept_compression_encodings: EnabledCompressionEncodings, /// Which compression encodings might the server use for responses. send_compression_encodings: EnabledCompressionEncodings, /// Limits the maximum size of a decoded message. max_decoding_message_size: Option, /// Limits the maximum size of an encoded message. max_encoding_message_size: Option, } impl Grpc where T: Codec, { /// Creates a new gRPC server with the provided [`Codec`]. pub fn new(codec: T) -> Self { Self { codec, accept_compression_encodings: EnabledCompressionEncodings::default(), send_compression_encodings: EnabledCompressionEncodings::default(), max_decoding_message_size: None, max_encoding_message_size: None, } } /// Enable accepting compressed requests. /// /// If a request with an unsupported encoding is received the server will respond with /// [`Code::UnUnimplemented`](crate::Code). /// /// # Example /// /// The most common way of using this is through a server generated by tonic-build: /// /// ```rust /// # enum CompressionEncoding { Gzip } /// # struct Svc; /// # struct ExampleServer(T); /// # impl ExampleServer { /// # fn new(svc: T) -> Self { Self(svc) } /// # fn accept_compressed(self, _: CompressionEncoding) -> Self { self } /// # } /// # #[tonic::async_trait] /// # trait Example {} /// /// #[tonic::async_trait] /// impl Example for Svc { /// // ... /// } /// /// let service = ExampleServer::new(Svc).accept_compressed(CompressionEncoding::Gzip); /// ``` pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Enable sending compressed responses. /// /// Requires the client to also support receiving compressed responses. /// /// # Example /// /// The most common way of using this is through a server generated by tonic-build: /// /// ```rust /// # enum CompressionEncoding { Gzip } /// # struct Svc; /// # struct ExampleServer(T); /// # impl ExampleServer { /// # fn new(svc: T) -> Self { Self(svc) } /// # fn send_compressed(self, _: CompressionEncoding) -> Self { self } /// # } /// # #[tonic::async_trait] /// # trait Example {} /// /// #[tonic::async_trait] /// impl Example for Svc { /// // ... /// } /// /// let service = ExampleServer::new(Svc).send_compressed(CompressionEncoding::Gzip); /// ``` pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. /// /// # Example /// /// The most common way of using this is through a server generated by tonic-build: /// /// ```rust /// # struct Svc; /// # struct ExampleServer(T); /// # impl ExampleServer { /// # fn new(svc: T) -> Self { Self(svc) } /// # fn max_decoding_message_size(self, _: usize) -> Self { self } /// # } /// # #[tonic::async_trait] /// # trait Example {} /// /// #[tonic::async_trait] /// impl Example for Svc { /// // ... /// } /// /// // Set the limit to 2MB, Defaults to 4MB. /// let limit = 2 * 1024 * 1024; /// let service = ExampleServer::new(Svc).max_decoding_message_size(limit); /// ``` pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of a encoded message. /// /// # Example /// /// The most common way of using this is through a server generated by tonic-build: /// /// ```rust /// # struct Svc; /// # struct ExampleServer(T); /// # impl ExampleServer { /// # fn new(svc: T) -> Self { Self(svc) } /// # fn max_encoding_message_size(self, _: usize) -> Self { self } /// # } /// # #[tonic::async_trait] /// # trait Example {} /// /// #[tonic::async_trait] /// impl Example for Svc { /// // ... /// } /// /// // Set the limit to 2MB, Defaults to 4MB. /// let limit = 2 * 1024 * 1024; /// let service = ExampleServer::new(Svc).max_encoding_message_size(limit); /// ``` pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.max_encoding_message_size = Some(limit); self } #[doc(hidden)] pub fn apply_compression_config( mut self, accept_encodings: EnabledCompressionEncodings, send_encodings: EnabledCompressionEncodings, ) -> Self { for &encoding in CompressionEncoding::ENCODINGS { if accept_encodings.is_enabled(encoding) { self = self.accept_compressed(encoding); } if send_encodings.is_enabled(encoding) { self = self.send_compressed(encoding); } } self } #[doc(hidden)] pub fn apply_max_message_size_config( mut self, max_decoding_message_size: Option, max_encoding_message_size: Option, ) -> Self { if let Some(limit) = max_decoding_message_size { self = self.max_decoding_message_size(limit); } if let Some(limit) = max_encoding_message_size { self = self.max_encoding_message_size(limit); } self } /// Handle a single unary gRPC request. pub async fn unary( &mut self, mut service: S, req: http::Request, ) -> http::Response where S: UnaryService, B: HttpBody + Send + 'static, B::Error: Into + Send, { let accept_encoding = CompressionEncoding::from_accept_encoding_header( req.headers(), self.send_compression_encodings, ); let request = match self.map_request_unary(req).await { Ok(r) => r, Err(status) => { return self.map_response::>>( Err(status), accept_encoding, SingleMessageCompressionOverride::default(), self.max_encoding_message_size, ); } }; let response = service .call(request) .await .map(|r| r.map(|m| tokio_stream::once(Ok(m)))); let compression_override = compression_override_from_response(&response); self.map_response( response, accept_encoding, compression_override, self.max_encoding_message_size, ) } /// Handle a server side streaming request. pub async fn server_streaming( &mut self, mut service: S, req: http::Request, ) -> http::Response where S: ServerStreamingService, S::ResponseStream: Send + 'static, B: HttpBody + Send + 'static, B::Error: Into + Send, { let accept_encoding = CompressionEncoding::from_accept_encoding_header( req.headers(), self.send_compression_encodings, ); let request = match self.map_request_unary(req).await { Ok(r) => r, Err(status) => { return self.map_response::( Err(status), accept_encoding, SingleMessageCompressionOverride::default(), self.max_encoding_message_size, ); } }; let response = service.call(request).await; self.map_response( response, accept_encoding, // disabling compression of individual stream items must be done on // the items themselves SingleMessageCompressionOverride::default(), self.max_encoding_message_size, ) } /// Handle a client side streaming gRPC request. pub async fn client_streaming( &mut self, mut service: S, req: http::Request, ) -> http::Response where S: ClientStreamingService, B: HttpBody + Send + 'static, B::Error: Into + Send + 'static, { let accept_encoding = CompressionEncoding::from_accept_encoding_header( req.headers(), self.send_compression_encodings, ); let request = t!(self.map_request_streaming(req)); let response = service .call(request) .await .map(|r| r.map(|m| tokio_stream::once(Ok(m)))); let compression_override = compression_override_from_response(&response); self.map_response( response, accept_encoding, compression_override, self.max_encoding_message_size, ) } /// Handle a bi-directional streaming gRPC request. pub async fn streaming( &mut self, mut service: S, req: http::Request, ) -> http::Response where S: StreamingService + Send, S::ResponseStream: Send + 'static, B: HttpBody + Send + 'static, B::Error: Into + Send, { let accept_encoding = CompressionEncoding::from_accept_encoding_header( req.headers(), self.send_compression_encodings, ); let request = t!(self.map_request_streaming(req)); let response = service.call(request).await; self.map_response( response, accept_encoding, SingleMessageCompressionOverride::default(), self.max_encoding_message_size, ) } async fn map_request_unary( &mut self, request: http::Request, ) -> Result, Status> where B: HttpBody + Send + 'static, B::Error: Into + Send, { let request_compression_encoding = self.request_encoding_if_supported(&request)?; let (parts, body) = request.into_parts(); let mut stream = pin!(Streaming::new_request( self.codec.decoder(), body, request_compression_encoding, self.max_decoding_message_size, )); let message = stream .try_next() .await? .ok_or_else(|| Status::internal("Missing request message."))?; let mut req = Request::from_http_parts(parts, message); if let Some(trailers) = stream.trailers().await? { req.metadata_mut().merge(trailers); } Ok(req) } fn map_request_streaming( &mut self, request: http::Request, ) -> Result>, Status> where B: HttpBody + Send + 'static, B::Error: Into + Send, { let encoding = self.request_encoding_if_supported(&request)?; let request = request.map(|body| { Streaming::new_request( self.codec.decoder(), body, encoding, self.max_decoding_message_size, ) }); Ok(Request::from_http(request)) } fn map_response( &mut self, response: Result, Status>, accept_encoding: Option, compression_override: SingleMessageCompressionOverride, max_message_size: Option, ) -> http::Response where B: Stream> + Send + 'static, { let response = t!(response); let (mut parts, body) = response.into_http().into_parts(); // Set the content type parts .headers .insert(http::header::CONTENT_TYPE, GRPC_CONTENT_TYPE); #[cfg(any(feature = "gzip", feature = "deflate", feature = "zstd"))] if let Some(encoding) = accept_encoding { // Set the content encoding parts.headers.insert( crate::codec::compression::ENCODING_HEADER, encoding.into_header_value(), ); } let body = EncodeBody::new_server( self.codec.encoder(), body, accept_encoding, compression_override, max_message_size, ); http::Response::from_parts(parts, Body::new(body)) } fn request_encoding_if_supported( &self, request: &http::Request, ) -> Result, Status> { CompressionEncoding::from_encoding_header( request.headers(), self.accept_compression_encodings, ) } } impl fmt::Debug for Grpc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Grpc") .field("codec", &self.codec) .field( "accept_compression_encodings", &self.accept_compression_encodings, ) .field( "send_compression_encodings", &self.send_compression_encodings, ) .finish() } } fn compression_override_from_response( res: &Result, E>, ) -> SingleMessageCompressionOverride { res.as_ref() .ok() .and_then(|response| { response .extensions() .get::() .copied() }) .unwrap_or_default() } ================================================ FILE: tonic/src/server/mod.rs ================================================ //! Generic server implementation. //! //! This module contains the low level components to build a gRPC server. It //! provides a codec agnostic gRPC server handler. //! //! The items in this module are generally designed to be used by some codegen //! tool that will provide the user some custom way to implement the server that //! will implement the proper gRPC service. Thusly, they are a bit hard to use //! by hand. mod grpc; mod service; pub use self::grpc::Grpc; pub use self::service::{ ClientStreamingService, ServerStreamingService, StreamingService, UnaryService, }; /// A trait to provide a static reference to the service's /// name. This is used for routing service's within the router. pub trait NamedService { /// The `Service-Name` as described [here]. /// /// [here]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests const NAME: &'static str; } ================================================ FILE: tonic/src/server/service.rs ================================================ use crate::{Request, Response, Status, Streaming}; use std::future::Future; use tokio_stream::Stream; use tower_service::Service; /// A specialization of tower_service::Service. /// /// Existing tower_service::Service implementations with the correct form will /// automatically implement `UnaryService`. pub trait UnaryService { /// Protobuf response message type type Response; /// Response future type Future: Future, Status>>; /// Call the service fn call(&mut self, request: Request) -> Self::Future; } impl UnaryService for T where T: Service, Response = Response, Error = crate::Status>, { type Response = M2; type Future = T::Future; fn call(&mut self, request: Request) -> Self::Future { Service::call(self, request) } } /// A specialization of tower_service::Service. /// /// Existing tower_service::Service implementations with the correct form will /// automatically implement `ServerStreamingService`. pub trait ServerStreamingService { /// Protobuf response message type type Response; /// Stream of outbound response messages type ResponseStream: Stream>; /// Response future type Future: Future, Status>>; /// Call the service fn call(&mut self, request: Request) -> Self::Future; } impl ServerStreamingService for T where T: Service, Response = Response, Error = crate::Status>, S: Stream>, { type Response = M2; type ResponseStream = S; type Future = T::Future; fn call(&mut self, request: Request) -> Self::Future { Service::call(self, request) } } /// A specialization of tower_service::Service. /// /// Existing tower_service::Service implementations with the correct form will /// automatically implement `ClientStreamingService`. pub trait ClientStreamingService { /// Protobuf response message type type Response; /// Response future type Future: Future, Status>>; /// Call the service fn call(&mut self, request: Request>) -> Self::Future; } impl ClientStreamingService for T where T: Service>, Response = Response, Error = crate::Status>, { type Response = M2; type Future = T::Future; fn call(&mut self, request: Request>) -> Self::Future { Service::call(self, request) } } /// A specialization of tower_service::Service. /// /// Existing tower_service::Service implementations with the correct form will /// automatically implement `StreamingService`. pub trait StreamingService { /// Protobuf response message type type Response; /// Stream of outbound response messages type ResponseStream: Stream>; /// Response future type Future: Future, Status>>; /// Call the service fn call(&mut self, request: Request>) -> Self::Future; } impl StreamingService for T where T: Service>, Response = Response, Error = crate::Status>, S: Stream>, { type Response = M2; type ResponseStream = S; type Future = T::Future; fn call(&mut self, request: Request>) -> Self::Future { Service::call(self, request) } } ================================================ FILE: tonic/src/service/interceptor.rs ================================================ //! gRPC interceptors which are a kind of middleware. //! //! See [`Interceptor`] for more details. use crate::{Status, request::SanitizeHeaders}; use pin_project::pin_project; use std::{ fmt, future::Future, pin::Pin, task::{Context, Poll}, }; use tower_layer::Layer; use tower_service::Service; /// A gRPC interceptor. /// /// gRPC interceptors are similar to middleware but have less flexibility. An interceptor allows /// you to do two main things, one is to add/remove/check items in the `MetadataMap` of each /// request. Two, cancel a request with a `Status`. /// /// Any function that satisfies the bound `FnMut(Request<()>) -> Result, Status>` can be /// used as an `Interceptor`. /// /// An interceptor can be used on both the server and client side through the `tonic-build` crate's /// generated structs. /// /// See the [interceptor example][example] for more details. /// /// If you need more powerful middleware, [tower] is the recommended approach. You can find /// examples of how to use tower with tonic [here][tower-example]. /// /// Additionally, interceptors is not the recommended way to add logging to your service. For that /// a [tower] middleware is more appropriate since it can also act on the response. For example /// tower-http's [`Trace`](https://docs.rs/tower-http/latest/tower_http/trace/index.html) /// middleware supports gRPC out of the box. /// /// [tower]: https://crates.io/crates/tower /// [example]: https://github.com/hyperium/tonic/tree/master/examples/src/interceptor /// [tower-example]: https://github.com/hyperium/tonic/tree/master/examples/src/tower pub trait Interceptor { /// Intercept a request before it is sent, optionally cancelling it. fn call(&mut self, request: crate::Request<()>) -> Result, Status>; } impl Interceptor for F where F: FnMut(crate::Request<()>) -> Result, Status>, { fn call(&mut self, request: crate::Request<()>) -> Result, Status> { self(request) } } /// A gRPC interceptor that can be used as a [`Layer`], /// /// See [`Interceptor`] for more details. #[derive(Debug, Clone, Copy)] pub struct InterceptorLayer { interceptor: I, } impl InterceptorLayer { /// Create a new interceptor layer. /// /// See [`Interceptor`] for more details. pub fn new(interceptor: I) -> Self { Self { interceptor } } } impl Layer for InterceptorLayer where I: Clone, { type Service = InterceptedService; fn layer(&self, service: S) -> Self::Service { InterceptedService::new(service, self.interceptor.clone()) } } /// A service wrapped in an interceptor middleware. /// /// See [`Interceptor`] for more details. #[derive(Clone, Copy)] pub struct InterceptedService { inner: S, interceptor: I, } impl InterceptedService { /// Create a new `InterceptedService` that wraps `S` and intercepts each request with the /// function `F`. pub fn new(service: S, interceptor: I) -> Self { Self { inner: service, interceptor, } } } impl fmt::Debug for InterceptedService where S: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("InterceptedService") .field("inner", &self.inner) .field("f", &format_args!("{}", std::any::type_name::())) .finish() } } impl Service> for InterceptedService where S: Service, Response = http::Response>, I: Interceptor, { type Response = http::Response>; type Error = S::Error; type Future = ResponseFuture; #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, req: http::Request) -> Self::Future { // It is bad practice to modify the body (i.e. Message) of the request via an interceptor. // To avoid exposing the body of the request to the interceptor function, we first remove it // here, allow the interceptor to modify the metadata and extensions, and then recreate the // HTTP request with the body. Tonic requests do not preserve the URI, HTTP version, and // HTTP method of the HTTP request, so we extract them here and then add them back in below. let uri = req.uri().clone(); let method = req.method().clone(); let version = req.version(); let req = crate::Request::from_http(req); let (metadata, extensions, msg) = req.into_parts(); match self .interceptor .call(crate::Request::from_parts(metadata, extensions, ())) { Ok(req) => { let (metadata, extensions, _) = req.into_parts(); let req = crate::Request::from_parts(metadata, extensions, msg); let req = req.into_http(uri, method, version, SanitizeHeaders::No); ResponseFuture::future(self.inner.call(req)) } Err(status) => ResponseFuture::status(status), } } } // required to use `InterceptedService` with `Router` impl crate::server::NamedService for InterceptedService where S: crate::server::NamedService, { const NAME: &'static str = S::NAME; } /// Response future for [`InterceptedService`]. #[pin_project] #[derive(Debug)] pub struct ResponseFuture { #[pin] kind: Kind, } impl ResponseFuture { fn future(future: F) -> Self { Self { kind: Kind::Future(future), } } fn status(status: Status) -> Self { Self { kind: Kind::Status(Some(status)), } } } #[pin_project(project = KindProj)] #[derive(Debug)] enum Kind { Future(#[pin] F), Status(Option), } impl Future for ResponseFuture where F: Future, E>>, { type Output = Result>, E>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.project().kind.project() { KindProj::Future(future) => future.poll(cx).map_ok(|res| res.map(ResponseBody::wrap)), KindProj::Status(status) => { let (parts, ()) = status.take().unwrap().into_http::<()>().into_parts(); let response = http::Response::from_parts(parts, ResponseBody::::empty()); Poll::Ready(Ok(response)) } } } } /// Response body for [`InterceptedService`]. #[pin_project] #[derive(Debug)] pub struct ResponseBody { #[pin] kind: ResponseBodyKind, } #[pin_project(project = ResponseBodyKindProj)] #[derive(Debug)] enum ResponseBodyKind { Empty, Wrap(#[pin] B), } impl ResponseBody { fn new(kind: ResponseBodyKind) -> Self { Self { kind } } fn empty() -> Self { Self::new(ResponseBodyKind::Empty) } fn wrap(body: B) -> Self { Self::new(ResponseBodyKind::Wrap(body)) } } impl http_body::Body for ResponseBody { type Data = B::Data; type Error = B::Error; fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { match self.project().kind.project() { ResponseBodyKindProj::Empty => Poll::Ready(None), ResponseBodyKindProj::Wrap(body) => body.poll_frame(cx), } } fn size_hint(&self) -> http_body::SizeHint { match &self.kind { ResponseBodyKind::Empty => http_body::SizeHint::with_exact(0), ResponseBodyKind::Wrap(body) => body.size_hint(), } } fn is_end_stream(&self) -> bool { match &self.kind { ResponseBodyKind::Empty => true, ResponseBodyKind::Wrap(body) => body.is_end_stream(), } } } #[cfg(test)] mod tests { use super::*; use tower::ServiceExt; #[tokio::test] async fn doesnt_remove_headers_from_requests() { let svc = tower::service_fn(|request: http::Request<()>| async move { assert_eq!( request .headers() .get("user-agent") .expect("missing in leaf service"), "test-tonic" ); Ok::<_, Status>(http::Response::new(())) }); let svc = InterceptedService::new(svc, |request: crate::Request<()>| { assert_eq!( request .metadata() .get("user-agent") .expect("missing in interceptor"), "test-tonic" ); Ok(request) }); let request = http::Request::builder() .header("user-agent", "test-tonic") .body(()) .unwrap(); svc.oneshot(request).await.unwrap(); } #[tokio::test] async fn handles_intercepted_status_as_response() { let message = "Blocked by the interceptor"; let expected = Status::permission_denied(message).into_http::<()>(); let svc = tower::service_fn(|_: http::Request<()>| async { Ok::<_, Status>(http::Response::new(())) }); let svc = InterceptedService::new(svc, |_: crate::Request<()>| { Err(Status::permission_denied(message)) }); let request = http::Request::builder().body(()).unwrap(); let response = svc.oneshot(request).await.unwrap(); assert_eq!(expected.status(), response.status()); assert_eq!(expected.version(), response.version()); assert_eq!(expected.headers(), response.headers()); } #[tokio::test] async fn doesnt_change_http_method() { let svc = tower::service_fn(|request: http::Request<()>| async move { assert_eq!(request.method(), http::Method::OPTIONS); Ok::<_, hyper::Error>(hyper::Response::new(())) }); let svc = InterceptedService::new(svc, Ok); let request = http::Request::builder() .method(http::Method::OPTIONS) .body(()) .unwrap(); svc.oneshot(request).await.unwrap(); } } ================================================ FILE: tonic/src/service/layered.rs ================================================ use std::{ marker::PhantomData, task::{Context, Poll}, }; use tower_layer::Layer; use tower_service::Service; use crate::server::NamedService; /// A layered service to propagate [`NamedService`] implementation. #[derive(Debug, Clone)] pub struct Layered { inner: S, _ty: PhantomData, } impl NamedService for Layered { const NAME: &'static str = T::NAME; } impl Service for Layered where S: Service, { type Response = S::Response; type Error = S::Error; type Future = S::Future; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, req: Req) -> Self::Future { self.inner.call(req) } } /// Extension trait which adds utility methods to types which implement [`tower_layer::Layer`]. pub trait LayerExt: sealed::Sealed { /// Applies the layer to a service and wraps it in [`Layered`]. fn named_layer(&self, service: S) -> Layered where L: Layer; } impl LayerExt for L { fn named_layer(&self, service: S) -> Layered<::Service, S> where L: Layer, { Layered { inner: self.layer(service), _ty: PhantomData, } } } mod sealed { pub trait Sealed {} impl Sealed for T {} } #[cfg(test)] mod tests { use super::*; #[derive(Debug, Default)] struct TestService {} const TEST_SERVICE_NAME: &str = "test-service-name"; impl NamedService for TestService { const NAME: &'static str = TEST_SERVICE_NAME; } // Checks if the argument implements `NamedService` and returns the implemented `NAME`. fn get_name_of_named_service(_s: &S) -> &'static str { S::NAME } #[test] fn named_service_is_propagated_to_layered() { use std::time::Duration; use tower::{limit::ConcurrencyLimitLayer, timeout::TimeoutLayer}; let layered = TimeoutLayer::new(Duration::from_secs(5)).named_layer(TestService::default()); assert_eq!(get_name_of_named_service(&layered), TEST_SERVICE_NAME); let layered = ConcurrencyLimitLayer::new(3).named_layer(layered); assert_eq!(get_name_of_named_service(&layered), TEST_SERVICE_NAME); } } ================================================ FILE: tonic/src/service/mod.rs ================================================ //! Utilities for using Tower services with Tonic. pub mod interceptor; pub(crate) mod layered; #[cfg(feature = "router")] pub(crate) mod router; #[doc(inline)] pub use self::interceptor::{Interceptor, InterceptorLayer}; pub use self::layered::{LayerExt, Layered}; #[doc(inline)] #[cfg(feature = "router")] pub use self::router::{Routes, RoutesBuilder}; #[cfg(feature = "router")] pub use axum::{Router as AxumRouter, body::Body as AxumBody}; pub mod recover_error; pub use self::recover_error::{RecoverError, RecoverErrorLayer}; ================================================ FILE: tonic/src/service/recover_error.rs ================================================ //! Middleware which recovers from error. use std::{ fmt, future::Future, pin::Pin, task::{Context, Poll, ready}, }; use http::Response; use pin_project::pin_project; use tower_layer::Layer; use tower_service::Service; use crate::Status; /// Layer which applies the [`RecoverError`] middleware. #[derive(Debug, Default, Clone)] pub struct RecoverErrorLayer { _priv: (), } impl RecoverErrorLayer { /// Create a new `RecoverErrorLayer`. pub fn new() -> Self { Self { _priv: () } } } impl Layer for RecoverErrorLayer { type Service = RecoverError; fn layer(&self, inner: S) -> Self::Service { RecoverError::new(inner) } } /// Middleware that attempts to recover from service errors by turning them into a response built /// from the `Status`. #[derive(Debug, Clone)] pub struct RecoverError { inner: S, } impl RecoverError { /// Create a new `RecoverError` middleware. pub fn new(inner: S) -> Self { Self { inner } } } impl Service for RecoverError where S: Service>, S::Error: Into, { type Response = Response>; type Error = crate::BoxError; type Future = ResponseFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, req: Req) -> Self::Future { ResponseFuture { inner: self.inner.call(req), } } } /// Response future for [`RecoverError`]. #[pin_project] pub struct ResponseFuture { #[pin] inner: F, } impl fmt::Debug for ResponseFuture { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ResponseFuture").finish() } } impl Future for ResponseFuture where F: Future, E>>, E: Into, { type Output = Result>, crate::BoxError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match ready!(self.project().inner.poll(cx)) { Ok(response) => { let response = response.map(ResponseBody::full); Poll::Ready(Ok(response)) } Err(err) => match Status::try_from_error(err.into()) { Ok(status) => { let (parts, ()) = status.into_http::<()>().into_parts(); let res = Response::from_parts(parts, ResponseBody::empty()); Poll::Ready(Ok(res)) } Err(err) => Poll::Ready(Err(err)), }, } } } /// Response body for [`RecoverError`]. #[pin_project] pub struct ResponseBody { #[pin] inner: Option, } impl fmt::Debug for ResponseBody { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ResponseBody").finish() } } impl ResponseBody { fn full(inner: B) -> Self { Self { inner: Some(inner) } } const fn empty() -> Self { Self { inner: None } } } impl http_body::Body for ResponseBody where B: http_body::Body, { type Data = B::Data; type Error = B::Error; fn poll_frame( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { match self.project().inner.as_pin_mut() { Some(b) => b.poll_frame(cx), None => Poll::Ready(None), } } fn is_end_stream(&self) -> bool { match &self.inner { Some(b) => b.is_end_stream(), None => true, } } fn size_hint(&self) -> http_body::SizeHint { match &self.inner { Some(body) => body.size_hint(), None => http_body::SizeHint::with_exact(0), } } } ================================================ FILE: tonic/src/service/router.rs ================================================ use crate::{Status, body::Body, server::NamedService}; use http::{Request, Response}; use std::{ convert::Infallible, fmt, future::Future, pin::Pin, task::{Context, Poll}, }; use tower::{Service, ServiceExt}; /// A [`Service`] router. #[derive(Debug, Clone)] pub struct Routes { router: axum::Router, } #[derive(Debug, Default, Clone)] /// Allows adding new services to routes by passing a mutable reference to this builder. pub struct RoutesBuilder { routes: Option, } impl RoutesBuilder { /// Add a new service. pub fn add_service(&mut self, svc: S) -> &mut Self where S: Service, Error = Infallible> + NamedService + Clone + Send + Sync + 'static, S::Response: axum::response::IntoResponse, S::Future: Send + 'static, { let routes = self.routes.take().unwrap_or_default(); self.routes.replace(routes.add_service(svc)); self } /// Returns the routes with added services or empty [`Routes`] if no service was added pub fn routes(self) -> Routes { self.routes.unwrap_or_default() } } impl Default for Routes { fn default() -> Self { Self { router: axum::Router::new().fallback(unimplemented), } } } impl Routes { /// Create a new routes with `svc` already added to it. pub fn new(svc: S) -> Self where S: Service, Error = Infallible> + NamedService + Clone + Send + Sync + 'static, S::Response: axum::response::IntoResponse, S::Future: Send + 'static, { Self::default().add_service(svc) } /// Create a new empty builder. pub fn builder() -> RoutesBuilder { RoutesBuilder::default() } /// Add a new service. pub fn add_service(mut self, svc: S) -> Self where S: Service, Error = Infallible> + NamedService + Clone + Send + Sync + 'static, S::Response: axum::response::IntoResponse, S::Future: Send + 'static, { self.router = self.router.route_service( &format!("/{}/{{*rest}}", S::NAME), svc.map_request(|req: Request| req.map(Body::new)), ); self } /// This makes axum perform update some internals of the router that improves perf. /// /// See pub fn prepare(self) -> Self { Self { router: self.router.with_state(()), } } /// Convert this `Routes` into an [`axum::Router`]. pub fn into_axum_router(self) -> axum::Router { self.router } /// Get a mutable reference to the [`axum::Router`]. pub fn axum_router_mut(&mut self) -> &mut axum::Router { &mut self.router } } impl From for RoutesBuilder { fn from(routes: Routes) -> Self { Self { routes: Some(routes), } } } impl From for RoutesBuilder { fn from(router: axum::Router) -> Self { Self { routes: Some(router.into()), } } } impl From for Routes { fn from(router: axum::Router) -> Self { Self { router } } } async fn unimplemented() -> Response { let (parts, ()) = Status::unimplemented("").into_http::<()>().into_parts(); Response::from_parts(parts, Body::empty()) } impl Service> for Routes where B: http_body::Body + Send + 'static, B::Error: Into, { type Response = Response; type Error = Infallible; type Future = RoutesFuture; #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { Service::>::poll_ready(&mut self.router, cx) } fn call(&mut self, req: Request) -> Self::Future { RoutesFuture(self.router.call(req)) } } pub struct RoutesFuture(axum::routing::future::RouteFuture); impl fmt::Debug for RoutesFuture { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("RoutesFuture").finish() } } impl Future for RoutesFuture { type Output = Result, Infallible>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { Pin::new(&mut self.as_mut().0) .poll(cx) .map_ok(|res| res.map(Body::new)) } } ================================================ FILE: tonic/src/status.rs ================================================ use crate::metadata::GRPC_CONTENT_TYPE; use crate::metadata::MetadataMap; use base64::Engine as _; use bytes::Bytes; use http::{ HeaderName, header::{HeaderMap, HeaderValue}, }; use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; use std::{borrow::Cow, error::Error, fmt, sync::Arc}; use tracing::{debug, trace, warn}; const ENCODING_SET: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') .add(b'#') .add(b'%') .add(b'<') .add(b'>') .add(b'`') .add(b'?') .add(b'{') .add(b'}'); /// A gRPC status describing the result of an RPC call. /// /// Values can be created using the `new` function or one of the specialized /// associated functions. /// ```rust /// # use tonic::{Status, Code}; /// let status1 = Status::new(Code::InvalidArgument, "name is invalid"); /// let status2 = Status::invalid_argument("name is invalid"); /// /// assert_eq!(status1.code(), Code::InvalidArgument); /// assert_eq!(status1.code(), status2.code()); /// ``` #[derive(Clone)] pub struct Status(Box); /// Box the contents of Status to avoid large error variants #[derive(Clone)] struct StatusInner { /// The gRPC status code, found in the `grpc-status` header. code: Code, /// A relevant error message, found in the `grpc-message` header. message: String, /// Binary opaque details, found in the `grpc-status-details-bin` header. details: Bytes, /// Custom metadata, found in the user-defined headers. /// If the metadata contains any headers with names reserved either by the gRPC spec /// or by `Status` fields above, they will be ignored. metadata: MetadataMap, /// Optional underlying error. source: Option>, } impl StatusInner { fn into_status(self) -> Status { Status(Box::new(self)) } } /// gRPC status codes used by [`Status`]. /// /// These variants match the [gRPC status codes]. /// /// [gRPC status codes]: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Code { /// The operation completed successfully. Ok = 0, /// The operation was cancelled. Cancelled = 1, /// Unknown error. Unknown = 2, /// Client specified an invalid argument. InvalidArgument = 3, /// Deadline expired before operation could complete. DeadlineExceeded = 4, /// Some requested entity was not found. NotFound = 5, /// Some entity that we attempted to create already exists. AlreadyExists = 6, /// The caller does not have permission to execute the specified operation. PermissionDenied = 7, /// Some resource has been exhausted. ResourceExhausted = 8, /// The system is not in a state required for the operation's execution. FailedPrecondition = 9, /// The operation was aborted. Aborted = 10, /// Operation was attempted past the valid range. OutOfRange = 11, /// Operation is not implemented or not supported. Unimplemented = 12, /// Internal error. Internal = 13, /// The service is currently unavailable. Unavailable = 14, /// Unrecoverable data loss or corruption. DataLoss = 15, /// The request does not have valid authentication credentials Unauthenticated = 16, } impl Code { /// Get description of this `Code`. /// ``` /// fn make_grpc_request() -> tonic::Code { /// // ... /// tonic::Code::Ok /// } /// let code = make_grpc_request(); /// println!("Operation completed. Human readable description: {}", code.description()); /// ``` /// If you only need description in `println`, `format`, `log` and other /// formatting contexts, you may want to use `Display` impl for `Code` /// instead. pub fn description(&self) -> &'static str { match self { Code::Ok => "The operation completed successfully", Code::Cancelled => "The operation was cancelled", Code::Unknown => "Unknown error", Code::InvalidArgument => "Client specified an invalid argument", Code::DeadlineExceeded => "Deadline expired before operation could complete", Code::NotFound => "Some requested entity was not found", Code::AlreadyExists => "Some entity that we attempted to create already exists", Code::PermissionDenied => { "The caller does not have permission to execute the specified operation" } Code::ResourceExhausted => "Some resource has been exhausted", Code::FailedPrecondition => { "The system is not in a state required for the operation's execution" } Code::Aborted => "The operation was aborted", Code::OutOfRange => "Operation was attempted past the valid range", Code::Unimplemented => "Operation is not implemented or not supported", Code::Internal => "Internal error", Code::Unavailable => "The service is currently unavailable", Code::DataLoss => "Unrecoverable data loss or corruption", Code::Unauthenticated => "The request does not have valid authentication credentials", } } } impl std::fmt::Display for Code { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.description(), f) } } // ===== impl Status ===== impl Status { /// Create a new `Status` with the associated code and message. pub fn new(code: Code, message: impl Into) -> Status { StatusInner { code, message: message.into(), details: Bytes::new(), metadata: MetadataMap::new(), source: None, } .into_status() } /// The operation completed successfully. pub fn ok(message: impl Into) -> Status { Status::new(Code::Ok, message) } /// The operation was cancelled (typically by the caller). pub fn cancelled(message: impl Into) -> Status { Status::new(Code::Cancelled, message) } /// Unknown error. An example of where this error may be returned is if a /// `Status` value received from another address space belongs to an error-space /// that is not known in this address space. Also errors raised by APIs that /// do not return enough error information may be converted to this error. pub fn unknown(message: impl Into) -> Status { Status::new(Code::Unknown, message) } /// Client specified an invalid argument. Note that this differs from /// `FailedPrecondition`. `InvalidArgument` indicates arguments that are /// problematic regardless of the state of the system (e.g., a malformed file /// name). pub fn invalid_argument(message: impl Into) -> Status { Status::new(Code::InvalidArgument, message) } /// Deadline expired before operation could complete. For operations that /// change the state of the system, this error may be returned even if the /// operation has completed successfully. For example, a successful response /// from a server could have been delayed long enough for the deadline to /// expire. pub fn deadline_exceeded(message: impl Into) -> Status { Status::new(Code::DeadlineExceeded, message) } /// Some requested entity (e.g., file or directory) was not found. pub fn not_found(message: impl Into) -> Status { Status::new(Code::NotFound, message) } /// Some entity that we attempted to create (e.g., file or directory) already /// exists. pub fn already_exists(message: impl Into) -> Status { Status::new(Code::AlreadyExists, message) } /// The caller does not have permission to execute the specified operation. /// `PermissionDenied` must not be used for rejections caused by exhausting /// some resource (use `ResourceExhausted` instead for those errors). /// `PermissionDenied` must not be used if the caller cannot be identified /// (use `Unauthenticated` instead for those errors). pub fn permission_denied(message: impl Into) -> Status { Status::new(Code::PermissionDenied, message) } /// Some resource has been exhausted, perhaps a per-user quota, or perhaps /// the entire file system is out of space. pub fn resource_exhausted(message: impl Into) -> Status { Status::new(Code::ResourceExhausted, message) } /// Operation was rejected because the system is not in a state required for /// the operation's execution. For example, directory to be deleted may be /// non-empty, an rmdir operation is applied to a non-directory, etc. /// /// A litmus test that may help a service implementor in deciding between /// `FailedPrecondition`, `Aborted`, and `Unavailable`: /// (a) Use `Unavailable` if the client can retry just the failing call. /// (b) Use `Aborted` if the client should retry at a higher-level (e.g., /// restarting a read-modify-write sequence). /// (c) Use `FailedPrecondition` if the client should not retry until the /// system state has been explicitly fixed. E.g., if an "rmdir" fails /// because the directory is non-empty, `FailedPrecondition` should be /// returned since the client should not retry unless they have first /// fixed up the directory by deleting files from it. pub fn failed_precondition(message: impl Into) -> Status { Status::new(Code::FailedPrecondition, message) } /// The operation was aborted, typically due to a concurrency issue like /// sequencer check failures, transaction aborts, etc. /// /// See litmus test above for deciding between `FailedPrecondition`, /// `Aborted`, and `Unavailable`. pub fn aborted(message: impl Into) -> Status { Status::new(Code::Aborted, message) } /// Operation was attempted past the valid range. E.g., seeking or reading /// past end of file. /// /// Unlike `InvalidArgument`, this error indicates a problem that may be /// fixed if the system state changes. For example, a 32-bit file system will /// generate `InvalidArgument` if asked to read at an offset that is not in the /// range [0,2^32-1], but it will generate `OutOfRange` if asked to read from /// an offset past the current file size. /// /// There is a fair bit of overlap between `FailedPrecondition` and /// `OutOfRange`. We recommend using `OutOfRange` (the more specific error) /// when it applies so that callers who are iterating through a space can /// easily look for an `OutOfRange` error to detect when they are done. pub fn out_of_range(message: impl Into) -> Status { Status::new(Code::OutOfRange, message) } /// Operation is not implemented or not supported/enabled in this service. pub fn unimplemented(message: impl Into) -> Status { Status::new(Code::Unimplemented, message) } /// Internal errors. Means some invariants expected by underlying system has /// been broken. If you see one of these errors, something is very broken. pub fn internal(message: impl Into) -> Status { Status::new(Code::Internal, message) } /// The service is currently unavailable. This is a most likely a transient /// condition and may be corrected by retrying with a back-off. /// /// See litmus test above for deciding between `FailedPrecondition`, /// `Aborted`, and `Unavailable`. pub fn unavailable(message: impl Into) -> Status { Status::new(Code::Unavailable, message) } /// Unrecoverable data loss or corruption. pub fn data_loss(message: impl Into) -> Status { Status::new(Code::DataLoss, message) } /// The request does not have valid authentication credentials for the /// operation. pub fn unauthenticated(message: impl Into) -> Status { Status::new(Code::Unauthenticated, message) } pub(crate) fn from_error_generic( err: impl Into>, ) -> Status { Self::from_error(err.into()) } /// Create a `Status` from various types of `Error`. /// /// Inspects the error source chain for recognizable errors, including statuses, HTTP2, and /// hyper, and attempts to maps them to a `Status`, or else returns an Unknown `Status`. pub fn from_error(err: Box) -> Status { Status::try_from_error(err).unwrap_or_else(|err| { let mut status = Status::new(Code::Unknown, err.to_string()); status.0.source = Some(err.into()); status }) } /// Create a `Status` from various types of `Error`. /// /// Returns the error if a status could not be created. /// /// # Downcast stability /// This function does not provide any stability guarantees around how it will downcast errors into /// status codes. pub fn try_from_error( err: Box, ) -> Result> { let err = match err.downcast::() { Ok(status) => { return Ok(*status); } Err(err) => err, }; #[cfg(feature = "server")] let err = match err.downcast::() { Ok(h2) => { return Ok(Status::from_h2_error(h2)); } Err(err) => err, }; // If the load shed middleware is enabled, respond to // service overloaded with an appropriate grpc status. #[cfg(feature = "server")] let err = match err.downcast::() { Ok(_) => { return Ok(Status::resource_exhausted( "Too many active requests for the connection", )); } Err(err) => err, }; if let Some(mut status) = find_status_in_source_chain(&*err) { status.0.source = Some(err.into()); return Ok(status); } Err(err) } // FIXME: bubble this into `transport` and expose generic http2 reasons. #[cfg(feature = "server")] fn from_h2_error(err: Box) -> Status { let code = Self::code_from_h2(&err); let mut status = Self::new(code, format!("h2 protocol error: {err}")); status.0.source = Some(Arc::new(*err)); status } #[cfg(feature = "server")] fn code_from_h2(err: &h2::Error) -> Code { // See https://github.com/grpc/grpc/blob/3977c30/doc/PROTOCOL-HTTP2.md#errors match err.reason() { Some(h2::Reason::NO_ERROR) | Some(h2::Reason::PROTOCOL_ERROR) | Some(h2::Reason::INTERNAL_ERROR) | Some(h2::Reason::FLOW_CONTROL_ERROR) | Some(h2::Reason::SETTINGS_TIMEOUT) | Some(h2::Reason::COMPRESSION_ERROR) | Some(h2::Reason::CONNECT_ERROR) => Code::Internal, Some(h2::Reason::REFUSED_STREAM) => Code::Unavailable, Some(h2::Reason::CANCEL) => Code::Cancelled, Some(h2::Reason::ENHANCE_YOUR_CALM) => Code::ResourceExhausted, Some(h2::Reason::INADEQUATE_SECURITY) => Code::PermissionDenied, _ => Code::Unknown, } } #[cfg(feature = "server")] fn to_h2_error(&self) -> h2::Error { // conservatively transform to h2 error codes... let reason = match self.code() { Code::Cancelled => h2::Reason::CANCEL, _ => h2::Reason::INTERNAL_ERROR, }; reason.into() } /// Handles hyper errors specifically, which expose a number of different parameters about the /// http stream's error: https://docs.rs/hyper/0.14.11/hyper/struct.Error.html. /// /// Returns Some if there's a way to handle the error, or None if the information from this /// hyper error, but perhaps not its source, should be ignored. #[cfg(any(feature = "server", feature = "channel"))] fn from_hyper_error(err: &hyper::Error) -> Option { // is_timeout results from hyper's keep-alive logic // (https://docs.rs/hyper/0.14.11/src/hyper/error.rs.html#192-194). Per the grpc spec // > An expired client initiated PING will cause all calls to be closed with an UNAVAILABLE // > status. Note that the frequency of PINGs is highly dependent on the network // > environment, implementations are free to adjust PING frequency based on network and // > application requirements, which is why it's mapped to unavailable here. if err.is_timeout() { return Some(Status::unavailable(err.to_string())); } if err.is_canceled() { return Some(Status::cancelled(err.to_string())); } #[cfg(feature = "server")] if let Some(h2_err) = err.source().and_then(|e| e.downcast_ref::()) { let code = Status::code_from_h2(h2_err); let status = Self::new(code, format!("h2 protocol error: {err}")); return Some(status); } None } pub(crate) fn map_error(err: E) -> Status where E: Into>, { let err: Box = err.into(); Status::from_error(err) } /// Extract a `Status` from a hyper `HeaderMap`. pub fn from_header_map(header_map: &HeaderMap) -> Option { let code = Code::from_bytes(header_map.get(Self::GRPC_STATUS)?.as_ref()); let error_message = match header_map.get(Self::GRPC_MESSAGE) { Some(header) => percent_decode(header.as_bytes()) .decode_utf8() .map(|cow| cow.to_string()), None => Ok(String::new()), }; let details = match header_map.get(Self::GRPC_STATUS_DETAILS) { Some(header) => crate::util::base64::STANDARD .decode(header.as_bytes()) .expect("Invalid status header, expected base64 encoded value") .into(), None => Bytes::new(), }; let other_headers = { let mut header_map = header_map.clone(); header_map.remove(Self::GRPC_STATUS); header_map.remove(Self::GRPC_MESSAGE); header_map.remove(Self::GRPC_STATUS_DETAILS); header_map }; let (code, message) = match error_message { Ok(message) => (code, message), Err(e) => { let error_message = format!("Error deserializing status message header: {e}"); warn!(error_message); (Code::Unknown, error_message) } }; Some( StatusInner { code, message, details, metadata: MetadataMap::from_headers(other_headers), source: None, } .into_status(), ) } /// Get the gRPC `Code` of this `Status`. pub fn code(&self) -> Code { self.0.code } /// Get the text error message of this `Status`. pub fn message(&self) -> &str { &self.0.message } /// Get the opaque error details of this `Status`. pub fn details(&self) -> &[u8] { &self.0.details } /// Get a reference to the custom metadata. pub fn metadata(&self) -> &MetadataMap { &self.0.metadata } /// Get a mutable reference to the custom metadata. pub fn metadata_mut(&mut self) -> &mut MetadataMap { &mut self.0.metadata } pub(crate) fn to_header_map(&self) -> Result { let mut header_map = HeaderMap::with_capacity(3 + self.0.metadata.len()); self.add_header(&mut header_map)?; Ok(header_map) } /// Add headers from this `Status` into `header_map`. pub fn add_header(&self, header_map: &mut HeaderMap) -> Result<(), Self> { header_map.extend(self.0.metadata.clone().into_sanitized_headers()); header_map.insert(Self::GRPC_STATUS, self.0.code.to_header_value()); if !self.0.message.is_empty() { let to_write = Bytes::copy_from_slice( Cow::from(percent_encode(self.message().as_bytes(), ENCODING_SET)).as_bytes(), ); header_map.insert( Self::GRPC_MESSAGE, HeaderValue::from_maybe_shared(to_write).map_err(invalid_header_value_byte)?, ); } if !self.0.details.is_empty() { let details = crate::util::base64::STANDARD_NO_PAD.encode(&self.0.details[..]); header_map.insert( Self::GRPC_STATUS_DETAILS, HeaderValue::from_maybe_shared(details).map_err(invalid_header_value_byte)?, ); } Ok(()) } /// Create a new `Status` with the associated code, message, and binary details field. pub fn with_details(code: Code, message: impl Into, details: Bytes) -> Status { Self::with_details_and_metadata(code, message, details, MetadataMap::new()) } /// Create a new `Status` with the associated code, message, and custom metadata pub fn with_metadata(code: Code, message: impl Into, metadata: MetadataMap) -> Status { Self::with_details_and_metadata(code, message, Bytes::new(), metadata) } /// Create a new `Status` with the associated code, message, binary details field and custom metadata pub fn with_details_and_metadata( code: Code, message: impl Into, details: Bytes, metadata: MetadataMap, ) -> Status { StatusInner { code, message: message.into(), details, metadata, source: None, } .into_status() } /// Add a source error to this status. pub fn set_source(&mut self, source: Arc) -> &mut Status { self.0.source = Some(source); self } /// Build an `http::Response` from the given `Status`. pub fn into_http(self) -> http::Response { let mut response = http::Response::new(B::default()); response .headers_mut() .insert(http::header::CONTENT_TYPE, GRPC_CONTENT_TYPE); self.add_header(response.headers_mut()).unwrap(); response.extensions_mut().insert(self); response } #[doc(hidden)] pub const GRPC_STATUS: HeaderName = HeaderName::from_static("grpc-status"); #[doc(hidden)] pub const GRPC_MESSAGE: HeaderName = HeaderName::from_static("grpc-message"); #[doc(hidden)] pub const GRPC_STATUS_DETAILS: HeaderName = HeaderName::from_static("grpc-status-details-bin"); } fn find_status_in_source_chain(err: &(dyn Error + 'static)) -> Option { let mut source = Some(err); while let Some(err) = source { if let Some(status) = err.downcast_ref::() { return Some( StatusInner { code: status.0.code, message: status.0.message.clone(), details: status.0.details.clone(), metadata: status.0.metadata.clone(), // Since `Status` is not `Clone`, any `source` on the original Status // cannot be cloned so must remain with the original `Status`. source: None, } .into_status(), ); } if let Some(timeout) = err.downcast_ref::() { return Some(Status::cancelled(timeout.to_string())); } // If we are unable to connect to the server, map this to UNAVAILABLE. This is // consistent with the behavior of a C++ gRPC client when the server is not running, and // matches the spec of: // > The service is currently unavailable. This is most likely a transient condition that // > can be corrected if retried with a backoff. if let Some(connect) = err.downcast_ref::() { return Some(Status::unavailable(connect.to_string())); } #[cfg(any(feature = "server", feature = "channel"))] if let Some(hyper) = err .downcast_ref::() .and_then(Status::from_hyper_error) { return Some(hyper); } source = err.source(); } None } impl fmt::Debug for Status { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl fmt::Debug for StatusInner { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // A manual impl to reduce the noise of frequently empty fields. let mut builder = f.debug_struct("Status"); builder.field("code", &self.code); if !self.message.is_empty() { builder.field("message", &self.message); } if !self.details.is_empty() { builder.field("details", &self.details); } if !self.metadata.is_empty() { builder.field("metadata", &self.metadata); } builder.field("source", &self.source); builder.finish() } } fn invalid_header_value_byte(err: Error) -> Status { debug!("Invalid header: {}", err); Status::new( Code::Internal, "Couldn't serialize non-text grpc status header".to_string(), ) } #[cfg(feature = "server")] impl From for Status { fn from(err: h2::Error) -> Self { Status::from_h2_error(Box::new(err)) } } #[cfg(feature = "server")] impl From for h2::Error { fn from(status: Status) -> Self { status.to_h2_error() } } impl From for Status { fn from(err: std::io::Error) -> Self { use std::io::ErrorKind; let code = match err.kind() { ErrorKind::BrokenPipe | ErrorKind::WouldBlock | ErrorKind::WriteZero | ErrorKind::Interrupted => Code::Internal, ErrorKind::ConnectionRefused | ErrorKind::ConnectionReset | ErrorKind::NotConnected | ErrorKind::AddrInUse | ErrorKind::AddrNotAvailable => Code::Unavailable, ErrorKind::AlreadyExists => Code::AlreadyExists, ErrorKind::ConnectionAborted => Code::Aborted, ErrorKind::InvalidData => Code::DataLoss, ErrorKind::InvalidInput => Code::InvalidArgument, ErrorKind::NotFound => Code::NotFound, ErrorKind::PermissionDenied => Code::PermissionDenied, ErrorKind::TimedOut => Code::DeadlineExceeded, ErrorKind::UnexpectedEof => Code::OutOfRange, _ => Code::Unknown, }; Status::new(code, err.to_string()) } } impl fmt::Display for Status { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "code: '{}'", self.code())?; if !self.message().is_empty() { write!(f, ", message: {:?}", self.message())?; } // We intentionally omit `self.details` since it's binary data, not fit for human eyes. // Additionally, `self.metadata` contains low-level details that only belong in the `Debug` // impl. if let Some(source) = self.source() { write!(f, ", source: {source:?}")?; } Ok(()) } } impl Error for Status { fn source(&self) -> Option<&(dyn Error + 'static)> { self.0.source.as_ref().map(|err| (&**err) as _) } } /// /// Take the `Status` value from `trailers` if it is available, else from `status_code`. /// pub(crate) fn infer_grpc_status( trailers: Option<&HeaderMap>, status_code: http::StatusCode, ) -> Result<(), Option> { if let Some(trailers) = trailers { if let Some(status) = Status::from_header_map(trailers) { if status.code() == Code::Ok { return Ok(()); } else { return Err(status.into()); } } } trace!("trailers missing grpc-status"); let code = match status_code { // Borrowed from https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md http::StatusCode::BAD_REQUEST => Code::Internal, http::StatusCode::UNAUTHORIZED => Code::Unauthenticated, http::StatusCode::FORBIDDEN => Code::PermissionDenied, http::StatusCode::NOT_FOUND => Code::Unimplemented, http::StatusCode::TOO_MANY_REQUESTS | http::StatusCode::BAD_GATEWAY | http::StatusCode::SERVICE_UNAVAILABLE | http::StatusCode::GATEWAY_TIMEOUT => Code::Unavailable, // We got a 200 but no trailers, we can infer that this request is finished. // // This can happen when a streaming response sends two Status but // gRPC requires that we end the stream after the first status. // // https://github.com/hyperium/tonic/issues/681 http::StatusCode::OK => return Err(None), _ => Code::Unknown, }; let msg = format!( "grpc-status header missing, mapped from HTTP status code {}", status_code.as_u16(), ); let status = Status::new(code, msg); Err(status.into()) } // ===== impl Code ===== impl Code { /// Get the `Code` that represents the integer, if known. /// /// If not known, returns `Code::Unknown` (surprise!). pub const fn from_i32(i: i32) -> Code { match i { 0 => Code::Ok, 1 => Code::Cancelled, 2 => Code::Unknown, 3 => Code::InvalidArgument, 4 => Code::DeadlineExceeded, 5 => Code::NotFound, 6 => Code::AlreadyExists, 7 => Code::PermissionDenied, 8 => Code::ResourceExhausted, 9 => Code::FailedPrecondition, 10 => Code::Aborted, 11 => Code::OutOfRange, 12 => Code::Unimplemented, 13 => Code::Internal, 14 => Code::Unavailable, 15 => Code::DataLoss, 16 => Code::Unauthenticated, _ => Code::Unknown, } } /// Convert the string representation of a `Code` (as stored, for example, in the `grpc-status` /// header in a response) into a `Code`. Returns `Code::Unknown` if the code string is not a /// valid gRPC status code. pub fn from_bytes(bytes: &[u8]) -> Code { match bytes.len() { 1 => match bytes[0] { b'0' => Code::Ok, b'1' => Code::Cancelled, b'2' => Code::Unknown, b'3' => Code::InvalidArgument, b'4' => Code::DeadlineExceeded, b'5' => Code::NotFound, b'6' => Code::AlreadyExists, b'7' => Code::PermissionDenied, b'8' => Code::ResourceExhausted, b'9' => Code::FailedPrecondition, _ => Code::parse_err(), }, 2 => match (bytes[0], bytes[1]) { (b'1', b'0') => Code::Aborted, (b'1', b'1') => Code::OutOfRange, (b'1', b'2') => Code::Unimplemented, (b'1', b'3') => Code::Internal, (b'1', b'4') => Code::Unavailable, (b'1', b'5') => Code::DataLoss, (b'1', b'6') => Code::Unauthenticated, _ => Code::parse_err(), }, _ => Code::parse_err(), } } fn to_header_value(self) -> HeaderValue { match self { Code::Ok => HeaderValue::from_static("0"), Code::Cancelled => HeaderValue::from_static("1"), Code::Unknown => HeaderValue::from_static("2"), Code::InvalidArgument => HeaderValue::from_static("3"), Code::DeadlineExceeded => HeaderValue::from_static("4"), Code::NotFound => HeaderValue::from_static("5"), Code::AlreadyExists => HeaderValue::from_static("6"), Code::PermissionDenied => HeaderValue::from_static("7"), Code::ResourceExhausted => HeaderValue::from_static("8"), Code::FailedPrecondition => HeaderValue::from_static("9"), Code::Aborted => HeaderValue::from_static("10"), Code::OutOfRange => HeaderValue::from_static("11"), Code::Unimplemented => HeaderValue::from_static("12"), Code::Internal => HeaderValue::from_static("13"), Code::Unavailable => HeaderValue::from_static("14"), Code::DataLoss => HeaderValue::from_static("15"), Code::Unauthenticated => HeaderValue::from_static("16"), } } fn parse_err() -> Code { trace!("error parsing grpc-status"); Code::Unknown } } impl From for Code { fn from(i: i32) -> Self { Code::from_i32(i) } } impl From for i32 { #[inline] fn from(code: Code) -> i32 { code as i32 } } #[cfg(test)] mod tests { use super::*; use crate::BoxError; #[derive(Debug)] struct Nested(BoxError); impl fmt::Display for Nested { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "nested error: {}", self.0) } } impl std::error::Error for Nested { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&*self.0) } } #[test] fn from_error_status() { let orig = Status::new(Code::OutOfRange, "weeaboo"); let found = Status::from_error(Box::new(orig)); assert_eq!(found.code(), Code::OutOfRange); assert_eq!(found.message(), "weeaboo"); } #[test] fn from_error_unknown() { let orig: BoxError = "peek-a-boo".into(); let found = Status::from_error(orig); assert_eq!(found.code(), Code::Unknown); assert_eq!(found.message(), "peek-a-boo".to_string()); } #[test] fn from_error_nested() { let orig = Nested(Box::new(Status::new(Code::OutOfRange, "weeaboo"))); let found = Status::from_error(Box::new(orig)); assert_eq!(found.code(), Code::OutOfRange); assert_eq!(found.message(), "weeaboo"); } #[test] #[cfg(feature = "server")] fn from_error_h2() { use std::error::Error as _; let orig = h2::Error::from(h2::Reason::CANCEL); let found = Status::from_error(Box::new(orig)); assert_eq!(found.code(), Code::Cancelled); let source = found .source() .and_then(|err| err.downcast_ref::()) .unwrap(); assert_eq!(source.reason(), Some(h2::Reason::CANCEL)); } #[test] #[cfg(feature = "server")] fn to_h2_error() { let orig = Status::new(Code::Cancelled, "stop eet!"); let err = orig.to_h2_error(); assert_eq!(err.reason(), Some(h2::Reason::CANCEL)); } #[test] fn code_from_i32() { // This for loop should catch if we ever add a new variant and don't // update From. for i in 0..(Code::Unauthenticated as i32) { let code = Code::from(i); assert_eq!( i, code as i32, "Code::from({}) returned {:?} which is {}", i, code, code as i32, ); } assert_eq!(Code::from(-1), Code::Unknown); } #[test] fn constructors() { assert_eq!(Status::ok("").code(), Code::Ok); assert_eq!(Status::cancelled("").code(), Code::Cancelled); assert_eq!(Status::unknown("").code(), Code::Unknown); assert_eq!(Status::invalid_argument("").code(), Code::InvalidArgument); assert_eq!(Status::deadline_exceeded("").code(), Code::DeadlineExceeded); assert_eq!(Status::not_found("").code(), Code::NotFound); assert_eq!(Status::already_exists("").code(), Code::AlreadyExists); assert_eq!(Status::permission_denied("").code(), Code::PermissionDenied); assert_eq!( Status::resource_exhausted("").code(), Code::ResourceExhausted ); assert_eq!( Status::failed_precondition("").code(), Code::FailedPrecondition ); assert_eq!(Status::aborted("").code(), Code::Aborted); assert_eq!(Status::out_of_range("").code(), Code::OutOfRange); assert_eq!(Status::unimplemented("").code(), Code::Unimplemented); assert_eq!(Status::internal("").code(), Code::Internal); assert_eq!(Status::unavailable("").code(), Code::Unavailable); assert_eq!(Status::data_loss("").code(), Code::DataLoss); assert_eq!(Status::unauthenticated("").code(), Code::Unauthenticated); } #[test] fn details() { const DETAILS: &[u8] = &[0, 2, 3]; let status = Status::with_details(Code::Unavailable, "some message", DETAILS.into()); assert_eq!(status.details(), DETAILS); let header_map = status.to_header_map().unwrap(); let b64_details = crate::util::base64::STANDARD_NO_PAD.encode(DETAILS); assert_eq!(header_map[Status::GRPC_STATUS_DETAILS], b64_details); let status = Status::from_header_map(&header_map).unwrap(); assert_eq!(status.details(), DETAILS); } } /// Error returned if a request didn't complete within the configured timeout. /// /// Timeouts can be configured either with [`Endpoint::timeout`], [`Server::timeout`], or by /// setting the [`grpc-timeout` metadata value][spec]. /// /// [`Endpoint::timeout`]: crate::transport::channel::Endpoint::timeout /// [`Server::timeout`]: crate::transport::server::Server::timeout /// [spec]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md #[derive(Debug)] pub struct TimeoutExpired(pub ()); impl fmt::Display for TimeoutExpired { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Timeout expired") } } // std::error::Error only requires a type to impl Debug and Display impl std::error::Error for TimeoutExpired {} /// Wrapper type to indicate that an error occurs during the connection /// process, so that the appropriate gRPC Status can be inferred. #[derive(Debug)] pub struct ConnectError(pub Box); impl fmt::Display for ConnectError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } impl std::error::Error for ConnectError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(self.0.as_ref()) } } ================================================ FILE: tonic/src/transport/channel/endpoint.rs ================================================ use super::Channel; #[cfg(feature = "_tls-any")] use super::ClientTlsConfig; #[cfg(feature = "_tls-any")] use super::service::TlsConnector; use super::service::{self, Executor, SharedExec}; use super::uds_connector::UdsConnector; use crate::transport::Error; #[cfg(feature = "_tls-any")] use crate::transport::error; use bytes::Bytes; use http::{HeaderValue, uri::Uri}; use hyper::rt; use hyper_util::client::legacy::connect::HttpConnector; use std::{fmt, future::Future, net::IpAddr, pin::Pin, str, str::FromStr, time::Duration}; use tower_service::Service; #[derive(Clone, PartialEq, Eq, Hash)] pub(crate) enum EndpointType { Uri(Uri), Uds(String), } /// Channel builder. /// /// This struct is used to build and configure HTTP/2 channels. #[derive(Clone)] pub struct Endpoint { pub(crate) uri: EndpointType, fallback_uri: Uri, pub(crate) origin: Option, pub(crate) user_agent: Option, pub(crate) timeout: Option, pub(crate) concurrency_limit: Option, pub(crate) rate_limit: Option<(u64, Duration)>, #[cfg(feature = "_tls-any")] pub(crate) tls: Option, pub(crate) buffer_size: Option, pub(crate) init_stream_window_size: Option, pub(crate) init_connection_window_size: Option, pub(crate) tcp_keepalive: Option, pub(crate) tcp_keepalive_interval: Option, pub(crate) tcp_keepalive_retries: Option, pub(crate) tcp_nodelay: bool, pub(crate) http2_keep_alive_interval: Option, pub(crate) http2_keep_alive_timeout: Option, pub(crate) http2_keep_alive_while_idle: Option, pub(crate) http2_max_header_list_size: Option, pub(crate) connect_timeout: Option, pub(crate) http2_adaptive_window: Option, pub(crate) local_address: Option, pub(crate) executor: SharedExec, } impl Endpoint { // FIXME: determine if we want to expose this or not. This is really // just used in codegen for a shortcut. #[doc(hidden)] pub fn new(dst: D) -> Result where D: TryInto, D::Error: Into, { let me = dst.try_into().map_err(|e| Error::from_source(e.into()))?; #[cfg(feature = "_tls-any")] if let EndpointType::Uri(uri) = &me.uri { if me.tls.is_none() && uri.scheme() == Some(&http::uri::Scheme::HTTPS) { return me.tls_config(ClientTlsConfig::new().with_enabled_roots()); } } Ok(me) } fn new_uri(uri: Uri) -> Self { Self { uri: EndpointType::Uri(uri.clone()), fallback_uri: uri, origin: None, user_agent: None, concurrency_limit: None, rate_limit: None, timeout: None, #[cfg(feature = "_tls-any")] tls: None, buffer_size: None, init_stream_window_size: None, init_connection_window_size: None, tcp_keepalive: None, tcp_keepalive_interval: None, tcp_keepalive_retries: None, tcp_nodelay: true, http2_keep_alive_interval: None, http2_keep_alive_timeout: None, http2_keep_alive_while_idle: None, http2_max_header_list_size: None, connect_timeout: None, http2_adaptive_window: None, executor: SharedExec::tokio(), local_address: None, } } fn new_uds(uds_filepath: &str) -> Self { Self { uri: EndpointType::Uds(uds_filepath.to_string()), fallback_uri: Uri::from_static("http://tonic"), origin: None, user_agent: None, concurrency_limit: None, rate_limit: None, timeout: None, #[cfg(feature = "_tls-any")] tls: None, buffer_size: None, init_stream_window_size: None, init_connection_window_size: None, tcp_keepalive: None, tcp_keepalive_interval: None, tcp_keepalive_retries: None, tcp_nodelay: true, http2_keep_alive_interval: None, http2_keep_alive_timeout: None, http2_keep_alive_while_idle: None, http2_max_header_list_size: None, connect_timeout: None, http2_adaptive_window: None, executor: SharedExec::tokio(), local_address: None, } } /// Convert an `Endpoint` from a static string. /// /// # Panics /// /// This function panics if the argument is an invalid URI. /// /// ``` /// # use tonic::transport::Endpoint; /// Endpoint::from_static("https://example.com"); /// ``` pub fn from_static(s: &'static str) -> Self { if s.starts_with("unix:") { let uds_filepath = s .strip_prefix("unix://") .or_else(|| s.strip_prefix("unix:")) .expect("Invalid unix domain socket URI"); Self::new_uds(uds_filepath) } else { let uri = Uri::from_static(s); Self::new_uri(uri) } } /// Convert an `Endpoint` from shared bytes. /// /// ``` /// # use tonic::transport::Endpoint; /// Endpoint::from_shared("https://example.com".to_string()); /// ``` pub fn from_shared(s: impl Into) -> Result { let s = str::from_utf8(&s.into()) .map_err(|e| Error::new_invalid_uri().with(e))? .to_string(); if s.starts_with("unix:") { let uds_filepath = s .strip_prefix("unix://") .or_else(|| s.strip_prefix("unix:")) .ok_or(Error::new_invalid_uri())?; Ok(Self::new_uds(uds_filepath)) } else { let uri = Uri::from_maybe_shared(s).map_err(|e| Error::new_invalid_uri().with(e))?; Ok(Self::from(uri)) } } /// Set a custom user-agent header. /// /// `user_agent` will be prepended to Tonic's default user-agent string (`tonic/x.x.x`). /// It must be a value that can be converted into a valid `http::HeaderValue` or building /// the endpoint will fail. /// ``` /// # use tonic::transport::Endpoint; /// # let mut builder = Endpoint::from_static("https://example.com"); /// builder.user_agent("Greeter").expect("Greeter should be a valid header value"); /// // user-agent: "Greeter tonic/x.x.x" /// ``` pub fn user_agent(self, user_agent: T) -> Result where T: TryInto, { user_agent .try_into() .map(|ua| Endpoint { user_agent: Some(ua), ..self }) .map_err(|_| Error::new_invalid_user_agent()) } /// Set a custom origin. /// /// Override the `origin`, mainly useful when you are reaching a Server/LoadBalancer /// which serves multiple services at the same time. /// It will play the role of SNI (Server Name Indication). /// /// ``` /// # use tonic::transport::Endpoint; /// # let mut builder = Endpoint::from_static("https://proxy.com"); /// builder.origin("https://example.com".parse().expect("http://example.com must be a valid URI")); /// // origin: "https://example.com" /// ``` pub fn origin(self, origin: Uri) -> Self { Endpoint { origin: Some(origin), ..self } } /// Apply a timeout to each request. /// /// ``` /// # use tonic::transport::Endpoint; /// # use std::time::Duration; /// # let mut builder = Endpoint::from_static("https://example.com"); /// builder.timeout(Duration::from_secs(5)); /// ``` /// /// # Notes /// /// This does **not** set the timeout metadata (`grpc-timeout` header) on /// the request, meaning the server will not be informed of this timeout, /// for that use [`Request::set_timeout`]. /// /// [`Request::set_timeout`]: crate::Request::set_timeout pub fn timeout(self, dur: Duration) -> Self { Endpoint { timeout: Some(dur), ..self } } /// Apply a timeout to connecting to the uri. /// /// Defaults to no timeout. /// /// ``` /// # use tonic::transport::Endpoint; /// # use std::time::Duration; /// # let mut builder = Endpoint::from_static("https://example.com"); /// builder.connect_timeout(Duration::from_secs(5)); /// ``` pub fn connect_timeout(self, dur: Duration) -> Self { Endpoint { connect_timeout: Some(dur), ..self } } /// Set whether TCP keepalive messages are enabled on accepted connections. /// /// If `None` is specified, keepalive is disabled, otherwise the duration /// specified will be the time to remain idle before sending TCP keepalive /// probes. /// /// Default is no keepalive (`None`) pub fn tcp_keepalive(self, tcp_keepalive: Option) -> Self { Endpoint { tcp_keepalive, ..self } } /// Set the duration between two successive TCP keepalive retransmissions, /// if acknowledgement to the previous keepalive transmission is not received. /// /// This is only used if `tcp_keepalive` is not None. /// /// Defaults to None, which is the system default. pub fn tcp_keepalive_interval(self, tcp_keepalive_interval: Option) -> Self { Endpoint { tcp_keepalive_interval, ..self } } /// Set the number of retransmissions to be carried out before declaring that remote end is not available. /// /// This is only used if `tcp_keepalive` is not None. /// /// Defaults to None, which is the system default. pub fn tcp_keepalive_retries(self, tcp_keepalive_retries: Option) -> Self { Endpoint { tcp_keepalive_retries, ..self } } /// Apply a concurrency limit to each request. /// /// ``` /// # use tonic::transport::Endpoint; /// # let mut builder = Endpoint::from_static("https://example.com"); /// builder.concurrency_limit(256); /// ``` pub fn concurrency_limit(self, limit: usize) -> Self { Endpoint { concurrency_limit: Some(limit), ..self } } /// Apply a rate limit to each request. /// /// ``` /// # use tonic::transport::Endpoint; /// # use std::time::Duration; /// # let mut builder = Endpoint::from_static("https://example.com"); /// builder.rate_limit(32, Duration::from_secs(1)); /// ``` pub fn rate_limit(self, limit: u64, duration: Duration) -> Self { Endpoint { rate_limit: Some((limit, duration)), ..self } } /// Sets the [`SETTINGS_INITIAL_WINDOW_SIZE`][spec] option for HTTP2 /// stream-level flow control. /// /// Default is 65,535 /// /// [spec]: https://httpwg.org/specs/rfc9113.html#InitialWindowSize pub fn initial_stream_window_size(self, sz: impl Into>) -> Self { Endpoint { init_stream_window_size: sz.into(), ..self } } /// Sets the max connection-level flow control for HTTP2 /// /// Default is 65,535 pub fn initial_connection_window_size(self, sz: impl Into>) -> Self { Endpoint { init_connection_window_size: sz.into(), ..self } } /// Sets the tower service default internal buffer size /// /// Default is 1024 pub fn buffer_size(self, sz: impl Into>) -> Self { Endpoint { buffer_size: sz.into(), ..self } } /// Configures TLS for the endpoint. #[cfg(feature = "_tls-any")] pub fn tls_config(self, tls_config: ClientTlsConfig) -> Result { match &self.uri { EndpointType::Uri(uri) => Ok(Endpoint { tls: Some( tls_config .into_tls_connector(uri) .map_err(Error::from_source)?, ), ..self }), EndpointType::Uds(_) => Err(Error::new(error::Kind::InvalidTlsConfigForUds)), } } /// Set the value of `TCP_NODELAY` option for accepted connections. Enabled by default. pub fn tcp_nodelay(self, enabled: bool) -> Self { Endpoint { tcp_nodelay: enabled, ..self } } /// Set http2 KEEP_ALIVE_INTERVAL. Uses `hyper`'s default otherwise. pub fn http2_keep_alive_interval(self, interval: Duration) -> Self { Endpoint { http2_keep_alive_interval: Some(interval), ..self } } /// Set http2 KEEP_ALIVE_TIMEOUT. Uses `hyper`'s default otherwise. pub fn keep_alive_timeout(self, duration: Duration) -> Self { Endpoint { http2_keep_alive_timeout: Some(duration), ..self } } /// Set http2 KEEP_ALIVE_WHILE_IDLE. Uses `hyper`'s default otherwise. pub fn keep_alive_while_idle(self, enabled: bool) -> Self { Endpoint { http2_keep_alive_while_idle: Some(enabled), ..self } } /// Sets whether to use an adaptive flow control. Uses `hyper`'s default otherwise. pub fn http2_adaptive_window(self, enabled: bool) -> Self { Endpoint { http2_adaptive_window: Some(enabled), ..self } } /// Sets the max size of received header frames. /// /// This will default to whatever the default in hyper is. As of v1.4.1, it is 16 KiB. pub fn http2_max_header_list_size(self, size: u32) -> Self { Endpoint { http2_max_header_list_size: Some(size), ..self } } /// Sets the executor used to spawn async tasks. /// /// Uses `tokio::spawn` by default. pub fn executor(mut self, executor: E) -> Self where E: Executor + Send>>> + Send + Sync + 'static, { self.executor = SharedExec::new(executor); self } pub(crate) fn connector(&self, c: C) -> service::Connector { service::Connector::new( c, #[cfg(feature = "_tls-any")] self.tls.clone(), ) } /// Set the local address. /// /// This sets the IP address the client will use. By default we let hyper select the IP address. pub fn local_address(self, addr: Option) -> Self { Endpoint { local_address: addr, ..self } } pub(crate) fn http_connector(&self) -> service::Connector { let mut http = HttpConnector::new(); http.enforce_http(false); http.set_nodelay(self.tcp_nodelay); http.set_keepalive(self.tcp_keepalive); http.set_keepalive_interval(self.tcp_keepalive_interval); http.set_keepalive_retries(self.tcp_keepalive_retries); http.set_connect_timeout(self.connect_timeout); http.set_local_address(self.local_address); self.connector(http) } pub(crate) fn uds_connector(&self, uds_filepath: &str) -> service::Connector { self.connector(UdsConnector::new(uds_filepath)) } /// Create a channel from this config. pub async fn connect(&self) -> Result { match &self.uri { EndpointType::Uri(_) => Channel::connect(self.http_connector(), self.clone()).await, EndpointType::Uds(uds_filepath) => { Channel::connect(self.uds_connector(uds_filepath.as_str()), self.clone()).await } } } /// Create a channel from this config. /// /// The channel returned by this method does not attempt to connect to the endpoint until first /// use. pub fn connect_lazy(&self) -> Channel { match &self.uri { EndpointType::Uri(_) => Channel::new(self.http_connector(), self.clone()), EndpointType::Uds(uds_filepath) => { Channel::new(self.uds_connector(uds_filepath.as_str()), self.clone()) } } } /// Connect with a custom connector. /// /// This allows you to build a [Channel](struct.Channel.html) that uses a non-HTTP transport. /// See the `uds` example for an example on how to use this function to build channel that /// uses a Unix socket transport. /// /// The [`connect_timeout`](Endpoint::connect_timeout) will still be applied. pub async fn connect_with_connector(&self, connector: C) -> Result where C: Service + Send + 'static, C::Response: rt::Read + rt::Write + Send + Unpin, C::Future: Send, crate::BoxError: From + Send, { let connector = self.connector(connector); if let Some(connect_timeout) = self.connect_timeout { let mut connector = hyper_timeout::TimeoutConnector::new(connector); connector.set_connect_timeout(Some(connect_timeout)); Channel::connect(connector, self.clone()).await } else { Channel::connect(connector, self.clone()).await } } /// Connect with a custom connector lazily. /// /// This allows you to build a [Channel](struct.Channel.html) that uses a non-HTTP transport /// connect to it lazily. /// /// See the `uds` example for an example on how to use this function to build channel that /// uses a Unix socket transport. pub fn connect_with_connector_lazy(&self, connector: C) -> Channel where C: Service + Send + 'static, C::Response: rt::Read + rt::Write + Send + Unpin, C::Future: Send, crate::BoxError: From + Send, { let connector = self.connector(connector); if let Some(connect_timeout) = self.connect_timeout { let mut connector = hyper_timeout::TimeoutConnector::new(connector); connector.set_connect_timeout(Some(connect_timeout)); Channel::new(connector, self.clone()) } else { Channel::new(connector, self.clone()) } } /// Get the endpoint uri. /// /// ``` /// # use tonic::transport::Endpoint; /// # use http::Uri; /// let endpoint = Endpoint::from_static("https://example.com"); /// /// assert_eq!(endpoint.uri(), &Uri::from_static("https://example.com")); /// ``` pub fn uri(&self) -> &Uri { match &self.uri { EndpointType::Uri(uri) => uri, EndpointType::Uds(_) => &self.fallback_uri, } } /// Get the value of `TCP_NODELAY` option for accepted connections. pub fn get_tcp_nodelay(&self) -> bool { self.tcp_nodelay } /// Get the connect timeout. pub fn get_connect_timeout(&self) -> Option { self.connect_timeout } /// Get whether TCP keepalive messages are enabled on accepted connections. /// /// If `None` is specified, keepalive is disabled, otherwise the duration /// specified will be the time to remain idle before sending TCP keepalive /// probes. pub fn get_tcp_keepalive(&self) -> Option { self.tcp_keepalive } /// Get whether TCP keepalive interval. pub fn get_tcp_keepalive_interval(&self) -> Option { self.tcp_keepalive_interval } /// Get whether TCP keepalive retries. pub fn get_tcp_keepalive_retries(&self) -> Option { self.tcp_keepalive_retries } } impl From for Endpoint { fn from(uri: Uri) -> Self { Self::new_uri(uri) } } impl TryFrom for Endpoint { type Error = Error; fn try_from(t: Bytes) -> Result { Self::from_shared(t) } } impl TryFrom for Endpoint { type Error = Error; fn try_from(t: String) -> Result { Self::from_shared(t.into_bytes()) } } impl TryFrom<&'static str> for Endpoint { type Error = Error; fn try_from(t: &'static str) -> Result { Self::from_shared(t.as_bytes()) } } impl fmt::Debug for Endpoint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Endpoint").finish() } } impl FromStr for Endpoint { type Err = Error; fn from_str(s: &str) -> Result { Self::try_from(s.to_string()) } } ================================================ FILE: tonic/src/transport/channel/mod.rs ================================================ //! Client implementation and builder. mod endpoint; pub(crate) mod service; #[cfg(feature = "_tls-any")] mod tls; mod uds_connector; pub use self::service::Change; pub use endpoint::Endpoint; #[cfg(feature = "_tls-any")] pub use tls::ClientTlsConfig; use self::service::{Connection, DynamicServiceStream, Executor, SharedExec}; use crate::body::Body; use bytes::Bytes; use http::{ Request, Response, uri::{InvalidUri, Uri}, }; use std::{ fmt, future::Future, hash::Hash, pin::Pin, task::{Context, Poll}, }; use tokio::sync::mpsc::{Sender, channel}; use hyper::rt; use tower::balance::p2c::Balance; use tower::{ Service, buffer::{Buffer, future::ResponseFuture as BufferResponseFuture}, discover::Discover, util::BoxService, }; type BoxFuture<'a, T> = Pin + Send + 'a>>; const DEFAULT_BUFFER_SIZE: usize = 1024; /// A default batteries included `transport` channel. /// /// This provides a fully featured http2 gRPC client based on `hyper` /// and `tower` services. /// /// # Multiplexing requests /// /// Sending a request on a channel requires a `&mut self` and thus can only send /// one request in flight. This is intentional and is required to follow the `Service` /// contract from the `tower` library which this channel implementation is built on /// top of. /// /// `tower` itself has a concept of `poll_ready` which is the main mechanism to apply /// back pressure. `poll_ready` takes a `&mut self` and when it returns `Poll::Ready` /// we know the `Service` is able to accept only one request before we must `poll_ready` /// again. Due to this fact any `async fn` that wants to poll for readiness and submit /// the request must have a `&mut self` reference. /// /// To work around this and to ease the use of the channel, `Channel` provides a /// `Clone` implementation that is _cheap_. This is because at the very top level /// the channel is backed by a `tower_buffer::Buffer` which runs the connection /// in a background task and provides a `mpsc` channel interface. Due to this /// cloning the `Channel` type is cheap and encouraged. #[derive(Clone)] pub struct Channel { svc: Buffer, BoxFuture<'static, Result, crate::BoxError>>>, } /// A future that resolves to an HTTP response. /// /// This is returned by the `Service::call` on [`Channel`]. pub struct ResponseFuture { inner: BufferResponseFuture, crate::BoxError>>>, } impl Channel { /// Create an [`Endpoint`] builder that can create [`Channel`]s. pub fn builder(uri: Uri) -> Endpoint { Endpoint::from(uri) } /// Create an [`Endpoint`] from a static string. /// /// ``` /// # use tonic::transport::Channel; /// Channel::from_static("https://example.com"); /// ``` pub fn from_static(s: &'static str) -> Endpoint { let uri = Uri::from_static(s); Self::builder(uri) } /// Create an [`Endpoint`] from shared bytes. /// /// ``` /// # use tonic::transport::Channel; /// Channel::from_shared("https://example.com"); /// ``` pub fn from_shared(s: impl Into) -> Result { let uri = Uri::from_maybe_shared(s.into())?; Ok(Self::builder(uri)) } /// Balance a list of [`Endpoint`]'s. /// /// This creates a [`Channel`] that will load balance across all the /// provided endpoints. pub fn balance_list(list: impl Iterator) -> Self { let (channel, tx) = Self::balance_channel(DEFAULT_BUFFER_SIZE); list.for_each(|endpoint| { tx.try_send(Change::Insert(endpoint.uri.clone(), endpoint)) .unwrap(); }); channel } /// Balance a list of [`Endpoint`]'s. /// /// This creates a [`Channel`] that will listen to a stream of change events and will add or remove provided endpoints. pub fn balance_channel(capacity: usize) -> (Self, Sender>) where K: Hash + Eq + Send + Clone + 'static, { Self::balance_channel_with_executor(capacity, SharedExec::tokio()) } /// Balance a list of [`Endpoint`]'s. /// /// This creates a [`Channel`] that will listen to a stream of change events and will add or remove provided endpoints. /// /// The [`Channel`] will use the given executor to spawn async tasks. pub fn balance_channel_with_executor( capacity: usize, executor: E, ) -> (Self, Sender>) where K: Hash + Eq + Send + Clone + 'static, E: Executor + Send>>> + Send + Sync + 'static, { let (tx, rx) = channel(capacity); let list = DynamicServiceStream::new(rx); (Self::balance(list, DEFAULT_BUFFER_SIZE, executor), tx) } /// Create a new [`Channel`] using a custom connector to the provided [Endpoint]. /// /// This is a lower level API, prefer to use [`Endpoint::connect_lazy`] if you are not using a custom connector. pub fn new(connector: C, endpoint: Endpoint) -> Self where C: Service + Send + 'static, C::Error: Into + Send, C::Future: Send, C::Response: rt::Read + rt::Write + Unpin + Send + 'static, { let buffer_size = endpoint.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE); let executor = endpoint.executor.clone(); let svc = Connection::lazy(connector, endpoint); let (svc, worker) = Buffer::pair(svc, buffer_size); executor.execute(worker); Channel { svc } } /// Connect to the provided [`Endpoint`] using the provided connector, and return a new [`Channel`]. /// /// This is a lower level API, prefer to use [`Endpoint::connect`] if you are not using a custom connector. pub async fn connect(connector: C, endpoint: Endpoint) -> Result where C: Service + Send + 'static, C::Error: Into + Send, C::Future: Unpin + Send, C::Response: rt::Read + rt::Write + Unpin + Send + 'static, { let buffer_size = endpoint.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE); let executor = endpoint.executor.clone(); let svc = Connection::connect(connector, endpoint) .await .map_err(super::Error::from_source)?; let (svc, worker) = Buffer::pair(svc, buffer_size); executor.execute(worker); Ok(Channel { svc }) } pub(crate) fn balance(discover: D, buffer_size: usize, executor: E) -> Self where D: Discover + Unpin + Send + 'static, D::Error: Into, D::Key: Hash + Send + Clone, E: Executor> + Send + Sync + 'static, { let svc = Balance::new(discover); let svc = BoxService::new(svc); let (svc, worker) = Buffer::pair(svc, buffer_size); executor.execute(Box::pin(worker)); Channel { svc } } } impl Service> for Channel { type Response = http::Response; type Error = super::Error; type Future = ResponseFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { Service::poll_ready(&mut self.svc, cx).map_err(super::Error::from_source) } fn call(&mut self, request: http::Request) -> Self::Future { let inner = Service::call(&mut self.svc, request); ResponseFuture { inner } } } impl Future for ResponseFuture { type Output = Result, super::Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { Pin::new(&mut self.inner) .poll(cx) .map_err(super::Error::from_source) } } impl fmt::Debug for Channel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Channel").finish() } } impl fmt::Debug for ResponseFuture { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ResponseFuture").finish() } } ================================================ FILE: tonic/src/transport/channel/service/add_origin.rs ================================================ use crate::transport::channel::BoxFuture; use http::uri::Authority; use http::uri::Scheme; use http::{Request, Uri}; use std::task::{Context, Poll}; use tower_service::Service; #[derive(Debug)] pub(crate) struct AddOrigin { inner: T, scheme: Option, authority: Option, } impl AddOrigin { pub(crate) fn new(inner: T, origin: Uri) -> Self { let http::uri::Parts { scheme, authority, .. } = origin.into_parts(); Self { inner, scheme, authority, } } } impl Service> for AddOrigin where T: Service>, T::Future: Send + 'static, T::Error: Into, { type Response = T::Response; type Error = crate::BoxError; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, req: Request) -> Self::Future { if self.scheme.is_none() || self.authority.is_none() { let err = crate::transport::Error::new_invalid_uri(); return Box::pin(async move { Err::(err.into()) }); } // Split the request into the head and the body. let (mut head, body) = req.into_parts(); // Update the request URI head.uri = { // Split the request URI into parts. let mut uri: http::uri::Parts = head.uri.into(); // Update the URI parts, setting the scheme and authority uri.scheme = self.scheme.clone(); uri.authority = self.authority.clone(); http::Uri::from_parts(uri).expect("valid uri") }; let request = Request::from_parts(head, body); let fut = self.inner.call(request); Box::pin(async move { fut.await.map_err(Into::into) }) } } ================================================ FILE: tonic/src/transport/channel/service/connection.rs ================================================ use super::{AddOrigin, Reconnect, SharedExec, UserAgent}; use crate::{ body::Body, transport::{Endpoint, channel::BoxFuture, service::GrpcTimeout}, }; use http::{Request, Response, Uri}; use hyper::rt; use hyper::{client::conn::http2::Builder, rt::Executor}; use hyper_util::rt::TokioTimer; use std::{ fmt, task::{Context, Poll}, }; use tower::load::Load; use tower::{ ServiceBuilder, ServiceExt, layer::Layer, limit::{concurrency::ConcurrencyLimitLayer, rate::RateLimitLayer}, util::BoxService, }; use tower_service::Service; pub(crate) struct Connection { inner: BoxService, Response, crate::BoxError>, } impl Connection { fn new(connector: C, endpoint: Endpoint, is_lazy: bool) -> Self where C: Service + Send + 'static, C::Error: Into + Send, C::Future: Send, C::Response: rt::Read + rt::Write + Unpin + Send + 'static, { let mut settings: Builder = Builder::new(endpoint.executor.clone()) .initial_stream_window_size(endpoint.init_stream_window_size) .initial_connection_window_size(endpoint.init_connection_window_size) .keep_alive_interval(endpoint.http2_keep_alive_interval) .timer(TokioTimer::new()) .clone(); if let Some(val) = endpoint.http2_keep_alive_timeout { settings.keep_alive_timeout(val); } if let Some(val) = endpoint.http2_keep_alive_while_idle { settings.keep_alive_while_idle(val); } if let Some(val) = endpoint.http2_adaptive_window { settings.adaptive_window(val); } if let Some(val) = endpoint.http2_max_header_list_size { settings.max_header_list_size(val); } let stack = ServiceBuilder::new() .layer_fn(|s| { let origin = endpoint.origin.as_ref().unwrap_or(endpoint.uri()).clone(); AddOrigin::new(s, origin) }) .layer_fn(|s| UserAgent::new(s, endpoint.user_agent.clone())) .layer_fn(|s| GrpcTimeout::new(s, endpoint.timeout)) .option_layer(endpoint.concurrency_limit.map(ConcurrencyLimitLayer::new)) .option_layer(endpoint.rate_limit.map(|(l, d)| RateLimitLayer::new(l, d))) .into_inner(); let make_service = MakeSendRequestService::new(connector, endpoint.executor.clone(), settings); let conn = Reconnect::new(make_service, endpoint.uri().clone(), is_lazy); Self { inner: BoxService::new(stack.layer(conn)), } } pub(crate) async fn connect( connector: C, endpoint: Endpoint, ) -> Result where C: Service + Send + 'static, C::Error: Into + Send, C::Future: Unpin + Send, C::Response: rt::Read + rt::Write + Unpin + Send + 'static, { Self::new(connector, endpoint, false).ready_oneshot().await } pub(crate) fn lazy(connector: C, endpoint: Endpoint) -> Self where C: Service + Send + 'static, C::Error: Into + Send, C::Future: Send, C::Response: rt::Read + rt::Write + Unpin + Send + 'static, { Self::new(connector, endpoint, true) } } impl Service> for Connection { type Response = Response; type Error = crate::BoxError; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { Service::poll_ready(&mut self.inner, cx).map_err(Into::into) } fn call(&mut self, req: Request) -> Self::Future { self.inner.call(req) } } impl Load for Connection { type Metric = usize; fn load(&self) -> Self::Metric { 0 } } impl fmt::Debug for Connection { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Connection").finish() } } struct SendRequest { inner: hyper::client::conn::http2::SendRequest, } impl From> for SendRequest { fn from(inner: hyper::client::conn::http2::SendRequest) -> Self { Self { inner } } } impl tower::Service> for SendRequest { type Response = Response; type Error = crate::BoxError; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, req: Request) -> Self::Future { let fut = self.inner.send_request(req); Box::pin(async move { fut.await.map_err(Into::into).map(|res| res.map(Body::new)) }) } } struct MakeSendRequestService { connector: C, executor: SharedExec, settings: Builder, } impl MakeSendRequestService { fn new(connector: C, executor: SharedExec, settings: Builder) -> Self { Self { connector, executor, settings, } } } impl tower::Service for MakeSendRequestService where C: Service + Send + 'static, C::Error: Into + Send, C::Future: Send, C::Response: rt::Read + rt::Write + Unpin + Send, { type Response = SendRequest; type Error = crate::BoxError; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.connector.poll_ready(cx).map_err(Into::into) } fn call(&mut self, req: Uri) -> Self::Future { let fut = self.connector.call(req); let builder = self.settings.clone(); let executor = self.executor.clone(); Box::pin(async move { let io = fut.await.map_err(Into::into)?; let (send_request, conn) = builder.handshake(io).await?; Executor::>::execute( &executor, Box::pin(async move { if let Err(e) = conn.await { tracing::debug!("connection task error: {:?}", e); } }) as _, ); Ok(SendRequest::from(send_request)) }) } } ================================================ FILE: tonic/src/transport/channel/service/connector.rs ================================================ use super::BoxedIo; #[cfg(feature = "_tls-any")] use super::TlsConnector; use crate::ConnectError; use crate::transport::channel::BoxFuture; use http::Uri; #[cfg(feature = "_tls-any")] use std::fmt; use std::task::{Context, Poll}; use hyper::rt; #[cfg(feature = "_tls-any")] use hyper_util::rt::TokioIo; use tower_service::Service; pub(crate) struct Connector { inner: C, #[cfg(feature = "_tls-any")] tls: Option, } impl Connector { pub(crate) fn new(inner: C, #[cfg(feature = "_tls-any")] tls: Option) -> Self { Self { inner, #[cfg(feature = "_tls-any")] tls, } } } impl Service for Connector where C: Service, C::Response: rt::Read + rt::Write + Unpin + Send + 'static, C::Future: Send + 'static, crate::BoxError: From + Send + 'static, { type Response = BoxedIo; type Error = ConnectError; type Future = BoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner .poll_ready(cx) .map_err(|err| ConnectError(From::from(err))) } fn call(&mut self, uri: Uri) -> Self::Future { #[cfg(feature = "_tls-any")] let tls = self.tls.clone(); #[cfg(feature = "_tls-any")] let is_https = uri.scheme_str() == Some("https"); let connect = self.inner.call(uri); Box::pin(async move { async { let io = connect.await?; #[cfg(feature = "_tls-any")] if is_https { return if let Some(tls) = tls { let io = tls.connect(TokioIo::new(io)).await?; Ok(io) } else { Err(HttpsUriWithoutTlsSupport(()).into()) }; } Ok::<_, crate::BoxError>(BoxedIo::new(io)) } .await .map_err(ConnectError) }) } } /// Error returned when trying to connect to an HTTPS endpoint without TLS enabled. #[cfg(feature = "_tls-any")] #[derive(Debug)] pub(crate) struct HttpsUriWithoutTlsSupport(()); #[cfg(feature = "_tls-any")] impl fmt::Display for HttpsUriWithoutTlsSupport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Connecting to HTTPS without TLS enabled") } } // std::error::Error only requires a type to impl Debug and Display #[cfg(feature = "_tls-any")] impl std::error::Error for HttpsUriWithoutTlsSupport {} ================================================ FILE: tonic/src/transport/channel/service/discover.rs ================================================ use super::super::{Connection, Endpoint}; use std::{ hash::Hash, pin::Pin, task::{Context, Poll}, }; use tokio::sync::mpsc::Receiver; use tokio_stream::Stream; use tower::discover::Change as TowerChange; /// A change in the service set. #[derive(Debug, Clone)] pub enum Change { /// A new service identified by key `K` was identified. Insert(K, V), /// The service identified by key `K` disappeared. Remove(K), } pub(crate) struct DynamicServiceStream { changes: Receiver>, } impl DynamicServiceStream { pub(crate) fn new(changes: Receiver>) -> Self { Self { changes } } } impl Stream for DynamicServiceStream { type Item = Result, crate::BoxError>; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match Pin::new(&mut self.changes).poll_recv(cx) { Poll::Pending | Poll::Ready(None) => Poll::Pending, Poll::Ready(Some(change)) => match change { Change::Insert(k, endpoint) => { let connection = Connection::lazy(endpoint.http_connector(), endpoint); Poll::Ready(Some(Ok(TowerChange::Insert(k, connection)))) } Change::Remove(k) => Poll::Ready(Some(Ok(TowerChange::Remove(k)))), }, } } } impl Unpin for DynamicServiceStream {} ================================================ FILE: tonic/src/transport/channel/service/executor.rs ================================================ use crate::transport::channel::BoxFuture; use hyper_util::rt::TokioExecutor; use std::{future::Future, sync::Arc}; pub(crate) use hyper::rt::Executor; #[derive(Clone)] pub(crate) struct SharedExec { inner: Arc> + Send + Sync + 'static>, } impl SharedExec { pub(crate) fn new(exec: E) -> Self where E: Executor> + Send + Sync + 'static, { Self { inner: Arc::new(exec), } } pub(crate) fn tokio() -> Self { Self::new(TokioExecutor::new()) } } impl Executor for SharedExec where F: Future + Send + 'static, { fn execute(&self, fut: F) { self.inner.execute(Box::pin(fut)) } } ================================================ FILE: tonic/src/transport/channel/service/io.rs ================================================ use std::io::{self, IoSlice}; use std::pin::Pin; use std::task::{Context, Poll}; use hyper::rt; use hyper_util::client::legacy::connect::{Connected as HyperConnected, Connection}; pub(in crate::transport) trait Io: rt::Read + rt::Write + Send + 'static { } impl Io for T where T: rt::Read + rt::Write + Send + 'static {} pub(crate) struct BoxedIo(Pin>); impl BoxedIo { pub(in crate::transport) fn new(io: I) -> Self { BoxedIo(Box::pin(io)) } } impl Connection for BoxedIo { fn connected(&self) -> HyperConnected { HyperConnected::new() } } impl rt::Read for BoxedIo { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: rt::ReadBufCursor<'_>, ) -> Poll> { Pin::new(&mut self.0).poll_read(cx, buf) } } impl rt::Write for BoxedIo { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { Pin::new(&mut self.0).poll_write(cx, buf) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.0).poll_flush(cx) } fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.0).poll_shutdown(cx) } fn poll_write_vectored( mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[IoSlice<'_>], ) -> Poll> { Pin::new(&mut self.0).poll_write_vectored(cx, bufs) } fn is_write_vectored(&self) -> bool { self.0.is_write_vectored() } } ================================================ FILE: tonic/src/transport/channel/service/mod.rs ================================================ mod add_origin; use self::add_origin::AddOrigin; mod user_agent; use self::user_agent::UserAgent; mod reconnect; use self::reconnect::Reconnect; mod connection; pub(super) use self::connection::Connection; mod discover; pub use self::discover::Change; pub(super) use self::discover::DynamicServiceStream; mod io; use self::io::BoxedIo; mod connector; pub(crate) use self::connector::Connector; mod executor; pub(super) use self::executor::{Executor, SharedExec}; #[cfg(feature = "_tls-any")] mod tls; #[cfg(feature = "_tls-any")] pub(super) use self::tls::TlsConnector; ================================================ FILE: tonic/src/transport/channel/service/reconnect.rs ================================================ use pin_project::pin_project; use std::fmt; use std::{ future::Future, pin::Pin, task::{Context, Poll}, }; use tower::make::MakeService; use tower_service::Service; use tracing::trace; pub(crate) struct Reconnect where M: Service, M::Error: Into, { mk_service: M, state: State, target: Target, error: Option, has_been_connected: bool, is_lazy: bool, } #[derive(Debug)] enum State { Idle, Connecting(F), Connected(S), } impl Reconnect where M: Service, M::Error: Into, { pub(crate) fn new(mk_service: M, target: Target, is_lazy: bool) -> Self { Reconnect { mk_service, state: State::Idle, target, error: None, has_been_connected: false, is_lazy, } } } impl Service for Reconnect where M: Service, S: Service, M::Future: Unpin, crate::BoxError: From + From, Target: Clone, >::Error: Into, { type Response = S::Response; type Error = crate::BoxError; type Future = ResponseFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { let mut state; if self.error.is_some() { return Poll::Ready(Ok(())); } loop { match self.state { State::Idle => { trace!("poll_ready; idle"); match self.mk_service.poll_ready(cx) { Poll::Ready(r) => r?, Poll::Pending => { trace!("poll_ready; MakeService not ready"); return Poll::Pending; } } let fut = self.mk_service.make_service(self.target.clone()); self.state = State::Connecting(fut); continue; } State::Connecting(ref mut f) => { trace!("poll_ready; connecting"); match Pin::new(f).poll(cx) { Poll::Ready(Ok(service)) => { state = State::Connected(service); } Poll::Pending => { trace!("poll_ready; not ready"); return Poll::Pending; } Poll::Ready(Err(e)) => { trace!("poll_ready; error"); state = State::Idle; if !(self.has_been_connected || self.is_lazy) { return Poll::Ready(Err(e.into())); } else { let error = e.into(); tracing::debug!("reconnect::poll_ready: {:?}", error); self.error = Some(error); break; } } } } State::Connected(ref mut inner) => { trace!("poll_ready; connected"); self.has_been_connected = true; match inner.poll_ready(cx) { Poll::Ready(Ok(())) => { trace!("poll_ready; ready"); return Poll::Ready(Ok(())); } Poll::Pending => { trace!("poll_ready; not ready"); return Poll::Pending; } Poll::Ready(Err(_)) => { trace!("poll_ready; error"); state = State::Idle; } } } } self.state = state; } self.state = state; Poll::Ready(Ok(())) } fn call(&mut self, request: Request) -> Self::Future { tracing::trace!("Reconnect::call"); if let Some(error) = self.error.take() { tracing::debug!("error: {}", error); return ResponseFuture::error(error); } let State::Connected(service) = &mut self.state else { panic!("service not ready; poll_ready must be called first"); }; let fut = service.call(request); ResponseFuture::new(fut) } } impl fmt::Debug for Reconnect where M: Service + fmt::Debug, M::Future: fmt::Debug, M::Response: fmt::Debug, Target: fmt::Debug, >::Error: Into, { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt.debug_struct("Reconnect") .field("mk_service", &self.mk_service) .field("state", &self.state) .field("target", &self.target) .finish() } } /// Future that resolves to the response or failure to connect. #[pin_project] #[derive(Debug)] pub(crate) struct ResponseFuture { #[pin] inner: Inner, } #[pin_project(project = InnerProj)] #[derive(Debug)] enum Inner { Future(#[pin] F), Error(Option), } impl ResponseFuture { pub(crate) fn new(inner: F) -> Self { ResponseFuture { inner: Inner::Future(inner), } } pub(crate) fn error(error: crate::BoxError) -> Self { ResponseFuture { inner: Inner::Error(Some(error)), } } } impl Future for ResponseFuture where F: Future>, E: Into, { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { //self.project().inner.poll(cx).map_err(Into::into) let me = self.project(); match me.inner.project() { InnerProj::Future(fut) => fut.poll(cx).map_err(Into::into), InnerProj::Error(e) => { let e = e.take().expect("Polled after ready."); Poll::Ready(Err(e)) } } } } ================================================ FILE: tonic/src/transport/channel/service/tls.rs ================================================ use std::fmt; use std::{sync::Arc, time::Duration}; use hyper_util::rt::TokioIo; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time; use tokio_rustls::{ TlsConnector as RustlsConnector, rustls::{ ClientConfig, ConfigBuilder, RootCertStore, WantsVerifier, crypto, pki_types::{ServerName, TrustAnchor}, }, }; use super::io::BoxedIo; use crate::transport::service::tls::{ ALPN_H2, TlsError, convert_certificate_to_pki_types, convert_identity_to_pki_types, }; use crate::transport::tls::{Certificate, Identity}; #[derive(Clone)] pub(crate) struct TlsConnector { config: Arc, domain: Arc>, assume_http2: bool, timeout: Option, } impl TlsConnector { #[allow(clippy::too_many_arguments)] pub(crate) fn new( ca_certs: Vec, trust_anchors: Vec>, identity: Option, domain: &str, assume_http2: bool, use_key_log: bool, timeout: Option, #[cfg(feature = "tls-native-roots")] with_native_roots: bool, #[cfg(feature = "tls-webpki-roots")] with_webpki_roots: bool, ) -> Result { fn with_provider( provider: Arc, ) -> ConfigBuilder { ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() .unwrap() } #[allow(unreachable_patterns)] let builder = match crypto::CryptoProvider::get_default() { Some(provider) => with_provider(provider.clone()), #[cfg(feature = "tls-ring")] None => with_provider(Arc::new(crypto::ring::default_provider())), #[cfg(feature = "tls-aws-lc")] None => with_provider(Arc::new(crypto::aws_lc_rs::default_provider())), // somehow tls is enabled, but neither of the crypto features are enabled. _ => ClientConfig::builder(), }; let mut roots = RootCertStore::from_iter(trust_anchors); #[cfg(feature = "tls-native-roots")] if with_native_roots { let rustls_native_certs::CertificateResult { certs, errors, .. } = rustls_native_certs::load_native_certs(); if !errors.is_empty() { tracing::debug!("errors occurred when loading native certs: {errors:?}"); } if certs.is_empty() { return Err(TlsError::NativeCertsNotFound.into()); } roots.add_parsable_certificates(certs); } #[cfg(feature = "tls-webpki-roots")] if with_webpki_roots { roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); } for cert in ca_certs { roots.add_parsable_certificates(convert_certificate_to_pki_types(&cert)?); } let builder = builder.with_root_certificates(roots); let mut config = match identity { Some(identity) => { let (client_cert, client_key) = convert_identity_to_pki_types(&identity)?; builder.with_client_auth_cert(client_cert, client_key)? } None => builder.with_no_client_auth(), }; if use_key_log { config.key_log = Arc::new(tokio_rustls::rustls::KeyLogFile::new()); } config.alpn_protocols.push(ALPN_H2.into()); Ok(Self { config: Arc::new(config), domain: Arc::new(ServerName::try_from(domain)?.to_owned()), assume_http2, timeout, }) } pub(crate) async fn connect(&self, io: I) -> Result where I: AsyncRead + AsyncWrite + Send + Unpin + 'static, { let conn_fut = RustlsConnector::from(self.config.clone()).connect(self.domain.as_ref().to_owned(), io); let io = match self.timeout { Some(timeout) => time::timeout(timeout, conn_fut) .await .map_err(|_| TlsError::HandshakeTimeout)?, None => conn_fut.await, }?; // Generally we require ALPN to be negotiated, but if the user has // explicitly set `assume_http2` to true, we'll allow it to be missing. let (_, session) = io.get_ref(); let alpn_protocol = session.alpn_protocol(); if !(alpn_protocol == Some(ALPN_H2) || self.assume_http2) { return Err(TlsError::H2NotNegotiated.into()); } Ok(BoxedIo::new(TokioIo::new(io))) } } impl fmt::Debug for TlsConnector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TlsConnector").finish() } } ================================================ FILE: tonic/src/transport/channel/service/user_agent.rs ================================================ use http::{HeaderValue, Request, header::USER_AGENT}; use std::task::{Context, Poll}; use tower_service::Service; const TONIC_USER_AGENT: &str = concat!("tonic/", env!("CARGO_PKG_VERSION")); #[derive(Debug)] pub(crate) struct UserAgent { inner: T, user_agent: HeaderValue, } impl UserAgent { pub(crate) fn new(inner: T, user_agent: Option) -> Self { let user_agent = user_agent .map(|value| { let mut buf = Vec::new(); buf.extend(value.as_bytes()); buf.push(b' '); buf.extend(TONIC_USER_AGENT.as_bytes()); HeaderValue::from_bytes(&buf).expect("user-agent should be valid") }) .unwrap_or_else(|| HeaderValue::from_static(TONIC_USER_AGENT)); Self { inner, user_agent } } } impl Service> for UserAgent where T: Service>, { type Response = T::Response; type Error = T::Error; type Future = T::Future; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, mut req: Request) -> Self::Future { if let Ok(Some(user_agent)) = req .headers_mut() .try_insert(USER_AGENT, self.user_agent.clone()) { // The User-Agent header has already been set on the request. Let's // append our user agent to the end. let mut buf = Vec::new(); buf.extend(user_agent.as_bytes()); buf.push(b' '); buf.extend(self.user_agent.as_bytes()); req.headers_mut().insert( USER_AGENT, HeaderValue::from_bytes(&buf).expect("user-agent should be valid"), ); } self.inner.call(req) } } #[cfg(test)] mod tests { use super::*; struct Svc; #[test] fn sets_default_if_no_custom_user_agent() { assert_eq!( UserAgent::new(Svc, None).user_agent, HeaderValue::from_static(TONIC_USER_AGENT) ) } #[test] fn prepends_custom_user_agent_to_default() { assert_eq!( UserAgent::new(Svc, Some(HeaderValue::from_static("Greeter 1.1"))).user_agent, HeaderValue::from_str(&format!("Greeter 1.1 {TONIC_USER_AGENT}")).unwrap() ) } struct TestSvc { pub expected_user_agent: String, } impl Service> for TestSvc { type Response = (); type Error = (); type Future = std::future::Ready>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: Request<()>) -> Self::Future { let user_agent = req.headers().get(USER_AGENT).unwrap().to_str().unwrap(); assert_eq!(user_agent, self.expected_user_agent); std::future::ready(Ok(())) } } #[tokio::test] async fn sets_default_user_agent_if_none_present() { let expected_user_agent = TONIC_USER_AGENT.to_string(); let mut ua = UserAgent::new( TestSvc { expected_user_agent, }, None, ); let _ = ua.call(Request::default()).await; } #[tokio::test] async fn sets_custom_user_agent_if_none_present() { let expected_user_agent = format!("Greeter 1.1 {TONIC_USER_AGENT}"); let mut ua = UserAgent::new( TestSvc { expected_user_agent, }, Some(HeaderValue::from_static("Greeter 1.1")), ); let _ = ua.call(Request::default()).await; } #[tokio::test] async fn appends_default_user_agent_to_request_user_agent() { let mut req = Request::default(); req.headers_mut() .insert(USER_AGENT, HeaderValue::from_static("request-ua/x.y")); let expected_user_agent = format!("request-ua/x.y {TONIC_USER_AGENT}"); let mut ua = UserAgent::new( TestSvc { expected_user_agent, }, None, ); let _ = ua.call(req).await; } #[tokio::test] async fn appends_custom_user_agent_to_request_user_agent() { let mut req = Request::default(); req.headers_mut() .insert(USER_AGENT, HeaderValue::from_static("request-ua/x.y")); let expected_user_agent = format!("request-ua/x.y Greeter 1.1 {TONIC_USER_AGENT}"); let mut ua = UserAgent::new( TestSvc { expected_user_agent, }, Some(HeaderValue::from_static("Greeter 1.1")), ); let _ = ua.call(req).await; } } ================================================ FILE: tonic/src/transport/channel/tls.rs ================================================ use super::service::TlsConnector; use crate::transport::{ Error, tls::{Certificate, Identity}, }; use http::Uri; use std::time::Duration; use tokio_rustls::rustls::pki_types::TrustAnchor; /// Configures TLS settings for endpoints. #[derive(Debug, Clone, Default)] pub struct ClientTlsConfig { domain: Option, certs: Vec, trust_anchors: Vec>, identity: Option, assume_http2: bool, #[cfg(feature = "tls-native-roots")] with_native_roots: bool, #[cfg(feature = "tls-webpki-roots")] with_webpki_roots: bool, use_key_log: bool, timeout: Option, } impl ClientTlsConfig { /// Creates a new `ClientTlsConfig` using Rustls. pub fn new() -> Self { Self::default() } /// Sets the domain name against which to verify the server's TLS certificate. pub fn domain_name(self, domain_name: impl Into) -> Self { ClientTlsConfig { domain: Some(domain_name.into()), ..self } } /// Adds the CA Certificate against which to verify the server's TLS certificate. pub fn ca_certificate(self, ca_certificate: Certificate) -> Self { let mut certs = self.certs; certs.push(ca_certificate); ClientTlsConfig { certs, ..self } } /// Adds the multiple CA Certificates against which to verify the server's TLS certificate. pub fn ca_certificates(self, ca_certificates: impl IntoIterator) -> Self { let mut certs = self.certs; certs.extend(ca_certificates); ClientTlsConfig { certs, ..self } } /// Adds the trust anchor which to verify the server's TLS certificate. pub fn trust_anchor(self, trust_anchor: TrustAnchor<'static>) -> Self { let mut trust_anchors = self.trust_anchors; trust_anchors.push(trust_anchor); ClientTlsConfig { trust_anchors, ..self } } /// Adds the multiple trust anchors which to verify the server's TLS certificate. pub fn trust_anchors( mut self, trust_anchors: impl IntoIterator>, ) -> Self { self.trust_anchors.extend(trust_anchors); self } /// Sets the client identity to present to the server. pub fn identity(self, identity: Identity) -> Self { ClientTlsConfig { identity: Some(identity), ..self } } /// If true, the connector should assume that the server supports HTTP/2, /// even if it doesn't provide protocol negotiation via ALPN. pub fn assume_http2(self, assume_http2: bool) -> Self { ClientTlsConfig { assume_http2, ..self } } /// Use key log as specified by the `SSLKEYLOGFILE` environment variable. pub fn use_key_log(self) -> Self { ClientTlsConfig { use_key_log: true, ..self } } /// Enables the platform's trusted certs. #[cfg(feature = "tls-native-roots")] pub fn with_native_roots(self) -> Self { ClientTlsConfig { with_native_roots: true, ..self } } /// Enables the webpki roots. #[cfg(feature = "tls-webpki-roots")] pub fn with_webpki_roots(self) -> Self { ClientTlsConfig { with_webpki_roots: true, ..self } } /// Activates all TLS roots enabled through `tls-*-roots` feature flags pub fn with_enabled_roots(self) -> Self { let config = self; #[cfg(feature = "tls-native-roots")] let config = config.with_native_roots(); #[cfg(feature = "tls-webpki-roots")] let config = config.with_webpki_roots(); config } /// Sets the timeout for the TLS handshake. pub fn timeout(self, timeout: Duration) -> Self { ClientTlsConfig { timeout: Some(timeout), ..self } } pub(crate) fn into_tls_connector(self, uri: &Uri) -> Result { let domain = match &self.domain { Some(domain) => domain, None => uri.host().ok_or_else(Error::new_invalid_uri)?, }; TlsConnector::new( self.certs, self.trust_anchors, self.identity, domain, self.assume_http2, self.use_key_log, self.timeout, #[cfg(feature = "tls-native-roots")] self.with_native_roots, #[cfg(feature = "tls-webpki-roots")] self.with_webpki_roots, ) } } ================================================ FILE: tonic/src/transport/channel/uds_connector.rs ================================================ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use http::Uri; use hyper_util::rt::TokioIo; use tower::Service; use crate::status::ConnectError; #[cfg(not(target_os = "windows"))] use tokio::net::UnixStream; #[cfg(not(target_os = "windows"))] async fn connect_uds(uds_path: String) -> Result { UnixStream::connect(uds_path) .await .map_err(|err| ConnectError(From::from(err))) } // Dummy type that will allow us to compile and match trait bounds // but is never used. #[cfg(target_os = "windows")] #[allow(dead_code)] type UnixStream = tokio::io::DuplexStream; #[cfg(target_os = "windows")] async fn connect_uds(_uds_path: String) -> Result { Err(ConnectError( "uds connections are not allowed on windows".into(), )) } pub(crate) struct UdsConnector { uds_filepath: String, } impl UdsConnector { pub(crate) fn new(uds_filepath: &str) -> Self { UdsConnector { uds_filepath: uds_filepath.to_string(), } } } impl Service for UdsConnector { type Response = TokioIo; type Error = ConnectError; type Future = UdsConnecting; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, _: Uri) -> Self::Future { let uds_path = self.uds_filepath.clone(); let fut = async move { let stream = connect_uds(uds_path).await?; Ok(TokioIo::new(stream)) }; UdsConnecting { inner: Box::pin(fut), } } } type ConnectResult = Result, ConnectError>; pub(crate) struct UdsConnecting { inner: Pin + Send>>, } impl Future for UdsConnecting { type Output = ConnectResult; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { self.get_mut().inner.as_mut().poll(cx) } } ================================================ FILE: tonic/src/transport/error.rs ================================================ use std::{error::Error as StdError, fmt}; type Source = Box; /// Error's that originate from the client or server; pub struct Error { inner: ErrorImpl, } struct ErrorImpl { kind: Kind, source: Option, } #[derive(Debug)] pub(crate) enum Kind { Transport, #[cfg(feature = "channel")] InvalidUri, #[cfg(feature = "channel")] InvalidUserAgent, #[cfg(all(feature = "_tls-any", feature = "channel"))] InvalidTlsConfigForUds, } impl Error { pub(crate) fn new(kind: Kind) -> Self { Self { inner: ErrorImpl { kind, source: None }, } } pub(crate) fn with(mut self, source: impl Into) -> Self { self.inner.source = Some(source.into()); self } pub(crate) fn from_source(source: impl Into) -> Self { Error::new(Kind::Transport).with(source) } #[cfg(feature = "channel")] pub(crate) fn new_invalid_uri() -> Self { Error::new(Kind::InvalidUri) } #[cfg(feature = "channel")] pub(crate) fn new_invalid_user_agent() -> Self { Error::new(Kind::InvalidUserAgent) } fn description(&self) -> &str { match &self.inner.kind { Kind::Transport => "transport error", #[cfg(feature = "channel")] Kind::InvalidUri => "invalid URI", #[cfg(feature = "channel")] Kind::InvalidUserAgent => "user agent is not a valid header value", #[cfg(all(feature = "_tls-any", feature = "channel"))] Kind::InvalidTlsConfigForUds => "cannot apply TLS config for unix domain socket", } } } impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut f = f.debug_tuple("tonic::transport::Error"); f.field(&self.inner.kind); if let Some(source) = &self.inner.source { f.field(source); } f.finish() } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.description()) } } impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { self.inner .source .as_ref() .map(|source| &**source as &(dyn StdError + 'static)) } } ================================================ FILE: tonic/src/transport/mod.rs ================================================ //! Batteries included server and client. //! //! This module provides a set of batteries included, fully featured and //! fast set of HTTP/2 server and client's. These components each provide a //! `rustls` tls backend when the respective feature flag is enabled, and //! provides builders to configure transport behavior. //! //! # Features //! //! - TLS support via [rustls]. //! - Load balancing //! - Timeouts //! - Concurrency Limits //! - Rate limiting //! //! # Examples //! //! ## Client //! //! ```no_run //! # #[cfg(feature = "rustls")] //! # use tonic::transport::{Channel, Certificate, ClientTlsConfig}; //! # use std::time::Duration; //! # use tonic::client::GrpcService;; //! # use http::Request; //! # #[cfg(feature = "rustls")] //! # async fn do_thing() -> Result<(), Box> { //! let cert = std::fs::read_to_string("ca.pem")?; //! //! let mut channel = Channel::from_static("https://example.com") //! .tls_config(ClientTlsConfig::new() //! .ca_certificate(Certificate::from_pem(&cert)) //! .domain_name("example.com".to_string()))? //! .timeout(Duration::from_secs(5)) //! .rate_limit(5, Duration::from_secs(1)) //! .concurrency_limit(256) //! .connect() //! .await?; //! //! channel.call(Request::new(tonic::body::empty_body())).await?; //! # Ok(()) //! # } //! ``` //! //! ## Server //! //! ```no_run //! # use std::convert::Infallible; //! # #[cfg(feature = "rustls")] //! # use tonic::transport::{Server, Identity, ServerTlsConfig}; //! # use tonic::body::Body; //! # use tower::Service; //! # #[cfg(feature = "rustls")] //! # async fn do_thing() -> Result<(), Box> { //! # #[derive(Clone)] //! # pub struct Svc; //! # impl Service> for Svc { //! # type Response = hyper::Response; //! # type Error = Infallible; //! # type Future = std::future::Ready>; //! # fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll> { //! # Ok(()).into() //! # } //! # fn call(&mut self, _req: hyper::Request) -> Self::Future { //! # unimplemented!() //! # } //! # } //! # impl tonic::server::NamedService for Svc { //! # const NAME: &'static str = "some_svc"; //! # } //! # let my_svc = Svc; //! let cert = std::fs::read_to_string("server.pem")?; //! let key = std::fs::read_to_string("server.key")?; //! //! let addr = "[::1]:50051".parse()?; //! //! Server::builder() //! .tls_config(ServerTlsConfig::new() //! .identity(Identity::from_pem(&cert, &key)))? //! .concurrency_limit_per_connection(256) //! .add_service(my_svc) //! .serve(addr) //! .await?; //! //! # Ok(()) //! # } //! ``` //! //! [rustls]: https://docs.rs/rustls/0.16.0/rustls/ #[cfg(feature = "channel")] pub mod channel; #[cfg(feature = "server")] pub mod server; mod error; mod service; #[cfg(feature = "_tls-any")] mod tls; #[doc(inline)] #[cfg(feature = "channel")] pub use self::channel::{Channel, Endpoint}; pub use self::error::Error; #[doc(inline)] #[cfg(feature = "server")] pub use self::server::Server; #[cfg(feature = "_tls-any")] pub use self::tls::Certificate; pub use hyper::{Uri, body::Body}; #[cfg(feature = "_tls-any")] pub use tokio_rustls::rustls::pki_types::CertificateDer; #[cfg(all(feature = "channel", feature = "_tls-any"))] pub use self::channel::ClientTlsConfig; #[cfg(all(feature = "server", feature = "_tls-any"))] pub use self::server::ServerTlsConfig; #[cfg(feature = "_tls-any")] pub use self::tls::Identity; ================================================ FILE: tonic/src/transport/server/conn.rs ================================================ use std::net::SocketAddr; use tokio::net::TcpStream; #[cfg(feature = "tls-connect-info")] use std::sync::Arc; #[cfg(feature = "tls-connect-info")] use tokio_rustls::rustls::pki_types::CertificateDer; #[cfg(feature = "tls-connect-info")] use tokio_rustls::server::TlsStream; /// Trait that connected IO resources implement and use to produce info about the connection. /// /// The goal for this trait is to allow users to implement /// custom IO types that can still provide the same connection /// metadata. /// /// # Example /// /// The `ConnectInfo` returned will be accessible through [request extensions][ext]: /// /// ``` /// use tonic::{Request, transport::server::Connected}; /// /// // A `Stream` that yields connections /// struct MyConnector {} /// /// // Return metadata about the connection as `MyConnectInfo` /// impl Connected for MyConnector { /// type ConnectInfo = MyConnectInfo; /// /// fn connect_info(&self) -> Self::ConnectInfo { /// MyConnectInfo {} /// } /// } /// /// #[derive(Clone)] /// struct MyConnectInfo { /// // Metadata about your connection /// } /// /// // The connect info can be accessed through request extensions: /// # fn foo(request: Request<()>) { /// let connect_info: &MyConnectInfo = request /// .extensions() /// .get::() /// .expect("bug in tonic"); /// # } /// ``` /// /// [ext]: crate::Request::extensions pub trait Connected { /// The connection info type the IO resources generates. // all these bounds are necessary to set this as a request extension type ConnectInfo: Clone + Send + Sync + 'static; /// Create type holding information about the connection. fn connect_info(&self) -> Self::ConnectInfo; } /// Connection info for standard TCP streams. /// /// This type will be accessible through [request extensions][ext] if you're using the default /// non-TLS connector. /// /// See [`Connected`] for more details. /// /// [ext]: crate::Request::extensions #[derive(Debug, Clone)] pub struct TcpConnectInfo { /// Returns the local address of this connection. pub local_addr: Option, /// Returns the remote (peer) address of this connection. pub remote_addr: Option, } impl TcpConnectInfo { /// Return the local address the IO resource is connected. pub fn local_addr(&self) -> Option { self.local_addr } /// Return the remote address the IO resource is connected too. pub fn remote_addr(&self) -> Option { self.remote_addr } } impl Connected for TcpStream { type ConnectInfo = TcpConnectInfo; fn connect_info(&self) -> Self::ConnectInfo { TcpConnectInfo { local_addr: self.local_addr().ok(), remote_addr: self.peer_addr().ok(), } } } impl Connected for tokio::io::DuplexStream { type ConnectInfo = (); fn connect_info(&self) -> Self::ConnectInfo {} } #[cfg(feature = "tls-connect-info")] impl Connected for TlsStream where T: Connected, { type ConnectInfo = TlsConnectInfo; fn connect_info(&self) -> Self::ConnectInfo { let (inner, session) = self.get_ref(); let inner = inner.connect_info(); let certs = session .peer_certificates() .map(|certs| certs.to_owned().into()); TlsConnectInfo { inner, certs } } } /// Connection info for TLS streams. /// /// This type will be accessible through [request extensions][ext] if you're using a TLS connector. /// /// See [`Connected`] for more details. /// /// [ext]: crate::Request::extensions #[cfg(feature = "tls-connect-info")] #[derive(Debug, Clone)] pub struct TlsConnectInfo { inner: T, certs: Option>>>, } #[cfg(feature = "tls-connect-info")] impl TlsConnectInfo { /// Get a reference to the underlying connection info. pub fn get_ref(&self) -> &T { &self.inner } /// Get a mutable reference to the underlying connection info. pub fn get_mut(&mut self) -> &mut T { &mut self.inner } /// Return the set of connected peer TLS certificates. pub fn peer_certs(&self) -> Option>>> { self.certs.clone() } } ================================================ FILE: tonic/src/transport/server/display_error_stack.rs ================================================ use std::error::Error; use std::fmt; use std::fmt::{Display, Formatter}; pub(crate) struct DisplayErrorStack<'a>(pub(crate) &'a (dyn Error + 'static)); impl<'a> Display for DisplayErrorStack<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0)?; let mut next = self.0.source(); while let Some(err) = next { write!(f, ": {err}")?; next = err.source(); } Ok(()) } } #[cfg(test)] mod tests { use crate::transport::server::display_error_stack::DisplayErrorStack; use std::error::Error; use std::fmt; use std::fmt::{Display, Formatter}; use std::sync::Arc; #[test] fn test_display_error_stack() { #[derive(Debug)] struct TestError(&'static str, Option>); impl Display for TestError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl Error for TestError { fn source(&self) -> Option<&(dyn Error + 'static)> { self.1.as_ref().map(|e| e as &(dyn Error + 'static)) } } let a = Arc::new(TestError("a", None)); let b = Arc::new(TestError("b", Some(a.clone()))); let c = Arc::new(TestError("c", Some(b.clone()))); assert_eq!("a", DisplayErrorStack(&a).to_string()); assert_eq!("b: a", DisplayErrorStack(&b).to_string()); assert_eq!("c: b: a", DisplayErrorStack(&c).to_string()); } } ================================================ FILE: tonic/src/transport/server/incoming.rs ================================================ use std::{ net::{SocketAddr, TcpListener as StdTcpListener}, pin::Pin, task::{Context, Poll}, time::Duration, }; use socket2::TcpKeepalive; use tokio::net::{TcpListener, TcpStream}; use tokio_stream::{Stream, wrappers::TcpListenerStream}; use tracing::warn; /// Binds a socket address for a [Router](super::Router) /// /// An incoming stream, usable with [Router::serve_with_incoming](super::Router::serve_with_incoming), /// of `AsyncRead + AsyncWrite` that communicate with clients that connect to a socket address. #[derive(Debug)] pub struct TcpIncoming { inner: TcpListenerStream, nodelay: Option, keepalive: Option, keepalive_time: Option, keepalive_interval: Option, keepalive_retries: Option, } impl TcpIncoming { /// Creates an instance by binding (opening) the specified socket address. /// /// Returns a TcpIncoming if the socket address was successfully bound. /// /// # Examples /// ```no_run /// # use tower_service::Service; /// # use http::{request::Request, response::Response}; /// # use tonic::{body::Body, server::NamedService, transport::{Server, server::TcpIncoming}}; /// # use core::convert::Infallible; /// # use std::error::Error; /// # fn main() { } // Cannot have type parameters, hence instead define: /// # fn run(some_service: S) -> Result<(), Box> /// # where /// # S: Service, Response = Response, Error = Infallible> + NamedService + Clone + Send + Sync + 'static, /// # S::Future: Send + 'static, /// # { /// // Find a free port /// let mut port = 1322; /// let tinc = loop { /// let addr = format!("127.0.0.1:{}", port).parse().unwrap(); /// match TcpIncoming::bind(addr) { /// Ok(t) => break t, /// Err(_) => port += 1 /// } /// }; /// Server::builder() /// .add_service(some_service) /// .serve_with_incoming(tinc); /// # Ok(()) /// # } pub fn bind(addr: SocketAddr) -> std::io::Result { let std_listener = StdTcpListener::bind(addr)?; std_listener.set_nonblocking(true)?; Ok(TcpListener::from_std(std_listener)?.into()) } /// Sets the `TCP_NODELAY` option on the accepted connection. pub fn with_nodelay(self, nodelay: Option) -> Self { Self { nodelay, ..self } } /// Sets the `TCP_KEEPALIVE` option on the accepted connection. pub fn with_keepalive(self, keepalive_time: Option) -> Self { Self { keepalive_time, keepalive: make_keepalive( keepalive_time, self.keepalive_interval, self.keepalive_retries, ), ..self } } /// Sets the `TCP_KEEPINTVL` option on the accepted connection. pub fn with_keepalive_interval(self, keepalive_interval: Option) -> Self { Self { keepalive_interval, keepalive: make_keepalive( self.keepalive_time, keepalive_interval, self.keepalive_retries, ), ..self } } /// Sets the `TCP_KEEPCNT` option on the accepted connection. pub fn with_keepalive_retries(self, keepalive_retries: Option) -> Self { Self { keepalive_retries, keepalive: make_keepalive( self.keepalive_time, self.keepalive_interval, keepalive_retries, ), ..self } } /// Returns the local address that this tcp incoming is bound to. pub fn local_addr(&self) -> std::io::Result { self.inner.as_ref().local_addr() } } impl From for TcpIncoming { fn from(listener: TcpListener) -> Self { Self { inner: TcpListenerStream::new(listener), nodelay: None, keepalive: None, keepalive_time: None, keepalive_interval: None, keepalive_retries: None, } } } impl Stream for TcpIncoming { type Item = std::io::Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let polled = Pin::new(&mut self.inner).poll_next(cx); if let Poll::Ready(Some(Ok(stream))) = &polled { set_accepted_socket_options(stream, self.nodelay, &self.keepalive); } polled } } // Consistent with hyper-0.14, this function does not return an error. fn set_accepted_socket_options( stream: &TcpStream, nodelay: Option, keepalive: &Option, ) { if let Some(nodelay) = nodelay { if let Err(e) = stream.set_nodelay(nodelay) { warn!("error trying to set TCP_NODELAY: {e}"); } } if let Some(keepalive) = keepalive { let sock_ref = socket2::SockRef::from(&stream); if let Err(e) = sock_ref.set_tcp_keepalive(keepalive) { warn!("error trying to set TCP_KEEPALIVE: {e}"); } } } fn make_keepalive( keepalive_time: Option, keepalive_interval: Option, keepalive_retries: Option, ) -> Option { let mut dirty = false; let mut keepalive = TcpKeepalive::new(); if let Some(t) = keepalive_time { keepalive = keepalive.with_time(t); dirty = true; } #[cfg( // See https://docs.rs/socket2/0.5.8/src/socket2/lib.rs.html#511-525 any( target_os = "android", target_os = "dragonfly", target_os = "freebsd", target_os = "fuchsia", target_os = "illumos", target_os = "ios", target_os = "visionos", target_os = "linux", target_os = "macos", target_os = "netbsd", target_os = "tvos", target_os = "watchos", target_os = "windows", ) )] if let Some(t) = keepalive_interval { keepalive = keepalive.with_interval(t); dirty = true; } #[cfg( // See https://docs.rs/socket2/0.5.8/src/socket2/lib.rs.html#557-570 any( target_os = "android", target_os = "dragonfly", target_os = "freebsd", target_os = "fuchsia", target_os = "illumos", target_os = "ios", target_os = "visionos", target_os = "linux", target_os = "macos", target_os = "netbsd", target_os = "tvos", target_os = "watchos", ) )] if let Some(r) = keepalive_retries { keepalive = keepalive.with_retries(r); dirty = true; } // avoid clippy errors for targets that do not use these fields. let _ = keepalive_retries; let _ = keepalive_interval; dirty.then_some(keepalive) } #[cfg(test)] mod tests { use crate::transport::server::TcpIncoming; #[tokio::test] async fn one_tcpincoming_at_a_time() { let addr = "127.0.0.1:1322".parse().unwrap(); { let _t1 = TcpIncoming::bind(addr).unwrap(); let _t2 = TcpIncoming::bind(addr).unwrap_err(); } let _t3 = TcpIncoming::bind(addr).unwrap(); } } ================================================ FILE: tonic/src/transport/server/io_stream.rs ================================================ #[cfg(feature = "_tls-any")] use std::future::Future; #[cfg(feature = "_tls-any")] use std::pin::pin; use std::{ io, ops::ControlFlow, pin::Pin, task::{Context, Poll, ready}, }; use pin_project::pin_project; use tokio::io::{AsyncRead, AsyncWrite}; #[cfg(feature = "_tls-any")] use tokio::task::JoinSet; use tokio_stream::Stream; #[cfg(feature = "_tls-any")] use tokio_stream::StreamExt as _; use super::service::ServerIo; #[cfg(feature = "_tls-any")] use super::service::TlsAcceptor; #[cfg(feature = "_tls-any")] struct State(TlsAcceptor, JoinSet, crate::BoxError>>); #[pin_project] pub(crate) struct ServerIoStream where S: Stream>, { #[pin] inner: S, #[cfg(feature = "_tls-any")] state: Option>, } impl ServerIoStream where S: Stream>, { pub(crate) fn new(incoming: S, #[cfg(feature = "_tls-any")] tls: Option) -> Self { Self { inner: incoming, #[cfg(feature = "_tls-any")] state: tls.map(|tls| State(tls, JoinSet::new())), } } fn poll_next_without_tls( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, crate::BoxError>>> where IE: Into, { match ready!(self.as_mut().project().inner.poll_next(cx)) { Some(Ok(io)) => Poll::Ready(Some(Ok(ServerIo::new_io(io)))), Some(Err(e)) => match handle_tcp_accept_error(e) { ControlFlow::Continue(()) => { cx.waker().wake_by_ref(); Poll::Pending } ControlFlow::Break(e) => Poll::Ready(Some(Err(e))), }, None => Poll::Ready(None), } } } impl Stream for ServerIoStream where S: Stream>, IO: AsyncRead + AsyncWrite + Unpin + Send + 'static, IE: Into, { type Item = Result, crate::BoxError>; #[cfg(not(feature = "_tls-any"))] fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.poll_next_without_tls(cx) } #[cfg(feature = "_tls-any")] fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut projected = self.as_mut().project(); let Some(State(tls, tasks)) = projected.state else { return self.poll_next_without_tls(cx); }; let select_output = ready!(pin!(select(&mut projected.inner, tasks)).poll(cx)); match select_output { SelectOutput::Incoming(stream) => { let tls = tls.clone(); tasks.spawn(async move { let io = tls.accept(stream).await?; Ok(ServerIo::new_tls_io(io)) }); cx.waker().wake_by_ref(); Poll::Pending } SelectOutput::Io(io) => Poll::Ready(Some(Ok(io))), SelectOutput::TcpErr(e) => match handle_tcp_accept_error(e) { ControlFlow::Continue(()) => { cx.waker().wake_by_ref(); Poll::Pending } ControlFlow::Break(e) => Poll::Ready(Some(Err(e))), }, SelectOutput::TlsErr(e) => { tracing::debug!(error = %e, "tls accept error"); cx.waker().wake_by_ref(); Poll::Pending } SelectOutput::Done => Poll::Ready(None), } } } fn handle_tcp_accept_error(e: impl Into) -> ControlFlow { let e = e.into(); tracing::debug!(error = %e, "accept loop error"); if let Some(e) = e.downcast_ref::() { if matches!( e.kind(), io::ErrorKind::ConnectionAborted | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe | io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut ) { return ControlFlow::Continue(()); } } ControlFlow::Break(e) } #[cfg(feature = "_tls-any")] async fn select( incoming: &mut (impl Stream> + Unpin), tasks: &mut JoinSet, crate::BoxError>>, ) -> SelectOutput where IE: Into, { let incoming_stream_future = async { match incoming.try_next().await { Ok(Some(stream)) => SelectOutput::Incoming(stream), Ok(None) => SelectOutput::Done, Err(e) => SelectOutput::TcpErr(e.into()), } }; if tasks.is_empty() { return incoming_stream_future.await; } tokio::select! { stream = incoming_stream_future => stream, accept = tasks.join_next() => { match accept.expect("JoinSet should never end") { Ok(Ok(io)) => SelectOutput::Io(io), Ok(Err(e)) => SelectOutput::TlsErr(e), Err(e) => SelectOutput::TlsErr(e.into()), } } } } #[cfg(feature = "_tls-any")] enum SelectOutput { Incoming(A), Io(ServerIo), TcpErr(crate::BoxError), TlsErr(crate::BoxError), Done, } ================================================ FILE: tonic/src/transport/server/mod.rs ================================================ //! Server implementation and builder. mod conn; mod display_error_stack; mod incoming; mod io_stream; mod service; #[cfg(feature = "_tls-any")] mod tls; #[cfg(unix)] mod unix; use tokio_stream::StreamExt as _; use tracing::{debug, trace}; #[cfg(feature = "router")] use crate::{server::NamedService, service::Routes}; #[cfg(feature = "router")] use std::convert::Infallible; pub use conn::{Connected, TcpConnectInfo}; use hyper_util::{ rt::{TokioExecutor, TokioIo, TokioTimer}, server::conn::auto::{Builder as ConnectionBuilder, HttpServerConnExec}, service::TowerToHyperService, }; #[cfg(feature = "_tls-any")] pub use tls::ServerTlsConfig; #[cfg(feature = "_tls-any")] pub use conn::TlsConnectInfo; #[cfg(feature = "_tls-any")] use self::service::TlsAcceptor; #[cfg(unix)] pub use unix::UdsConnectInfo; pub use incoming::TcpIncoming; #[cfg(feature = "_tls-any")] use crate::transport::Error; use self::service::{ConnectInfoLayer, ServerIo}; use super::service::GrpcTimeout; use crate::body::Body; use crate::service::RecoverErrorLayer; use crate::transport::server::display_error_stack::DisplayErrorStack; use bytes::Bytes; use http::{Request, Response}; use http_body_util::BodyExt; use hyper::{body::Incoming, service::Service as HyperService}; use pin_project::pin_project; use std::{ fmt, future::{self, Future}, marker::PhantomData, net::SocketAddr, pin::{Pin, pin}, sync::Arc, task::{Context, Poll, ready}, time::Duration, }; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_stream::Stream; use tower::{ Service, ServiceBuilder, ServiceExt, layer::Layer, layer::util::{Identity, Stack}, limit::concurrency::ConcurrencyLimitLayer, load_shed::LoadShedLayer, util::BoxCloneService, }; type BoxService = tower::util::BoxCloneService, Response, crate::BoxError>; type TraceInterceptor = Arc) -> tracing::Span + Send + Sync + 'static>; const DEFAULT_HTTP2_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(20); /// A default batteries included `transport` server. /// /// This provides an easy builder pattern style builder [`Server`] on top of /// `hyper` connections. This builder exposes easy configuration parameters /// for providing a fully featured http2 based gRPC server. This should provide /// a very good out of the box http2 server for use with tonic but is also a /// reference implementation that should be a good starting point for anyone /// wanting to create a more complex and/or specific implementation. #[derive(Clone)] pub struct Server { trace_interceptor: Option, concurrency_limit: Option, load_shed: bool, timeout: Option, #[cfg(feature = "_tls-any")] tls: Option, init_stream_window_size: Option, init_connection_window_size: Option, max_concurrent_streams: Option, tcp_keepalive: Option, tcp_keepalive_interval: Option, tcp_keepalive_retries: Option, tcp_nodelay: bool, http2_keepalive_interval: Option, http2_keepalive_timeout: Duration, http2_adaptive_window: Option, http2_max_pending_accept_reset_streams: Option, http2_max_local_error_reset_streams: Option, http2_max_header_list_size: Option, max_frame_size: Option, accept_http1: bool, service_builder: ServiceBuilder, max_connection_age: Option, max_connection_age_grace: Option, } impl Default for Server { fn default() -> Self { Self { trace_interceptor: None, concurrency_limit: None, load_shed: false, timeout: None, #[cfg(feature = "_tls-any")] tls: None, init_stream_window_size: None, init_connection_window_size: None, max_concurrent_streams: None, tcp_keepalive: None, tcp_keepalive_interval: None, tcp_keepalive_retries: None, tcp_nodelay: true, http2_keepalive_interval: None, http2_keepalive_timeout: DEFAULT_HTTP2_KEEPALIVE_TIMEOUT, http2_adaptive_window: None, http2_max_pending_accept_reset_streams: None, http2_max_local_error_reset_streams: None, http2_max_header_list_size: None, max_frame_size: None, accept_http1: false, service_builder: Default::default(), max_connection_age: None, max_connection_age_grace: None, } } } /// A stack based [`Service`] router. #[cfg(feature = "router")] #[derive(Clone, Debug)] pub struct Router { server: Server, routes: Routes, } impl Server { /// Create a new server builder that can configure a [`Server`]. pub fn builder() -> Self { Self::default() } } impl Server { /// Configure TLS for this server. #[cfg(feature = "_tls-any")] pub fn tls_config(self, tls_config: ServerTlsConfig) -> Result { Ok(Server { tls: Some(tls_config.tls_acceptor().map_err(Error::from_source)?), ..self }) } /// Set the concurrency limit applied to on requests inbound per connection. /// /// # Example /// /// ``` /// # use tonic::transport::Server; /// # use tower_service::Service; /// # let builder = Server::builder(); /// builder.concurrency_limit_per_connection(32); /// ``` #[must_use] pub fn concurrency_limit_per_connection(self, limit: usize) -> Self { Server { concurrency_limit: Some(limit), ..self } } /// Enable or disable load shedding. The default is disabled. /// /// When load shedding is enabled, if the service responds with not ready /// the request will immediately be rejected with a /// [`resource_exhausted`](https://docs.rs/tonic/latest/tonic/struct.Status.html#method.resource_exhausted) error. /// The default is to buffer requests. This is especially useful in combination with /// setting a concurrency limit per connection. /// /// # Example /// /// ``` /// # use tonic::transport::Server; /// # use tower_service::Service; /// # let builder = Server::builder(); /// builder.load_shed(true); /// ``` #[must_use] pub fn load_shed(self, load_shed: bool) -> Self { Server { load_shed, ..self } } /// Set a timeout on for all request handlers. /// /// # Example /// /// ``` /// # use tonic::transport::Server; /// # use tower_service::Service; /// # use std::time::Duration; /// # let builder = Server::builder(); /// builder.timeout(Duration::from_secs(30)); /// ``` #[must_use] pub fn timeout(self, timeout: Duration) -> Self { Server { timeout: Some(timeout), ..self } } /// Sets the [`SETTINGS_INITIAL_WINDOW_SIZE`][spec] option for HTTP2 /// stream-level flow control. /// /// Default is 65,535 /// /// [spec]: https://httpwg.org/specs/rfc9113.html#InitialWindowSize #[must_use] pub fn initial_stream_window_size(self, sz: impl Into>) -> Self { Server { init_stream_window_size: sz.into(), ..self } } /// Sets the max connection-level flow control for HTTP2 /// /// Default is 65,535 #[must_use] pub fn initial_connection_window_size(self, sz: impl Into>) -> Self { Server { init_connection_window_size: sz.into(), ..self } } /// Sets the [`SETTINGS_MAX_CONCURRENT_STREAMS`][spec] option for HTTP2 /// connections. /// /// Default is no limit (`None`). /// /// [spec]: https://httpwg.org/specs/rfc9113.html#n-stream-concurrency #[must_use] pub fn max_concurrent_streams(self, max: impl Into>) -> Self { Server { max_concurrent_streams: max.into(), ..self } } /// Sets the maximum time option in milliseconds that a connection may exist /// /// Default is no limit (`None`). /// /// # Example /// /// ``` /// # use tonic::transport::Server; /// # use tower_service::Service; /// # use std::time::Duration; /// # let builder = Server::builder(); /// builder.max_connection_age(Duration::from_secs(60)); /// ``` #[must_use] pub fn max_connection_age(self, max_connection_age: Duration) -> Self { Server { max_connection_age: Some(max_connection_age), ..self } } /// Sets the maximum duration that a connection may continue to exist /// **after** the graceful shutdown period (`max_connection_age`) has elapsed. /// /// This timeout only takes effect *after* a connection has exceeded its /// configured `max_connection_age`. Once that happens, the server will begin /// graceful shutdown for the connection. If the connection does not close /// gracefully within the `max_connection_age_grace` duration, the server will then /// forcefully terminate it. /// /// If no `max_connection_age` is configured, this forced shutdown timeout will /// **never trigger**, because the server will not know when to begin the /// graceful shutdown phase. /// /// Default is no limit (`None`). /// /// ``` /// # use tonic::transport::Server; /// # use tower_service::Service; /// # use std::time::Duration; /// # let builder = Server::builder(); /// builder.max_connection_age_grace(Duration::from_secs(60)); /// ``` #[must_use] pub fn max_connection_age_grace(self, max_connection_age_grace: Duration) -> Self { Server { max_connection_age_grace: Some(max_connection_age_grace), ..self } } /// Set whether HTTP2 Ping frames are enabled on accepted connections. /// /// If `None` is specified, HTTP2 keepalive is disabled, otherwise the duration /// specified will be the time interval between HTTP2 Ping frames. /// The timeout for receiving an acknowledgement of the keepalive ping /// can be set with [`Server::http2_keepalive_timeout`]. /// /// Default is no HTTP2 keepalive (`None`) /// #[must_use] pub fn http2_keepalive_interval(self, http2_keepalive_interval: Option) -> Self { Server { http2_keepalive_interval, ..self } } /// Sets a timeout for receiving an acknowledgement of the keepalive ping. /// /// If the ping is not acknowledged within the timeout, the connection will be closed. /// Does nothing if http2_keep_alive_interval is disabled. /// /// Default is 20 seconds. /// #[must_use] pub fn http2_keepalive_timeout(mut self, http2_keepalive_timeout: Option) -> Self { if let Some(timeout) = http2_keepalive_timeout { self.http2_keepalive_timeout = timeout; } self } /// Sets whether to use an adaptive flow control. Defaults to false. /// Enabling this will override the limits set in http2_initial_stream_window_size and /// http2_initial_connection_window_size. #[must_use] pub fn http2_adaptive_window(self, enabled: Option) -> Self { Server { http2_adaptive_window: enabled, ..self } } /// Configures the maximum number of pending reset streams allowed before a GOAWAY will be sent. /// /// This will default to whatever the default in h2 is. As of v0.3.17, it is 20. /// /// See for more information. #[must_use] pub fn http2_max_pending_accept_reset_streams(self, max: Option) -> Self { Server { http2_max_pending_accept_reset_streams: max, ..self } } /// Configures the maximum number of local reset streams allowed before a GOAWAY will be sent. /// /// This will default to whatever the default in hyper is. #[must_use] pub fn http2_max_local_error_reset_streams(self, max: Option) -> Self { Server { http2_max_local_error_reset_streams: max, ..self } } /// Set whether TCP keepalive messages are enabled on accepted connections. /// /// If `None` is specified, keepalive is disabled, otherwise the duration /// specified will be the time to remain idle before sending TCP keepalive /// probes. /// /// Important: This setting is ignored when using `serve_with_incoming`. /// /// Default is no keepalive (`None`) /// #[must_use] pub fn tcp_keepalive(self, tcp_keepalive: Option) -> Self { Server { tcp_keepalive, ..self } } /// Set the value of `TCP_KEEPINTVL` option for accepted connections. /// /// This option specifies the time interval between subsequent keepalive probes. /// This setting only takes effect if [`tcp_keepalive`](Self::tcp_keepalive) is also set. /// /// Important: This setting is ignored when using `serve_with_incoming`. /// /// Default is `None` (system default). /// /// Note: This option is only available on some platforms (Linux, macOS, Windows, etc.). #[must_use] pub fn tcp_keepalive_interval(self, tcp_keepalive_interval: Option) -> Self { Server { tcp_keepalive_interval, ..self } } /// Set the value of `TCP_KEEPCNT` option for accepted connections. /// /// This option specifies the maximum number of keepalive probes that should be sent /// before dropping the connection. /// This setting only takes effect if [`tcp_keepalive`](Self::tcp_keepalive) is also set. /// /// Important: This setting is ignored when using `serve_with_incoming`. /// /// Default is `None` (system default). /// /// Note: This option is only available on some platforms (Linux, macOS, Windows, etc.). #[must_use] pub fn tcp_keepalive_retries(self, tcp_keepalive_retries: Option) -> Self { Server { tcp_keepalive_retries, ..self } } /// Set the value of `TCP_NODELAY` option for accepted connections. Enabled by default. /// /// Important: This setting is ignored when using `serve_with_incoming`. #[must_use] pub fn tcp_nodelay(self, enabled: bool) -> Self { Server { tcp_nodelay: enabled, ..self } } /// Sets the max size of received header frames. /// /// This will default to whatever the default in hyper is. As of v1.4.1, it is 16 KiB. #[must_use] pub fn http2_max_header_list_size(self, max: impl Into>) -> Self { Server { http2_max_header_list_size: max.into(), ..self } } /// Sets the maximum frame size to use for HTTP2. /// /// Passing `None` will do nothing. /// /// If not set, will default from underlying transport. #[must_use] pub fn max_frame_size(self, frame_size: impl Into>) -> Self { Server { max_frame_size: frame_size.into(), ..self } } /// Allow this server to accept http1 requests. /// /// Accepting http1 requests is only useful when developing `grpc-web` /// enabled services. If this setting is set to `true` but services are /// not correctly configured to handle grpc-web requests, your server may /// return confusing (but correct) protocol errors. /// /// Default is `false`. #[must_use] pub fn accept_http1(self, accept_http1: bool) -> Self { Server { accept_http1, ..self } } /// Intercept inbound headers and add a [`tracing::Span`] to each response future. #[must_use] pub fn trace_fn(self, f: F) -> Self where F: Fn(&http::Request<()>) -> tracing::Span + Send + Sync + 'static, { Server { trace_interceptor: Some(Arc::new(f)), ..self } } /// Create a router with the `S` typed service as the first service. /// /// This will clone the `Server` builder and create a router that will /// route around different services. #[cfg(feature = "router")] pub fn add_service(&mut self, svc: S) -> Router where S: Service, Error = Infallible> + NamedService + Clone + Send + Sync + 'static, S::Response: axum::response::IntoResponse, S::Future: Send + 'static, L: Clone, { Router::new(self.clone(), Routes::new(svc)) } /// Create a router with the optional `S` typed service as the first service. /// /// This will clone the `Server` builder and create a router that will /// route around different services. /// /// # Note /// Even when the argument given is `None` this will capture *all* requests to this service name. /// As a result, one cannot use this to toggle between two identically named implementations. #[cfg(feature = "router")] pub fn add_optional_service(&mut self, svc: Option) -> Router where S: Service, Error = Infallible> + NamedService + Clone + Send + Sync + 'static, S::Response: axum::response::IntoResponse, S::Future: Send + 'static, L: Clone, { let routes = svc.map(Routes::new).unwrap_or_default(); Router::new(self.clone(), routes) } /// Create a router with given [`Routes`]. /// /// This will clone the `Server` builder and create a router that will /// route around different services that were already added to the provided `routes`. #[cfg(feature = "router")] pub fn add_routes(&mut self, routes: Routes) -> Router where L: Clone, { Router::new(self.clone(), routes) } /// Set the [Tower] [`Layer`] all services will be wrapped in. /// /// This enables using middleware from the [Tower ecosystem][eco]. /// /// # Example /// /// ``` /// # use tonic::transport::Server; /// # use tower_service::Service; /// use tower::timeout::TimeoutLayer; /// use std::time::Duration; /// /// # let mut builder = Server::builder(); /// builder.layer(TimeoutLayer::new(Duration::from_secs(30))); /// ``` /// /// Note that timeouts should be set using [`Server::timeout`]. `TimeoutLayer` is only used /// here as an example. /// /// You can build more complex layers using [`ServiceBuilder`]. Those layers can include /// [interceptors]: /// /// ``` /// # use tonic::transport::Server; /// # use tower_service::Service; /// use tower::ServiceBuilder; /// use std::time::Duration; /// use tonic::{Request, Status, service::InterceptorLayer}; /// /// fn auth_interceptor(request: Request<()>) -> Result, Status> { /// if valid_credentials(&request) { /// Ok(request) /// } else { /// Err(Status::unauthenticated("invalid credentials")) /// } /// } /// /// fn valid_credentials(request: &Request<()>) -> bool { /// // ... /// # true /// } /// /// fn some_other_interceptor(request: Request<()>) -> Result, Status> { /// Ok(request) /// } /// /// let layer = ServiceBuilder::new() /// .load_shed() /// .timeout(Duration::from_secs(30)) /// .layer(InterceptorLayer::new(auth_interceptor)) /// .layer(InterceptorLayer::new(some_other_interceptor)) /// .into_inner(); /// /// Server::builder().layer(layer); /// ``` /// /// [Tower]: https://github.com/tower-rs/tower /// [`Layer`]: tower::layer::Layer /// [eco]: https://github.com/tower-rs /// [`ServiceBuilder`]: tower::ServiceBuilder /// [interceptors]: crate::service::Interceptor pub fn layer(self, new_layer: NewLayer) -> Server> { Server { service_builder: self.service_builder.layer(new_layer), trace_interceptor: self.trace_interceptor, concurrency_limit: self.concurrency_limit, load_shed: self.load_shed, timeout: self.timeout, #[cfg(feature = "_tls-any")] tls: self.tls, init_stream_window_size: self.init_stream_window_size, init_connection_window_size: self.init_connection_window_size, max_concurrent_streams: self.max_concurrent_streams, tcp_keepalive: self.tcp_keepalive, tcp_keepalive_interval: self.tcp_keepalive_interval, tcp_keepalive_retries: self.tcp_keepalive_retries, tcp_nodelay: self.tcp_nodelay, http2_keepalive_interval: self.http2_keepalive_interval, http2_keepalive_timeout: self.http2_keepalive_timeout, http2_adaptive_window: self.http2_adaptive_window, http2_max_pending_accept_reset_streams: self.http2_max_pending_accept_reset_streams, http2_max_header_list_size: self.http2_max_header_list_size, http2_max_local_error_reset_streams: self.http2_max_local_error_reset_streams, max_frame_size: self.max_frame_size, accept_http1: self.accept_http1, max_connection_age: self.max_connection_age, max_connection_age_grace: self.max_connection_age_grace, } } fn bind_incoming(&self, addr: SocketAddr) -> Result { Ok(TcpIncoming::bind(addr) .map_err(super::Error::from_source)? .with_nodelay(Some(self.tcp_nodelay)) .with_keepalive(self.tcp_keepalive) .with_keepalive_interval(self.tcp_keepalive_interval) .with_keepalive_retries(self.tcp_keepalive_retries)) } /// Serve the service. pub async fn serve(self, addr: SocketAddr, svc: S) -> Result<(), super::Error> where L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, <>::Service as Service>>::Future: Send, <>::Service as Service>>::Error: Into + Send + 'static, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { let incoming = self.bind_incoming(addr)?; self.serve_with_incoming(svc, incoming).await } /// Serve the service with the shutdown signal. pub async fn serve_with_shutdown( self, addr: SocketAddr, svc: S, signal: F, ) -> Result<(), super::Error> where L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, <>::Service as Service>>::Future: Send, <>::Service as Service>>::Error: Into + Send + 'static, F: Future, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { let incoming = self.bind_incoming(addr)?; self.serve_with_incoming_shutdown(svc, incoming, signal) .await } /// Serve the service on the provided incoming stream. /// /// The `tcp_nodelay` and `tcp_keepalive` settings are ignored when using this method. pub async fn serve_with_incoming( self, svc: S, incoming: I, ) -> Result<(), super::Error> where L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, <>::Service as Service>>::Future: Send, <>::Service as Service>>::Error: Into + Send + 'static, I: Stream>, IO: AsyncRead + AsyncWrite + Connected + Unpin + Send + 'static, IE: Into, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { self.serve_internal(svc, incoming, Option::>::None) .await } /// Serve the service with the signal on the provided incoming stream. pub async fn serve_with_incoming_shutdown( self, svc: S, incoming: I, signal: F, ) -> Result<(), super::Error> where L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, <>::Service as Service>>::Future: Send, <>::Service as Service>>::Error: Into + Send + 'static, I: Stream>, IO: AsyncRead + AsyncWrite + Connected + Unpin + Send + 'static, IE: Into, F: Future, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { self.serve_internal(svc, incoming, Some(signal)).await } async fn serve_internal( self, svc: S, incoming: I, signal: Option, ) -> Result<(), super::Error> where L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, <>::Service as Service>>::Future: Send, <>::Service as Service>>::Error: Into + Send + 'static, I: Stream>, IO: AsyncRead + AsyncWrite + Connected + Unpin + Send + 'static, IE: Into, F: Future, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { let trace_interceptor = self.trace_interceptor.clone(); let concurrency_limit = self.concurrency_limit; let load_shed = self.load_shed; let init_connection_window_size = self.init_connection_window_size; let init_stream_window_size = self.init_stream_window_size; let max_concurrent_streams = self.max_concurrent_streams; let timeout = self.timeout; let max_header_list_size = self.http2_max_header_list_size; let max_frame_size = self.max_frame_size; let http2_only = !self.accept_http1; let http2_keepalive_interval = self.http2_keepalive_interval; let http2_keepalive_timeout = self.http2_keepalive_timeout; let http2_adaptive_window = self.http2_adaptive_window; let http2_max_pending_accept_reset_streams = self.http2_max_pending_accept_reset_streams; let http2_max_local_error_reset_streams = self.http2_max_local_error_reset_streams; let max_connection_age = self.max_connection_age; let max_connection_age_grace = self.max_connection_age_grace; let svc = self.service_builder.service(svc); let incoming = io_stream::ServerIoStream::new( incoming, #[cfg(feature = "_tls-any")] self.tls, ); let mut svc = MakeSvc { inner: svc, concurrency_limit, load_shed, timeout, trace_interceptor, _io: PhantomData, }; let server = { let mut builder = ConnectionBuilder::new(TokioExecutor::new()); if http2_only { builder = builder.http2_only(); } builder .http2() .timer(TokioTimer::new()) .initial_connection_window_size(init_connection_window_size) .initial_stream_window_size(init_stream_window_size) .max_concurrent_streams(max_concurrent_streams) .keep_alive_interval(http2_keepalive_interval) .keep_alive_timeout(http2_keepalive_timeout) .adaptive_window(http2_adaptive_window.unwrap_or_default()) .max_pending_accept_reset_streams(http2_max_pending_accept_reset_streams) .max_local_error_reset_streams(http2_max_local_error_reset_streams) .max_frame_size(max_frame_size); if let Some(max_header_list_size) = max_header_list_size { builder.http2().max_header_list_size(max_header_list_size); } builder }; let (signal_tx, signal_rx) = tokio::sync::watch::channel(()); let signal_tx = Arc::new(signal_tx); let graceful = signal.is_some(); let mut sig = pin!(Fuse { inner: signal }); let mut incoming = pin!(incoming); loop { tokio::select! { _ = &mut sig => { trace!("signal received, shutting down"); break; }, io = incoming.next() => { let io = match io { Some(Ok(io)) => io, Some(Err(e)) => { trace!("error accepting connection: {}", DisplayErrorStack(&*e)); continue; }, None => { break }, }; trace!("connection accepted"); let req_svc = svc .call(&io) .await .map_err(super::Error::from_source)?; let hyper_io = TokioIo::new(io); let hyper_svc = TowerToHyperService::new(req_svc.map_request(|req: Request| req.map(Body::new))); serve_connection(hyper_io, hyper_svc, server.clone(), graceful.then(|| signal_rx.clone()), max_connection_age, max_connection_age_grace); } } } if graceful { let _ = signal_tx.send(()); drop(signal_rx); trace!( "waiting for {} connections to close", signal_tx.receiver_count() ); // Wait for all connections to close signal_tx.closed().await; } Ok(()) } } enum TimeoutAction { GracefulShutdown, ForcefulShutdown, } async fn connection_timeout_future( max_connection_age: Option, max_connection_age_grace: Option, ) -> TimeoutAction { if let Some(age) = max_connection_age { tokio::time::sleep(age).await; if let Some(grace) = max_connection_age_grace { tokio::time::sleep(grace).await; TimeoutAction::ForcefulShutdown } else { TimeoutAction::GracefulShutdown } } else { future::pending().await } } // This is moved to its own function as a way to get around // https://github.com/rust-lang/rust/issues/102211 fn serve_connection( hyper_io: IO, hyper_svc: S, builder: ConnectionBuilder, mut watcher: Option>, max_connection_age: Option, max_connection_age_grace: Option, ) where B: http_body::Body + Send + 'static, B::Data: Send, B::Error: Into> + Send + Sync, IO: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static, S: HyperService, Response = Response> + Clone + Send + 'static, S::Future: Send + 'static, S::Error: Into> + Send, E: HttpServerConnExec + Send + Sync + 'static, { tokio::spawn(async move { { let mut sig = pin!(Fuse { inner: watcher.as_mut().map(|w| w.changed()), }); let mut conn = pin!(builder.serve_connection(hyper_io, hyper_svc)); let mut connection_timeout = pin!(connection_timeout_future( max_connection_age, max_connection_age_grace, )); loop { tokio::select! { rv = &mut conn => { if let Err(err) = rv { debug!("failed serving connection: {}", DisplayErrorStack(&*err)); } break; }, timeout_action = &mut connection_timeout => { match timeout_action { TimeoutAction::GracefulShutdown => { conn.as_mut().graceful_shutdown(); }, TimeoutAction::ForcefulShutdown => { debug!("forcefully closed connection"); break; } } }, _ = &mut sig => { conn.as_mut().graceful_shutdown(); }, } } } drop(watcher); trace!("connection closed"); }); } #[cfg(feature = "router")] impl Router { pub(crate) fn new(server: Server, routes: Routes) -> Self { Self { server, routes } } } #[cfg(feature = "router")] impl Router { /// Add a new service to this router. pub fn add_service(mut self, svc: S) -> Self where S: Service, Error = Infallible> + NamedService + Clone + Send + Sync + 'static, S::Response: axum::response::IntoResponse, S::Future: Send + 'static, { self.routes = self.routes.add_service(svc); self } /// Add a new optional service to this router. /// /// # Note /// Even when the argument given is `None` this will capture *all* requests to this service name. /// As a result, one cannot use this to toggle between two identically named implementations. pub fn add_optional_service(mut self, svc: Option) -> Self where S: Service, Error = Infallible> + NamedService + Clone + Send + Sync + 'static, S::Response: axum::response::IntoResponse, S::Future: Send + 'static, { if let Some(svc) = svc { self.routes = self.routes.add_service(svc); } self } /// Consume this [`Server`] creating a future that will execute the server /// on [tokio]'s default executor. /// /// [`Server`]: struct.Server.html /// [tokio]: https://docs.rs/tokio pub async fn serve(self, addr: SocketAddr) -> Result<(), super::Error> where L: Layer + Clone, L::Service: Service, Response = Response> + Clone + Send + 'static, <>::Service as Service>>::Future: Send, <>::Service as Service>>::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { self.server.serve(addr, self.routes.prepare()).await } /// Consume this [`Server`] creating a future that will execute the server /// on [tokio]'s default executor. And shutdown when the provided signal /// is received. /// /// [`Server`]: struct.Server.html /// [tokio]: https://docs.rs/tokio pub async fn serve_with_shutdown, ResBody>( self, addr: SocketAddr, signal: F, ) -> Result<(), super::Error> where L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, <>::Service as Service>>::Future: Send, <>::Service as Service>>::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { self.server .serve_with_shutdown(addr, self.routes.prepare(), signal) .await } /// Consume this [`Server`] creating a future that will execute the server /// on the provided incoming stream of `AsyncRead + AsyncWrite`. /// /// This method discards any provided [`Server`] TCP configuration. /// /// [`Server`]: struct.Server.html pub async fn serve_with_incoming( self, incoming: I, ) -> Result<(), super::Error> where I: Stream>, IO: AsyncRead + AsyncWrite + Connected + Unpin + Send + 'static, IE: Into, L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, <>::Service as Service>>::Future: Send, <>::Service as Service>>::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { self.server .serve_with_incoming(self.routes.prepare(), incoming) .await } /// Consume this [`Server`] creating a future that will execute the server /// on the provided incoming stream of `AsyncRead + AsyncWrite`. Similar to /// `serve_with_shutdown` this method will also take a signal future to /// gracefully shutdown the server. /// /// This method discards any provided [`Server`] TCP configuration. /// /// [`Server`]: struct.Server.html pub async fn serve_with_incoming_shutdown( self, incoming: I, signal: F, ) -> Result<(), super::Error> where I: Stream>, IO: AsyncRead + AsyncWrite + Connected + Unpin + Send + 'static, IE: Into, F: Future, L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, <>::Service as Service>>::Future: Send, <>::Service as Service>>::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { self.server .serve_with_incoming_shutdown(self.routes.prepare(), incoming, signal) .await } } impl fmt::Debug for Server { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Builder").finish() } } #[derive(Clone)] struct Svc { inner: S, trace_interceptor: Option, } impl Service> for Svc where S: Service, Response = Response>, S::Error: Into, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { type Response = Response; type Error = crate::BoxError; type Future = SvcFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, mut req: Request) -> Self::Future { let span = if let Some(trace_interceptor) = &self.trace_interceptor { let (parts, body) = req.into_parts(); let bodyless_request = Request::from_parts(parts, ()); let span = trace_interceptor(&bodyless_request); let (parts, _) = bodyless_request.into_parts(); req = Request::from_parts(parts, body); span } else { tracing::Span::none() }; SvcFuture { inner: self.inner.call(req), span, } } } #[pin_project] struct SvcFuture { #[pin] inner: F, span: tracing::Span, } impl Future for SvcFuture where F: Future, E>>, E: Into, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { type Output = Result, crate::BoxError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let _guard = this.span.enter(); let response: Response = ready!(this.inner.poll(cx)).map_err(Into::into)?; let response = response.map(|body| Body::new(body.map_err(Into::into))); Poll::Ready(Ok(response)) } } impl fmt::Debug for Svc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Svc").finish() } } #[derive(Clone)] struct MakeSvc { concurrency_limit: Option, load_shed: bool, timeout: Option, inner: S, trace_interceptor: Option, _io: PhantomData IO>, } impl Service<&ServerIo> for MakeSvc where IO: Connected + 'static, S: Service, Response = Response> + Clone + Send + 'static, S::Future: Send, S::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { type Response = BoxService; type Error = crate::BoxError; type Future = future::Ready>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Ok(()).into() } fn call(&mut self, io: &ServerIo) -> Self::Future { let conn_info = io.connect_info(); let svc = self.inner.clone(); let concurrency_limit = self.concurrency_limit; let timeout = self.timeout; let trace_interceptor = self.trace_interceptor.clone(); let svc = ServiceBuilder::new() .layer(RecoverErrorLayer::new()) .option_layer(self.load_shed.then_some(LoadShedLayer::new())) .option_layer(concurrency_limit.map(ConcurrencyLimitLayer::new)) .layer_fn(|s| GrpcTimeout::new(s, timeout)) .service(svc); let svc = ServiceBuilder::new() .layer(BoxCloneService::layer()) .layer(ConnectInfoLayer::new(conn_info.clone())) .service(Svc { inner: svc, trace_interceptor, }); future::ready(Ok(svc)) } } // From `futures-util` crate, borrowed since this is the only dependency tonic requires. // LICENSE: MIT or Apache-2.0 // A future which only yields `Poll::Ready` once, and thereafter yields `Poll::Pending`. #[pin_project] struct Fuse { #[pin] inner: Option, } impl Future for Fuse where F: Future, { type Output = F::Output; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.as_mut().project().inner.as_pin_mut() { Some(fut) => fut.poll(cx).map(|output| { self.project().inner.set(None); output }), None => Poll::Pending, } } } #[cfg(test)] mod tests { use super::*; use crate::transport::Server; use std::time::Duration; #[tokio::test(start_paused = true)] async fn test_connection_timeout_no_max_age() { let future = connection_timeout_future(None, None); tokio::select! { _ = future => { panic!("timeout future should never complete when max_connection_age is None"); } _ = tokio::time::sleep(Duration::from_secs(1000)) => { } } } #[tokio::test(start_paused = true)] async fn test_connection_timeout_with_max_connection_age() { let future = connection_timeout_future(Some(Duration::from_secs(10)), None); let action = future.await; assert!(matches!(action, TimeoutAction::GracefulShutdown)); } #[tokio::test(start_paused = true)] async fn test_connection_timeout_with_max_connection_age_grace() { let mut future = pin!(connection_timeout_future( Some(Duration::from_secs(10)), Some(Duration::from_secs(5)), )); tokio::select! { _ = &mut future => { panic!("should not complete before max_connection_age"); } _ = tokio::time::sleep(Duration::from_secs(9)) => {} } tokio::select! { _ = &mut future => { panic!("should not complete before max_connection_age_grace"); } _ = tokio::time::sleep(Duration::from_secs(4)) => {} } let action = future.await; assert!(matches!(action, TimeoutAction::ForcefulShutdown)); } #[test] fn server_tcp_defaults() { const EXAMPLE_TCP_KEEPALIVE: Duration = Duration::from_secs(10); const EXAMPLE_TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5); const EXAMPLE_TCP_KEEPALIVE_RETRIES: u32 = 3; // Using ::builder() or ::default() should do the same thing let server_via_builder = Server::builder(); assert!(server_via_builder.tcp_nodelay); assert_eq!(server_via_builder.tcp_keepalive, None); assert_eq!(server_via_builder.tcp_keepalive_interval, None); assert_eq!(server_via_builder.tcp_keepalive_retries, None); let server_via_default = Server::default(); assert!(server_via_default.tcp_nodelay); assert_eq!(server_via_default.tcp_keepalive, None); assert_eq!(server_via_default.tcp_keepalive_interval, None); assert_eq!(server_via_default.tcp_keepalive_retries, None); // overriding should be possible let server_via_builder = Server::builder() .tcp_nodelay(false) .tcp_keepalive(Some(EXAMPLE_TCP_KEEPALIVE)) .tcp_keepalive_interval(Some(EXAMPLE_TCP_KEEPALIVE_INTERVAL)) .tcp_keepalive_retries(Some(EXAMPLE_TCP_KEEPALIVE_RETRIES)); assert!(!server_via_builder.tcp_nodelay); assert_eq!( server_via_builder.tcp_keepalive, Some(EXAMPLE_TCP_KEEPALIVE) ); assert_eq!( server_via_builder.tcp_keepalive_interval, Some(EXAMPLE_TCP_KEEPALIVE_INTERVAL) ); assert_eq!( server_via_builder.tcp_keepalive_retries, Some(EXAMPLE_TCP_KEEPALIVE_RETRIES) ); } } ================================================ FILE: tonic/src/transport/server/service/io.rs ================================================ use crate::transport::server::Connected; use std::io; use std::io::IoSlice; use std::pin::Pin; use std::task::{Context, Poll}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; #[cfg(feature = "_tls-any")] use tokio_rustls::server::TlsStream; use tower_layer::Layer; use tower_service::Service; #[derive(Debug, Clone)] pub(crate) struct ConnectInfoLayer { connect_info: T, } impl ConnectInfoLayer { pub(crate) fn new(connect_info: T) -> Self { Self { connect_info } } } impl Layer for ConnectInfoLayer where T: Clone, { type Service = ConnectInfo; fn layer(&self, inner: S) -> Self::Service { ConnectInfo::new(inner, self.connect_info.clone()) } } #[derive(Debug, Clone)] pub(crate) struct ConnectInfo { inner: S, connect_info: T, } impl ConnectInfo { fn new(inner: S, connect_info: T) -> Self { Self { inner, connect_info, } } } impl Service> for ConnectInfo> where S: Service>, IO: Connected, { type Response = S::Response; type Error = S::Error; type Future = S::Future; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, mut req: http::Request) -> Self::Future { match self.connect_info.clone() { ServerIoConnectInfo::Io(inner) => { req.extensions_mut().insert(inner); } #[cfg(feature = "_tls-any")] ServerIoConnectInfo::TlsIo(inner) => { req.extensions_mut().insert(inner.get_ref().clone()); req.extensions_mut().insert(inner); } } self.inner.call(req) } } pub(crate) enum ServerIo { Io(IO), #[cfg(feature = "_tls-any")] TlsIo(Box>), } pub(crate) enum ServerIoConnectInfo { Io(::ConnectInfo), #[cfg(feature = "_tls-any")] TlsIo( as Connected>::ConnectInfo), } impl Clone for ServerIoConnectInfo { fn clone(&self) -> Self { match self { Self::Io(io) => Self::Io(io.clone()), #[cfg(feature = "_tls-any")] Self::TlsIo(io) => Self::TlsIo(io.clone()), } } } impl ServerIo { pub(in crate::transport) fn new_io(io: IO) -> Self { Self::Io(io) } #[cfg(feature = "_tls-any")] pub(in crate::transport) fn new_tls_io(io: TlsStream) -> Self { Self::TlsIo(Box::new(io)) } pub(in crate::transport) fn connect_info(&self) -> ServerIoConnectInfo where IO: Connected, { match self { Self::Io(io) => ServerIoConnectInfo::Io(io.connect_info()), #[cfg(feature = "_tls-any")] Self::TlsIo(io) => ServerIoConnectInfo::TlsIo(io.connect_info()), } } } impl AsyncRead for ServerIo where IO: AsyncWrite + AsyncRead + Unpin, { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { match &mut *self { Self::Io(io) => Pin::new(io).poll_read(cx, buf), #[cfg(feature = "_tls-any")] Self::TlsIo(io) => Pin::new(io).poll_read(cx, buf), } } } impl AsyncWrite for ServerIo where IO: AsyncWrite + AsyncRead + Unpin, { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { match &mut *self { Self::Io(io) => Pin::new(io).poll_write(cx, buf), #[cfg(feature = "_tls-any")] Self::TlsIo(io) => Pin::new(io).poll_write(cx, buf), } } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match &mut *self { Self::Io(io) => Pin::new(io).poll_flush(cx), #[cfg(feature = "_tls-any")] Self::TlsIo(io) => Pin::new(io).poll_flush(cx), } } fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match &mut *self { Self::Io(io) => Pin::new(io).poll_shutdown(cx), #[cfg(feature = "_tls-any")] Self::TlsIo(io) => Pin::new(io).poll_shutdown(cx), } } fn poll_write_vectored( mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[IoSlice<'_>], ) -> Poll> { match &mut *self { Self::Io(io) => Pin::new(io).poll_write_vectored(cx, bufs), #[cfg(feature = "_tls-any")] Self::TlsIo(io) => Pin::new(io).poll_write_vectored(cx, bufs), } } fn is_write_vectored(&self) -> bool { match self { Self::Io(io) => io.is_write_vectored(), #[cfg(feature = "_tls-any")] Self::TlsIo(io) => io.is_write_vectored(), } } } ================================================ FILE: tonic/src/transport/server/service/mod.rs ================================================ mod io; pub(crate) use self::io::{ConnectInfoLayer, ServerIo}; #[cfg(feature = "_tls-any")] mod tls; #[cfg(feature = "_tls-any")] pub(crate) use self::tls::TlsAcceptor; ================================================ FILE: tonic/src/transport/server/service/tls.rs ================================================ use std::{fmt, sync::Arc, time::Duration}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time; use tokio_rustls::{ TlsAcceptor as RustlsAcceptor, rustls::{RootCertStore, ServerConfig, server::WebPkiClientVerifier}, server::TlsStream, }; use crate::transport::{ Certificate, Identity, service::tls::{ ALPN_H2, TlsError, convert_certificate_to_pki_types, convert_identity_to_pki_types, }, }; #[derive(Clone)] pub(crate) struct TlsAcceptor { inner: Arc, timeout: Option, } impl TlsAcceptor { pub(crate) fn new( identity: &Identity, client_ca_root: Option<&Certificate>, client_auth_optional: bool, ignore_client_order: bool, use_key_log: bool, timeout: Option, ) -> Result { let builder = ServerConfig::builder(); let builder = match client_ca_root { None => builder.with_no_client_auth(), Some(cert) => { let mut roots = RootCertStore::empty(); roots.add_parsable_certificates(convert_certificate_to_pki_types(cert)?); let verifier = if client_auth_optional { WebPkiClientVerifier::builder(roots.into()).allow_unauthenticated() } else { WebPkiClientVerifier::builder(roots.into()) } .build()?; builder.with_client_cert_verifier(verifier) } }; let (cert, key) = convert_identity_to_pki_types(identity)?; let mut config = builder.with_single_cert(cert, key)?; config.ignore_client_order = ignore_client_order; if use_key_log { config.key_log = Arc::new(tokio_rustls::rustls::KeyLogFile::new()); } config.alpn_protocols.push(ALPN_H2.into()); Ok(Self { inner: Arc::new(config), timeout, }) } pub(crate) async fn accept(&self, io: IO) -> Result, crate::BoxError> where IO: AsyncRead + AsyncWrite + Unpin, { let acceptor = RustlsAcceptor::from(self.inner.clone()); let accept_fut = acceptor.accept(io); match self.timeout { Some(timeout) => time::timeout(timeout, accept_fut) .await .map_err(|_| TlsError::HandshakeTimeout)?, None => accept_fut.await, } .map_err(Into::into) } } impl fmt::Debug for TlsAcceptor { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TlsAcceptor").finish() } } ================================================ FILE: tonic/src/transport/server/tls.rs ================================================ use std::{fmt, time::Duration}; use super::service::TlsAcceptor; use crate::transport::tls::{Certificate, Identity}; /// Configures TLS settings for servers. #[derive(Clone, Default)] pub struct ServerTlsConfig { identity: Option, client_ca_root: Option, client_auth_optional: bool, ignore_client_order: bool, use_key_log: bool, timeout: Option, } impl fmt::Debug for ServerTlsConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ServerTlsConfig").finish() } } impl ServerTlsConfig { /// Creates a new `ServerTlsConfig`. pub fn new() -> Self { ServerTlsConfig::default() } /// Sets the [`Identity`] of the server. pub fn identity(self, identity: Identity) -> Self { ServerTlsConfig { identity: Some(identity), ..self } } /// Sets a certificate against which to validate client TLS certificates. pub fn client_ca_root(self, cert: Certificate) -> Self { ServerTlsConfig { client_ca_root: Some(cert), ..self } } /// Sets whether client certificate verification is optional. /// /// This option has effect only if CA certificate is set. /// /// # Default /// By default, this option is set to `false`. pub fn client_auth_optional(self, optional: bool) -> Self { ServerTlsConfig { client_auth_optional: optional, ..self } } /// Sets whether the server's cipher preferences are followed instead of the client's. /// /// # Default /// By default, this option is set to `false`. pub fn ignore_client_order(self, ignore_client_order: bool) -> Self { ServerTlsConfig { ignore_client_order, ..self } } /// Use key log as specified by the `SSLKEYLOGFILE` environment variable. pub fn use_key_log(self) -> Self { ServerTlsConfig { use_key_log: true, ..self } } /// Sets the timeout for the TLS handshake. pub fn timeout(self, timeout: Duration) -> Self { ServerTlsConfig { timeout: Some(timeout), ..self } } pub(crate) fn tls_acceptor(&self) -> Result { TlsAcceptor::new( self.identity.as_ref().unwrap(), self.client_ca_root.as_ref(), self.client_auth_optional, self.ignore_client_order, self.use_key_log, self.timeout, ) } } ================================================ FILE: tonic/src/transport/server/unix.rs ================================================ use super::Connected; use std::sync::Arc; /// Connection info for Unix domain socket streams. /// /// This type will be accessible through [request extensions][ext] if you're using /// a unix stream. /// /// See [Connected] for more details. /// /// [ext]: crate::Request::extensions #[derive(Clone, Debug)] pub struct UdsConnectInfo { /// Peer address. This will be "unnamed" for client unix sockets. pub peer_addr: Option>, /// Process credentials for the unix socket. pub peer_cred: Option, } impl Connected for tokio::net::UnixStream { type ConnectInfo = UdsConnectInfo; fn connect_info(&self) -> Self::ConnectInfo { UdsConnectInfo { peer_addr: self.peer_addr().ok().map(Arc::new), peer_cred: self.peer_cred().ok(), } } } ================================================ FILE: tonic/src/transport/service/grpc_timeout.rs ================================================ use crate::{TimeoutExpired, metadata::GRPC_TIMEOUT_HEADER}; use http::{HeaderMap, HeaderValue, Request}; use pin_project::pin_project; use std::{ future::Future, pin::Pin, task::{Context, Poll, ready}, time::Duration, }; use tokio::time::Sleep; use tower_service::Service; #[derive(Debug, Clone)] pub(crate) struct GrpcTimeout { inner: S, server_timeout: Option, } impl GrpcTimeout { pub(crate) fn new(inner: S, server_timeout: Option) -> Self { Self { inner, server_timeout, } } } impl Service> for GrpcTimeout where S: Service>, S::Error: Into, { type Response = S::Response; type Error = crate::BoxError; type Future = ResponseFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, req: Request) -> Self::Future { let client_timeout = try_parse_grpc_timeout(req.headers()).unwrap_or_else(|e| { tracing::trace!("Error parsing `grpc-timeout` header {:?}", e); None }); // Use the shorter of the two durations, if either are set let timeout_duration = match (client_timeout, self.server_timeout) { (None, None) => None, (Some(dur), None) => Some(dur), (None, Some(dur)) => Some(dur), (Some(header), Some(server)) => { let shorter_duration = std::cmp::min(header, server); Some(shorter_duration) } }; ResponseFuture { inner: self.inner.call(req), sleep: timeout_duration.map(tokio::time::sleep), } } } #[pin_project] pub(crate) struct ResponseFuture { #[pin] inner: F, #[pin] sleep: Option, } impl Future for ResponseFuture where F: Future>, E: Into, { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); if let ready @ Poll::Ready(_) = this.inner.poll(cx) { return ready.map_err(Into::into); } if let Some(sleep) = this.sleep.as_pin_mut() { ready!(sleep.poll(cx)); return Poll::Ready(Err(TimeoutExpired(()).into())); } Poll::Pending } } const SECONDS_IN_HOUR: u64 = 60 * 60; const SECONDS_IN_MINUTE: u64 = 60; /// Tries to parse the `grpc-timeout` header if it is present. If we fail to parse, returns /// the value we attempted to parse. /// /// Follows the [gRPC over HTTP2 spec](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md). fn try_parse_grpc_timeout( headers: &HeaderMap, ) -> Result, &HeaderValue> { let Some(val) = headers.get(GRPC_TIMEOUT_HEADER) else { return Ok(None); }; let (timeout_value, timeout_unit) = val .to_str() .map_err(|_| val) .and_then(|s| if s.is_empty() { Err(val) } else { Ok(s) })? // `HeaderValue::to_str` only returns `Ok` if the header contains ASCII so this // `split_at` will never panic from trying to split in the middle of a character. // See https://docs.rs/http/1/http/header/struct.HeaderValue.html#method.to_str // // `len - 1` also wont panic since we just checked `s.is_empty`. .split_at(val.len() - 1); // gRPC spec specifies `TimeoutValue` will be at most 8 digits // Caping this at 8 digits also prevents integer overflow from ever occurring if timeout_value.len() > 8 { return Err(val); } let timeout_value: u64 = timeout_value.parse().map_err(|_| val)?; let duration = match timeout_unit { // Hours "H" => Duration::from_secs(timeout_value * SECONDS_IN_HOUR), // Minutes "M" => Duration::from_secs(timeout_value * SECONDS_IN_MINUTE), // Seconds "S" => Duration::from_secs(timeout_value), // Milliseconds "m" => Duration::from_millis(timeout_value), // Microseconds "u" => Duration::from_micros(timeout_value), // Nanoseconds "n" => Duration::from_nanos(timeout_value), _ => return Err(val), }; Ok(Some(duration)) } #[cfg(test)] mod tests { use super::*; use quickcheck::{Arbitrary, Gen}; use quickcheck_macros::quickcheck; // Helper function to reduce the boiler plate of our test cases fn setup_map_try_parse(val: Option<&str>) -> Result, HeaderValue> { let mut hm = HeaderMap::new(); if let Some(v) = val { let hv = HeaderValue::from_str(v).unwrap(); hm.insert(GRPC_TIMEOUT_HEADER, hv); }; try_parse_grpc_timeout(&hm).map_err(|e| e.clone()) } #[test] fn test_hours() { let parsed_duration = setup_map_try_parse(Some("3H")).unwrap().unwrap(); assert_eq!(Duration::from_secs(3 * 60 * 60), parsed_duration); } #[test] fn test_minutes() { let parsed_duration = setup_map_try_parse(Some("1M")).unwrap().unwrap(); assert_eq!(Duration::from_secs(60), parsed_duration); } #[test] fn test_seconds() { let parsed_duration = setup_map_try_parse(Some("42S")).unwrap().unwrap(); assert_eq!(Duration::from_secs(42), parsed_duration); } #[test] fn test_milliseconds() { let parsed_duration = setup_map_try_parse(Some("13m")).unwrap().unwrap(); assert_eq!(Duration::from_millis(13), parsed_duration); } #[test] fn test_microseconds() { let parsed_duration = setup_map_try_parse(Some("2u")).unwrap().unwrap(); assert_eq!(Duration::from_micros(2), parsed_duration); } #[test] fn test_nanoseconds() { let parsed_duration = setup_map_try_parse(Some("82n")).unwrap().unwrap(); assert_eq!(Duration::from_nanos(82), parsed_duration); } #[test] fn test_header_not_present() { let parsed_duration = setup_map_try_parse(None).unwrap(); assert!(parsed_duration.is_none()); } #[test] #[should_panic(expected = "82f")] fn test_invalid_unit() { // "f" is not a valid TimeoutUnit setup_map_try_parse(Some("82f")).unwrap().unwrap(); } #[test] #[should_panic(expected = "123456789H")] fn test_too_many_digits() { // gRPC spec states TimeoutValue will be at most 8 digits setup_map_try_parse(Some("123456789H")).unwrap().unwrap(); } #[test] #[should_panic(expected = "oneH")] fn test_invalid_digits() { // gRPC spec states TimeoutValue will be at most 8 digits setup_map_try_parse(Some("oneH")).unwrap().unwrap(); } #[quickcheck] fn fuzz(header_value: HeaderValueGen) -> bool { let header_value = header_value.0; // this just shouldn't panic let _ = setup_map_try_parse(Some(&header_value)); true } /// Newtype to implement `Arbitrary` for generating `String`s that are valid `HeaderValue`s. #[derive(Clone, Debug)] struct HeaderValueGen(String); impl Arbitrary for HeaderValueGen { fn arbitrary(g: &mut Gen) -> Self { let max = g.choose(&(1..70).collect::>()).copied().unwrap(); Self(gen_string(g, 0, max)) } } // copied from https://github.com/hyperium/http/blob/master/tests/header_map_fuzz.rs fn gen_string(g: &mut Gen, min: usize, max: usize) -> String { let bytes: Vec<_> = (min..max) .map(|_| { // Chars to pick from g.choose(b"ABCDEFGHIJKLMNOPQRSTUVabcdefghilpqrstuvwxyz----") .copied() .unwrap() }) .collect(); String::from_utf8(bytes).unwrap() } } ================================================ FILE: tonic/src/transport/service/mod.rs ================================================ pub(crate) mod grpc_timeout; #[cfg(feature = "_tls-any")] pub(crate) mod tls; pub(crate) use self::grpc_timeout::GrpcTimeout; ================================================ FILE: tonic/src/transport/service/tls.rs ================================================ use std::{fmt, io::Cursor}; use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject as _}; use crate::transport::{Certificate, Identity}; /// h2 alpn in plain format for rustls. pub(crate) const ALPN_H2: &[u8] = b"h2"; #[derive(Debug)] pub(crate) enum TlsError { #[cfg(feature = "channel")] H2NotNegotiated, #[cfg(feature = "tls-native-roots")] NativeCertsNotFound, CertificateParseError, PrivateKeyParseError, HandshakeTimeout, } impl fmt::Display for TlsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { #[cfg(feature = "channel")] TlsError::H2NotNegotiated => write!(f, "HTTP/2 was not negotiated."), #[cfg(feature = "tls-native-roots")] TlsError::NativeCertsNotFound => write!(f, "no native certs found"), TlsError::CertificateParseError => write!(f, "Error parsing TLS certificate."), TlsError::PrivateKeyParseError => write!( f, "Error parsing TLS private key - no RSA or PKCS8-encoded keys found." ), TlsError::HandshakeTimeout => write!(f, "TLS handshake timeout."), } } } impl std::error::Error for TlsError {} pub(crate) fn convert_certificate_to_pki_types( certificate: &Certificate, ) -> Result>, TlsError> { CertificateDer::pem_reader_iter(&mut Cursor::new(certificate)) .collect::, _>>() .map_err(|_| TlsError::CertificateParseError) } pub(crate) fn convert_identity_to_pki_types( identity: &Identity, ) -> Result<(Vec>, PrivateKeyDer<'static>), TlsError> { let cert = convert_certificate_to_pki_types(&identity.cert)?; let key = PrivateKeyDer::from_pem_reader(&mut Cursor::new(&identity.key)) .map_err(|_| TlsError::PrivateKeyParseError)?; Ok((cert, key)) } ================================================ FILE: tonic/src/transport/tls.rs ================================================ /// Represents a X509 certificate. #[derive(Debug, Clone)] pub struct Certificate { pub(crate) pem: Vec, } /// Represents a private key and X509 certificate. #[derive(Debug, Clone)] pub struct Identity { pub(crate) cert: Certificate, pub(crate) key: Vec, } impl Certificate { /// Parse a PEM encoded X509 Certificate. /// /// The provided PEM should include at least one PEM encoded certificate. pub fn from_pem(pem: impl AsRef<[u8]>) -> Self { let pem = pem.as_ref().into(); Self { pem } } /// Get a immutable reference to underlying certificate pub fn get_ref(&self) -> &[u8] { self.pem.as_slice() } /// Get a mutable reference to underlying certificate pub fn get_mut(&mut self) -> &mut [u8] { self.pem.as_mut() } /// Consumes `self`, returning the underlying certificate pub fn into_inner(self) -> Vec { self.pem } } impl AsRef<[u8]> for Certificate { fn as_ref(&self) -> &[u8] { self.pem.as_ref() } } impl AsMut<[u8]> for Certificate { fn as_mut(&mut self) -> &mut [u8] { self.pem.as_mut() } } impl Identity { /// Parse a PEM encoded certificate and private key. /// /// The provided cert must contain at least one PEM encoded certificate. pub fn from_pem(cert: impl AsRef<[u8]>, key: impl AsRef<[u8]>) -> Self { let cert = Certificate::from_pem(cert); let key = key.as_ref().into(); Self { cert, key } } } ================================================ FILE: tonic/src/util.rs ================================================ //! Various utilities used throughout tonic. // some combinations of features might cause things here not to be used #![allow(dead_code)] pub(crate) mod base64 { use base64::{ alphabet, engine::{ DecodePaddingMode, general_purpose::{GeneralPurpose, GeneralPurposeConfig}, }, }; pub(crate) const STANDARD: GeneralPurpose = GeneralPurpose::new( &alphabet::STANDARD, GeneralPurposeConfig::new() .with_encode_padding(true) .with_decode_padding_mode(DecodePaddingMode::Indifferent), ); pub(crate) const STANDARD_NO_PAD: GeneralPurpose = GeneralPurpose::new( &alphabet::STANDARD, GeneralPurposeConfig::new() .with_encode_padding(false) .with_decode_padding_mode(DecodePaddingMode::Indifferent), ); } ================================================ FILE: tonic-build/Cargo.toml ================================================ [package] authors = ["Lucio Franco "] categories = ["network-programming", "asynchronous"] description = """ Codegen module of `tonic` gRPC implementation. """ edition = "2024" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "async", "codegen", "protobuf"] license = "MIT" name = "tonic-build" readme = "README.md" repository = "https://github.com/hyperium/tonic" version = "0.14.5" rust-version = { workspace = true } [dependencies] prettyplease = { version = "0.2" } proc-macro2 = "1.0" quote = "1.0" syn = "2.0" [features] default = ["transport"] transport = [] [lints] workspace = true [package.metadata.docs.rs] all-features = true [package.metadata.cargo_check_external_types] allowed_external_types = [ # major released "proc_macro2::*", ] ================================================ FILE: tonic-build/README.md ================================================ # tonic-build Provides code generation for service stubs to use with tonic. For protobuf compilation via prost, use the `tonic-prost-build` crate instead. # Feature flags - `transport`: Enables generation of `connect` method using `tonic::transport::Channel` (enabled by default). ## Features Required dependencies ```toml [dependencies] tonic = "" prost = "" [build-dependencies] tonic-prost-build = "" ``` ## Getting Started For protobuf compilation, use `tonic-prost-build` in your [`build.rs` file](https://doc.rust-lang.org/cargo/reference/build-scripts.html) at the root of the binary/library. You can rely on the defaults via ```rust,no_run,ignore fn main() -> Result<(), Box> { tonic_prost_build::compile_protos("proto/service.proto")?; Ok(()) } ``` Or configure the generated code deeper via ```rust,no_run,ignore fn main() -> Result<(), Box> { tonic_prost_build::configure() .build_server(false) .compile_protos( &["proto/helloworld/helloworld.proto"], &["proto/helloworld"], )?; Ok(()) } ``` For further details how to use the generated client/server, see the [examples here](https://github.com/hyperium/tonic/tree/master/examples) or the Google APIs example below. ## NixOS related hints On NixOS, it is better to specify the location of `PROTOC` and `PROTOC_INCLUDE` explicitly. ```bash $ export PROTOBUF_LOCATION=$(nix-env -q protobuf --out-path --no-name) $ export PROTOC=$PROTOBUF_LOCATION/bin/protoc $ export PROTOC_INCLUDE=$PROTOBUF_LOCATION/include $ cargo build ``` The reason being that if `prost_build::compile_protos` fails to generate the resultant package, the failure is not obvious until the `include!(concat!(env!("OUT_DIR"), "/resultant.rs"));` fails with `No such file or directory` error. ### Google APIs example A good way to use Google API is probably using git submodules. So suppose in our `proto` folder we do: ```bash git submodule add https://github.com/googleapis/googleapis git submodule update --remote ``` And a bunch of Google proto files in structure will be like this: ```raw ├── googleapis │   └── google │   ├── api │   │   ├── annotations.proto │   │   ├── client.proto │   │   ├── field_behavior.proto │   │   ├── http.proto │   │   └── resource.proto │   └── pubsub │   └── v1 │   ├── pubsub.proto │   └── schema.proto ``` Then we can generate Rust code via this setup in our `build.rs`: ```rust,no_run,ignore fn main() -> Result<(), Box> { tonic_prost_build::configure() .build_server(false) //.out_dir("src/google") // you can change the generated code's location .compile_protos( &["proto/googleapis/google/pubsub/v1/pubsub.proto"], &["proto/googleapis"], // specify the root location to search proto dependencies )?; Ok(()) } ``` Then you can reference the generated Rust like this in your code: ```rust,ignore pub mod api { tonic::include_proto!("google.pubsub.v1"); } use api::{publisher_client::PublisherClient, ListTopicsRequest}; ``` Or if you want to save the generated code in your own code base, you can uncomment the line `.out_dir(...)` above, and in your lib file config a mod like this: ```rust,ignore pub mod google { #[path = ""] pub mod pubsub { #[path = "google.pubsub.v1.rs"] pub mod v1; } } ``` See [the example here](https://github.com/hyperium/tonic/tree/master/examples/src/gcp) ================================================ FILE: tonic-build/src/client.rs ================================================ use std::collections::HashSet; use super::{Attributes, Method, Service}; use crate::{ format_method_name, format_method_path, format_service_name, generate_deprecated, generate_doc_comments, naive_snake_case, }; use proc_macro2::TokenStream; use quote::{format_ident, quote}; pub(crate) fn generate_internal( service: &T, emit_package: bool, proto_path: &str, compile_well_known_types: bool, build_transport: bool, attributes: &Attributes, disable_comments: &HashSet, ) -> TokenStream { let service_ident = quote::format_ident!("{}Client", service.name()); let client_mod = quote::format_ident!("{}_client", naive_snake_case(service.name())); let methods = generate_methods( service, emit_package, proto_path, compile_well_known_types, disable_comments, ); let connect = generate_connect(&service_ident, build_transport); let package = if emit_package { service.package() } else { "" }; let service_name = format_service_name(service, emit_package); let service_doc = if disable_comments.contains(&service_name) { TokenStream::new() } else { generate_doc_comments(service.comment()) }; let mod_attributes = attributes.for_mod(package); let struct_attributes = attributes.for_struct(&service_name); quote! { /// Generated client implementations. #(#mod_attributes)* pub mod #client_mod { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, // will trigger if compression is disabled clippy::let_unit_value, )] use tonic::codegen::*; use tonic::codegen::http::Uri; #service_doc #(#struct_attributes)* #[derive(Debug, Clone)] pub struct #service_ident { inner: tonic::client::Grpc, } #connect impl #service_ident where T: tonic::client::GrpcService, T::Error: Into, T::ResponseBody: Body + std::marker::Send + 'static, ::Error: Into + std::marker::Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } pub fn with_origin(inner: T, origin: Uri) -> Self { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } pub fn with_interceptor(inner: T, interceptor: F) -> #service_ident> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response<>::ResponseBody> >, >>::Error: Into + std::marker::Send + std::marker::Sync, { #service_ident::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); self } /// Enable decompressing responses. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.accept_compressed(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_decoding_message_size(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_encoding_message_size(limit); self } #methods } } } } #[cfg(feature = "transport")] fn generate_connect(service_ident: &syn::Ident, enabled: bool) -> TokenStream { let connect_impl = quote! { impl #service_ident { /// Attempt to create a new client by connecting to a given endpoint. pub async fn connect(dst: D) -> Result where D: TryInto, D::Error: Into, { let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; Ok(Self::new(conn)) } } }; if enabled { connect_impl } else { TokenStream::new() } } #[cfg(not(feature = "transport"))] fn generate_connect(_service_ident: &syn::Ident, _enabled: bool) -> TokenStream { TokenStream::new() } fn generate_methods( service: &T, emit_package: bool, proto_path: &str, compile_well_known_types: bool, disable_comments: &HashSet, ) -> TokenStream { let mut stream = TokenStream::new(); for method in service.methods() { if !disable_comments.contains(&format_method_name(service, method, emit_package)) { stream.extend(generate_doc_comments(method.comment())); } if method.deprecated() { stream.extend(generate_deprecated()); } let method = match (method.client_streaming(), method.server_streaming()) { (false, false) => generate_unary( service, method, emit_package, proto_path, compile_well_known_types, ), (false, true) => generate_server_streaming( service, method, emit_package, proto_path, compile_well_known_types, ), (true, false) => generate_client_streaming( service, method, emit_package, proto_path, compile_well_known_types, ), (true, true) => generate_streaming( service, method, emit_package, proto_path, compile_well_known_types, ), }; stream.extend(method); } stream } fn generate_unary( service: &T, method: &T::Method, emit_package: bool, proto_path: &str, compile_well_known_types: bool, ) -> TokenStream { let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let ident = format_ident!("{}", method.name()); let (request, response) = method.request_response_name(proto_path, compile_well_known_types); let service_name = format_service_name(service, emit_package); let path = format_method_path(service, method, emit_package); let method_name = method.identifier(); quote! { pub async fn #ident( &mut self, request: impl tonic::IntoRequest<#request>, ) -> std::result::Result, tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::unknown(format!("Service was not ready: {}", e.into())) })?; let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); let mut req = request.into_request(); req.extensions_mut().insert(GrpcMethod::new(#service_name, #method_name)); self.inner.unary(req, path, codec).await } } } fn generate_server_streaming( service: &T, method: &T::Method, emit_package: bool, proto_path: &str, compile_well_known_types: bool, ) -> TokenStream { let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let ident = format_ident!("{}", method.name()); let (request, response) = method.request_response_name(proto_path, compile_well_known_types); let service_name = format_service_name(service, emit_package); let path = format_method_path(service, method, emit_package); let method_name = method.identifier(); quote! { pub async fn #ident( &mut self, request: impl tonic::IntoRequest<#request>, ) -> std::result::Result>, tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::unknown(format!("Service was not ready: {}", e.into())) })?; let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); let mut req = request.into_request(); req.extensions_mut().insert(GrpcMethod::new(#service_name, #method_name)); self.inner.server_streaming(req, path, codec).await } } } fn generate_client_streaming( service: &T, method: &T::Method, emit_package: bool, proto_path: &str, compile_well_known_types: bool, ) -> TokenStream { let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let ident = format_ident!("{}", method.name()); let (request, response) = method.request_response_name(proto_path, compile_well_known_types); let service_name = format_service_name(service, emit_package); let path = format_method_path(service, method, emit_package); let method_name = method.identifier(); quote! { pub async fn #ident( &mut self, request: impl tonic::IntoStreamingRequest ) -> std::result::Result, tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::unknown(format!("Service was not ready: {}", e.into())) })?; let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); let mut req = request.into_streaming_request(); req.extensions_mut().insert(GrpcMethod::new(#service_name, #method_name)); self.inner.client_streaming(req, path, codec).await } } } fn generate_streaming( service: &T, method: &T::Method, emit_package: bool, proto_path: &str, compile_well_known_types: bool, ) -> TokenStream { let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let ident = format_ident!("{}", method.name()); let (request, response) = method.request_response_name(proto_path, compile_well_known_types); let service_name = format_service_name(service, emit_package); let path = format_method_path(service, method, emit_package); let method_name = method.identifier(); quote! { pub async fn #ident( &mut self, request: impl tonic::IntoStreamingRequest ) -> std::result::Result>, tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::unknown(format!("Service was not ready: {}", e.into())) })?; let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); let mut req = request.into_streaming_request(); req.extensions_mut().insert(GrpcMethod::new(#service_name,#method_name)); self.inner.streaming(req, path, codec).await } } } ================================================ FILE: tonic-build/src/code_gen.rs ================================================ //! Generic code generation for gRPC services. //! //! This module provides the generic infrastructure for generating //! client and server code from service definitions. use std::collections::HashSet; use proc_macro2::TokenStream; use crate::{Attributes, Service}; /// Builder for the generic code generation of server and clients. #[derive(Debug)] pub struct CodeGenBuilder { emit_package: bool, compile_well_known_types: bool, attributes: Attributes, build_transport: bool, disable_comments: HashSet, use_arc_self: bool, generate_default_stubs: bool, } impl CodeGenBuilder { /// Create a new code gen builder with default options. pub fn new() -> Self { Default::default() } /// Enable code generation to emit the package name. pub fn emit_package(&mut self, enable: bool) -> &mut Self { self.emit_package = enable; self } /// Attributes that will be added to `mod` and `struct` items. /// /// Reference [`Attributes`] for more information. pub fn attributes(&mut self, attributes: Attributes) -> &mut Self { self.attributes = attributes; self } /// Enable transport code to be generated, this requires `tonic`'s `transport` /// feature. /// /// This allows codegen level control of generating the transport code and /// is a work around when other crates in a workspace enable this feature. pub fn build_transport(&mut self, build_transport: bool) -> &mut Self { self.build_transport = build_transport; self } /// Enable compiling well known types, this will force codegen to not /// use the well known types from `prost-types`. pub fn compile_well_known_types(&mut self, enable: bool) -> &mut Self { self.compile_well_known_types = enable; self } /// Disable comments based on a proto path. pub fn disable_comments(&mut self, disable_comments: HashSet) -> &mut Self { self.disable_comments = disable_comments; self } /// Emit `Arc` instead of `&self` in service trait. pub fn use_arc_self(&mut self, enable: bool) -> &mut Self { self.use_arc_self = enable; self } /// Enable or disable returning automatic unimplemented gRPC error code for generated traits. pub fn generate_default_stubs(&mut self, generate_default_stubs: bool) -> &mut Self { self.generate_default_stubs = generate_default_stubs; self } /// Generate client code based on `Service`. /// /// This takes some `Service` and will generate a `TokenStream` that contains /// a public module with the generated client. pub fn generate_client(&self, service: &impl Service, proto_path: &str) -> TokenStream { crate::client::generate_internal( service, self.emit_package, proto_path, self.compile_well_known_types, self.build_transport, &self.attributes, &self.disable_comments, ) } /// Generate server code based on `Service`. /// /// This takes some `Service` and will generate a `TokenStream` that contains /// a public module with the generated client. pub fn generate_server(&self, service: &impl Service, proto_path: &str) -> TokenStream { crate::server::generate_internal( service, self.emit_package, proto_path, self.compile_well_known_types, &self.attributes, &self.disable_comments, self.use_arc_self, self.generate_default_stubs, ) } } impl Default for CodeGenBuilder { fn default() -> Self { Self { emit_package: true, compile_well_known_types: false, attributes: Attributes::default(), build_transport: true, disable_comments: HashSet::default(), use_arc_self: false, generate_default_stubs: false, } } } ================================================ FILE: tonic-build/src/lib.rs ================================================ #![doc = include_str!("../README.md")] #![recursion_limit = "256"] #![doc( html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" )] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] #![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))] #![cfg_attr(docsrs, feature(doc_cfg))] use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream}; use quote::TokenStreamExt; // Prost functionality has been moved to tonic-prost-build pub mod manual; /// Service code generation for client mod client; /// Service code generation for Server mod server; mod code_gen; pub use code_gen::CodeGenBuilder; /// Service generation trait. /// /// This trait can be implemented and consumed /// by `client::generate` and `server::generate` /// to allow any codegen module to generate service /// abstractions. pub trait Service { /// Comment type. type Comment: AsRef; /// Method type. type Method: Method; /// Name of service. fn name(&self) -> &str; /// Package name of service. fn package(&self) -> &str; /// Identifier used to generate type name. fn identifier(&self) -> &str; /// Methods provided by service. fn methods(&self) -> &[Self::Method]; /// Get comments about this item. fn comment(&self) -> &[Self::Comment]; } /// Method generation trait. /// /// Each service contains a set of generic /// `Methods`'s that will be used by codegen /// to generate abstraction implementations for /// the provided methods. pub trait Method { /// Comment type. type Comment: AsRef; /// Name of method. fn name(&self) -> &str; /// Identifier used to generate type name. fn identifier(&self) -> &str; /// Path to the codec. fn codec_path(&self) -> &str; /// Method is streamed by client. fn client_streaming(&self) -> bool; /// Method is streamed by server. fn server_streaming(&self) -> bool; /// Get comments about this item. fn comment(&self) -> &[Self::Comment]; /// Method is deprecated. fn deprecated(&self) -> bool { false } /// Type name of request and response. fn request_response_name( &self, proto_path: &str, compile_well_known_types: bool, ) -> (TokenStream, TokenStream); } /// Attributes that will be added to `mod` and `struct` items. #[derive(Debug, Default, Clone)] pub struct Attributes { /// `mod` attributes. module: Vec<(String, String)>, /// `struct` attributes. structure: Vec<(String, String)>, /// `trait` attributes. trait_attributes: Vec<(String, String)>, } impl Attributes { fn for_mod(&self, name: &str) -> Vec { generate_attributes(name, &self.module) } fn for_struct(&self, name: &str) -> Vec { generate_attributes(name, &self.structure) } fn for_trait(&self, name: &str) -> Vec { generate_attributes(name, &self.trait_attributes) } /// Add an attribute that will be added to `mod` items matching the given pattern. /// /// # Examples /// /// ``` /// # use tonic_build::*; /// let mut attributes = Attributes::default(); /// attributes.push_mod("my.proto.package", r#"#[cfg(feature = "server")]"#); /// ``` pub fn push_mod(&mut self, pattern: impl Into, attr: impl Into) { self.module.push((pattern.into(), attr.into())); } /// Add an attribute that will be added to `struct` items matching the given pattern. /// /// # Examples /// /// ``` /// # use tonic_build::*; /// let mut attributes = Attributes::default(); /// attributes.push_struct("EchoService", "#[derive(PartialEq)]"); /// ``` pub fn push_struct(&mut self, pattern: impl Into, attr: impl Into) { self.structure.push((pattern.into(), attr.into())); } /// Add an attribute that will be added to `trait` items matching the given pattern. /// /// # Examples /// /// ``` /// # use tonic_build::*; /// let mut attributes = Attributes::default(); /// attributes.push_trait("Server", "#[mockall::automock]"); /// ``` pub fn push_trait(&mut self, pattern: impl Into, attr: impl Into) { self.trait_attributes.push((pattern.into(), attr.into())); } } fn format_service_name(service: &T, emit_package: bool) -> String { let package = if emit_package { service.package() } else { "" }; format!( "{}{}{}", package, if package.is_empty() { "" } else { "." }, service.identifier(), ) } fn format_method_path(service: &T, method: &T::Method, emit_package: bool) -> String { format!( "/{}/{}", format_service_name(service, emit_package), method.identifier() ) } fn format_method_name(service: &T, method: &T::Method, emit_package: bool) -> String { format!( "{}.{}", format_service_name(service, emit_package), method.identifier() ) } // Generates attributes given a list of (`pattern`, `attribute`) pairs. If `pattern` matches `name`, `attribute` will be included. fn generate_attributes<'a>( name: &str, attrs: impl IntoIterator, ) -> Vec { attrs .into_iter() .filter(|(matcher, _)| match_name(matcher, name)) .flat_map(|(_, attr)| { // attributes cannot be parsed directly, so we pretend they're on a struct syn::parse_str::(&format!("{attr}\nstruct fake;")) .unwrap() .attrs }) .collect::>() } fn generate_deprecated() -> TokenStream { let mut deprecated_stream = TokenStream::new(); deprecated_stream.append(Ident::new("deprecated", Span::call_site())); let group = Group::new(Delimiter::Bracket, deprecated_stream); let mut stream = TokenStream::new(); stream.append(Punct::new('#', Spacing::Alone)); stream.append(group); stream } // Generate a singular line of a doc comment fn generate_doc_comment>(comment: S) -> TokenStream { let comment = comment.as_ref(); let comment = if !comment.starts_with(' ') { format!(" {comment}") } else { comment.to_string() }; let mut doc_stream = TokenStream::new(); doc_stream.append(Ident::new("doc", Span::call_site())); doc_stream.append(Punct::new('=', Spacing::Alone)); doc_stream.append(Literal::string(comment.as_ref())); let group = Group::new(Delimiter::Bracket, doc_stream); let mut stream = TokenStream::new(); stream.append(Punct::new('#', Spacing::Alone)); stream.append(group); stream } // Generate a larger doc comment composed of many lines of doc comments fn generate_doc_comments>(comments: &[T]) -> TokenStream { let mut stream = TokenStream::new(); for comment in comments { stream.extend(generate_doc_comment(comment)); } stream } // Checks whether a path pattern matches a given path. pub(crate) fn match_name(pattern: &str, path: &str) -> bool { if pattern.is_empty() { false } else if pattern == "." || pattern == path { true } else { let pattern_segments = pattern.split('.').collect::>(); let path_segments = path.split('.').collect::>(); if &pattern[..1] == "." { // prefix match if pattern_segments.len() > path_segments.len() { false } else { pattern_segments[..] == path_segments[..pattern_segments.len()] } // suffix match } else if pattern_segments.len() > path_segments.len() { false } else { pattern_segments[..] == path_segments[path_segments.len() - pattern_segments.len()..] } } } fn naive_snake_case(name: &str) -> String { let mut s = String::new(); let mut it = name.chars().peekable(); while let Some(x) = it.next() { s.push(x.to_ascii_lowercase()); if let Some(y) = it.peek() { if y.is_uppercase() { s.push('_'); } } } s } #[cfg(test)] mod tests { use super::*; #[test] fn test_match_name() { assert!(match_name(".", ".my.protos")); assert!(match_name(".", ".protos")); assert!(match_name(".my", ".my")); assert!(match_name(".my", ".my.protos")); assert!(match_name(".my.protos.Service", ".my.protos.Service")); assert!(match_name("Service", ".my.protos.Service")); assert!(!match_name(".m", ".my.protos")); assert!(!match_name(".p", ".protos")); assert!(!match_name(".my", ".myy")); assert!(!match_name(".protos", ".my.protos")); assert!(!match_name(".Service", ".my.protos.Service")); assert!(!match_name("service", ".my.protos.Service")); } #[test] fn test_snake_case() { for case in &[ ("Service", "service"), ("ThatHasALongName", "that_has_a_long_name"), ("greeter", "greeter"), ("ABCServiceX", "a_b_c_service_x"), ] { assert_eq!(naive_snake_case(case.0), case.1) } } } ================================================ FILE: tonic-build/src/manual.rs ================================================ //! This module provides utilities for generating `tonic` service stubs and clients //! purely in Rust without the need of `proto` files. It also enables you to set a custom `Codec` //! if you want to use a custom serialization format other than `protobuf`. //! //! # Example //! //! ```rust,no_run //! fn main() -> Result<(), Box> { //! let greeter_service = tonic_build::manual::Service::builder() //! .name("Greeter") //! .package("helloworld") //! .method( //! tonic_build::manual::Method::builder() //! .name("say_hello") //! .route_name("SayHello") //! // Provide the path to the Request type //! .input_type("crate::HelloRequest") //! // Provide the path to the Response type //! .output_type("super::HelloResponse") //! // Provide the path to the Codec to use //! .codec_path("crate::JsonCodec") //! .build(), //! ) //! .build(); //! //! tonic_build::manual::Builder::new().compile(&[greeter_service]); //! Ok(()) //! } //! ``` use crate::code_gen::CodeGenBuilder; use proc_macro2::TokenStream; use quote::ToTokens; use std::{ fs, path::{Path, PathBuf}, }; /// Service builder. /// /// This builder can be used to manually define a gRPC service in rust code without the use of a /// .proto file. /// /// # Example /// /// ``` /// # use tonic_build::manual::Service; /// let greeter_service = Service::builder() /// .name("Greeter") /// .package("helloworld") /// // Add various methods to the service /// // .method() /// .build(); /// ``` #[derive(Debug, Default)] pub struct ServiceBuilder { /// The service name in Rust style. name: Option, /// The package name as it appears in the .proto file. package: Option, /// The service comments. comments: Vec, /// The service methods. methods: Vec, } impl ServiceBuilder { /// Set the name for this Service. /// /// This value will be used both as the base for the generated rust types and service trait as /// well as part of the route for calling this service. Routes have the form: /// `/./` pub fn name(mut self, name: impl AsRef) -> Self { self.name = Some(name.as_ref().to_owned()); self } /// Set the package this Service is part of. /// /// This value will be used as part of the route for calling this service. /// Routes have the form: `/./` pub fn package(mut self, package: impl AsRef) -> Self { self.package = Some(package.as_ref().to_owned()); self } /// Add a comment string that should be included as a doc comment for this Service. pub fn comment(mut self, comment: impl AsRef) -> Self { self.comments.push(comment.as_ref().to_owned()); self } /// Adds a Method to this Service. pub fn method(mut self, method: Method) -> Self { self.methods.push(method); self } /// Build a Service. /// /// Panics if `name` or `package` weren't set. pub fn build(self) -> Service { Service { name: self.name.unwrap(), comments: self.comments, package: self.package.unwrap(), methods: self.methods, } } } /// A service descriptor. #[derive(Debug)] pub struct Service { /// The service name in Rust style. name: String, /// The package name as it appears in the .proto file. package: String, /// The service comments. comments: Vec, /// The service methods. methods: Vec, } impl Service { /// Create a new `ServiceBuilder` pub fn builder() -> ServiceBuilder { ServiceBuilder::default() } } impl crate::Service for Service { type Comment = String; type Method = Method; fn name(&self) -> &str { &self.name } fn package(&self) -> &str { &self.package } fn identifier(&self) -> &str { &self.name } fn methods(&self) -> &[Self::Method] { &self.methods } fn comment(&self) -> &[Self::Comment] { &self.comments } } /// A service method descriptor. #[derive(Debug)] pub struct Method { /// The name of the method in Rust style. name: String, /// The name of the method as should be used when constructing a route route_name: String, /// The method comments. comments: Vec, /// The input Rust type. input_type: String, /// The output Rust type. output_type: String, /// Identifies if client streams multiple client messages. client_streaming: bool, /// Identifies if server streams multiple server messages. server_streaming: bool, /// Identifies if the method is deprecated. deprecated: bool, /// The path to the codec to use for this method codec_path: String, } impl Method { /// Create a new `MethodBuilder` pub fn builder() -> MethodBuilder { MethodBuilder::default() } } impl crate::Method for Method { type Comment = String; fn name(&self) -> &str { &self.name } fn identifier(&self) -> &str { &self.route_name } fn codec_path(&self) -> &str { &self.codec_path } fn client_streaming(&self) -> bool { self.client_streaming } fn server_streaming(&self) -> bool { self.server_streaming } fn comment(&self) -> &[Self::Comment] { &self.comments } fn deprecated(&self) -> bool { self.deprecated } fn request_response_name( &self, _proto_path: &str, _compile_well_known_types: bool, ) -> (TokenStream, TokenStream) { let request = syn::parse_str::(&self.input_type) .unwrap() .to_token_stream(); let response = syn::parse_str::(&self.output_type) .unwrap() .to_token_stream(); (request, response) } } /// Method builder. /// /// This builder can be used to manually define gRPC method, which can be added to a gRPC service, /// in rust code without the use of a .proto file. /// /// # Example /// /// ``` /// # use tonic_build::manual::Method; /// let say_hello_method = Method::builder() /// .name("say_hello") /// .route_name("SayHello") /// // Provide the path to the Request type /// .input_type("crate::common::HelloRequest") /// // Provide the path to the Response type /// .output_type("crate::common::HelloResponse") /// // Provide the path to the Codec to use /// .codec_path("crate::common::JsonCodec") /// .build(); /// ``` #[derive(Debug, Default)] pub struct MethodBuilder { /// The name of the method in Rust style. name: Option, /// The name of the method as should be used when constructing a route route_name: Option, /// The method comments. comments: Vec, /// The input Rust type. input_type: Option, /// The output Rust type. output_type: Option, /// Identifies if client streams multiple client messages. client_streaming: bool, /// Identifies if server streams multiple server messages. server_streaming: bool, /// Identifies if the method is deprecated. deprecated: bool, /// The path to the codec to use for this method codec_path: Option, } impl MethodBuilder { /// Set the name for this Method. /// /// This value will be used for generating the client functions for calling this Method. /// /// Generally this is formatted in snake_case. pub fn name(mut self, name: impl AsRef) -> Self { self.name = Some(name.as_ref().to_owned()); self } /// Set the route_name for this Method. /// /// This value will be used as part of the route for calling this method. /// Routes have the form: `/./` /// /// Generally this is formatted in PascalCase. pub fn route_name(mut self, route_name: impl AsRef) -> Self { self.route_name = Some(route_name.as_ref().to_owned()); self } /// Add a comment string that should be included as a doc comment for this Method. pub fn comment(mut self, comment: impl AsRef) -> Self { self.comments.push(comment.as_ref().to_owned()); self } /// Set the path to the Rust type that should be use for the Request type of this method. pub fn input_type(mut self, input_type: impl AsRef) -> Self { self.input_type = Some(input_type.as_ref().to_owned()); self } /// Set the path to the Rust type that should be use for the Response type of this method. pub fn output_type(mut self, output_type: impl AsRef) -> Self { self.output_type = Some(output_type.as_ref().to_owned()); self } /// Set the path to the Rust type that should be used as the `Codec` for this method. /// /// Currently the codegen assumes that this type implements `Default`. pub fn codec_path(mut self, codec_path: impl AsRef) -> Self { self.codec_path = Some(codec_path.as_ref().to_owned()); self } /// Sets if the Method request from the client is streamed. pub fn client_streaming(mut self) -> Self { self.client_streaming = true; self } /// Sets if the Method response from the server is streamed. pub fn server_streaming(mut self) -> Self { self.server_streaming = true; self } /// Build a Method /// /// Panics if `name`, `route_name`, `input_type`, `output_type`, or `codec_path` weren't set. pub fn build(self) -> Method { Method { name: self.name.unwrap(), route_name: self.route_name.unwrap(), comments: self.comments, input_type: self.input_type.unwrap(), output_type: self.output_type.unwrap(), client_streaming: self.client_streaming, server_streaming: self.server_streaming, deprecated: self.deprecated, codec_path: self.codec_path.unwrap(), } } } struct ServiceGenerator { builder: Builder, clients: TokenStream, servers: TokenStream, } impl ServiceGenerator { fn generate(&mut self, service: &Service) { if self.builder.build_server { let server = CodeGenBuilder::new() .emit_package(true) .compile_well_known_types(false) .generate_server(service, ""); self.servers.extend(server); } if self.builder.build_client { let client = CodeGenBuilder::new() .emit_package(true) .compile_well_known_types(false) .build_transport(self.builder.build_transport) .generate_client(service, ""); self.clients.extend(client); } } fn finalize(&mut self, buf: &mut String) { if self.builder.build_client && !self.clients.is_empty() { let clients = &self.clients; let client_service = quote::quote! { #clients }; let ast: syn::File = syn::parse2(client_service).expect("not a valid tokenstream"); let code = prettyplease::unparse(&ast); buf.push_str(&code); self.clients = TokenStream::default(); } if self.builder.build_server && !self.servers.is_empty() { let servers = &self.servers; let server_service = quote::quote! { #servers }; let ast: syn::File = syn::parse2(server_service).expect("not a valid tokenstream"); let code = prettyplease::unparse(&ast); buf.push_str(&code); self.servers = TokenStream::default(); } } } /// Service generator builder. #[derive(Debug)] pub struct Builder { build_server: bool, build_client: bool, build_transport: bool, out_dir: Option, } impl Default for Builder { fn default() -> Self { Self { build_server: true, build_client: true, build_transport: true, out_dir: None, } } } impl Builder { /// Create a new Builder pub fn new() -> Self { Self::default() } /// Enable or disable gRPC client code generation. /// /// Defaults to enabling client code generation. pub fn build_client(mut self, enable: bool) -> Self { self.build_client = enable; self } /// Enable or disable gRPC server code generation. /// /// Defaults to enabling server code generation. pub fn build_server(mut self, enable: bool) -> Self { self.build_server = enable; self } /// Enable or disable generated clients and servers to have built-in tonic /// transport features. /// /// When the `transport` feature is disabled this does nothing. pub fn build_transport(mut self, enable: bool) -> Self { self.build_transport = enable; self } /// Set the output directory to generate code to. /// /// Defaults to the `OUT_DIR` environment variable. pub fn out_dir(mut self, out_dir: impl AsRef) -> Self { self.out_dir = Some(out_dir.as_ref().to_path_buf()); self } /// Performs code generation for the provided services. /// /// Generated services will be output into the directory specified by `out_dir` /// with files named `..rs`. pub fn compile(self, services: &[Service]) { let out_dir = if let Some(out_dir) = self.out_dir.as_ref() { out_dir.clone() } else { PathBuf::from(std::env::var("OUT_DIR").unwrap()) }; let mut generator = ServiceGenerator { builder: self, clients: TokenStream::default(), servers: TokenStream::default(), }; for service in services { generator.generate(service); let mut output = String::new(); generator.finalize(&mut output); let out_file = out_dir.join(format!("{}.{}.rs", service.package, service.name)); fs::write(out_file, output).unwrap(); } } } ================================================ FILE: tonic-build/src/server.rs ================================================ use std::collections::HashSet; use super::{Attributes, Method, Service}; use crate::{ format_method_name, format_method_path, format_service_name, generate_doc_comment, generate_doc_comments, naive_snake_case, }; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{Ident, Lit, LitStr}; #[allow(clippy::too_many_arguments)] pub(crate) fn generate_internal( service: &T, emit_package: bool, proto_path: &str, compile_well_known_types: bool, attributes: &Attributes, disable_comments: &HashSet, use_arc_self: bool, generate_default_stubs: bool, ) -> TokenStream { let methods = generate_methods( service, emit_package, proto_path, compile_well_known_types, use_arc_self, generate_default_stubs, ); let server_service = quote::format_ident!("{}Server", service.name()); let server_trait = quote::format_ident!("{}", service.name()); let server_mod = quote::format_ident!("{}_server", naive_snake_case(service.name())); let trait_attributes = attributes.for_trait(service.name()); let generated_trait = generate_trait( service, emit_package, proto_path, compile_well_known_types, server_trait.clone(), disable_comments, use_arc_self, generate_default_stubs, trait_attributes, ); let package = if emit_package { service.package() } else { "" }; // Transport based implementations let service_name = format_service_name(service, emit_package); let service_doc = if disable_comments.contains(&service_name) { TokenStream::new() } else { generate_doc_comments(service.comment()) }; let named = generate_named(&server_service, &service_name); let mod_attributes = attributes.for_mod(package); let struct_attributes = attributes.for_struct(&service_name); let configure_compression_methods = quote! { /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Compress responses with the given encoding, if the client supports it. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } }; let configure_max_message_size_methods = quote! { /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.max_encoding_message_size = Some(limit); self } }; quote! { /// Generated server implementations. #(#mod_attributes)* pub mod #server_mod { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, // will trigger if compression is disabled clippy::let_unit_value, )] use tonic::codegen::*; #generated_trait #service_doc #(#struct_attributes)* #[derive(Debug)] pub struct #server_service { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } impl #server_service { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } pub fn from_arc(inner: Arc) -> Self { Self { inner, accept_compression_encodings: Default::default(), send_compression_encodings: Default::default(), max_decoding_message_size: None, max_encoding_message_size: None, } } pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService where F: tonic::service::Interceptor, { InterceptedService::new(Self::new(inner), interceptor) } #configure_compression_methods #configure_max_message_size_methods } impl tonic::codegen::Service> for #server_service where T: #server_trait, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { type Response = http::Response; type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { #methods _ => Box::pin(async move { let mut response = http::Response::new(tonic::body::Body::default()); let headers = response.headers_mut(); headers.insert(tonic::Status::GRPC_STATUS, (tonic::Code::Unimplemented as i32).into()); headers.insert(http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE); Ok(response) }), } } } impl Clone for #server_service { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { inner, accept_compression_encodings: self.accept_compression_encodings, send_compression_encodings: self.send_compression_encodings, max_decoding_message_size: self.max_decoding_message_size, max_encoding_message_size: self.max_encoding_message_size, } } } #named } } } #[allow(clippy::too_many_arguments)] fn generate_trait( service: &T, emit_package: bool, proto_path: &str, compile_well_known_types: bool, server_trait: Ident, disable_comments: &HashSet, use_arc_self: bool, generate_default_stubs: bool, trait_attributes: Vec, ) -> TokenStream { let methods = generate_trait_methods( service, emit_package, proto_path, compile_well_known_types, disable_comments, use_arc_self, generate_default_stubs, ); let trait_doc = generate_doc_comment(format!( " Generated trait containing gRPC methods that should be implemented for use with {}Server.", service.name() )); quote! { #trait_doc #(#trait_attributes)* #[async_trait] pub trait #server_trait : std::marker::Send + std::marker::Sync + 'static { #methods } } } fn generate_trait_methods( service: &T, emit_package: bool, proto_path: &str, compile_well_known_types: bool, disable_comments: &HashSet, use_arc_self: bool, generate_default_stubs: bool, ) -> TokenStream { let mut stream = TokenStream::new(); for method in service.methods() { let name = quote::format_ident!("{}", method.name()); let (req_message, res_message) = method.request_response_name(proto_path, compile_well_known_types); let method_doc = if disable_comments.contains(&format_method_name(service, method, emit_package)) { TokenStream::new() } else { generate_doc_comments(method.comment()) }; let self_param = if use_arc_self { quote!(self: std::sync::Arc) } else { quote!(&self) }; let method = match ( method.client_streaming(), method.server_streaming(), generate_default_stubs, ) { (false, false, true) => { quote! { #method_doc async fn #name(#self_param, request: tonic::Request<#req_message>) -> std::result::Result, tonic::Status> { Err(tonic::Status::unimplemented("Not yet implemented")) } } } (false, false, false) => { quote! { #method_doc async fn #name(#self_param, request: tonic::Request<#req_message>) -> std::result::Result, tonic::Status>; } } (true, false, true) => { quote! { #method_doc async fn #name(#self_param, request: tonic::Request>) -> std::result::Result, tonic::Status> { Err(tonic::Status::unimplemented("Not yet implemented")) } } } (true, false, false) => { quote! { #method_doc async fn #name(#self_param, request: tonic::Request>) -> std::result::Result, tonic::Status>; } } (false, true, true) => { quote! { #method_doc async fn #name(#self_param, request: tonic::Request<#req_message>) -> std::result::Result>, tonic::Status> { Err(tonic::Status::unimplemented("Not yet implemented")) } } } (false, true, false) => { let stream = quote::format_ident!("{}Stream", method.identifier()); let stream_doc = generate_doc_comment(format!( " Server streaming response type for the {} method.", method.identifier() )); quote! { #stream_doc type #stream: tonic::codegen::tokio_stream::Stream> + std::marker::Send + 'static; #method_doc async fn #name(#self_param, request: tonic::Request<#req_message>) -> std::result::Result, tonic::Status>; } } (true, true, true) => { quote! { #method_doc async fn #name(#self_param, request: tonic::Request>) -> std::result::Result>, tonic::Status> { Err(tonic::Status::unimplemented("Not yet implemented")) } } } (true, true, false) => { let stream = quote::format_ident!("{}Stream", method.identifier()); let stream_doc = generate_doc_comment(format!( " Server streaming response type for the {} method.", method.identifier() )); quote! { #stream_doc type #stream: tonic::codegen::tokio_stream::Stream> + std::marker::Send + 'static; #method_doc async fn #name(#self_param, request: tonic::Request>) -> std::result::Result, tonic::Status>; } } }; stream.extend(method); } stream } fn generate_named(server_service: &syn::Ident, service_name: &str) -> TokenStream { let service_name = syn::LitStr::new(service_name, proc_macro2::Span::call_site()); let name_doc = generate_doc_comment(" Generated gRPC service name"); quote! { #name_doc pub const SERVICE_NAME: &str = #service_name; impl tonic::server::NamedService for #server_service { const NAME: &'static str = SERVICE_NAME; } } } fn generate_methods( service: &T, emit_package: bool, proto_path: &str, compile_well_known_types: bool, use_arc_self: bool, generate_default_stubs: bool, ) -> TokenStream { let mut stream = TokenStream::new(); for method in service.methods() { let path = format_method_path(service, method, emit_package); let method_path = Lit::Str(LitStr::new(&path, Span::call_site())); let ident = quote::format_ident!("{}", method.name()); let server_trait = quote::format_ident!("{}", service.name()); let method_stream = match (method.client_streaming(), method.server_streaming()) { (false, false) => generate_unary( method, proto_path, compile_well_known_types, ident, server_trait, use_arc_self, ), (false, true) => generate_server_streaming( method, proto_path, compile_well_known_types, ident.clone(), server_trait, use_arc_self, generate_default_stubs, ), (true, false) => generate_client_streaming( method, proto_path, compile_well_known_types, ident.clone(), server_trait, use_arc_self, ), (true, true) => generate_streaming( method, proto_path, compile_well_known_types, ident.clone(), server_trait, use_arc_self, generate_default_stubs, ), }; let method = quote! { #method_path => { #method_stream } }; stream.extend(method); } stream } fn generate_unary( method: &T, proto_path: &str, compile_well_known_types: bool, method_ident: Ident, server_trait: Ident, use_arc_self: bool, ) -> TokenStream { let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let service_ident = quote::format_ident!("{}Svc", method.identifier()); let (request, response) = method.request_response_name(proto_path, compile_well_known_types); let inner_arg = if use_arc_self { quote!(inner) } else { quote!(&inner) }; quote! { #[allow(non_camel_case_types)] struct #service_ident(pub Arc); impl tonic::server::UnaryService<#request> for #service_ident { type Response = #response; type Future = BoxFuture, tonic::Status>; fn call(&mut self, request: tonic::Request<#request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::#method_ident(#inner_arg, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = #service_ident(inner); let codec = #codec_name::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config(accept_compression_encodings, send_compression_encodings) .apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size); let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) } } fn generate_server_streaming( method: &T, proto_path: &str, compile_well_known_types: bool, method_ident: Ident, server_trait: Ident, use_arc_self: bool, generate_default_stubs: bool, ) -> TokenStream { let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let service_ident = quote::format_ident!("{}Svc", method.identifier()); let (request, response) = method.request_response_name(proto_path, compile_well_known_types); let response_stream = if !generate_default_stubs { let stream = quote::format_ident!("{}Stream", method.identifier()); quote!(type ResponseStream = T::#stream) } else { quote!(type ResponseStream = BoxStream<#response>) }; let inner_arg = if use_arc_self { quote!(inner) } else { quote!(&inner) }; quote! { #[allow(non_camel_case_types)] struct #service_ident(pub Arc); impl tonic::server::ServerStreamingService<#request> for #service_ident { type Response = #response; #response_stream; type Future = BoxFuture, tonic::Status>; fn call(&mut self, request: tonic::Request<#request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::#method_ident(#inner_arg, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = #service_ident(inner); let codec = #codec_name::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config(accept_compression_encodings, send_compression_encodings) .apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size); let res = grpc.server_streaming(method, req).await; Ok(res) }; Box::pin(fut) } } fn generate_client_streaming( method: &T, proto_path: &str, compile_well_known_types: bool, method_ident: Ident, server_trait: Ident, use_arc_self: bool, ) -> TokenStream { let service_ident = quote::format_ident!("{}Svc", method.identifier()); let (request, response) = method.request_response_name(proto_path, compile_well_known_types); let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let inner_arg = if use_arc_self { quote!(inner) } else { quote!(&inner) }; quote! { #[allow(non_camel_case_types)] struct #service_ident(pub Arc); impl tonic::server::ClientStreamingService<#request> for #service_ident { type Response = #response; type Future = BoxFuture, tonic::Status>; fn call(&mut self, request: tonic::Request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::#method_ident(#inner_arg, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = #service_ident(inner); let codec = #codec_name::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config(accept_compression_encodings, send_compression_encodings) .apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size); let res = grpc.client_streaming(method, req).await; Ok(res) }; Box::pin(fut) } } fn generate_streaming( method: &T, proto_path: &str, compile_well_known_types: bool, method_ident: Ident, server_trait: Ident, use_arc_self: bool, generate_default_stubs: bool, ) -> TokenStream { let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let service_ident = quote::format_ident!("{}Svc", method.identifier()); let (request, response) = method.request_response_name(proto_path, compile_well_known_types); let response_stream = if !generate_default_stubs { let stream = quote::format_ident!("{}Stream", method.identifier()); quote!(type ResponseStream = T::#stream) } else { quote!(type ResponseStream = BoxStream<#response>) }; let inner_arg = if use_arc_self { quote!(inner) } else { quote!(&inner) }; quote! { #[allow(non_camel_case_types)] struct #service_ident(pub Arc); impl tonic::server::StreamingService<#request> for #service_ident { type Response = #response; #response_stream; type Future = BoxFuture, tonic::Status>; fn call(&mut self, request: tonic::Request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::#method_ident(#inner_arg, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = #service_ident(inner); let codec = #codec_name::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config(accept_compression_encodings, send_compression_encodings) .apply_max_message_size_config(max_decoding_message_size, max_encoding_message_size); let res = grpc.streaming(method, req).await; Ok(res) }; Box::pin(fut) } } ================================================ FILE: tonic-health/Cargo.toml ================================================ [package] authors = ["James Nugent "] categories = ["network-programming", "asynchronous"] description = """ Health Checking module of `tonic` gRPC implementation. """ edition = "2024" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "async", "healthcheck"] license = "MIT" name = "tonic-health" readme = "README.md" repository = "https://github.com/hyperium/tonic" version = "0.14.5" rust-version = { workspace = true } [dependencies] prost = "0.14" tokio = {version = "1.0", features = ["sync"]} tokio-stream = {version = "0.1", default-features = false, features = ["sync"]} tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["codegen"] } tonic-prost = { version = "0.14.0", path = "../tonic-prost", default-features = false } [dev-dependencies] tokio = {version = "1.0", features = ["rt-multi-thread", "macros"]} prost-types = "0.14.0" [lints] workspace = true [package.metadata.cargo_check_external_types] allowed_external_types = [ "tonic::*", # major released "bytes::*", "http::*", "http_body::*", # not major released "prost::*", "futures_core::stream::Stream", "tower_service::Service", ] ================================================ FILE: tonic-health/README.md ================================================ # tonic-health A `tonic` based gRPC healthcheck implementation. It closely follows the official [health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md), although it may not implement all features described in the specs. Please follow the example in the [main repo](https://github.com/hyperium/tonic/tree/master/examples/src/health) to see how it works. ## Features - transport: Provides the ability to set the service by using the type system and the `NamedService` trait. You can use it like that: ```rust let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; let client = HealthClient::new(conn); ``` ================================================ FILE: tonic-health/proto/health.proto ================================================ // Copyright 2015 The gRPC Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // The canonical version of this proto can be found at // https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto syntax = "proto3"; package grpc.health.v1; option csharp_namespace = "Grpc.Health.V1"; option go_package = "google.golang.org/grpc/health/grpc_health_v1"; option java_multiple_files = true; option java_outer_classname = "HealthProto"; option java_package = "io.grpc.health.v1"; message HealthCheckRequest { string service = 1; } message HealthCheckResponse { enum ServingStatus { UNKNOWN = 0; SERVING = 1; NOT_SERVING = 2; SERVICE_UNKNOWN = 3; // Used only by the Watch method. } ServingStatus status = 1; } service Health { // If the requested service is unknown, the call will fail with status // NOT_FOUND. rpc Check(HealthCheckRequest) returns (HealthCheckResponse); // Performs a watch for the serving status of the requested service. // The server will immediately send back a message indicating the current // serving status. It will then subsequently send a new message whenever // the service's serving status changes. // // If the requested service is unknown when the call is received, the // server will send a message setting the serving status to // SERVICE_UNKNOWN but will *not* terminate the call. If at some // future point, the serving status of the service becomes known, the // server will send a new message with the service's serving status. // // If the call terminates with status UNIMPLEMENTED, then clients // should assume this method is not supported and should not retry the // call. If the call terminates with any other status (including OK), // clients should retry the call with appropriate exponential backoff. rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); } ================================================ FILE: tonic-health/src/generated/grpc_health_v1.rs ================================================ // This file is @generated by prost-build. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct HealthCheckRequest { #[prost(string, tag = "1")] pub service: ::prost::alloc::string::String, } #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct HealthCheckResponse { #[prost(enumeration = "health_check_response::ServingStatus", tag = "1")] pub status: i32, } /// Nested message and enum types in `HealthCheckResponse`. pub mod health_check_response { #[derive( Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration )] #[repr(i32)] pub enum ServingStatus { Unknown = 0, Serving = 1, NotServing = 2, /// Used only by the Watch method. ServiceUnknown = 3, } impl ServingStatus { /// String value of the enum field names used in the ProtoBuf definition. /// /// The values are not transformed in any way and thus are considered stable /// (if the ProtoBuf definition does not change) and safe for programmatic use. pub fn as_str_name(&self) -> &'static str { match self { Self::Unknown => "UNKNOWN", Self::Serving => "SERVING", Self::NotServing => "NOT_SERVING", Self::ServiceUnknown => "SERVICE_UNKNOWN", } } /// Creates an enum from field names used in the ProtoBuf definition. pub fn from_str_name(value: &str) -> ::core::option::Option { match value { "UNKNOWN" => Some(Self::Unknown), "SERVING" => Some(Self::Serving), "NOT_SERVING" => Some(Self::NotServing), "SERVICE_UNKNOWN" => Some(Self::ServiceUnknown), _ => None, } } } } /// Generated client implementations. pub mod health_client { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; use tonic::codegen::http::Uri; #[derive(Debug, Clone)] pub struct HealthClient { inner: tonic::client::Grpc, } impl HealthClient where T: tonic::client::GrpcService, T::Error: Into, T::ResponseBody: Body + std::marker::Send + 'static, ::Error: Into + std::marker::Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } pub fn with_origin(inner: T, origin: Uri) -> Self { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } pub fn with_interceptor( inner: T, interceptor: F, ) -> HealthClient> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response< >::ResponseBody, >, >, , >>::Error: Into + std::marker::Send + std::marker::Sync, { HealthClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); self } /// Enable decompressing responses. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.accept_compressed(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_decoding_message_size(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_encoding_message_size(limit); self } /// If the requested service is unknown, the call will fail with status /// NOT_FOUND. pub async fn check( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result< tonic::Response, tonic::Status, > { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.health.v1.Health/Check", ); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("grpc.health.v1.Health", "Check")); self.inner.unary(req, path, codec).await } /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current /// serving status. It will then subsequently send a new message whenever /// the service's serving status changes. /// /// If the requested service is unknown when the call is received, the /// server will send a message setting the serving status to /// SERVICE_UNKNOWN but will *not* terminate the call. If at some /// future point, the serving status of the service becomes known, the /// server will send a new message with the service's serving status. /// /// If the call terminates with status UNIMPLEMENTED, then clients /// should assume this method is not supported and should not retry the /// call. If the call terminates with any other status (including OK), /// clients should retry the call with appropriate exponential backoff. pub async fn watch( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result< tonic::Response>, tonic::Status, > { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.health.v1.Health/Watch", ); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("grpc.health.v1.Health", "Watch")); self.inner.server_streaming(req, path, codec).await } } } /// Generated server implementations. pub mod health_server { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with HealthServer. #[async_trait] pub trait Health: std::marker::Send + std::marker::Sync + 'static { /// If the requested service is unknown, the call will fail with status /// NOT_FOUND. async fn check( &self, request: tonic::Request, ) -> std::result::Result< tonic::Response, tonic::Status, >; /// Server streaming response type for the Watch method. type WatchStream: tonic::codegen::tokio_stream::Stream< Item = std::result::Result, > + std::marker::Send + 'static; /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current /// serving status. It will then subsequently send a new message whenever /// the service's serving status changes. /// /// If the requested service is unknown when the call is received, the /// server will send a message setting the serving status to /// SERVICE_UNKNOWN but will *not* terminate the call. If at some /// future point, the serving status of the service becomes known, the /// server will send a new message with the service's serving status. /// /// If the call terminates with status UNIMPLEMENTED, then clients /// should assume this method is not supported and should not retry the /// call. If the call terminates with any other status (including OK), /// clients should retry the call with appropriate exponential backoff. async fn watch( &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; } #[derive(Debug)] pub struct HealthServer { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } impl HealthServer { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } pub fn from_arc(inner: Arc) -> Self { Self { inner, accept_compression_encodings: Default::default(), send_compression_encodings: Default::default(), max_decoding_message_size: None, max_encoding_message_size: None, } } pub fn with_interceptor( inner: T, interceptor: F, ) -> InterceptedService where F: tonic::service::Interceptor, { InterceptedService::new(Self::new(inner), interceptor) } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Compress responses with the given encoding, if the client supports it. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.max_encoding_message_size = Some(limit); self } } impl tonic::codegen::Service> for HealthServer where T: Health, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { type Response = http::Response; type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready( &mut self, _cx: &mut Context<'_>, ) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { "/grpc.health.v1.Health/Check" => { #[allow(non_camel_case_types)] struct CheckSvc(pub Arc); impl< T: Health, > tonic::server::UnaryService for CheckSvc { type Response = super::HealthCheckResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::check(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = CheckSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) } "/grpc.health.v1.Health/Watch" => { #[allow(non_camel_case_types)] struct WatchSvc(pub Arc); impl< T: Health, > tonic::server::ServerStreamingService for WatchSvc { type Response = super::HealthCheckResponse; type ResponseStream = T::WatchStream; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::watch(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = WatchSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.server_streaming(method, req).await; Ok(res) }; Box::pin(fut) } _ => { Box::pin(async move { let mut response = http::Response::new( tonic::body::Body::default(), ); let headers = response.headers_mut(); headers .insert( tonic::Status::GRPC_STATUS, (tonic::Code::Unimplemented as i32).into(), ); headers .insert( http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE, ); Ok(response) }) } } } } impl Clone for HealthServer { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { inner, accept_compression_encodings: self.accept_compression_encodings, send_compression_encodings: self.send_compression_encodings, max_decoding_message_size: self.max_decoding_message_size, max_encoding_message_size: self.max_encoding_message_size, } } } /// Generated gRPC service name pub const SERVICE_NAME: &str = "grpc.health.v1.Health"; impl tonic::server::NamedService for HealthServer { const NAME: &'static str = SERVICE_NAME; } } ================================================ FILE: tonic-health/src/generated/grpc_health_v1_fds.rs ================================================ // This file is @generated by codegen. // Copyright 2015 The gRPC Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // The canonical version of this proto can be found at // https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto // /// Byte encoded FILE_DESCRIPTOR_SET. pub const FILE_DESCRIPTOR_SET: &[u8] = &[ 10u8, 158u8, 4u8, 10u8, 12u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 18u8, 14u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 34u8, 46u8, 10u8, 18u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 24u8, 10u8, 7u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 34u8, 177u8, 1u8, 10u8, 19u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 73u8, 10u8, 6u8, 115u8, 116u8, 97u8, 116u8, 117u8, 115u8, 24u8, 1u8, 32u8, 1u8, 40u8, 14u8, 50u8, 49u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 46u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 46u8, 83u8, 101u8, 114u8, 118u8, 105u8, 110u8, 103u8, 83u8, 116u8, 97u8, 116u8, 117u8, 115u8, 82u8, 6u8, 115u8, 116u8, 97u8, 116u8, 117u8, 115u8, 34u8, 79u8, 10u8, 13u8, 83u8, 101u8, 114u8, 118u8, 105u8, 110u8, 103u8, 83u8, 116u8, 97u8, 116u8, 117u8, 115u8, 18u8, 11u8, 10u8, 7u8, 85u8, 78u8, 75u8, 78u8, 79u8, 87u8, 78u8, 16u8, 0u8, 18u8, 11u8, 10u8, 7u8, 83u8, 69u8, 82u8, 86u8, 73u8, 78u8, 71u8, 16u8, 1u8, 18u8, 15u8, 10u8, 11u8, 78u8, 79u8, 84u8, 95u8, 83u8, 69u8, 82u8, 86u8, 73u8, 78u8, 71u8, 16u8, 2u8, 18u8, 19u8, 10u8, 15u8, 83u8, 69u8, 82u8, 86u8, 73u8, 67u8, 69u8, 95u8, 85u8, 78u8, 75u8, 78u8, 79u8, 87u8, 78u8, 16u8, 3u8, 50u8, 174u8, 1u8, 10u8, 6u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 18u8, 80u8, 10u8, 5u8, 67u8, 104u8, 101u8, 99u8, 107u8, 18u8, 34u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 46u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 35u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 46u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 82u8, 10u8, 5u8, 87u8, 97u8, 116u8, 99u8, 104u8, 18u8, 34u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 46u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 35u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 46u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 48u8, 1u8, 66u8, 97u8, 10u8, 17u8, 105u8, 111u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 66u8, 11u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 80u8, 114u8, 111u8, 116u8, 111u8, 80u8, 1u8, 90u8, 44u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 103u8, 111u8, 108u8, 97u8, 110u8, 103u8, 46u8, 111u8, 114u8, 103u8, 47u8, 103u8, 114u8, 112u8, 99u8, 47u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 47u8, 103u8, 114u8, 112u8, 99u8, 95u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 95u8, 118u8, 49u8, 170u8, 2u8, 14u8, 71u8, 114u8, 112u8, 99u8, 46u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 86u8, 49u8, 98u8, 6u8, 112u8, 114u8, 111u8, 116u8, 111u8, 51u8, ]; ================================================ FILE: tonic-health/src/lib.rs ================================================ //! A `tonic` based gRPC healthcheck implementation. //! //! # Example //! //! An example can be found [here]. //! //! [here]: https://github.com/hyperium/tonic/blob/master/examples/src/health/server.rs #![doc( html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" )] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] #![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))] #![cfg_attr(docsrs, feature(doc_cfg))] use std::fmt::{Display, Formatter}; mod generated { #![allow(unreachable_pub)] #![allow(missing_docs)] #[rustfmt::skip] pub mod grpc_health_v1; #[rustfmt::skip] pub mod grpc_health_v1_fds; pub use grpc_health_v1_fds::FILE_DESCRIPTOR_SET; #[cfg(test)] mod tests { use super::FILE_DESCRIPTOR_SET; use prost::Message as _; #[test] fn file_descriptor_set_is_valid() { prost_types::FileDescriptorSet::decode(FILE_DESCRIPTOR_SET).unwrap(); } } } /// Generated protobuf types from the `grpc.health.v1` package. pub mod pb { pub use crate::generated::{FILE_DESCRIPTOR_SET, grpc_health_v1::*}; } pub mod server; /// An enumeration of values representing gRPC service health. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ServingStatus { /// Unknown status Unknown, /// The service is currently up and serving requests. Serving, /// The service is currently down and not serving requests. NotServing, } impl Display for ServingStatus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ServingStatus::Unknown => f.write_str("Unknown"), ServingStatus::Serving => f.write_str("Serving"), ServingStatus::NotServing => f.write_str("NotServing"), } } } impl From for pb::health_check_response::ServingStatus { fn from(s: ServingStatus) -> Self { match s { ServingStatus::Unknown => pb::health_check_response::ServingStatus::Unknown, ServingStatus::Serving => pb::health_check_response::ServingStatus::Serving, ServingStatus::NotServing => pb::health_check_response::ServingStatus::NotServing, } } } ================================================ FILE: tonic-health/src/server.rs ================================================ //! Contains all healthcheck based server utilities. use crate::ServingStatus; use crate::pb::health_server::{Health, HealthServer}; use crate::pb::{HealthCheckRequest, HealthCheckResponse}; use std::collections::HashMap; use std::fmt; use std::sync::Arc; use tokio::sync::{RwLock, watch}; use tokio_stream::Stream; use tonic::{Request, Response, Status, server::NamedService}; /// Creates a `HealthReporter` and a linked `HealthServer` pair. Together, /// these types can be used to serve the gRPC Health Checking service. /// /// A `HealthReporter` is used to update the state of gRPC services. /// /// A `HealthServer` is a Tonic gRPC server for the `grpc.health.v1.Health`, /// which can be added to a Tonic runtime using `add_service` on the runtime /// builder. pub fn health_reporter() -> (HealthReporter, HealthServer) { let reporter = HealthReporter::new(); let service = HealthService::new(reporter.statuses.clone()); let server = HealthServer::new(service); (reporter, server) } type StatusPair = (watch::Sender, watch::Receiver); /// A handle providing methods to update the health status of gRPC services. A /// `HealthReporter` is connected to a `HealthServer` which serves the statuses /// over the `grpc.health.v1.Health` service. #[derive(Clone, Debug)] pub struct HealthReporter { statuses: Arc>>, } impl HealthReporter { /// Create a new HealthReporter with an initial service (named ""), corresponding to overall server health pub fn new() -> Self { // According to the gRPC Health Check specification, the empty service "" corresponds to the overall server health let server_status = ("".to_string(), watch::channel(ServingStatus::Serving)); let statuses = Arc::new(RwLock::new(HashMap::from([server_status]))); HealthReporter { statuses } } /// Sets the status of the service implemented by `S` to `Serving`. This notifies any watchers /// if there is a change in status. pub async fn set_serving(&self) where S: NamedService, { let service_name = ::NAME; self.set_service_status(service_name, ServingStatus::Serving) .await; } /// Sets the status of the service implemented by `S` to `NotServing`. This notifies any watchers /// if there is a change in status. pub async fn set_not_serving(&self) where S: NamedService, { let service_name = ::NAME; self.set_service_status(service_name, ServingStatus::NotServing) .await; } /// Sets the status of the service with `service_name` to `status`. This notifies any watchers /// if there is a change in status. pub async fn set_service_status(&self, service_name: S, status: ServingStatus) where S: AsRef, { let service_name = service_name.as_ref(); let mut writer = self.statuses.write().await; match writer.get(service_name) { Some((tx, _)) => { // We only ever hand out clones of the receiver, so the originally-created // receiver should always be present, only being dropped when clearing the // service status. Consequently, `tx.send` should not fail, making use // of `expect` here safe. tx.send(status).expect("channel should not be closed"); } None => { writer.insert(service_name.to_string(), watch::channel(status)); } }; } /// Clear the status of the given service. pub async fn clear_service_status(&mut self, service_name: &str) { let mut writer = self.statuses.write().await; let _ = writer.remove(service_name); } } impl Default for HealthReporter { fn default() -> Self { Self::new() } } /// A service providing implementations of gRPC health checking protocol. #[derive(Debug)] pub struct HealthService { statuses: Arc>>, } impl HealthService { fn new(services: Arc>>) -> Self { HealthService { statuses: services } } /// Create a HealthService, carrying across the statuses from an existing HealthReporter pub fn from_health_reporter(health_reporter: HealthReporter) -> Self { Self::new(health_reporter.statuses) } async fn service_health(&self, service_name: &str) -> Option { let reader = self.statuses.read().await; reader.get(service_name).map(|p| *p.1.borrow()) } } #[tonic::async_trait] impl Health for HealthService { async fn check( &self, request: Request, ) -> Result, Status> { let service_name = request.get_ref().service.as_str(); let Some(status) = self.service_health(service_name).await else { return Err(Status::not_found("service not registered")); }; Ok(Response::new(HealthCheckResponse::new(status))) } type WatchStream = WatchStream; async fn watch( &self, request: Request, ) -> Result, Status> { let service_name = request.get_ref().service.as_str(); let status_rx = match self.statuses.read().await.get(service_name) { Some((_tx, rx)) => rx.clone(), None => return Err(Status::not_found("service not registered")), }; Ok(Response::new(WatchStream::new(status_rx))) } } /// A watch stream for the health service. pub struct WatchStream { inner: tokio_stream::wrappers::WatchStream, } impl WatchStream { fn new(status_rx: watch::Receiver) -> Self { let inner = tokio_stream::wrappers::WatchStream::new(status_rx); Self { inner } } } impl Stream for WatchStream { type Item = Result; fn poll_next( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { std::pin::Pin::new(&mut self.inner) .poll_next(cx) .map(|opt| opt.map(|status| Ok(HealthCheckResponse::new(status)))) } } impl fmt::Debug for WatchStream { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("WatchStream").finish() } } impl HealthCheckResponse { fn new(status: ServingStatus) -> Self { let status = crate::pb::health_check_response::ServingStatus::from(status) as i32; Self { status } } } #[cfg(test)] mod tests { use crate::ServingStatus; use crate::pb::HealthCheckRequest; use crate::pb::health_server::Health; use crate::server::{HealthReporter, HealthService}; use tokio::sync::watch; use tokio_stream::StreamExt; use tonic::{Code, Request, Status}; fn assert_serving_status(wire: i32, expected: ServingStatus) { let expected = crate::pb::health_check_response::ServingStatus::from(expected) as i32; assert_eq!(wire, expected); } fn assert_grpc_status(wire: Option, expected: Code) { let wire = wire.expect("status is not None").code(); assert_eq!(wire, expected); } async fn make_test_service() -> (HealthReporter, HealthService) { let health_reporter = HealthReporter::new(); // insert test value { let mut statuses = health_reporter.statuses.write().await; statuses.insert( "TestService".to_string(), watch::channel(ServingStatus::Unknown), ); } let health_service = HealthService::new(health_reporter.statuses.clone()); (health_reporter, health_service) } #[tokio::test] async fn test_service_check() { let (reporter, service) = make_test_service().await; // Overall server health let resp = service .check(Request::new(HealthCheckRequest { service: "".to_string(), })) .await; assert!(resp.is_ok()); let resp = resp.unwrap().into_inner(); assert_serving_status(resp.status, ServingStatus::Serving); // Unregistered service let resp = service .check(Request::new(HealthCheckRequest { service: "Unregistered".to_string(), })) .await; assert!(resp.is_err()); assert_grpc_status(resp.err(), Code::NotFound); // Registered service - initial state let resp = service .check(Request::new(HealthCheckRequest { service: "TestService".to_string(), })) .await; assert!(resp.is_ok()); let resp = resp.unwrap().into_inner(); assert_serving_status(resp.status, ServingStatus::Unknown); // Registered service - updated state reporter .set_service_status("TestService", ServingStatus::Serving) .await; let resp = service .check(Request::new(HealthCheckRequest { service: "TestService".to_string(), })) .await; assert!(resp.is_ok()); let resp = resp.unwrap().into_inner(); assert_serving_status(resp.status, ServingStatus::Serving); } #[tokio::test] async fn test_service_watch() { let (mut reporter, service) = make_test_service().await; // Overall server health let resp = service .watch(Request::new(HealthCheckRequest { service: "".to_string(), })) .await; assert!(resp.is_ok()); let mut resp = resp.unwrap().into_inner(); let item = resp .next() .await .expect("streamed response is Some") .expect("response is ok"); assert_serving_status(item.status, ServingStatus::Serving); // Unregistered service let resp = service .watch(Request::new(HealthCheckRequest { service: "Unregistered".to_string(), })) .await; assert!(resp.is_err()); assert_grpc_status(resp.err(), Code::NotFound); // Registered service let resp = service .watch(Request::new(HealthCheckRequest { service: "TestService".to_string(), })) .await; assert!(resp.is_ok()); let mut resp = resp.unwrap().into_inner(); // Registered service - initial state let item = resp .next() .await .expect("streamed response is Some") .expect("response is ok"); assert_serving_status(item.status, ServingStatus::Unknown); // Registered service - updated state reporter .set_service_status("TestService", ServingStatus::NotServing) .await; let item = resp .next() .await .expect("streamed response is Some") .expect("response is ok"); assert_serving_status(item.status, ServingStatus::NotServing); // Registered service - updated state reporter .set_service_status("TestService", ServingStatus::Serving) .await; let item = resp .next() .await .expect("streamed response is Some") .expect("response is ok"); assert_serving_status(item.status, ServingStatus::Serving); // De-registered service reporter.clear_service_status("TestService").await; let item = resp.next().await; assert!(item.is_none()); } } ================================================ FILE: tonic-prost/Cargo.toml ================================================ [package] name = "tonic-prost" version = "0.14.5" authors = ["Lucio Franco "] edition = "2024" license = "MIT" repository = "https://github.com/hyperium/tonic" homepage = "https://github.com/hyperium/tonic" description = "Prost codec implementation for tonic" readme = "README.md" categories = ["network-programming", "asynchronous"] keywords = ["rpc", "grpc", "prost", "protobuf", "tonic"] rust-version = { workspace = true } [dependencies] tonic = { version = "0.14.0", path = "../tonic", default-features = false } prost = "0.14" bytes = "1" [dev-dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1" http-body = "1" http-body-util = "0.1" [package.metadata.cargo_check_external_types] allowed_external_types = [ "tonic::*", "prost::*", "prost" ] ================================================ FILE: tonic-prost/README.md ================================================ # tonic-prost Prost codec implementation for [tonic] gRPC framework. ## Overview This crate provides the `ProstCodec` for encoding and decoding protobuf messages using the [prost] library. ## Usage This crate is typically used through the main `tonic` crate with the `prost` feature enabled (which is enabled by default). ```toml [dependencies] tonic = "0.14" ``` [tonic]: https://github.com/hyperium/tonic [prost]: https://github.com/tokio-rs/prost ================================================ FILE: tonic-prost/src/codec.rs ================================================ use prost::Message; use std::marker::PhantomData; use tonic::Status; use tonic::codec::{BufferSettings, Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}; /// A [`Codec`] that implements `application/grpc+proto` via the prost library. #[derive(Debug, Clone)] pub struct ProstCodec { _pd: PhantomData<(T, U)>, } impl ProstCodec { /// Configure a ProstCodec with encoder/decoder buffer settings. This is used to control /// how memory is allocated and grows per RPC. pub fn new() -> Self { Self { _pd: PhantomData } } } impl Default for ProstCodec { fn default() -> Self { Self::new() } } impl ProstCodec where T: Message + Send + 'static, U: Message + Default + Send + 'static, { /// A tool for building custom codecs based on prost encoding and decoding. /// See the codec_buffers example for one possible way to use this. pub fn raw_encoder(buffer_settings: BufferSettings) -> ::Encoder { ProstEncoder { _pd: PhantomData, buffer_settings, } } /// A tool for building custom codecs based on prost encoding and decoding. /// See the codec_buffers example for one possible way to use this. pub fn raw_decoder(buffer_settings: BufferSettings) -> ::Decoder { ProstDecoder { _pd: PhantomData, buffer_settings, } } } impl Codec for ProstCodec where T: Message + Send + 'static, U: Message + Default + Send + 'static, { type Encode = T; type Decode = U; type Encoder = ProstEncoder; type Decoder = ProstDecoder; fn encoder(&mut self) -> Self::Encoder { ProstEncoder { _pd: PhantomData, buffer_settings: BufferSettings::default(), } } fn decoder(&mut self) -> Self::Decoder { ProstDecoder { _pd: PhantomData, buffer_settings: BufferSettings::default(), } } } /// A [`Encoder`] that knows how to encode `T`. #[derive(Debug, Clone, Default)] pub struct ProstEncoder { _pd: PhantomData, buffer_settings: BufferSettings, } impl ProstEncoder { /// Get a new encoder with explicit buffer settings pub fn new(buffer_settings: BufferSettings) -> Self { Self { _pd: PhantomData, buffer_settings, } } } impl Encoder for ProstEncoder { type Item = T; type Error = Status; fn encode(&mut self, item: Self::Item, buf: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { item.encode(buf) .expect("Message only errors if not enough space"); Ok(()) } fn buffer_settings(&self) -> BufferSettings { self.buffer_settings } } /// A [`Decoder`] that knows how to decode `U`. #[derive(Debug, Clone, Default)] pub struct ProstDecoder { _pd: PhantomData, buffer_settings: BufferSettings, } impl ProstDecoder { /// Get a new decoder with explicit buffer settings pub fn new(buffer_settings: BufferSettings) -> Self { Self { _pd: PhantomData, buffer_settings, } } } impl Decoder for ProstDecoder { type Item = U; type Error = Status; fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error> { let item = Message::decode(buf) .map(Option::Some) .map_err(from_decode_error)?; Ok(item) } fn buffer_settings(&self) -> BufferSettings { self.buffer_settings } } fn from_decode_error(error: prost::DecodeError) -> Status { // Map Protobuf parse errors to an INTERNAL status code, as per // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md Status::internal(error.to_string()) } #[cfg(test)] mod tests { use super::*; use bytes::{Buf, BufMut, BytesMut}; use http_body::Body; use http_body_util::BodyExt as _; use std::pin::pin; use tonic::codec::SingleMessageCompressionOverride; use tonic::codec::{EncodeBody, HEADER_SIZE, Streaming}; const LEN: usize = 10000; // The maximum uncompressed size in bytes for a message. Set to 2MB. const MAX_MESSAGE_SIZE: usize = 2 * 1024 * 1024; #[tokio::test] async fn decode() { let decoder = MockDecoder::default(); let msg = vec![0u8; LEN]; let mut buf = BytesMut::new(); buf.reserve(msg.len() + HEADER_SIZE); buf.put_u8(0); buf.put_u32(msg.len() as u32); buf.put(&msg[..]); let body = body::MockBody::new(&buf[..], 10005, 0); let mut stream = Streaming::new_request(decoder, body, None, None); let mut i = 0usize; while let Some(output_msg) = stream.message().await.unwrap() { assert_eq!(output_msg.len(), msg.len()); i += 1; } assert_eq!(i, 1); } #[tokio::test] async fn decode_max_message_size_exceeded() { let decoder = MockDecoder::default(); let msg = vec![0u8; MAX_MESSAGE_SIZE + 1]; let mut buf = BytesMut::new(); buf.reserve(msg.len() + HEADER_SIZE); buf.put_u8(0); buf.put_u32(msg.len() as u32); buf.put(&msg[..]); let body = body::MockBody::new(&buf[..], MAX_MESSAGE_SIZE + HEADER_SIZE + 1, 0); let mut stream = Streaming::new_request(decoder, body, None, Some(MAX_MESSAGE_SIZE)); let actual = stream.message().await.unwrap_err(); let expected = Status::out_of_range(format!( "Error, decoded message length too large: found {} bytes, the limit is: {} bytes", msg.len(), MAX_MESSAGE_SIZE )); assert_eq!(actual.code(), expected.code()); assert_eq!(actual.message(), expected.message()); } #[tokio::test] async fn encode() { let encoder = MockEncoder::default(); let msg = Vec::from(&[0u8; 1024][..]); let messages = std::iter::repeat_with(move || Ok::<_, Status>(msg.clone())).take(10000); let source = tokio_stream::iter(messages); let mut body = pin!(EncodeBody::new_server( encoder, source, None, SingleMessageCompressionOverride::default(), None, )); while let Some(r) = body.frame().await { r.unwrap(); } } #[tokio::test] async fn encode_max_message_size_exceeded() { let encoder = MockEncoder::default(); let msg = vec![0u8; MAX_MESSAGE_SIZE + 1]; let messages = std::iter::once(Ok::<_, Status>(msg)); let source = tokio_stream::iter(messages); let mut body = pin!(EncodeBody::new_server( encoder, source, None, SingleMessageCompressionOverride::default(), Some(MAX_MESSAGE_SIZE), )); let frame = body .frame() .await .expect("at least one frame") .expect("no error polling frame"); assert_eq!( frame .into_trailers() .expect("got trailers") .get(Status::GRPC_STATUS) .expect("grpc-status header"), "11" ); assert!(body.is_end_stream()); } // skip on windows because CI stumbles over our 4GB allocation #[cfg(not(target_family = "windows"))] #[tokio::test] async fn encode_too_big() { let encoder = MockEncoder::default(); let msg = vec![0u8; u32::MAX as usize + 1]; let messages = std::iter::once(Ok::<_, Status>(msg)); let source = tokio_stream::iter(messages); let mut body = pin!(EncodeBody::new_server( encoder, source, None, SingleMessageCompressionOverride::default(), Some(usize::MAX), )); let frame = body .frame() .await .expect("at least one frame") .expect("no error polling frame"); assert_eq!( frame .into_trailers() .expect("got trailers") .get(Status::GRPC_STATUS) .expect("grpc-status header"), "8" ); assert!(body.is_end_stream()); } #[derive(Debug, Clone, Default)] struct MockEncoder {} impl Encoder for MockEncoder { type Item = Vec; type Error = Status; fn encode(&mut self, item: Self::Item, buf: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { buf.put(&item[..]); Ok(()) } fn buffer_settings(&self) -> BufferSettings { Default::default() } } #[derive(Debug, Clone, Default)] struct MockDecoder {} impl Decoder for MockDecoder { type Item = Vec; type Error = Status; fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error> { let out = Vec::from(buf.chunk()); buf.advance(LEN); Ok(Some(out)) } fn buffer_settings(&self) -> BufferSettings { Default::default() } } mod body { use bytes::Bytes; use http_body::{Body, Frame}; use std::{ pin::Pin, task::{Context, Poll}, }; use tonic::Status; #[derive(Debug)] pub(super) struct MockBody { data: Bytes, // the size of the partial message to send partial_len: usize, // the number of times we've sent count: usize, } impl MockBody { pub(super) fn new(b: &[u8], partial_len: usize, count: usize) -> Self { MockBody { data: Bytes::copy_from_slice(b), partial_len, count, } } } impl Body for MockBody { type Data = Bytes; type Error = Status; fn poll_frame( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { // every other call to poll_data returns data let should_send = self.count % 2 == 0; let data_len = self.data.len(); let partial_len = self.partial_len; let count = self.count; if data_len > 0 { let result = if should_send { let response = self.data .split_to(if count == 0 { partial_len } else { data_len }); Poll::Ready(Some(Ok(Frame::data(response)))) } else { cx.waker().wake_by_ref(); Poll::Pending }; // make some fake progress self.count += 1; result } else { Poll::Ready(None) } } } } } ================================================ FILE: tonic-prost/src/lib.rs ================================================ //! Prost codec implementation for tonic. //! //! This crate provides the [`ProstCodec`] for encoding and decoding protobuf //! messages using the [`prost`] library. //! //! # Example //! //! ```rust,ignore //! use tonic_prost::ProstCodec; //! //! let codec = ProstCodec::::default(); //! ``` #![warn( missing_docs, missing_debug_implementations, rust_2018_idioms, unreachable_pub )] #![doc( html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" )] #![doc(html_root_url = "https://docs.rs/tonic-prost/0.13.1")] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] mod codec; pub use codec::{ProstCodec, ProstDecoder, ProstEncoder}; // Re-export prost types that users might need pub use prost; ================================================ FILE: tonic-prost-build/Cargo.toml ================================================ [package] name = "tonic-prost-build" version = "0.14.5" authors = ["Lucio Franco "] edition = "2024" license = "MIT" repository = "https://github.com/hyperium/tonic" homepage = "https://github.com/hyperium/tonic" description = "Prost build integration for tonic" readme = "README.md" categories = ["development-tools::build-utils", "network-programming", "asynchronous"] keywords = ["rpc", "grpc", "prost", "protobuf", "tonic"] rust-version = { workspace = true } [features] default = ["transport", "cleanup-markdown"] transport = ["tonic-build/transport"] cleanup-markdown = ["prost-build/cleanup-markdown"] [dependencies] tonic-build = { version = "0.14.0", path = "../tonic-build", default-features = false } prost-build = { version = "0.14" } prost-types = { version = "0.14" } prettyplease = { version = "0.2" } proc-macro2 = "1.0" quote = "1.0" syn = "2.0" tempfile = "3.0" [dev-dependencies] tonic = { version = "0.14.0", path = "../tonic", default-features = false } [package.metadata.cargo_check_external_types] allowed_external_types = [ "tonic_build::*", "prost_build::*", "prost_types::*" ] ================================================ FILE: tonic-prost-build/README.md ================================================ # tonic-prost-build Prost build integration for [tonic] gRPC framework. ## Overview This crate provides code generation for gRPC services using protobuf definitions via the [prost] ecosystem. It bridges [prost-build] with [tonic]'s generic code generation infrastructure. ## Usage Add to your `build.rs`: ```rust fn main() { tonic_prost_build::configure() .compile_protos(&["proto/service.proto"], &["proto"]) .unwrap(); } ``` ## Features - `transport`: Enables transport layer code generation - `cleanup-markdown`: Enables markdown cleanup in generated documentation [tonic]: https://github.com/hyperium/tonic [prost]: https://github.com/tokio-rs/prost [prost-build]: https://github.com/tokio-rs/prost ================================================ FILE: tonic-prost-build/src/lib.rs ================================================ //! Prost build integration for tonic. //! //! This crate provides code generation for gRPC services using protobuf definitions //! via the [`prost`] ecosystem. //! //! [`prost`]: https://github.com/tokio-rs/prost //! //! # Example //! //! ```rust,ignore //! use tonic_prost_build::configure; //! //! fn main() { //! configure() //! .compile_protos(&["proto/service.proto"], &["proto"]) //! .unwrap(); //! } //! ``` #![warn( missing_docs, missing_debug_implementations, rust_2018_idioms, unreachable_pub )] #![doc( html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" )] #![doc(html_root_url = "https://docs.rs/tonic-prost-build/0.14.0")] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] use proc_macro2::TokenStream; use prost_build::{Method, Service}; use quote::{ToTokens, quote}; use std::cell::RefCell; use std::{ collections::HashSet, ffi::OsString, io, path::{Path, PathBuf}, }; use tonic_build::{Attributes, CodeGenBuilder}; #[cfg(test)] mod tests; // Re-export core build functionality from tonic-build pub use tonic_build::{ Attributes as TonicAttributes, Method as TonicMethod, Service as TonicService, manual, }; // Re-export prost types that users might need pub use prost_build::Config; pub use prost_types::FileDescriptorSet; /// Configure `tonic-prost-build` code generation. /// /// Use [`compile_protos`] instead if you don't need to tweak anything. pub fn configure() -> Builder { Builder { build_client: true, build_server: true, build_transport: true, file_descriptor_set_path: None, skip_protoc_run: false, out_dir: None, extern_path: Vec::new(), field_attributes: Vec::new(), message_attributes: Vec::new(), enum_attributes: Vec::new(), type_attributes: Vec::new(), boxed: Vec::new(), btree_map: None, bytes: None, server_attributes: Attributes::default(), client_attributes: Attributes::default(), proto_path: "super".to_string(), compile_well_known_types: false, emit_package: true, with_extended_rust_types: false, protoc_args: Vec::new(), include_file: None, emit_rerun_if_changed: std::env::var_os("CARGO").is_some(), disable_comments: HashSet::default(), use_arc_self: false, generate_default_stubs: false, codec_path: "tonic_prost::ProstCodec".to_string(), skip_debug: HashSet::default(), } } /// Simple `.proto` compiling. Use [`configure`] instead if you need more options. /// /// The include directory will be the parent folder of the specified path. /// The package name will be the filename without the extension. pub fn compile_protos(proto: impl AsRef) -> io::Result<()> { let proto_path: &Path = proto.as_ref(); // directory the main .proto file resides in let proto_dir = proto_path .parent() .expect("proto file should reside in a directory"); self::configure().compile_protos(&[proto_path], &[proto_dir]) } /// Simple file descriptor set compiling. Use [`configure`] instead if you need more options. pub fn compile_fds(fds: prost_types::FileDescriptorSet) -> io::Result<()> { self::configure().compile_fds(fds) } /// Extended list of Non-path Rust types allowed for request/response types. /// Needed for compiling well known types as request/responses. const EXTENDED_NON_PATH_TYPE_ALLOWLIST: &[&str] = &["()", "bool", "i32", "i64", "u32", "u64", "f32", "f64"]; /// List of Non-path Rust types allowed for request/response types. const DEFAULT_NON_PATH_TYPE_ALLOWLIST: &[&str] = &["()"]; thread_local! { /// Chosen allowlist, which would be [`EXTENDED_NON_PATH_TYPE_ALLOWLIST`] if /// [`Builder::with_extended_rust_types`] was called. pub static NON_PATH_TYPE_ALLOWLIST: RefCell<&'static [&'static str]> = const { RefCell::new(DEFAULT_NON_PATH_TYPE_ALLOWLIST) }; } /// Newtype wrapper for prost to add tonic-specific extensions struct TonicBuildService { prost_service: Service, methods: Vec, } impl TonicBuildService { fn new(prost_service: Service, codec_path: String) -> Self { Self { // The tonic_build::Service trait specifies that methods are borrowed, so they have to reified up front. methods: prost_service .methods .iter() .map(|prost_method| TonicBuildMethod { prost_method: prost_method.clone(), codec_path: codec_path.clone(), }) .collect(), prost_service, } } } /// Newtype wrapper for prost to add tonic-specific extensions struct TonicBuildMethod { prost_method: Method, codec_path: String, } impl tonic_build::Service for TonicBuildService { type Method = TonicBuildMethod; type Comment = String; fn name(&self) -> &str { &self.prost_service.name } fn package(&self) -> &str { &self.prost_service.package } fn identifier(&self) -> &str { &self.prost_service.proto_name } fn methods(&self) -> &[Self::Method] { &self.methods } fn comment(&self) -> &[Self::Comment] { &self.prost_service.comments.leading } } impl tonic_build::Method for TonicBuildMethod { type Comment = String; fn name(&self) -> &str { &self.prost_method.name } fn identifier(&self) -> &str { &self.prost_method.proto_name } fn client_streaming(&self) -> bool { self.prost_method.client_streaming } fn server_streaming(&self) -> bool { self.prost_method.server_streaming } fn comment(&self) -> &[Self::Comment] { &self.prost_method.comments.leading } fn request_response_name( &self, proto_path: &str, compile_well_known_types: bool, ) -> (TokenStream, TokenStream) { let request = if is_google_type(&self.prost_method.input_type) && !compile_well_known_types { // For well-known types, map to absolute paths that will work with super:: match self.prost_method.input_type.as_str() { ".google.protobuf.Empty" => quote!(()), ".google.protobuf.Any" => quote!(::prost_types::Any), ".google.protobuf.StringValue" => quote!(::prost::alloc::string::String), _ => { // For other google types, assume they're in prost_types let type_name = self .prost_method .input_type .trim_start_matches(".google.protobuf.") .to_string(); syn::parse_str::(&format!("::prost_types::{type_name}")) .unwrap() .to_token_stream() } } } else if is_non_path_type(&self.prost_method.input_type) { self.prost_method.input_type.parse::().unwrap() } else { // Check if this is an extern type that starts with :: or crate:: if self.prost_method.input_type.starts_with("::") || self.prost_method.input_type.starts_with("crate::") { // This is an extern type, use it directly self.prost_method.input_type.parse::().unwrap() } else { // Replace dots with double colons for the type name let rust_type = self.prost_method.input_type.replace('.', "::"); // Remove leading :: if present let rust_type = rust_type.trim_start_matches("::"); syn::parse_str::(&format!("{proto_path}::{rust_type}")) .unwrap() .to_token_stream() } }; let response = if is_google_type(&self.prost_method.output_type) && !compile_well_known_types { // For well-known types, map to absolute paths that will work with super:: match self.prost_method.output_type.as_str() { ".google.protobuf.Empty" => quote!(()), ".google.protobuf.Any" => quote!(::prost_types::Any), ".google.protobuf.StringValue" => quote!(::prost::alloc::string::String), _ => { // For other google types, assume they're in prost_types let type_name = self .prost_method .output_type .trim_start_matches(".google.protobuf.") .to_string(); syn::parse_str::(&format!("::prost_types::{type_name}")) .unwrap() .to_token_stream() } } } else if is_non_path_type(&self.prost_method.output_type) { self.prost_method .output_type .parse::() .unwrap() } else { // Check if this is an extern type that starts with :: or crate:: if self.prost_method.output_type.starts_with("::") || self.prost_method.output_type.starts_with("crate::") { // This is an extern type, use it directly self.prost_method .output_type .parse::() .unwrap() } else { // Replace dots with double colons for the type name let rust_type = self.prost_method.output_type.replace('.', "::"); // Remove leading :: if present let rust_type = rust_type.trim_start_matches("::"); syn::parse_str::(&format!("{proto_path}::{rust_type}")) .unwrap() .to_token_stream() } }; (request, response) } fn codec_path(&self) -> &str { &self.codec_path } fn deprecated(&self) -> bool { self.prost_method.options.deprecated() } } fn is_non_path_type(ty: &str) -> bool { NON_PATH_TYPE_ALLOWLIST.with(|allowlist| { allowlist .borrow() .iter() .any(|allowlist_type| ty.ends_with(allowlist_type)) }) } fn is_google_type(ty: &str) -> bool { ty.starts_with(".google.protobuf") } /// Service generator that is compatible with prost-build #[derive(Debug)] struct ServiceGenerator { build_client: bool, build_server: bool, build_transport: bool, client_attributes: Attributes, server_attributes: Attributes, use_arc_self: bool, generate_default_stubs: bool, proto_path: String, compile_well_known_types: bool, codec_path: String, disable_comments: HashSet, } impl ServiceGenerator { /// Create a new ServiceGenerator #[allow(clippy::too_many_arguments)] fn new( build_client: bool, build_server: bool, build_transport: bool, client_attributes: Attributes, server_attributes: Attributes, use_arc_self: bool, generate_default_stubs: bool, proto_path: String, compile_well_known_types: bool, codec_path: String, disable_comments: HashSet, ) -> Self { ServiceGenerator { build_client, build_server, build_transport, client_attributes, server_attributes, use_arc_self, generate_default_stubs, proto_path, compile_well_known_types, codec_path, disable_comments, } } } impl prost_build::ServiceGenerator for ServiceGenerator { fn generate(&mut self, service: Service, buf: &mut String) { let tonic_service = TonicBuildService::new(service, self.codec_path.clone()); let mut builder = CodeGenBuilder::new(); builder .emit_package(true) .build_transport(self.build_transport) .compile_well_known_types(self.compile_well_known_types) .disable_comments(self.disable_comments.clone()) .use_arc_self(self.use_arc_self) .generate_default_stubs(self.generate_default_stubs); let mut tokens = TokenStream::new(); if self.build_client { builder.attributes(self.client_attributes.clone()); let client_code = builder.generate_client(&tonic_service, &self.proto_path); tokens.extend(client_code); } if self.build_server { builder.attributes(self.server_attributes.clone()); let server_code = builder.generate_server(&tonic_service, &self.proto_path); tokens.extend(server_code); } let formatted = prettyplease::unparse(&syn::parse2(tokens).unwrap()); buf.push_str(&formatted); } } /// Builder for configuring and generating code from `.proto` files. #[derive(Debug, Clone)] pub struct Builder { build_client: bool, build_server: bool, build_transport: bool, file_descriptor_set_path: Option, skip_protoc_run: bool, out_dir: Option, extern_path: Vec<(String, String)>, field_attributes: Vec<(String, String)>, message_attributes: Vec<(String, String)>, enum_attributes: Vec<(String, String)>, type_attributes: Vec<(String, String)>, boxed: Vec, btree_map: Option>, bytes: Option>, server_attributes: Attributes, client_attributes: Attributes, proto_path: String, compile_well_known_types: bool, emit_package: bool, with_extended_rust_types: bool, protoc_args: Vec, include_file: Option, emit_rerun_if_changed: bool, disable_comments: HashSet, use_arc_self: bool, generate_default_stubs: bool, codec_path: String, skip_debug: HashSet, } impl Builder { /// Enable or disable gRPC client code generation. pub fn build_client(mut self, enable: bool) -> Self { self.build_client = enable; self } /// Enable or disable gRPC server code generation. pub fn build_server(mut self, enable: bool) -> Self { self.build_server = enable; self } /// Enable or disable transport-related features. pub fn build_transport(mut self, enable: bool) -> Self { self.build_transport = enable; self } /// Configure the output directory where generated Rust files are written. /// /// If unset, defaults to the `OUT_DIR` environment variable. `OUT_DIR` is set by Cargo when /// executing build scripts, so `out_dir` typically does not need to be configured. pub fn out_dir(mut self, out_dir: impl AsRef) -> Self { self.out_dir = Some(out_dir.as_ref().to_path_buf()); self } /// Declare externally provided Protobuf package or type. /// /// Passed directly to `prost_build::Config.extern_path`. /// Note that both the Protobuf path and the rust package paths should both be fully qualified. /// i.e. Protobuf path should start with "." and rust path should start with "::" pub fn extern_path(mut self, proto_path: impl AsRef, rust_path: impl AsRef) -> Self { self.extern_path.push(( proto_path.as_ref().to_string(), rust_path.as_ref().to_string(), )); self } /// Add additional attribute to matched messages, enums, and one-offs. /// /// Passed directly to `prost_build::Config.field_attribute`. pub fn field_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.field_attributes .push((path.as_ref().to_string(), attribute.as_ref().to_string())); self } /// Add additional attribute to matched messages, enums, and one-offs. /// /// Passed directly to `prost_build::Config.message_attribute`. pub fn message_attribute, A: AsRef>( mut self, path: P, attribute: A, ) -> Self { self.message_attributes .push((path.as_ref().to_string(), attribute.as_ref().to_string())); self } /// Add additional attribute to matched enums and one-offs. /// /// Passed directly to `prost_build::Config.enum_attribute`. pub fn enum_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.enum_attributes .push((path.as_ref().to_string(), attribute.as_ref().to_string())); self } /// Add additional attribute to matched messages, enums, and one-offs. /// /// Passed directly to `prost_build::Config.type_attribute`. pub fn type_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.type_attributes .push((path.as_ref().to_string(), attribute.as_ref().to_string())); self } /// Add a field that should be boxed. /// /// Passed directly to `prost_build::Config.boxed`. pub fn boxed>(mut self, path: P) -> Self { self.boxed.push(path.as_ref().to_string()); self } /// Configure btree_map on a message. /// /// Passed directly to `prost_build::Config.btree_map`. pub fn btree_map>(mut self, path: P) -> Self { match &mut self.btree_map { Some(paths) => paths.push(path.as_ref().to_string()), None => self.btree_map = Some(vec![path.as_ref().to_string()]), } self } /// Configure bytes on a message. /// /// Passed directly to `prost_build::Config.bytes`. pub fn bytes>(mut self, path: P) -> Self { match &mut self.bytes { Some(paths) => paths.push(path.as_ref().to_string()), None => self.bytes = Some(vec![path.as_ref().to_string()]), } self } /// Add additional attribute to matched server `mod`s. Passed directly to /// `prost_build::Config.message_attribute` pub fn server_mod_attribute, A: AsRef>( mut self, path: P, attribute: A, ) -> Self { self.server_attributes .push_mod(path.as_ref(), attribute.as_ref()); self } /// Add additional attribute to matched service servers. Passed directly to /// `prost_build::Config.message_attribute` pub fn server_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.server_attributes .push_struct(path.as_ref(), attribute.as_ref()); self } /// Add additional attribute to matched traits. Passed directly to /// `prost_build::Config.message_attribute` pub fn trait_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.server_attributes .push_trait(path.as_ref(), attribute.as_ref()); self } /// Add additional attribute to matched client `mod`s. Passed directly to /// `prost_build::Config.message_attribute` pub fn client_mod_attribute, A: AsRef>( mut self, path: P, attribute: A, ) -> Self { self.client_attributes .push_mod(path.as_ref(), attribute.as_ref()); self } /// Add additional attribute to matched service clients. Passed directly to /// `prost_build::Config.message_attribute` pub fn client_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.client_attributes .push_struct(path.as_ref(), attribute.as_ref()); self } /// Set the path to where protobuf types are generated in the module tree. /// Default is `super`. /// /// This should be used in combination with `extern_path` when you want to use types that are /// defined in other crates or modules, since types generated with `.proto_path("my_types")` /// will be at the path `my_types::generated_package_name`. /// /// This will change the path that is used to include the generated types, for example: /// - `my_package::my_type` (default `proto_path`) /// - `my_types::my_package::my_type` (with `proto_path("my_types")`) /// /// Use `.extern_path("my.package", "::my_types::my_package")` to use the generated types. pub fn proto_path(mut self, proto_path: impl AsRef) -> Self { self.proto_path = proto_path.as_ref().to_string(); self } /// Enable or disable directing Protobuf to compile well-known types. /// /// Passed directly to `prost_build::Config.compile_well_known_types`. pub fn compile_well_known_types(mut self, enable: bool) -> Self { self.compile_well_known_types = enable; self } /// Use the extended mapping of well-known types to rust types. /// /// Default is `false`. pub fn with_extended_rust_types(mut self, enable: bool) -> Self { self.with_extended_rust_types = enable; self } /// Enable or disable emitting package information. /// /// Passed directly to `prost_build::Config.emit_package`. pub fn emit_package(mut self, enable: bool) -> Self { self.emit_package = enable; self } /// Set the output file's path, used to write the file descriptor set. /// /// Passed directly to `prost_build::Config.file_descriptor_set_path`. pub fn file_descriptor_set_path(mut self, path: impl AsRef) -> Self { self.file_descriptor_set_path = Some(path.as_ref().to_path_buf()); self } /// Skip building protos and just generate code using the provided file descriptor set. /// /// Passed directly to `prost_build::Config.skip_protoc_run`. pub fn skip_protoc_run(mut self) -> Self { self.skip_protoc_run = true; self } /// Add additional protoc arguments. /// /// Passed directly to `prost_build::Config.protoc_arg`. pub fn protoc_arg>(mut self, arg: A) -> Self { self.protoc_args.push(arg.as_ref().into()); self } /// Set the include file. /// /// Passed directly to `prost_build::Config.include_file`. pub fn include_file(mut self, path: impl AsRef) -> Self { self.include_file = Some(path.as_ref().to_path_buf()); self } /// Controls the generation of `include!` statements in the output files. /// /// Passed directly to `prost_build::Config.emit_rerun_if_changed`. pub fn emit_rerun_if_changed(mut self, enable: bool) -> Self { self.emit_rerun_if_changed = enable; self } /// Set the comments that should be disabled. /// /// Passed directly to `prost_build::Config.disable_comments`. pub fn disable_comments(mut self, path: I) -> Self where I: IntoIterator, S: AsRef, { self.disable_comments .extend(path.into_iter().map(|s| s.as_ref().to_string())); self } /// Use `Arc` on the Server trait. pub fn use_arc_self(mut self, enable: bool) -> Self { self.use_arc_self = enable; self } /// Generate the default stubs for gRPC services. This code will be generated /// inside of your service module. Ex: `pub mod helloworld { ... }` pub fn generate_default_stubs(mut self, enable: bool) -> Self { self.generate_default_stubs = enable; self } /// Set the codec path for generated gRPC services. pub fn codec_path(mut self, path: impl AsRef) -> Self { self.codec_path = path.as_ref().to_string(); self } /// Configure the code generator not to strip the `Debug` implementation for the request and /// response types from the generated code. /// /// Passed directly to `prost_build::Config.skip_debug`. pub fn skip_debug(mut self, paths: I) -> Self where I: IntoIterator, S: AsRef, { self.skip_debug .extend(paths.into_iter().map(|s| s.as_ref().to_string())); self } /// Compile the .proto files and execute code generation. pub fn compile_protos

(self, protos: &[P], includes: &[P]) -> io::Result<()> where P: AsRef, { self.compile_with_config(Config::new(), protos, includes) } /// Compile the .proto files and execute code generation with a custom `prost_build::Config`. /// /// Note: When using a custom config, any disable_comments settings on the Builder will be ignored /// to preserve the disable_comments already configured on the provided Config. pub fn compile_with_config

( self, mut config: Config, protos: &[P], includes: &[P], ) -> io::Result<()> where P: AsRef, { /// Drop guard that will set [`NON_PATH_TYPE_ALLOWLIST`] back /// to its default on exit. struct Defer; impl Drop for Defer { fn drop(&mut self) { NON_PATH_TYPE_ALLOWLIST.set(DEFAULT_NON_PATH_TYPE_ALLOWLIST); } } let _defer_guard = Defer; let out_dir = if let Some(out_dir) = self.out_dir.as_ref() { out_dir.clone() } else { PathBuf::from(std::env::var("OUT_DIR").unwrap()) }; config.out_dir(&out_dir); for (proto_path, rust_path) in &self.extern_path { config.extern_path(proto_path, rust_path); } for (prost_path, attr) in &self.field_attributes { config.field_attribute(prost_path, attr); } for (prost_path, attr) in &self.message_attributes { config.message_attribute(prost_path, attr); } for (prost_path, attr) in &self.enum_attributes { config.enum_attribute(prost_path, attr); } for (prost_path, attr) in &self.type_attributes { config.type_attribute(prost_path, attr); } for prost_path in &self.boxed { config.boxed(prost_path); } if let Some(ref paths) = self.btree_map { config.btree_map(paths); } if let Some(ref paths) = self.bytes { config.bytes(paths); } if self.compile_well_known_types { config.compile_well_known_types(); } for arg in &self.protoc_args { config.protoc_arg(arg); } if let Some(path) = &self.include_file { config.include_file(path); } if self.with_extended_rust_types { NON_PATH_TYPE_ALLOWLIST.set(EXTENDED_NON_PATH_TYPE_ALLOWLIST); } // Note: We don't pass self.disable_comments to prost Config here // because those are meant for service/method paths which are handled // by the ServiceGenerator, not for message paths if !self.skip_debug.is_empty() { config.skip_debug(self.skip_debug.clone()); } if let Some(path) = &self.file_descriptor_set_path { config.file_descriptor_set_path(path); } if self.skip_protoc_run { config.skip_protoc_run(); } if self.build_client || self.build_server { let service_generator = ServiceGenerator::new( self.build_client, self.build_server, self.build_transport, self.client_attributes, self.server_attributes, self.use_arc_self, self.generate_default_stubs, self.proto_path, self.compile_well_known_types, self.codec_path.clone(), self.disable_comments, ); config.service_generator(Box::new(service_generator)); }; config.compile_protos(protos, includes)?; Ok(()) } /// Compile a [`prost_types::FileDescriptorSet`] and execute code generation. pub fn compile_fds(self, fds: prost_types::FileDescriptorSet) -> io::Result<()> { self.compile_fds_with_config(fds, Config::new()) } /// Compile a [`prost_types::FileDescriptorSet`] with a custom `prost_build::Config`. pub fn compile_fds_with_config( self, fds: prost_types::FileDescriptorSet, mut config: Config, ) -> io::Result<()> { let out_dir = if let Some(out_dir) = self.out_dir.as_ref() { out_dir.clone() } else { PathBuf::from(std::env::var("OUT_DIR").unwrap()) }; config.out_dir(&out_dir); for (proto_path, rust_path) in &self.extern_path { config.extern_path(proto_path, rust_path); } for (prost_path, attr) in &self.field_attributes { config.field_attribute(prost_path, attr); } for (prost_path, attr) in &self.message_attributes { config.message_attribute(prost_path, attr); } for (prost_path, attr) in &self.enum_attributes { config.enum_attribute(prost_path, attr); } for (prost_path, attr) in &self.type_attributes { config.type_attribute(prost_path, attr); } for prost_path in &self.boxed { config.boxed(prost_path); } if let Some(ref paths) = self.btree_map { config.btree_map(paths); } if let Some(ref paths) = self.bytes { config.bytes(paths); } if self.compile_well_known_types { config.compile_well_known_types(); } for arg in &self.protoc_args { config.protoc_arg(arg); } if let Some(path) = &self.include_file { config.include_file(path); } // Note: We don't pass self.disable_comments to prost Config here // because those are meant for service/method paths which are handled // by the ServiceGenerator, not for message paths if !self.skip_debug.is_empty() { config.skip_debug(self.skip_debug.clone()); } if let Some(path) = &self.file_descriptor_set_path { config.file_descriptor_set_path(path); } if self.skip_protoc_run { config.skip_protoc_run(); } if self.build_client || self.build_server { let service_generator = ServiceGenerator::new( self.build_client, self.build_server, self.build_transport, self.client_attributes, self.server_attributes, self.use_arc_self, self.generate_default_stubs, self.proto_path, self.compile_well_known_types, self.codec_path.clone(), self.disable_comments, ); config.service_generator(Box::new(service_generator)); }; config.compile_fds(fds)?; Ok(()) } /// Turn the builder into a `ServiceGenerator` ready to be passed to `prost-build`s /// `Config::service_generator`. pub fn service_generator(self) -> Box { Box::new(ServiceGenerator::new( self.build_client, self.build_server, self.build_transport, self.client_attributes, self.server_attributes, self.use_arc_self, self.generate_default_stubs, self.proto_path, self.compile_well_known_types, self.codec_path.clone(), self.disable_comments, )) } } ================================================ FILE: tonic-prost-build/src/tests.rs ================================================ use super::*; use prost_build::{Comments, Method}; use quote::quote; fn create_test_method(input_type: String, output_type: String) -> TonicBuildMethod { TonicBuildMethod { prost_method: Method { name: "TestMethod".to_string(), proto_name: "testMethod".to_string(), comments: Comments { leading: vec![], trailing: vec![], leading_detached: vec![], }, input_type: input_type.clone(), output_type: output_type.clone(), input_proto_type: input_type, output_proto_type: output_type, client_streaming: false, server_streaming: false, options: prost_types::MethodOptions::default(), }, codec_path: "tonic_prost::ProstCodec".to_string(), } } #[test] fn test_request_response_name_google_types_not_compiled() { // Test Google well-known types when compile_well_known_types is false let test_cases = vec![ (".google.protobuf.Empty", quote!(())), (".google.protobuf.Any", quote!(::prost_types::Any)), ( ".google.protobuf.StringValue", quote!(::prost::alloc::string::String), ), ( ".google.protobuf.Timestamp", quote!(::prost_types::Timestamp), ), (".google.protobuf.Duration", quote!(::prost_types::Duration)), (".google.protobuf.Value", quote!(::prost_types::Value)), ]; for (type_name, expected) in test_cases { let method = create_test_method(type_name.to_string(), type_name.to_string()); let (request, response) = method.request_response_name("super", false); assert_eq!( request.to_string(), expected.to_string(), "Failed for input type: {type_name}" ); assert_eq!( response.to_string(), expected.to_string(), "Failed for output type: {type_name}" ); } } #[test] fn test_request_response_name_google_types_compiled() { // Test Google well-known types when compile_well_known_types is true let test_cases = vec![ ".google.protobuf.Empty", ".google.protobuf.Any", ".google.protobuf.StringValue", ".google.protobuf.Timestamp", ]; for type_name in test_cases { let method = create_test_method(type_name.to_string(), type_name.to_string()); let (request, response) = method.request_response_name("super", true); // When compile_well_known_types is true, it should use the normal path logic let expected_path = format!( "super :: google :: protobuf :: {}", type_name.trim_start_matches(".google.protobuf.") ); assert_eq!( request.to_string(), expected_path, "Failed for input type: {type_name}" ); assert_eq!( response.to_string(), expected_path, "Failed for output type: {type_name}" ); } } #[test] fn test_request_response_name_non_path_types() { // Test types in NON_PATH_TYPE_ALLOWLIST let method = create_test_method("()".to_string(), "()".to_string()); let (request, response) = method.request_response_name("super", false); assert_eq!(request.to_string(), "()"); assert_eq!(response.to_string(), "()"); } #[test] fn test_request_response_name_extern_types() { // Test extern types that start with :: or crate:: let test_cases = vec![ "::my_crate::MyType", "crate::module::MyType", "::external::lib::Type", ]; for type_name in test_cases { let method = create_test_method(type_name.to_string(), type_name.to_string()); let (request, response) = method.request_response_name("super", false); // The parsed TokenStream includes spaces between path segments let expected = match type_name { "::my_crate::MyType" => ":: my_crate :: MyType", "crate::module::MyType" => "crate :: module :: MyType", "::external::lib::Type" => ":: external :: lib :: Type", _ => panic!("Unknown test case: {type_name}"), }; assert_eq!( request.to_string(), expected, "Failed for input type: {type_name}" ); assert_eq!( response.to_string(), expected, "Failed for output type: {type_name}" ); } } #[test] fn test_request_response_name_regular_protobuf_types() { // Test regular protobuf types (with dots) let test_cases = vec![ ("mypackage.MyMessage", "super :: mypackage :: MyMessage"), ("com.example.User", "super :: com :: example :: User"), (".mypackage.MyMessage", "super :: mypackage :: MyMessage"), // Leading dot ( "nested.package.Message", "super :: nested :: package :: Message", ), ]; for (input, expected) in test_cases { let method = create_test_method(input.to_string(), input.to_string()); let (request, response) = method.request_response_name("super", false); assert_eq!( request.to_string(), expected, "Failed for input type: {input}" ); assert_eq!( response.to_string(), expected, "Failed for output type: {input}" ); } } #[test] fn test_request_response_name_different_proto_paths() { // Test with different proto_path values let method = create_test_method( "mypackage.MyMessage".to_string(), "mypackage.MyResponse".to_string(), ); let test_paths = vec!["super", "crate::proto", "crate"]; for proto_path in test_paths { let (request, response) = method.request_response_name(proto_path, false); // Handle the case where proto_path contains :: which gets spaced out let expected_request = if proto_path.contains("::") { format!( "{} :: mypackage :: MyMessage", proto_path.replace("::", " :: ") ) } else { format!("{proto_path} :: mypackage :: MyMessage") }; let expected_response = if proto_path.contains("::") { format!( "{} :: mypackage :: MyResponse", proto_path.replace("::", " :: ") ) } else { format!("{proto_path} :: mypackage :: MyResponse") }; assert_eq!( request.to_string(), expected_request, "Failed for proto_path: {proto_path}" ); assert_eq!( response.to_string(), expected_response, "Failed for proto_path: {proto_path}" ); } } #[test] fn test_request_response_name_mixed_types() { // Test with different request and response types let method = create_test_method( ".google.protobuf.Empty".to_string(), "mypackage.MyResponse".to_string(), ); let (request, response) = method.request_response_name("super", false); assert_eq!(request.to_string(), "()"); assert_eq!(response.to_string(), "super :: mypackage :: MyResponse"); // Test with extern type as request and google type as response let method = create_test_method( "::external::Request".to_string(), ".google.protobuf.Any".to_string(), ); let (request, response) = method.request_response_name("super", false); assert_eq!(request.to_string(), ":: external :: Request"); assert_eq!(response.to_string(), ":: prost_types :: Any"); } #[test] fn test_is_google_type() { assert!(is_google_type(".google.protobuf.Empty")); assert!(is_google_type(".google.protobuf.Any")); assert!(is_google_type(".google.protobuf.Timestamp")); assert!(!is_google_type("google.protobuf.Empty")); // Missing leading dot assert!(!is_google_type(".google.api.Http")); // Not protobuf package assert!(!is_google_type("mypackage.Message")); assert!(!is_google_type("")); } #[test] fn test_extended_non_path_type_allowlist() { // prost-build already compiles some well known types into // their rust primitive type counterpart. tonic-prost build must // use the given primitive type. let test_cases = vec![ ("()", quote!(())), ("bool", quote!(bool)), ("i32", quote!(i32)), ("i64", quote!(i64)), ("u32", quote!(u32)), ("u64", quote!(u64)), ("f32", quote!(f32)), ("f64", quote!(f64)), ]; NON_PATH_TYPE_ALLOWLIST.set(EXTENDED_NON_PATH_TYPE_ALLOWLIST); NON_PATH_TYPE_ALLOWLIST.with(|set_allowlist| { let allowlist = set_allowlist.borrow(); assert_eq!(allowlist.len(), test_cases.len()); for (type_name, expected) in test_cases { assert!(allowlist.contains(&type_name)); let method = create_test_method(type_name.to_string(), type_name.to_string()); let (request, response) = method.request_response_name("super", false); assert_eq!( request.to_string(), expected.to_string(), "Failed for input type: {type_name}" ); assert_eq!( response.to_string(), expected.to_string(), "Failed for output type: {type_name}" ); } }); } #[test] fn test_default_non_path_type_allowlist() { NON_PATH_TYPE_ALLOWLIST.with(|set_allowlist| { let allowlist = set_allowlist.borrow(); // Verify that the default NON_PATH_TYPE_ALLOWLIST contains expected values. assert!(allowlist.contains(&"()")); assert_eq!(allowlist.len(), 1); }); } #[test] fn test_edge_cases() { // Test empty string types - skip this test as empty strings cause parse errors // This is an edge case that should be handled at a higher level // Test types with multiple dots let method = create_test_method("a.b.c.d.Message".to_string(), "x.y.z.Response".to_string()); let (request, response) = method.request_response_name("super", false); assert_eq!(request.to_string(), "super :: a :: b :: c :: d :: Message"); assert_eq!(response.to_string(), "super :: x :: y :: z :: Response"); // Test type that ends with () but has a package let method = create_test_method("mypackage.()".to_string(), "mypackage.()".to_string()); let (request, response) = method.request_response_name("super", false); assert_eq!(request.to_string(), "mypackage . ()"); assert_eq!(response.to_string(), "mypackage . ()"); } ================================================ FILE: tonic-protobuf/Cargo.toml ================================================ [package] name = "tonic-protobuf" version = "0.14.0" edition = "2024" authors = ["gRPC Authors"] license = "MIT" publish = false rust-version = { workspace = true } [dependencies] tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["codegen"] } bytes = "1" protobuf = { version = "4.33.0-release" } [package.metadata.cargo_check_external_types] allowed_external_types = [ "tonic::*", "protobuf::codegen_traits::Message", ] ================================================ FILE: tonic-protobuf/src/lib.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use bytes::{Buf, BufMut}; use std::marker::PhantomData; use tonic::{ Status, codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}, }; pub use protobuf; use protobuf::Message; /// A [`Codec`] that implements `application/grpc+proto` via the protobuf /// library. #[derive(Debug, Clone)] pub struct ProtoCodec { _pd: PhantomData<(T, U)>, } impl Default for ProtoCodec { fn default() -> Self { Self { _pd: PhantomData } } } impl Codec for ProtoCodec where T: Message + Send + 'static, U: Message + Default + Send + 'static, { type Encode = T; type Decode = U; type Encoder = ProtoEncoder; type Decoder = ProtoDecoder; fn encoder(&mut self) -> Self::Encoder { ProtoEncoder { _pd: PhantomData } } fn decoder(&mut self) -> Self::Decoder { ProtoDecoder { _pd: PhantomData } } } /// A [`Encoder`] that knows how to encode `T`. #[derive(Debug, Clone, Default)] pub struct ProtoEncoder { _pd: PhantomData, } impl ProtoEncoder { /// Get a new encoder with explicit buffer settings pub fn new() -> Self { Self { _pd: PhantomData } } } impl Encoder for ProtoEncoder { type Item = T; type Error = Status; fn encode(&mut self, item: Self::Item, buf: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { // The protobuf library doesn't support serializing into a user-provided // buffer. Instead, it allocates its own buffer, resulting in an extra // copy and allocation. // TODO: #2345 - Find a way to avoid this extra copy. let serialized = item.serialize().map_err(from_decode_error)?; buf.put_slice(serialized.as_slice()); Ok(()) } } /// A [`Decoder`] that knows how to decode `U`. #[derive(Debug, Clone, Default)] pub struct ProtoDecoder { _pd: PhantomData, } impl ProtoDecoder { /// Get a new decoder. pub fn new() -> Self { Self { _pd: PhantomData } } } impl Decoder for ProtoDecoder { type Item = U; type Error = Status; fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error> { let slice = buf.chunk(); let item = U::parse(slice).map_err(from_decode_error)?; buf.advance(slice.len()); Ok(Some(item)) } } fn from_decode_error(error: impl std::error::Error) -> tonic::Status { // Map Protobuf parse errors to an INTERNAL status code, as per // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md Status::internal(error.to_string()) } ================================================ FILE: tonic-protobuf-build/Cargo.toml ================================================ [package] name = "tonic-protobuf-build" version = "0.14.0" edition = "2024" authors = ["gRPC Authors"] license = "MIT" publish = false rust-version = { workspace = true } [dependencies] prettyplease = "0.2.35" protobuf-codegen = { version = "4.33.0-release" } syn = "2.0.104" [build-dependencies] cmake = "0.1" ================================================ FILE: tonic-protobuf-build/README.md ================================================ # tonic-protobuf-build Compiles proto files via protobuf rust and generates service stubs and proto definitions for use with tonic. ## Features Required dependencies ```toml [dependencies] tonic = "" protobuf = "" tonic-protobuf = "" [build-dependencies] tonic-protobuf-build = "" ``` You must ensure you have the following programs in your PATH: 1. protoc 1. protoc-gen-rust-grpc ## Getting Started `tonic-protobuf-build` works by being included as a [`build.rs` file](https://doc.rust-lang.org/cargo/reference/build-scripts.html) at the root of the binary/library. You can rely on the defaults via ```rust,no_run fn main() -> Result<(), Box> { tonic_protobuf_build::CodeGen::new() .include("proto") .inputs(["service.proto"]) .compile()?; Ok(()) } ``` Or configure the generated code deeper via ```rust,no_run fn main() -> Result<(), Box> { let dependency = tonic_protobuf_build::Dependency::builder() .crate_name("external_protos".to_string()) .proto_import_paths(vec![PathBuf::from("external/message.proto")]) .proto_files(vec!["message.proto".to_string()]) .build()?; tonic_protobuf_build::CodeGen::new() .generate_message_code(false) .inputs(["proto/helloworld/helloworld.proto"]) .include("external") .message_module_path("super::proto") .dependencies(vec![dependency]) //.out_dir("src/generated") // you can change the generated code's location .compile()?; Ok(()) } ``` Then you can reference the generated Rust like this in your code: ```rust,ignore mod protos { // Include message code. include!(concat!(env!("OUT_DIR"), "proto/helloworld/generated.rs")); } mod grpc { // Include service code. include!(concat!(env!("OUT_DIR"), "proto/helloworld/helloworld_grpc.pb.rs")); } ``` If you don't modify the `message_module_path`, you can use the `include_proto` macro to simplify the import code. ```rust,ignore pub mod grpc_pb { grpc::include_proto!("proto/helloworld", "helloworld"); } ``` Or if you want to save the generated code in your own code base, you can uncomment the line `.output_dir(...)` above, and in your lib file config a mod like this: ```rust,ignore pub mod generated { pub mod helloworld { pub mod proto { include!("helloworld/generated.rs"); } pub mod grpc { include!("helloworld/test_grpc.pb.rs"); } } } ``` ================================================ FILE: tonic-protobuf-build/build.rs ================================================ fn main() { cmake::build("../protoc-gen-rust-grpc"); println!("cargo:rerun-if-changed=../protoc-gen-rust-grpc/cmake"); println!("cargo:rerun-if-changed=../protoc-gen-rust-grpc/CMakeLists.txt"); } ================================================ FILE: tonic-protobuf-build/src/lib.rs ================================================ /* * * Copyright 2025 gRPC authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * */ use std::fs::{self, read_to_string}; use std::io::Write; use std::path::{Path, PathBuf}; use syn::parse_file; pub fn protoc() -> String { format!("{}/bin/protoc", env!("OUT_DIR")) } pub fn protoc_gen_rust_grpc() -> String { format!("{}/bin/protoc-gen-rust-grpc", env!("OUT_DIR")) } pub fn bin() -> String { format!("{}/bin", env!("OUT_DIR")) } /// Details about a crate containing proto files with symbols referenced in /// the file being compiled currently. #[derive(Debug, Clone)] pub struct Dependency { crate_name: String, proto_import_paths: Vec, proto_files: Vec, } impl Dependency { pub fn builder() -> DependencyBuilder { DependencyBuilder::default() } } #[derive(Default, Debug)] pub struct DependencyBuilder { crate_name: Option, proto_import_paths: Vec, proto_files: Vec, } impl DependencyBuilder { /// Name of the external crate. pub fn crate_name(mut self, name: impl Into) -> Self { self.crate_name = Some(name.into()); self } /// List of paths .proto files whose codegen is present in the crate. This /// is used to re-run the build command if required. pub fn proto_import_path(mut self, path: impl Into) -> Self { self.proto_import_paths.push(path.into()); self } /// List of .proto file names whose codegen is present in the crate. pub fn proto_import_paths(mut self, paths: Vec) -> Self { self.proto_import_paths = paths; self } pub fn proto_file(mut self, file: impl Into) -> Self { self.proto_files.push(file.into()); self } pub fn proto_files(mut self, files: Vec) -> Self { self.proto_files = files; self } pub fn build(self) -> Result { let crate_name = self.crate_name.ok_or("crate_name is required")?; Ok(Dependency { crate_name, proto_import_paths: self.proto_import_paths, proto_files: self.proto_files, }) } } impl From<&Dependency> for protobuf_codegen::Dependency { fn from(val: &Dependency) -> Self { protobuf_codegen::Dependency { crate_name: val.crate_name.clone(), proto_import_paths: val.proto_import_paths.clone(), proto_files: val.proto_files.clone(), } } } /// Service generator builder. #[derive(Debug, Clone)] pub struct CodeGen { inputs: Vec, output_dir: PathBuf, includes: Vec, dependencies: Vec, message_module_path: Option, // Whether to generate message code, defaults to true. generate_message_code: bool, should_format_code: bool, } impl CodeGen { pub fn new() -> Self { Self { inputs: Vec::new(), output_dir: PathBuf::from(std::env::var("OUT_DIR").unwrap()), includes: Vec::new(), dependencies: Vec::new(), message_module_path: None, generate_message_code: true, should_format_code: true, } } /// Sets whether to generate the message code. This can be disabled if the /// message code is being generated independently. pub fn generate_message_code(&mut self, enable: bool) -> &mut Self { self.generate_message_code = enable; self } /// Adds a proto file to compile. pub fn input(&mut self, input: impl AsRef) -> &mut Self { self.inputs.push(input.as_ref().to_owned()); self } /// Adds a proto file to compile. pub fn inputs(&mut self, inputs: impl IntoIterator>) -> &mut Self { self.inputs .extend(inputs.into_iter().map(|input| input.as_ref().to_owned())); self } /// Enables or disables formatting of generated code. pub fn should_format_code(&mut self, enable: bool) -> &mut Self { self.should_format_code = enable; self } /// Sets the directory for the files generated by protoc. The generated code /// will be present in a subdirectory corresponding to the path of the /// proto file withing the included directories. pub fn output_dir(&mut self, output_dir: impl AsRef) -> &mut Self { self.output_dir = output_dir.as_ref().to_owned(); self } /// Add a directory for protoc to scan for .proto files. pub fn include(&mut self, include: impl AsRef) -> &mut Self { self.includes.push(include.as_ref().to_owned()); self } /// Add a directory for protoc to scan for .proto files. pub fn includes(&mut self, includes: impl Iterator>) -> &mut Self { self.includes.extend( includes .into_iter() .map(|include| include.as_ref().to_owned()), ); self } /// Adds a list of Rust crates along with the proto files whose generated /// messages they contains. pub fn dependencies(&mut self, deps: Vec) -> &mut Self { self.dependencies.extend(deps); self } /// Sets path of the module containing the generated message code. This is /// "self" by default, i.e. the service code expects the message structs to /// be present in the same module. Set this if the message and service /// codegen needs to live in separate modules. pub fn message_module_path(&mut self, message_path: &str) -> &mut Self { self.message_module_path = Some(message_path.to_string()); self } pub fn compile(&self) -> Result<(), String> { // Generate the message code. if self.generate_message_code { protobuf_codegen::CodeGen::new() .inputs(self.inputs.clone()) .output_dir(self.output_dir.clone()) .includes(self.includes.iter()) .dependency(self.dependencies.iter().map(|d| d.into()).collect()) .generate_and_compile() .unwrap(); } let crate_mapping_path = if self.generate_message_code { self.output_dir.join("crate_mapping.txt") } else { self.generate_crate_mapping_file() }; // Generate the service code. let mut cmd = std::process::Command::new("protoc"); for input in &self.inputs { cmd.arg(input); } if !self.output_dir.exists() { // Attempt to make the directory if it doesn't exist let _ = std::fs::create_dir(&self.output_dir); } if !self.generate_message_code { for include in &self.includes { println!("cargo:rerun-if-changed={}", include.display()); } for dep in &self.dependencies { for path in &dep.proto_import_paths { println!("cargo:rerun-if-changed={}", path.display()); } } } cmd.arg(format!("--rust-grpc_out={}", self.output_dir.display())); cmd.arg(format!( "--rust-grpc_opt=crate_mapping={}", crate_mapping_path.display() )); if let Some(message_path) = &self.message_module_path { cmd.arg(format!( "--rust-grpc_opt=message_module_path={message_path}", )); } for include in &self.includes { cmd.arg(format!("--proto_path={}", include.display())); } for dep in &self.dependencies { for path in &dep.proto_import_paths { cmd.arg(format!("--proto_path={}", path.display())); } } let output = cmd .output() .map_err(|e| format!("failed to run protoc: {e}"))?; println!("{}", std::str::from_utf8(&output.stdout).unwrap()); eprintln!("{}", std::str::from_utf8(&output.stderr).unwrap()); assert!(output.status.success()); if self.should_format_code { self.format_code(); } Ok(()) } fn format_code(&self) { let mut generated_file_paths = Vec::new(); let output_dir = &self.output_dir; if self.generate_message_code { generated_file_paths.push(output_dir.join("generated.rs")); } for proto_path in &self.inputs { let Some(stem) = proto_path.file_stem().and_then(|s| s.to_str()) else { continue; }; generated_file_paths.push(output_dir.join(format!("{stem}_grpc.pb.rs"))); if self.generate_message_code { generated_file_paths.push(output_dir.join(format!("{stem}.u.pb.rs"))); } } for path in &generated_file_paths { // The path may not exist if there are no services present in the // proto file. if path.exists() { let src = read_to_string(path).expect("Failed to read generated file"); let syntax = parse_file(&src).unwrap(); let formatted = prettyplease::unparse(&syntax); fs::write(path, formatted).unwrap(); } } } fn generate_crate_mapping_file(&self) -> PathBuf { let crate_mapping_path = self.output_dir.join("crate_mapping.txt"); let mut file = fs::File::create(crate_mapping_path.clone()).unwrap(); for dep in &self.dependencies { file.write_all(format!("{}\n", dep.crate_name).as_bytes()) .unwrap(); file.write_all(format!("{}\n", dep.proto_files.len()).as_bytes()) .unwrap(); for f in &dep.proto_files { file.write_all(format!("{f}\n").as_bytes()).unwrap(); } } crate_mapping_path } } impl Default for CodeGen { fn default() -> Self { Self::new() } } ================================================ FILE: tonic-reflection/Cargo.toml ================================================ [package] authors = [ "James Nugent ", "Samani G. Gikandi ", ] categories = ["network-programming", "asynchronous"] description = """ Server Reflection module of `tonic` gRPC implementation. """ edition = "2024" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "async", "reflection"] license = "MIT" name = "tonic-reflection" readme = "README.md" repository = "https://github.com/hyperium/tonic" version = "0.14.5" rust-version = { workspace = true } [package.metadata.docs.rs] all-features = true [features] server = ["dep:prost-types", "dep:tokio", "dep:tokio-stream"] default = ["server"] [dependencies] prost = "0.14" prost-types = {version = "0.14", optional = true} tokio = { version = "1.0", features = ["sync", "rt"], optional = true } tokio-stream = {version = "0.1", default-features = false, optional = true } tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["codegen"] } tonic-prost = { version = "0.14.0", path = "../tonic-prost", default-features = false } [dev-dependencies] tokio-stream = {version = "0.1", default-features = false, features = ["net"]} tonic = { version = "0.14.0", path = "../tonic", default-features = false, features = ["transport"] } [lints] workspace = true [package.metadata.cargo_check_external_types] allowed_external_types = [ "tonic::*", # major released "bytes::*", "http::*", "http_body::*", # not major released "prost::*", "prost_types::*", "futures_core::stream::Stream", "tower_service::Service", ] ================================================ FILE: tonic-reflection/README.md ================================================ # tonic-reflection A `tonic` based gRPC reflection implementation. ================================================ FILE: tonic-reflection/proto/reflection_v1.proto ================================================ // Copyright 2016 The gRPC Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Service exported by server reflection. A more complete description of how // server reflection works can be found at // https://github.com/grpc/grpc/blob/master/doc/server-reflection.md // // The canonical version of this proto can be found at // https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto syntax = "proto3"; package grpc.reflection.v1; option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1"; option java_multiple_files = true; option java_package = "io.grpc.reflection.v1"; option java_outer_classname = "ServerReflectionProto"; service ServerReflection { // The reflection service is structured as a bidirectional stream, ensuring // all related requests go to a single server. rpc ServerReflectionInfo(stream ServerReflectionRequest) returns (stream ServerReflectionResponse); } // The message sent by the client when calling ServerReflectionInfo method. message ServerReflectionRequest { string host = 1; // To use reflection service, the client should set one of the following // fields in message_request. The server distinguishes requests by their // defined field and then handles them using corresponding methods. oneof message_request { // Find a proto file by the file name. string file_by_filename = 3; // Find the proto file that declares the given fully-qualified symbol name. // This field should be a fully-qualified symbol name // (e.g. .[.] or .). string file_containing_symbol = 4; // Find the proto file which defines an extension extending the given // message type with the given field number. ExtensionRequest file_containing_extension = 5; // Finds the tag numbers used by all known extensions of the given message // type, and appends them to ExtensionNumberResponse in an undefined order. // Its corresponding method is best-effort: it's not guaranteed that the // reflection service will implement this method, and it's not guaranteed // that this method will provide all extensions. Returns // StatusCode::UNIMPLEMENTED if it's not implemented. // This field should be a fully-qualified type name. The format is // . string all_extension_numbers_of_type = 6; // List the full names of registered services. The content will not be // checked. string list_services = 7; } } // The type name and extension number sent by the client when requesting // file_containing_extension. message ExtensionRequest { // Fully-qualified type name. The format should be . string containing_type = 1; int32 extension_number = 2; } // The message sent by the server to answer ServerReflectionInfo method. message ServerReflectionResponse { string valid_host = 1; ServerReflectionRequest original_request = 2; // The server sets one of the following fields according to the message_request // in the request. oneof message_response { // This message is used to answer file_by_filename, file_containing_symbol, // file_containing_extension requests with transitive dependencies. // As the repeated label is not allowed in oneof fields, we use a // FileDescriptorResponse message to encapsulate the repeated fields. // The reflection service is allowed to avoid sending FileDescriptorProtos // that were previously sent in response to earlier requests in the stream. FileDescriptorResponse file_descriptor_response = 4; // This message is used to answer all_extension_numbers_of_type requests. ExtensionNumberResponse all_extension_numbers_response = 5; // This message is used to answer list_services requests. ListServiceResponse list_services_response = 6; // This message is used when an error occurs. ErrorResponse error_response = 7; } } // Serialized FileDescriptorProto messages sent by the server answering // a file_by_filename, file_containing_symbol, or file_containing_extension // request. message FileDescriptorResponse { // Serialized FileDescriptorProto messages. We avoid taking a dependency on // descriptor.proto, which uses proto2 only features, by making them opaque // bytes instead. repeated bytes file_descriptor_proto = 1; } // A list of extension numbers sent by the server answering // all_extension_numbers_of_type request. message ExtensionNumberResponse { // Full name of the base type, including the package name. The format // is . string base_type_name = 1; repeated int32 extension_number = 2; } // A list of ServiceResponse sent by the server answering list_services request. message ListServiceResponse { // The information of each service may be expanded in the future, so we use // ServiceResponse message to encapsulate it. repeated ServiceResponse service = 1; } // The information of a single service used by ListServiceResponse to answer // list_services request. message ServiceResponse { // Full name of a registered service, including its package name. The format // is . string name = 1; } // The error code and error message sent by the server when an error occurs. message ErrorResponse { // This field uses the error codes defined in grpc::StatusCode. int32 error_code = 1; string error_message = 2; } ================================================ FILE: tonic-reflection/proto/reflection_v1alpha.proto ================================================ // Copyright 2016 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Service exported by server reflection syntax = "proto3"; package grpc.reflection.v1alpha; service ServerReflection { // The reflection service is structured as a bidirectional stream, ensuring // all related requests go to a single server. rpc ServerReflectionInfo(stream ServerReflectionRequest) returns (stream ServerReflectionResponse); } // The message sent by the client when calling ServerReflectionInfo method. message ServerReflectionRequest { string host = 1; // To use reflection service, the client should set one of the following // fields in message_request. The server distinguishes requests by their // defined field and then handles them using corresponding methods. oneof message_request { // Find a proto file by the file name. string file_by_filename = 3; // Find the proto file that declares the given fully-qualified symbol name. // This field should be a fully-qualified symbol name // (e.g. .[.] or .). string file_containing_symbol = 4; // Find the proto file which defines an extension extending the given // message type with the given field number. ExtensionRequest file_containing_extension = 5; // Finds the tag numbers used by all known extensions of extendee_type, and // appends them to ExtensionNumberResponse in an undefined order. // Its corresponding method is best-effort: it's not guaranteed that the // reflection service will implement this method, and it's not guaranteed // that this method will provide all extensions. Returns // StatusCode::UNIMPLEMENTED if it's not implemented. // This field should be a fully-qualified type name. The format is // . string all_extension_numbers_of_type = 6; // List the full names of registered services. The content will not be // checked. string list_services = 7; } } // The type name and extension number sent by the client when requesting // file_containing_extension. message ExtensionRequest { // Fully-qualified type name. The format should be . string containing_type = 1; int32 extension_number = 2; } // The message sent by the server to answer ServerReflectionInfo method. message ServerReflectionResponse { string valid_host = 1; ServerReflectionRequest original_request = 2; // The server sets one of the following fields according to the // message_request in the request. oneof message_response { // This message is used to answer file_by_filename, file_containing_symbol, // file_containing_extension requests with transitive dependencies. // As the repeated label is not allowed in oneof fields, we use a // FileDescriptorResponse message to encapsulate the repeated fields. // The reflection service is allowed to avoid sending FileDescriptorProtos // that were previously sent in response to earlier requests in the stream. FileDescriptorResponse file_descriptor_response = 4; // This message is used to answer all_extension_numbers_of_type requests. ExtensionNumberResponse all_extension_numbers_response = 5; // This message is used to answer list_services requests. ListServiceResponse list_services_response = 6; // This message is used when an error occurs. ErrorResponse error_response = 7; } } // Serialized FileDescriptorProto messages sent by the server answering // a file_by_filename, file_containing_symbol, or file_containing_extension // request. message FileDescriptorResponse { // Serialized FileDescriptorProto messages. We avoid taking a dependency on // descriptor.proto, which uses proto2 only features, by making them opaque // bytes instead. repeated bytes file_descriptor_proto = 1; } // A list of extension numbers sent by the server answering // all_extension_numbers_of_type request. message ExtensionNumberResponse { // Full name of the base type, including the package name. The format // is . string base_type_name = 1; repeated int32 extension_number = 2; } // A list of ServiceResponse sent by the server answering list_services request. message ListServiceResponse { // The information of each service may be expanded in the future, so we use // ServiceResponse message to encapsulate it. repeated ServiceResponse service = 1; } // The information of a single service used by ListServiceResponse to answer // list_services request. message ServiceResponse { // Full name of a registered service, including its package name. The format // is . string name = 1; } // The error code and error message sent by the server when an error occurs. message ErrorResponse { // This field uses the error codes defined in grpc::StatusCode. int32 error_code = 1; string error_message = 2; } ================================================ FILE: tonic-reflection/src/generated/grpc_reflection_v1.rs ================================================ // This file is @generated by prost-build. /// The message sent by the client when calling ServerReflectionInfo method. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ServerReflectionRequest { #[prost(string, tag = "1")] pub host: ::prost::alloc::string::String, /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. #[prost(oneof = "server_reflection_request::MessageRequest", tags = "3, 4, 5, 6, 7")] pub message_request: ::core::option::Option< server_reflection_request::MessageRequest, >, } /// Nested message and enum types in `ServerReflectionRequest`. pub mod server_reflection_request { /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] pub enum MessageRequest { /// Find a proto file by the file name. #[prost(string, tag = "3")] FileByFilename(::prost::alloc::string::String), /// Find the proto file that declares the given fully-qualified symbol name. /// This field should be a fully-qualified symbol name /// (e.g. .\[.\] or .). #[prost(string, tag = "4")] FileContainingSymbol(::prost::alloc::string::String), /// Find the proto file which defines an extension extending the given /// message type with the given field number. #[prost(message, tag = "5")] FileContainingExtension(super::ExtensionRequest), /// Finds the tag numbers used by all known extensions of the given message /// type, and appends them to ExtensionNumberResponse in an undefined order. /// Its corresponding method is best-effort: it's not guaranteed that the /// reflection service will implement this method, and it's not guaranteed /// that this method will provide all extensions. Returns /// StatusCode::UNIMPLEMENTED if it's not implemented. /// This field should be a fully-qualified type name. The format is /// . #[prost(string, tag = "6")] AllExtensionNumbersOfType(::prost::alloc::string::String), /// List the full names of registered services. The content will not be /// checked. #[prost(string, tag = "7")] ListServices(::prost::alloc::string::String), } } /// The type name and extension number sent by the client when requesting /// file_containing_extension. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExtensionRequest { /// Fully-qualified type name. The format should be . #[prost(string, tag = "1")] pub containing_type: ::prost::alloc::string::String, #[prost(int32, tag = "2")] pub extension_number: i32, } /// The message sent by the server to answer ServerReflectionInfo method. #[derive(Clone, PartialEq, ::prost::Message)] pub struct ServerReflectionResponse { #[prost(string, tag = "1")] pub valid_host: ::prost::alloc::string::String, #[prost(message, optional, tag = "2")] pub original_request: ::core::option::Option, /// The server sets one of the following fields according to the message_request /// in the request. #[prost(oneof = "server_reflection_response::MessageResponse", tags = "4, 5, 6, 7")] pub message_response: ::core::option::Option< server_reflection_response::MessageResponse, >, } /// Nested message and enum types in `ServerReflectionResponse`. pub mod server_reflection_response { /// The server sets one of the following fields according to the message_request /// in the request. #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum MessageResponse { /// This message is used to answer file_by_filename, file_containing_symbol, /// file_containing_extension requests with transitive dependencies. /// As the repeated label is not allowed in oneof fields, we use a /// FileDescriptorResponse message to encapsulate the repeated fields. /// The reflection service is allowed to avoid sending FileDescriptorProtos /// that were previously sent in response to earlier requests in the stream. #[prost(message, tag = "4")] FileDescriptorResponse(super::FileDescriptorResponse), /// This message is used to answer all_extension_numbers_of_type requests. #[prost(message, tag = "5")] AllExtensionNumbersResponse(super::ExtensionNumberResponse), /// This message is used to answer list_services requests. #[prost(message, tag = "6")] ListServicesResponse(super::ListServiceResponse), /// This message is used when an error occurs. #[prost(message, tag = "7")] ErrorResponse(super::ErrorResponse), } } /// Serialized FileDescriptorProto messages sent by the server answering /// a file_by_filename, file_containing_symbol, or file_containing_extension /// request. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct FileDescriptorResponse { /// Serialized FileDescriptorProto messages. We avoid taking a dependency on /// descriptor.proto, which uses proto2 only features, by making them opaque /// bytes instead. #[prost(bytes = "vec", repeated, tag = "1")] pub file_descriptor_proto: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } /// A list of extension numbers sent by the server answering /// all_extension_numbers_of_type request. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExtensionNumberResponse { /// Full name of the base type, including the package name. The format /// is . #[prost(string, tag = "1")] pub base_type_name: ::prost::alloc::string::String, #[prost(int32, repeated, tag = "2")] pub extension_number: ::prost::alloc::vec::Vec, } /// A list of ServiceResponse sent by the server answering list_services request. #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListServiceResponse { /// The information of each service may be expanded in the future, so we use /// ServiceResponse message to encapsulate it. #[prost(message, repeated, tag = "1")] pub service: ::prost::alloc::vec::Vec, } /// The information of a single service used by ListServiceResponse to answer /// list_services request. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ServiceResponse { /// Full name of a registered service, including its package name. The format /// is . #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, } /// The error code and error message sent by the server when an error occurs. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ErrorResponse { /// This field uses the error codes defined in grpc::StatusCode. #[prost(int32, tag = "1")] pub error_code: i32, #[prost(string, tag = "2")] pub error_message: ::prost::alloc::string::String, } /// Generated client implementations. pub mod server_reflection_client { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; use tonic::codegen::http::Uri; #[derive(Debug, Clone)] pub struct ServerReflectionClient { inner: tonic::client::Grpc, } impl ServerReflectionClient where T: tonic::client::GrpcService, T::Error: Into, T::ResponseBody: Body + std::marker::Send + 'static, ::Error: Into + std::marker::Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } pub fn with_origin(inner: T, origin: Uri) -> Self { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } pub fn with_interceptor( inner: T, interceptor: F, ) -> ServerReflectionClient> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response< >::ResponseBody, >, >, , >>::Error: Into + std::marker::Send + std::marker::Sync, { ServerReflectionClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); self } /// Enable decompressing responses. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.accept_compressed(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_decoding_message_size(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_encoding_message_size(limit); self } /// The reflection service is structured as a bidirectional stream, ensuring /// all related requests go to a single server. pub async fn server_reflection_info( &mut self, request: impl tonic::IntoStreamingRequest< Message = super::ServerReflectionRequest, >, ) -> std::result::Result< tonic::Response>, tonic::Status, > { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", ); let mut req = request.into_streaming_request(); req.extensions_mut() .insert( GrpcMethod::new( "grpc.reflection.v1.ServerReflection", "ServerReflectionInfo", ), ); self.inner.streaming(req, path, codec).await } } } /// Generated server implementations. pub mod server_reflection_server { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with ServerReflectionServer. #[async_trait] pub trait ServerReflection: std::marker::Send + std::marker::Sync + 'static { /// Server streaming response type for the ServerReflectionInfo method. type ServerReflectionInfoStream: tonic::codegen::tokio_stream::Stream< Item = std::result::Result< super::ServerReflectionResponse, tonic::Status, >, > + std::marker::Send + 'static; /// The reflection service is structured as a bidirectional stream, ensuring /// all related requests go to a single server. async fn server_reflection_info( &self, request: tonic::Request>, ) -> std::result::Result< tonic::Response, tonic::Status, >; } #[derive(Debug)] pub struct ServerReflectionServer { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } impl ServerReflectionServer { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } pub fn from_arc(inner: Arc) -> Self { Self { inner, accept_compression_encodings: Default::default(), send_compression_encodings: Default::default(), max_decoding_message_size: None, max_encoding_message_size: None, } } pub fn with_interceptor( inner: T, interceptor: F, ) -> InterceptedService where F: tonic::service::Interceptor, { InterceptedService::new(Self::new(inner), interceptor) } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Compress responses with the given encoding, if the client supports it. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.max_encoding_message_size = Some(limit); self } } impl tonic::codegen::Service> for ServerReflectionServer where T: ServerReflection, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { type Response = http::Response; type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready( &mut self, _cx: &mut Context<'_>, ) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo" => { #[allow(non_camel_case_types)] struct ServerReflectionInfoSvc(pub Arc); impl< T: ServerReflection, > tonic::server::StreamingService for ServerReflectionInfoSvc { type Response = super::ServerReflectionResponse; type ResponseStream = T::ServerReflectionInfoStream; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request< tonic::Streaming, >, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::server_reflection_info( &inner, request, ) .await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = ServerReflectionInfoSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.streaming(method, req).await; Ok(res) }; Box::pin(fut) } _ => { Box::pin(async move { let mut response = http::Response::new( tonic::body::Body::default(), ); let headers = response.headers_mut(); headers .insert( tonic::Status::GRPC_STATUS, (tonic::Code::Unimplemented as i32).into(), ); headers .insert( http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE, ); Ok(response) }) } } } } impl Clone for ServerReflectionServer { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { inner, accept_compression_encodings: self.accept_compression_encodings, send_compression_encodings: self.send_compression_encodings, max_decoding_message_size: self.max_decoding_message_size, max_encoding_message_size: self.max_encoding_message_size, } } } /// Generated gRPC service name pub const SERVICE_NAME: &str = "grpc.reflection.v1.ServerReflection"; impl tonic::server::NamedService for ServerReflectionServer { const NAME: &'static str = SERVICE_NAME; } } ================================================ FILE: tonic-reflection/src/generated/grpc_reflection_v1alpha.rs ================================================ // This file is @generated by prost-build. /// The message sent by the client when calling ServerReflectionInfo method. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ServerReflectionRequest { #[prost(string, tag = "1")] pub host: ::prost::alloc::string::String, /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. #[prost(oneof = "server_reflection_request::MessageRequest", tags = "3, 4, 5, 6, 7")] pub message_request: ::core::option::Option< server_reflection_request::MessageRequest, >, } /// Nested message and enum types in `ServerReflectionRequest`. pub mod server_reflection_request { /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] pub enum MessageRequest { /// Find a proto file by the file name. #[prost(string, tag = "3")] FileByFilename(::prost::alloc::string::String), /// Find the proto file that declares the given fully-qualified symbol name. /// This field should be a fully-qualified symbol name /// (e.g. .\[.\] or .). #[prost(string, tag = "4")] FileContainingSymbol(::prost::alloc::string::String), /// Find the proto file which defines an extension extending the given /// message type with the given field number. #[prost(message, tag = "5")] FileContainingExtension(super::ExtensionRequest), /// Finds the tag numbers used by all known extensions of extendee_type, and /// appends them to ExtensionNumberResponse in an undefined order. /// Its corresponding method is best-effort: it's not guaranteed that the /// reflection service will implement this method, and it's not guaranteed /// that this method will provide all extensions. Returns /// StatusCode::UNIMPLEMENTED if it's not implemented. /// This field should be a fully-qualified type name. The format is /// . #[prost(string, tag = "6")] AllExtensionNumbersOfType(::prost::alloc::string::String), /// List the full names of registered services. The content will not be /// checked. #[prost(string, tag = "7")] ListServices(::prost::alloc::string::String), } } /// The type name and extension number sent by the client when requesting /// file_containing_extension. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExtensionRequest { /// Fully-qualified type name. The format should be . #[prost(string, tag = "1")] pub containing_type: ::prost::alloc::string::String, #[prost(int32, tag = "2")] pub extension_number: i32, } /// The message sent by the server to answer ServerReflectionInfo method. #[derive(Clone, PartialEq, ::prost::Message)] pub struct ServerReflectionResponse { #[prost(string, tag = "1")] pub valid_host: ::prost::alloc::string::String, #[prost(message, optional, tag = "2")] pub original_request: ::core::option::Option, /// The server sets one of the following fields according to the /// message_request in the request. #[prost(oneof = "server_reflection_response::MessageResponse", tags = "4, 5, 6, 7")] pub message_response: ::core::option::Option< server_reflection_response::MessageResponse, >, } /// Nested message and enum types in `ServerReflectionResponse`. pub mod server_reflection_response { /// The server sets one of the following fields according to the /// message_request in the request. #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum MessageResponse { /// This message is used to answer file_by_filename, file_containing_symbol, /// file_containing_extension requests with transitive dependencies. /// As the repeated label is not allowed in oneof fields, we use a /// FileDescriptorResponse message to encapsulate the repeated fields. /// The reflection service is allowed to avoid sending FileDescriptorProtos /// that were previously sent in response to earlier requests in the stream. #[prost(message, tag = "4")] FileDescriptorResponse(super::FileDescriptorResponse), /// This message is used to answer all_extension_numbers_of_type requests. #[prost(message, tag = "5")] AllExtensionNumbersResponse(super::ExtensionNumberResponse), /// This message is used to answer list_services requests. #[prost(message, tag = "6")] ListServicesResponse(super::ListServiceResponse), /// This message is used when an error occurs. #[prost(message, tag = "7")] ErrorResponse(super::ErrorResponse), } } /// Serialized FileDescriptorProto messages sent by the server answering /// a file_by_filename, file_containing_symbol, or file_containing_extension /// request. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct FileDescriptorResponse { /// Serialized FileDescriptorProto messages. We avoid taking a dependency on /// descriptor.proto, which uses proto2 only features, by making them opaque /// bytes instead. #[prost(bytes = "vec", repeated, tag = "1")] pub file_descriptor_proto: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } /// A list of extension numbers sent by the server answering /// all_extension_numbers_of_type request. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExtensionNumberResponse { /// Full name of the base type, including the package name. The format /// is . #[prost(string, tag = "1")] pub base_type_name: ::prost::alloc::string::String, #[prost(int32, repeated, tag = "2")] pub extension_number: ::prost::alloc::vec::Vec, } /// A list of ServiceResponse sent by the server answering list_services request. #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListServiceResponse { /// The information of each service may be expanded in the future, so we use /// ServiceResponse message to encapsulate it. #[prost(message, repeated, tag = "1")] pub service: ::prost::alloc::vec::Vec, } /// The information of a single service used by ListServiceResponse to answer /// list_services request. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ServiceResponse { /// Full name of a registered service, including its package name. The format /// is . #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, } /// The error code and error message sent by the server when an error occurs. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ErrorResponse { /// This field uses the error codes defined in grpc::StatusCode. #[prost(int32, tag = "1")] pub error_code: i32, #[prost(string, tag = "2")] pub error_message: ::prost::alloc::string::String, } /// Generated client implementations. pub mod server_reflection_client { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; use tonic::codegen::http::Uri; #[derive(Debug, Clone)] pub struct ServerReflectionClient { inner: tonic::client::Grpc, } impl ServerReflectionClient where T: tonic::client::GrpcService, T::Error: Into, T::ResponseBody: Body + std::marker::Send + 'static, ::Error: Into + std::marker::Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } pub fn with_origin(inner: T, origin: Uri) -> Self { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } pub fn with_interceptor( inner: T, interceptor: F, ) -> ServerReflectionClient> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response< >::ResponseBody, >, >, , >>::Error: Into + std::marker::Send + std::marker::Sync, { ServerReflectionClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); self } /// Enable decompressing responses. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.accept_compressed(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_decoding_message_size(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_encoding_message_size(limit); self } /// The reflection service is structured as a bidirectional stream, ensuring /// all related requests go to a single server. pub async fn server_reflection_info( &mut self, request: impl tonic::IntoStreamingRequest< Message = super::ServerReflectionRequest, >, ) -> std::result::Result< tonic::Response>, tonic::Status, > { self.inner .ready() .await .map_err(|e| { tonic::Status::unknown( format!("Service was not ready: {}", e.into()), ) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static( "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", ); let mut req = request.into_streaming_request(); req.extensions_mut() .insert( GrpcMethod::new( "grpc.reflection.v1alpha.ServerReflection", "ServerReflectionInfo", ), ); self.inner.streaming(req, path, codec).await } } } /// Generated server implementations. pub mod server_reflection_server { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value, )] use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with ServerReflectionServer. #[async_trait] pub trait ServerReflection: std::marker::Send + std::marker::Sync + 'static { /// Server streaming response type for the ServerReflectionInfo method. type ServerReflectionInfoStream: tonic::codegen::tokio_stream::Stream< Item = std::result::Result< super::ServerReflectionResponse, tonic::Status, >, > + std::marker::Send + 'static; /// The reflection service is structured as a bidirectional stream, ensuring /// all related requests go to a single server. async fn server_reflection_info( &self, request: tonic::Request>, ) -> std::result::Result< tonic::Response, tonic::Status, >; } #[derive(Debug)] pub struct ServerReflectionServer { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } impl ServerReflectionServer { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } pub fn from_arc(inner: Arc) -> Self { Self { inner, accept_compression_encodings: Default::default(), send_compression_encodings: Default::default(), max_decoding_message_size: None, max_encoding_message_size: None, } } pub fn with_interceptor( inner: T, interceptor: F, ) -> InterceptedService where F: tonic::service::Interceptor, { InterceptedService::new(Self::new(inner), interceptor) } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Compress responses with the given encoding, if the client supports it. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.max_encoding_message_size = Some(limit); self } } impl tonic::codegen::Service> for ServerReflectionServer where T: ServerReflection, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { type Response = http::Response; type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready( &mut self, _cx: &mut Context<'_>, ) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" => { #[allow(non_camel_case_types)] struct ServerReflectionInfoSvc(pub Arc); impl< T: ServerReflection, > tonic::server::StreamingService for ServerReflectionInfoSvc { type Response = super::ServerReflectionResponse; type ResponseStream = T::ServerReflectionInfoStream; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, request: tonic::Request< tonic::Streaming, >, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::server_reflection_info( &inner, request, ) .await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = ServerReflectionInfoSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.streaming(method, req).await; Ok(res) }; Box::pin(fut) } _ => { Box::pin(async move { let mut response = http::Response::new( tonic::body::Body::default(), ); let headers = response.headers_mut(); headers .insert( tonic::Status::GRPC_STATUS, (tonic::Code::Unimplemented as i32).into(), ); headers .insert( http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE, ); Ok(response) }) } } } } impl Clone for ServerReflectionServer { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { inner, accept_compression_encodings: self.accept_compression_encodings, send_compression_encodings: self.send_compression_encodings, max_decoding_message_size: self.max_decoding_message_size, max_encoding_message_size: self.max_encoding_message_size, } } } /// Generated gRPC service name pub const SERVICE_NAME: &str = "grpc.reflection.v1alpha.ServerReflection"; impl tonic::server::NamedService for ServerReflectionServer { const NAME: &'static str = SERVICE_NAME; } } ================================================ FILE: tonic-reflection/src/generated/reflection_v1_fds.rs ================================================ // This file is @generated by codegen. // Copyright 2016 The gRPC Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Service exported by server reflection. A more complete description of how // server reflection works can be found at // https://github.com/grpc/grpc/blob/master/doc/server-reflection.md // // The canonical version of this proto can be found at // https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto // /// Byte encoded FILE_DESCRIPTOR_SET. pub const FILE_DESCRIPTOR_SET: &[u8] = &[ 10u8, 192u8, 13u8, 10u8, 19u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 95u8, 118u8, 49u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 18u8, 18u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 34u8, 243u8, 2u8, 10u8, 23u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 18u8, 10u8, 4u8, 104u8, 111u8, 115u8, 116u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 4u8, 104u8, 111u8, 115u8, 116u8, 18u8, 42u8, 10u8, 16u8, 102u8, 105u8, 108u8, 101u8, 95u8, 98u8, 121u8, 95u8, 102u8, 105u8, 108u8, 101u8, 110u8, 97u8, 109u8, 101u8, 24u8, 3u8, 32u8, 1u8, 40u8, 9u8, 72u8, 0u8, 82u8, 14u8, 102u8, 105u8, 108u8, 101u8, 66u8, 121u8, 70u8, 105u8, 108u8, 101u8, 110u8, 97u8, 109u8, 101u8, 18u8, 54u8, 10u8, 22u8, 102u8, 105u8, 108u8, 101u8, 95u8, 99u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 95u8, 115u8, 121u8, 109u8, 98u8, 111u8, 108u8, 24u8, 4u8, 32u8, 1u8, 40u8, 9u8, 72u8, 0u8, 82u8, 20u8, 102u8, 105u8, 108u8, 101u8, 67u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 83u8, 121u8, 109u8, 98u8, 111u8, 108u8, 18u8, 98u8, 10u8, 25u8, 102u8, 105u8, 108u8, 101u8, 95u8, 99u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 95u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 24u8, 5u8, 32u8, 1u8, 40u8, 11u8, 50u8, 36u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 46u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 72u8, 0u8, 82u8, 23u8, 102u8, 105u8, 108u8, 101u8, 67u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 18u8, 66u8, 10u8, 29u8, 97u8, 108u8, 108u8, 95u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 95u8, 110u8, 117u8, 109u8, 98u8, 101u8, 114u8, 115u8, 95u8, 111u8, 102u8, 95u8, 116u8, 121u8, 112u8, 101u8, 24u8, 6u8, 32u8, 1u8, 40u8, 9u8, 72u8, 0u8, 82u8, 25u8, 97u8, 108u8, 108u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 115u8, 79u8, 102u8, 84u8, 121u8, 112u8, 101u8, 18u8, 37u8, 10u8, 13u8, 108u8, 105u8, 115u8, 116u8, 95u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 115u8, 24u8, 7u8, 32u8, 1u8, 40u8, 9u8, 72u8, 0u8, 82u8, 12u8, 108u8, 105u8, 115u8, 116u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 115u8, 66u8, 17u8, 10u8, 15u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 95u8, 114u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 34u8, 102u8, 10u8, 16u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 39u8, 10u8, 15u8, 99u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 95u8, 116u8, 121u8, 112u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 14u8, 99u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 84u8, 121u8, 112u8, 101u8, 18u8, 41u8, 10u8, 16u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 95u8, 110u8, 117u8, 109u8, 98u8, 101u8, 114u8, 24u8, 2u8, 32u8, 1u8, 40u8, 5u8, 82u8, 15u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 34u8, 174u8, 4u8, 10u8, 24u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 29u8, 10u8, 10u8, 118u8, 97u8, 108u8, 105u8, 100u8, 95u8, 104u8, 111u8, 115u8, 116u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 9u8, 118u8, 97u8, 108u8, 105u8, 100u8, 72u8, 111u8, 115u8, 116u8, 18u8, 86u8, 10u8, 16u8, 111u8, 114u8, 105u8, 103u8, 105u8, 110u8, 97u8, 108u8, 95u8, 114u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 24u8, 2u8, 32u8, 1u8, 40u8, 11u8, 50u8, 43u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 46u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 82u8, 15u8, 111u8, 114u8, 105u8, 103u8, 105u8, 110u8, 97u8, 108u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 102u8, 10u8, 24u8, 102u8, 105u8, 108u8, 101u8, 95u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 24u8, 4u8, 32u8, 1u8, 40u8, 11u8, 50u8, 42u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 46u8, 70u8, 105u8, 108u8, 101u8, 68u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 72u8, 0u8, 82u8, 22u8, 102u8, 105u8, 108u8, 101u8, 68u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 114u8, 10u8, 30u8, 97u8, 108u8, 108u8, 95u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 95u8, 110u8, 117u8, 109u8, 98u8, 101u8, 114u8, 115u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 24u8, 5u8, 32u8, 1u8, 40u8, 11u8, 50u8, 43u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 46u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 72u8, 0u8, 82u8, 27u8, 97u8, 108u8, 108u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 115u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 95u8, 10u8, 22u8, 108u8, 105u8, 115u8, 116u8, 95u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 115u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 24u8, 6u8, 32u8, 1u8, 40u8, 11u8, 50u8, 39u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 46u8, 76u8, 105u8, 115u8, 116u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 72u8, 0u8, 82u8, 20u8, 108u8, 105u8, 115u8, 116u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 115u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 74u8, 10u8, 14u8, 101u8, 114u8, 114u8, 111u8, 114u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 24u8, 7u8, 32u8, 1u8, 40u8, 11u8, 50u8, 33u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 46u8, 69u8, 114u8, 114u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 72u8, 0u8, 82u8, 13u8, 101u8, 114u8, 114u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 66u8, 18u8, 10u8, 16u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 34u8, 76u8, 10u8, 22u8, 70u8, 105u8, 108u8, 101u8, 68u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 50u8, 10u8, 21u8, 102u8, 105u8, 108u8, 101u8, 95u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 95u8, 112u8, 114u8, 111u8, 116u8, 111u8, 24u8, 1u8, 32u8, 3u8, 40u8, 12u8, 82u8, 19u8, 102u8, 105u8, 108u8, 101u8, 68u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 80u8, 114u8, 111u8, 116u8, 111u8, 34u8, 106u8, 10u8, 23u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 36u8, 10u8, 14u8, 98u8, 97u8, 115u8, 101u8, 95u8, 116u8, 121u8, 112u8, 101u8, 95u8, 110u8, 97u8, 109u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 12u8, 98u8, 97u8, 115u8, 101u8, 84u8, 121u8, 112u8, 101u8, 78u8, 97u8, 109u8, 101u8, 18u8, 41u8, 10u8, 16u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 95u8, 110u8, 117u8, 109u8, 98u8, 101u8, 114u8, 24u8, 2u8, 32u8, 3u8, 40u8, 5u8, 82u8, 15u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 34u8, 84u8, 10u8, 19u8, 76u8, 105u8, 115u8, 116u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 61u8, 10u8, 7u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 24u8, 1u8, 32u8, 3u8, 40u8, 11u8, 50u8, 35u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 46u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 82u8, 7u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 34u8, 37u8, 10u8, 15u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 18u8, 10u8, 4u8, 110u8, 97u8, 109u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 4u8, 110u8, 97u8, 109u8, 101u8, 34u8, 83u8, 10u8, 13u8, 69u8, 114u8, 114u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 29u8, 10u8, 10u8, 101u8, 114u8, 114u8, 111u8, 114u8, 95u8, 99u8, 111u8, 100u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 5u8, 82u8, 9u8, 101u8, 114u8, 114u8, 111u8, 114u8, 67u8, 111u8, 100u8, 101u8, 18u8, 35u8, 10u8, 13u8, 101u8, 114u8, 114u8, 111u8, 114u8, 95u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 12u8, 101u8, 114u8, 114u8, 111u8, 114u8, 77u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 50u8, 137u8, 1u8, 10u8, 16u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 18u8, 117u8, 10u8, 20u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 73u8, 110u8, 102u8, 111u8, 18u8, 43u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 46u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 44u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 46u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 40u8, 1u8, 48u8, 1u8, 66u8, 102u8, 10u8, 21u8, 105u8, 111u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 66u8, 21u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 80u8, 114u8, 111u8, 116u8, 111u8, 80u8, 1u8, 90u8, 52u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 103u8, 111u8, 108u8, 97u8, 110u8, 103u8, 46u8, 111u8, 114u8, 103u8, 47u8, 103u8, 114u8, 112u8, 99u8, 47u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 47u8, 103u8, 114u8, 112u8, 99u8, 95u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 95u8, 118u8, 49u8, 98u8, 6u8, 112u8, 114u8, 111u8, 116u8, 111u8, 51u8, ]; ================================================ FILE: tonic-reflection/src/generated/reflection_v1alpha1_fds.rs ================================================ // This file is @generated by codegen. // Copyright 2016 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Service exported by server reflection // /// Byte encoded FILE_DESCRIPTOR_SET. pub const FILE_DESCRIPTOR_SET: &[u8] = &[ 10u8, 143u8, 13u8, 10u8, 24u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 95u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 18u8, 23u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 34u8, 248u8, 2u8, 10u8, 23u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 18u8, 10u8, 4u8, 104u8, 111u8, 115u8, 116u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 4u8, 104u8, 111u8, 115u8, 116u8, 18u8, 42u8, 10u8, 16u8, 102u8, 105u8, 108u8, 101u8, 95u8, 98u8, 121u8, 95u8, 102u8, 105u8, 108u8, 101u8, 110u8, 97u8, 109u8, 101u8, 24u8, 3u8, 32u8, 1u8, 40u8, 9u8, 72u8, 0u8, 82u8, 14u8, 102u8, 105u8, 108u8, 101u8, 66u8, 121u8, 70u8, 105u8, 108u8, 101u8, 110u8, 97u8, 109u8, 101u8, 18u8, 54u8, 10u8, 22u8, 102u8, 105u8, 108u8, 101u8, 95u8, 99u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 95u8, 115u8, 121u8, 109u8, 98u8, 111u8, 108u8, 24u8, 4u8, 32u8, 1u8, 40u8, 9u8, 72u8, 0u8, 82u8, 20u8, 102u8, 105u8, 108u8, 101u8, 67u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 83u8, 121u8, 109u8, 98u8, 111u8, 108u8, 18u8, 103u8, 10u8, 25u8, 102u8, 105u8, 108u8, 101u8, 95u8, 99u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 95u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 24u8, 5u8, 32u8, 1u8, 40u8, 11u8, 50u8, 41u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 72u8, 0u8, 82u8, 23u8, 102u8, 105u8, 108u8, 101u8, 67u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 18u8, 66u8, 10u8, 29u8, 97u8, 108u8, 108u8, 95u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 95u8, 110u8, 117u8, 109u8, 98u8, 101u8, 114u8, 115u8, 95u8, 111u8, 102u8, 95u8, 116u8, 121u8, 112u8, 101u8, 24u8, 6u8, 32u8, 1u8, 40u8, 9u8, 72u8, 0u8, 82u8, 25u8, 97u8, 108u8, 108u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 115u8, 79u8, 102u8, 84u8, 121u8, 112u8, 101u8, 18u8, 37u8, 10u8, 13u8, 108u8, 105u8, 115u8, 116u8, 95u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 115u8, 24u8, 7u8, 32u8, 1u8, 40u8, 9u8, 72u8, 0u8, 82u8, 12u8, 108u8, 105u8, 115u8, 116u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 115u8, 66u8, 17u8, 10u8, 15u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 95u8, 114u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 34u8, 102u8, 10u8, 16u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 39u8, 10u8, 15u8, 99u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 95u8, 116u8, 121u8, 112u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 14u8, 99u8, 111u8, 110u8, 116u8, 97u8, 105u8, 110u8, 105u8, 110u8, 103u8, 84u8, 121u8, 112u8, 101u8, 18u8, 41u8, 10u8, 16u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 95u8, 110u8, 117u8, 109u8, 98u8, 101u8, 114u8, 24u8, 2u8, 32u8, 1u8, 40u8, 5u8, 82u8, 15u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 34u8, 199u8, 4u8, 10u8, 24u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 29u8, 10u8, 10u8, 118u8, 97u8, 108u8, 105u8, 100u8, 95u8, 104u8, 111u8, 115u8, 116u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 9u8, 118u8, 97u8, 108u8, 105u8, 100u8, 72u8, 111u8, 115u8, 116u8, 18u8, 91u8, 10u8, 16u8, 111u8, 114u8, 105u8, 103u8, 105u8, 110u8, 97u8, 108u8, 95u8, 114u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 24u8, 2u8, 32u8, 1u8, 40u8, 11u8, 50u8, 48u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 82u8, 15u8, 111u8, 114u8, 105u8, 103u8, 105u8, 110u8, 97u8, 108u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 107u8, 10u8, 24u8, 102u8, 105u8, 108u8, 101u8, 95u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 24u8, 4u8, 32u8, 1u8, 40u8, 11u8, 50u8, 47u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 70u8, 105u8, 108u8, 101u8, 68u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 72u8, 0u8, 82u8, 22u8, 102u8, 105u8, 108u8, 101u8, 68u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 119u8, 10u8, 30u8, 97u8, 108u8, 108u8, 95u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 95u8, 110u8, 117u8, 109u8, 98u8, 101u8, 114u8, 115u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 24u8, 5u8, 32u8, 1u8, 40u8, 11u8, 50u8, 48u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 72u8, 0u8, 82u8, 27u8, 97u8, 108u8, 108u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 115u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 100u8, 10u8, 22u8, 108u8, 105u8, 115u8, 116u8, 95u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 115u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 24u8, 6u8, 32u8, 1u8, 40u8, 11u8, 50u8, 44u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 76u8, 105u8, 115u8, 116u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 72u8, 0u8, 82u8, 20u8, 108u8, 105u8, 115u8, 116u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 115u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 79u8, 10u8, 14u8, 101u8, 114u8, 114u8, 111u8, 114u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 24u8, 7u8, 32u8, 1u8, 40u8, 11u8, 50u8, 38u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 69u8, 114u8, 114u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 72u8, 0u8, 82u8, 13u8, 101u8, 114u8, 114u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 66u8, 18u8, 10u8, 16u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 95u8, 114u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 34u8, 76u8, 10u8, 22u8, 70u8, 105u8, 108u8, 101u8, 68u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 50u8, 10u8, 21u8, 102u8, 105u8, 108u8, 101u8, 95u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 95u8, 112u8, 114u8, 111u8, 116u8, 111u8, 24u8, 1u8, 32u8, 3u8, 40u8, 12u8, 82u8, 19u8, 102u8, 105u8, 108u8, 101u8, 68u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 111u8, 114u8, 80u8, 114u8, 111u8, 116u8, 111u8, 34u8, 106u8, 10u8, 23u8, 69u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 36u8, 10u8, 14u8, 98u8, 97u8, 115u8, 101u8, 95u8, 116u8, 121u8, 112u8, 101u8, 95u8, 110u8, 97u8, 109u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 12u8, 98u8, 97u8, 115u8, 101u8, 84u8, 121u8, 112u8, 101u8, 78u8, 97u8, 109u8, 101u8, 18u8, 41u8, 10u8, 16u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 95u8, 110u8, 117u8, 109u8, 98u8, 101u8, 114u8, 24u8, 2u8, 32u8, 3u8, 40u8, 5u8, 82u8, 15u8, 101u8, 120u8, 116u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 78u8, 117u8, 109u8, 98u8, 101u8, 114u8, 34u8, 89u8, 10u8, 19u8, 76u8, 105u8, 115u8, 116u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 66u8, 10u8, 7u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 24u8, 1u8, 32u8, 3u8, 40u8, 11u8, 50u8, 40u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 82u8, 7u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 34u8, 37u8, 10u8, 15u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 18u8, 10u8, 4u8, 110u8, 97u8, 109u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 4u8, 110u8, 97u8, 109u8, 101u8, 34u8, 83u8, 10u8, 13u8, 69u8, 114u8, 114u8, 111u8, 114u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 18u8, 29u8, 10u8, 10u8, 101u8, 114u8, 114u8, 111u8, 114u8, 95u8, 99u8, 111u8, 100u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 5u8, 82u8, 9u8, 101u8, 114u8, 114u8, 111u8, 114u8, 67u8, 111u8, 100u8, 101u8, 18u8, 35u8, 10u8, 13u8, 101u8, 114u8, 114u8, 111u8, 114u8, 95u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 12u8, 101u8, 114u8, 114u8, 111u8, 114u8, 77u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 50u8, 147u8, 1u8, 10u8, 16u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 18u8, 127u8, 10u8, 20u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 73u8, 110u8, 102u8, 111u8, 18u8, 48u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 49u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 114u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 46u8, 118u8, 49u8, 97u8, 108u8, 112u8, 104u8, 97u8, 46u8, 83u8, 101u8, 114u8, 118u8, 101u8, 114u8, 82u8, 101u8, 102u8, 108u8, 101u8, 99u8, 116u8, 105u8, 111u8, 110u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 40u8, 1u8, 48u8, 1u8, 98u8, 6u8, 112u8, 114u8, 111u8, 116u8, 111u8, 51u8, ]; ================================================ FILE: tonic-reflection/src/lib.rs ================================================ //! A `tonic` based gRPC Server Reflection implementation. #![doc( html_logo_url = "https://github.com/hyperium/tonic/raw/master/.github/assets/tonic-docs.png" )] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] #![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))] #![cfg_attr(docsrs, feature(doc_cfg))] mod generated { #![allow(unreachable_pub)] #![allow(missing_docs)] #![allow(rustdoc::invalid_html_tags)] #[rustfmt::skip] pub mod grpc_reflection_v1alpha; #[rustfmt::skip] pub mod grpc_reflection_v1; #[rustfmt::skip] pub mod reflection_v1_fds; #[rustfmt::skip] pub mod reflection_v1alpha1_fds; pub use reflection_v1_fds::FILE_DESCRIPTOR_SET as FILE_DESCRIPTOR_SET_V1; pub use reflection_v1alpha1_fds::FILE_DESCRIPTOR_SET as FILE_DESCRIPTOR_SET_V1ALPHA; #[cfg(test)] mod tests { use super::{FILE_DESCRIPTOR_SET_V1, FILE_DESCRIPTOR_SET_V1ALPHA}; use prost::Message as _; #[test] fn v1alpha_file_descriptor_set_is_valid() { prost_types::FileDescriptorSet::decode(FILE_DESCRIPTOR_SET_V1ALPHA).unwrap(); } #[test] fn v1_file_descriptor_set_is_valid() { prost_types::FileDescriptorSet::decode(FILE_DESCRIPTOR_SET_V1).unwrap(); } } } /// Generated protobuf types from the `grpc.reflection` namespace. pub mod pb { /// Generated protobuf types from the `grpc.reflection.v1` package. pub mod v1 { pub use crate::generated::{ FILE_DESCRIPTOR_SET_V1 as FILE_DESCRIPTOR_SET, grpc_reflection_v1::*, }; } /// Generated protobuf types from the `grpc.reflection.v1alpha` package. pub mod v1alpha { pub use crate::generated::{ FILE_DESCRIPTOR_SET_V1ALPHA as FILE_DESCRIPTOR_SET, grpc_reflection_v1alpha::*, }; } } /// Implementation of the server component of gRPC Server Reflection. #[cfg(feature = "server")] pub mod server; ================================================ FILE: tonic-reflection/src/server/mod.rs ================================================ use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::sync::Arc; use prost::{DecodeError, Message}; use prost_types::{ DescriptorProto, EnumDescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet, }; use tonic::Status; /// v1 interface for the gRPC Reflection Service server. pub mod v1; /// v1alpha interface for the gRPC Reflection Service server. pub mod v1alpha; /// A builder used to construct a gRPC Reflection Service. #[derive(Debug)] pub struct Builder<'b> { file_descriptor_sets: Vec, encoded_file_descriptor_sets: Vec<&'b [u8]>, include_reflection_service: bool, service_names: Vec, use_all_service_names: bool, } impl<'b> Builder<'b> { /// Create a new builder that can configure a gRPC Reflection Service. pub fn configure() -> Self { Builder { file_descriptor_sets: Vec::new(), encoded_file_descriptor_sets: Vec::new(), include_reflection_service: true, service_names: Vec::new(), use_all_service_names: true, } } /// Registers an instance of `prost_types::FileDescriptorSet` with the gRPC Reflection /// Service builder. pub fn register_file_descriptor_set(mut self, file_descriptor_set: FileDescriptorSet) -> Self { self.file_descriptor_sets.push(file_descriptor_set); self } /// Registers a byte slice containing an encoded `prost_types::FileDescriptorSet` with /// the gRPC Reflection Service builder. pub fn register_encoded_file_descriptor_set( mut self, encoded_file_descriptor_set: &'b [u8], ) -> Self { self.encoded_file_descriptor_sets .push(encoded_file_descriptor_set); self } /// Serve the gRPC Reflection Service descriptor via the Reflection Service. This is enabled /// by default - set `include` to false to disable. pub fn include_reflection_service(mut self, include: bool) -> Self { self.include_reflection_service = include; self } /// Advertise a fully-qualified gRPC service name. /// /// If not called, then all services present in the registered file descriptor sets /// will be advertised. pub fn with_service_name(mut self, name: impl Into) -> Self { self.use_all_service_names = false; self.service_names.push(name.into()); self } /// Build a v1 gRPC Reflection Service to be served via Tonic. pub fn build_v1( mut self, ) -> Result, Error> { if self.include_reflection_service { self = self.register_encoded_file_descriptor_set(crate::pb::v1::FILE_DESCRIPTOR_SET); } Ok(v1::ServerReflectionServer::new( v1::ReflectionService::from(ReflectionServiceState::new( self.service_names, self.encoded_file_descriptor_sets, self.file_descriptor_sets, self.use_all_service_names, )?), )) } /// Build a v1alpha gRPC Reflection Service to be served via Tonic. pub fn build_v1alpha( mut self, ) -> Result, Error> { if self.include_reflection_service { self = self.register_encoded_file_descriptor_set(crate::pb::v1alpha::FILE_DESCRIPTOR_SET); } Ok(v1alpha::ServerReflectionServer::new( v1alpha::ReflectionService::from(ReflectionServiceState::new( self.service_names, self.encoded_file_descriptor_sets, self.file_descriptor_sets, self.use_all_service_names, )?), )) } } #[derive(Debug)] struct ReflectionServiceState { service_names: Vec, files: HashMap>, symbols: HashMap>, } impl ReflectionServiceState { fn new( service_names: Vec, encoded_file_descriptor_sets: Vec<&[u8]>, mut file_descriptor_sets: Vec, use_all_service_names: bool, ) -> Result { for encoded in encoded_file_descriptor_sets { file_descriptor_sets.push(FileDescriptorSet::decode(encoded)?); } let mut state = ReflectionServiceState { service_names, files: HashMap::new(), symbols: HashMap::new(), }; for fds in file_descriptor_sets { for fd in fds.file { let name = match fd.name.clone() { None => { return Err(Error::InvalidFileDescriptorSet("missing name".to_string())); } Some(n) => n, }; if state.files.contains_key(&name) { continue; } let fd = Arc::new(fd); state.files.insert(name, fd.clone()); state.process_file(fd, use_all_service_names)?; } } Ok(state) } fn process_file( &mut self, fd: Arc, use_all_service_names: bool, ) -> Result<(), Error> { let prefix = &fd.package.clone().unwrap_or_default(); for msg in &fd.message_type { self.process_message(fd.clone(), prefix, msg)?; } for en in &fd.enum_type { self.process_enum(fd.clone(), prefix, en)?; } for service in &fd.service { let service_name = extract_name(prefix, "service", service.name.as_ref())?; if use_all_service_names { self.service_names.push(service_name.clone()); } self.symbols.insert(service_name.clone(), fd.clone()); for method in &service.method { let method_name = extract_name(&service_name, "method", method.name.as_ref())?; self.symbols.insert(method_name, fd.clone()); } } Ok(()) } fn process_message( &mut self, fd: Arc, prefix: &str, msg: &DescriptorProto, ) -> Result<(), Error> { let message_name = extract_name(prefix, "message", msg.name.as_ref())?; self.symbols.insert(message_name.clone(), fd.clone()); for nested in &msg.nested_type { self.process_message(fd.clone(), &message_name, nested)?; } for en in &msg.enum_type { self.process_enum(fd.clone(), &message_name, en)?; } for field in &msg.field { self.process_field(fd.clone(), &message_name, field)?; } for oneof in &msg.oneof_decl { let oneof_name = extract_name(&message_name, "oneof", oneof.name.as_ref())?; self.symbols.insert(oneof_name, fd.clone()); } Ok(()) } fn process_enum( &mut self, fd: Arc, prefix: &str, en: &EnumDescriptorProto, ) -> Result<(), Error> { let enum_name = extract_name(prefix, "enum", en.name.as_ref())?; self.symbols.insert(enum_name.clone(), fd.clone()); for value in &en.value { let value_name = extract_name(&enum_name, "enum value", value.name.as_ref())?; self.symbols.insert(value_name, fd.clone()); } Ok(()) } fn process_field( &mut self, fd: Arc, prefix: &str, field: &FieldDescriptorProto, ) -> Result<(), Error> { let field_name = extract_name(prefix, "field", field.name.as_ref())?; self.symbols.insert(field_name, fd); Ok(()) } fn list_services(&self) -> &[String] { &self.service_names } fn symbol_by_name(&self, symbol: &str) -> Result, Status> { match self.symbols.get(symbol) { None => Err(Status::not_found(format!("symbol '{symbol}' not found"))), Some(fd) => { let mut encoded_fd = Vec::new(); if fd.clone().encode(&mut encoded_fd).is_err() { return Err(Status::internal("encoding error")); }; Ok(encoded_fd) } } } fn file_by_filename(&self, filename: &str) -> Result, Status> { match self.files.get(filename) { None => Err(Status::not_found(format!("file '{filename}' not found"))), Some(fd) => { let mut encoded_fd = Vec::new(); if fd.clone().encode(&mut encoded_fd).is_err() { return Err(Status::internal("encoding error")); } Ok(encoded_fd) } } } } fn extract_name( prefix: &str, name_type: &str, maybe_name: Option<&String>, ) -> Result { match maybe_name { None => Err(Error::InvalidFileDescriptorSet(format!( "missing {name_type} name" ))), Some(name) => { if prefix.is_empty() { Ok(name.to_string()) } else { Ok(format!("{prefix}.{name}")) } } } } /// Represents an error in the construction of a gRPC Reflection Service. #[derive(Debug)] pub enum Error { /// An error was encountered decoding a `prost_types::FileDescriptorSet` from a buffer. DecodeError(prost::DecodeError), /// An invalid `prost_types::FileDescriptorProto` was encountered. InvalidFileDescriptorSet(String), } impl From for Error { fn from(e: DecodeError) -> Self { Error::DecodeError(e) } } impl std::error::Error for Error {} impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Error::DecodeError(_) => f.write_str("error decoding FileDescriptorSet from buffer"), Error::InvalidFileDescriptorSet(s) => { write!(f, "invalid FileDescriptorSet - {s}") } } } } ================================================ FILE: tonic-reflection/src/server/v1.rs ================================================ use std::{fmt, sync::Arc}; use tokio::sync::mpsc; use tokio_stream::{Stream, StreamExt}; use tonic::{Request, Response, Status, Streaming}; use super::ReflectionServiceState; use crate::pb::v1::server_reflection_request::MessageRequest; use crate::pb::v1::server_reflection_response::MessageResponse; pub use crate::pb::v1::server_reflection_server::{ServerReflection, ServerReflectionServer}; use crate::pb::v1::{ ExtensionNumberResponse, FileDescriptorResponse, ListServiceResponse, ServerReflectionRequest, ServerReflectionResponse, ServiceResponse, }; /// An implementation for `ServerReflection`. #[derive(Debug)] pub struct ReflectionService { state: Arc, } #[tonic::async_trait] impl ServerReflection for ReflectionService { type ServerReflectionInfoStream = ServerReflectionInfoStream; async fn server_reflection_info( &self, req: Request>, ) -> Result, Status> { let mut req_rx = req.into_inner(); let (resp_tx, resp_rx) = mpsc::channel::>(1); let state = self.state.clone(); tokio::spawn(async move { while let Some(req) = req_rx.next().await { let Ok(req) = req else { return; }; let resp_msg = match req.message_request.clone() { None => Err(Status::invalid_argument("invalid MessageRequest")), Some(msg) => match msg { MessageRequest::FileByFilename(s) => state.file_by_filename(&s).map(|fd| { MessageResponse::FileDescriptorResponse(FileDescriptorResponse { file_descriptor_proto: vec![fd], }) }), MessageRequest::FileContainingSymbol(s) => { state.symbol_by_name(&s).map(|fd| { MessageResponse::FileDescriptorResponse(FileDescriptorResponse { file_descriptor_proto: vec![fd], }) }) } MessageRequest::FileContainingExtension(_) => { Err(Status::not_found("extensions are not supported")) } MessageRequest::AllExtensionNumbersOfType(_) => { // NOTE: Workaround. Some grpc clients (e.g. grpcurl) expect this method not to fail. // https://github.com/hyperium/tonic/issues/1077 Ok(MessageResponse::AllExtensionNumbersResponse( ExtensionNumberResponse::default(), )) } MessageRequest::ListServices(_) => { Ok(MessageResponse::ListServicesResponse(ListServiceResponse { service: state .list_services() .iter() .map(|s| ServiceResponse { name: s.clone() }) .collect(), })) } }, }; match resp_msg { Ok(resp_msg) => { let resp = ServerReflectionResponse { valid_host: req.host.clone(), original_request: Some(req.clone()), message_response: Some(resp_msg), }; resp_tx.send(Ok(resp)).await.expect("send"); } Err(status) => { resp_tx.send(Err(status)).await.expect("send"); return; } } } }); Ok(Response::new(ServerReflectionInfoStream::new(resp_rx))) } } impl From for ReflectionService { fn from(state: ReflectionServiceState) -> Self { Self { state: Arc::new(state), } } } /// A response stream. pub struct ServerReflectionInfoStream { inner: tokio_stream::wrappers::ReceiverStream>, } impl ServerReflectionInfoStream { fn new(resp_rx: mpsc::Receiver>) -> Self { let inner = tokio_stream::wrappers::ReceiverStream::new(resp_rx); Self { inner } } } impl Stream for ServerReflectionInfoStream { type Item = Result; fn poll_next( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { std::pin::Pin::new(&mut self.inner).poll_next(cx) } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } impl fmt::Debug for ServerReflectionInfoStream { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("ServerReflectionInfoStream").finish() } } ================================================ FILE: tonic-reflection/src/server/v1alpha.rs ================================================ use std::{fmt, sync::Arc}; use tokio::sync::mpsc; use tokio_stream::{Stream, StreamExt}; use tonic::{Request, Response, Status, Streaming}; use super::ReflectionServiceState; use crate::pb::v1alpha::server_reflection_request::MessageRequest; use crate::pb::v1alpha::server_reflection_response::MessageResponse; pub use crate::pb::v1alpha::server_reflection_server::{ServerReflection, ServerReflectionServer}; use crate::pb::v1alpha::{ ExtensionNumberResponse, FileDescriptorResponse, ListServiceResponse, ServerReflectionRequest, ServerReflectionResponse, ServiceResponse, }; /// An implementation for `ServerReflection`. #[derive(Debug)] pub struct ReflectionService { state: Arc, } #[tonic::async_trait] impl ServerReflection for ReflectionService { type ServerReflectionInfoStream = ServerReflectionInfoStream; async fn server_reflection_info( &self, req: Request>, ) -> Result, Status> { let mut req_rx = req.into_inner(); let (resp_tx, resp_rx) = mpsc::channel::>(1); let state = self.state.clone(); tokio::spawn(async move { while let Some(req) = req_rx.next().await { let Ok(req) = req else { return; }; let resp_msg = match req.message_request.clone() { None => Err(Status::invalid_argument("invalid MessageRequest")), Some(msg) => match msg { MessageRequest::FileByFilename(s) => state.file_by_filename(&s).map(|fd| { MessageResponse::FileDescriptorResponse(FileDescriptorResponse { file_descriptor_proto: vec![fd], }) }), MessageRequest::FileContainingSymbol(s) => { state.symbol_by_name(&s).map(|fd| { MessageResponse::FileDescriptorResponse(FileDescriptorResponse { file_descriptor_proto: vec![fd], }) }) } MessageRequest::FileContainingExtension(_) => { Err(Status::not_found("extensions are not supported")) } MessageRequest::AllExtensionNumbersOfType(_) => { // NOTE: Workaround. Some grpc clients (e.g. grpcurl) expect this method not to fail. // https://github.com/hyperium/tonic/issues/1077 Ok(MessageResponse::AllExtensionNumbersResponse( ExtensionNumberResponse::default(), )) } MessageRequest::ListServices(_) => { Ok(MessageResponse::ListServicesResponse(ListServiceResponse { service: state .list_services() .iter() .map(|s| ServiceResponse { name: s.clone() }) .collect(), })) } }, }; match resp_msg { Ok(resp_msg) => { let resp = ServerReflectionResponse { valid_host: req.host.clone(), original_request: Some(req.clone()), message_response: Some(resp_msg), }; resp_tx.send(Ok(resp)).await.expect("send"); } Err(status) => { resp_tx.send(Err(status)).await.expect("send"); return; } } } }); Ok(Response::new(ServerReflectionInfoStream::new(resp_rx))) } } impl From for ReflectionService { fn from(state: ReflectionServiceState) -> Self { Self { state: Arc::new(state), } } } /// A response stream. pub struct ServerReflectionInfoStream { inner: tokio_stream::wrappers::ReceiverStream>, } impl ServerReflectionInfoStream { fn new(resp_rx: mpsc::Receiver>) -> Self { let inner = tokio_stream::wrappers::ReceiverStream::new(resp_rx); Self { inner } } } impl Stream for ServerReflectionInfoStream { type Item = Result; fn poll_next( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { std::pin::Pin::new(&mut self.inner).poll_next(cx) } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } impl fmt::Debug for ServerReflectionInfoStream { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("ServerReflectionInfoStream").finish() } } ================================================ FILE: tonic-reflection/tests/server.rs ================================================ #![allow(missing_docs)] use prost::Message; use std::net::SocketAddr; use tokio::sync::oneshot; use tokio_stream::{StreamExt, wrappers::TcpListenerStream}; use tonic::{Request, transport::Server}; use tonic_reflection::{ pb::v1::{ FILE_DESCRIPTOR_SET, ServerReflectionRequest, ServiceResponse, server_reflection_client::ServerReflectionClient, server_reflection_request::MessageRequest, server_reflection_response::MessageResponse, }, server::Builder, }; pub(crate) fn get_encoded_reflection_service_fd() -> Vec { let mut expected = Vec::new(); prost_types::FileDescriptorSet::decode(FILE_DESCRIPTOR_SET) .expect("decode reflection service file descriptor set") .file[0] .encode(&mut expected) .expect("encode reflection service file descriptor"); expected } #[tokio::test] async fn test_list_services() { let response = make_test_reflection_request(ServerReflectionRequest { host: "".to_string(), message_request: Some(MessageRequest::ListServices(String::new())), }) .await; if let MessageResponse::ListServicesResponse(services) = response { assert_eq!( services.service, vec![ServiceResponse { name: String::from("grpc.reflection.v1.ServerReflection") }] ); } else { panic!("Expected a ListServicesResponse variant"); } } #[tokio::test] async fn test_file_by_filename() { let response = make_test_reflection_request(ServerReflectionRequest { host: "".to_string(), message_request: Some(MessageRequest::FileByFilename(String::from( "reflection_v1.proto", ))), }) .await; if let MessageResponse::FileDescriptorResponse(descriptor) = response { let file_descriptor_proto = descriptor .file_descriptor_proto .first() .expect("descriptor"); assert_eq!( file_descriptor_proto.as_ref(), get_encoded_reflection_service_fd() ); } else { panic!("Expected a FileDescriptorResponse variant"); } } #[tokio::test] async fn test_file_containing_symbol() { let response = make_test_reflection_request(ServerReflectionRequest { host: "".to_string(), message_request: Some(MessageRequest::FileContainingSymbol(String::from( "grpc.reflection.v1.ServerReflection", ))), }) .await; if let MessageResponse::FileDescriptorResponse(descriptor) = response { let file_descriptor_proto = descriptor .file_descriptor_proto .first() .expect("descriptor"); assert_eq!( file_descriptor_proto.as_ref(), get_encoded_reflection_service_fd() ); } else { panic!("Expected a FileDescriptorResponse variant"); } } async fn make_test_reflection_request(request: ServerReflectionRequest) -> MessageResponse { // Run a test server let (shutdown_tx, shutdown_rx) = oneshot::channel(); let addr: SocketAddr = "127.0.0.1:0".parse().expect("SocketAddr parse"); let listener = tokio::net::TcpListener::bind(addr).await.expect("bind"); let local_addr = format!("http://{}", listener.local_addr().expect("local address")); let jh = tokio::spawn(async move { let service = Builder::configure() .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET) .build_v1() .unwrap(); Server::builder() .add_service(service) .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async { drop(shutdown_rx.await) }) .await .unwrap(); }); // Give the test server a few ms to become available tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Construct client and send request, extract response let conn = tonic::transport::Endpoint::new(local_addr) .unwrap() .connect() .await .unwrap(); let mut client = ServerReflectionClient::new(conn); let request = Request::new(tokio_stream::once(request)); let mut inbound = client .server_reflection_info(request) .await .expect("request") .into_inner(); let response = inbound .next() .await .expect("steamed response") .expect("successful response") .message_response .expect("some MessageResponse"); // We only expect one response per request assert!(inbound.next().await.is_none()); // Shut down test server shutdown_tx.send(()).expect("send shutdown"); jh.await.expect("server shutdown"); response } ================================================ FILE: tonic-reflection/tests/versions.rs ================================================ #![allow(missing_docs)] use std::net::SocketAddr; use tokio::sync::oneshot; use tokio_stream::{StreamExt, wrappers::TcpListenerStream}; use tonic::{Request, transport::Server}; use tonic_reflection::pb::{v1, v1alpha}; use tonic_reflection::server::Builder; #[tokio::test] async fn test_v1() { let response = make_v1_request(v1::ServerReflectionRequest { host: "".to_string(), message_request: Some(v1::server_reflection_request::MessageRequest::ListServices( String::new(), )), }) .await; if let v1::server_reflection_response::MessageResponse::ListServicesResponse(services) = response { assert_eq!( services.service, vec![v1::ServiceResponse { name: String::from("grpc.reflection.v1.ServerReflection") }] ); } else { panic!("Expected a ListServicesResponse variant"); } } #[tokio::test] async fn test_v1alpha() { let response = make_v1alpha_request(v1alpha::ServerReflectionRequest { host: "".to_string(), message_request: Some( v1alpha::server_reflection_request::MessageRequest::ListServices(String::new()), ), }) .await; if let v1alpha::server_reflection_response::MessageResponse::ListServicesResponse(services) = response { assert_eq!( services.service, vec![v1alpha::ServiceResponse { name: String::from("grpc.reflection.v1alpha.ServerReflection") }] ); } else { panic!("Expected a ListServicesResponse variant"); } } async fn make_v1_request( request: v1::ServerReflectionRequest, ) -> v1::server_reflection_response::MessageResponse { // Run a test server let (shutdown_tx, shutdown_rx) = oneshot::channel(); let addr: SocketAddr = "127.0.0.1:0".parse().expect("SocketAddr parse"); let listener = tokio::net::TcpListener::bind(addr).await.expect("bind"); let local_addr = format!("http://{}", listener.local_addr().expect("local address")); let jh = tokio::spawn(async move { let service = Builder::configure().build_v1().unwrap(); Server::builder() .add_service(service) .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async { drop(shutdown_rx.await) }) .await .unwrap(); }); // Give the test server a few ms to become available tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Construct client and send request, extract response let conn = tonic::transport::Endpoint::new(local_addr) .unwrap() .connect() .await .unwrap(); let mut client = v1::server_reflection_client::ServerReflectionClient::new(conn); let request = Request::new(tokio_stream::once(request)); let mut inbound = client .server_reflection_info(request) .await .expect("request") .into_inner(); let response = inbound .next() .await .expect("steamed response") .expect("successful response") .message_response .expect("some MessageResponse"); // We only expect one response per request assert!(inbound.next().await.is_none()); // Shut down test server shutdown_tx.send(()).expect("send shutdown"); jh.await.expect("server shutdown"); response } async fn make_v1alpha_request( request: v1alpha::ServerReflectionRequest, ) -> v1alpha::server_reflection_response::MessageResponse { // Run a test server let (shutdown_tx, shutdown_rx) = oneshot::channel(); let addr: SocketAddr = "127.0.0.1:0".parse().expect("SocketAddr parse"); let listener = tokio::net::TcpListener::bind(addr).await.expect("bind"); let local_addr = format!("http://{}", listener.local_addr().expect("local address")); let jh = tokio::spawn(async move { let service = Builder::configure().build_v1alpha().unwrap(); Server::builder() .add_service(service) .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async { drop(shutdown_rx.await) }) .await .unwrap(); }); // Give the test server a few ms to become available tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Construct client and send request, extract response let conn = tonic::transport::Endpoint::new(local_addr) .unwrap() .connect() .await .unwrap(); let mut client = v1alpha::server_reflection_client::ServerReflectionClient::new(conn); let request = Request::new(tokio_stream::once(request)); let mut inbound = client .server_reflection_info(request) .await .expect("request") .into_inner(); let response = inbound .next() .await .expect("steamed response") .expect("successful response") .message_response .expect("some MessageResponse"); // We only expect one response per request assert!(inbound.next().await.is_none()); // Shut down test server shutdown_tx.send(()).expect("send shutdown"); jh.await.expect("server shutdown"); response } ================================================ FILE: tonic-types/Cargo.toml ================================================ [package] authors = [ "Lucio Franco ", "Rafael Lemos " ] categories = ["web-programming", "network-programming", "asynchronous"] description = """ A collection of useful protobuf types that can be used with `tonic`. """ edition = "2024" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "protobuf"] license = "MIT" name = "tonic-types" readme = "README.md" repository = "https://github.com/hyperium/tonic" version = "0.14.5" rust-version = { workspace = true } [dependencies] prost = "0.14" prost-types = "0.14" tonic = { version = "0.14.0", path = "../tonic", default-features = false } [lints] workspace = true [package.metadata.cargo_check_external_types] allowed_external_types = [ "tonic::*", # not major released "prost::*", "prost_types::*", ] ================================================ FILE: tonic-types/README.md ================================================ # tonic-types A collection of useful protobuf types that can be used with `tonic`. This crate also introduces the [`StatusExt`] trait and implements it in [`tonic::Status`], allowing the implementation of the [gRPC Richer Error Model] with [`tonic`] in a convenient way. ## Usage Useful protobuf types are available through the [`pb`] module. They can be imported and worked with directly. The [`StatusExt`] trait adds associated functions to [`tonic::Status`] that can be used on the server side to create a status with error details, which can then be returned to gRPC clients. Moreover, the trait also adds methods to [`tonic::Status`] that can be used by a tonic client to extract error details, and handle them with ease. ## Examples The examples below cover a basic use case of the [gRPC Richer Error Model]. More complete server and client implementations are provided in the **Richer Error example**, located in the main repo [examples] directory. ### Server Side: Generating [`tonic::Status`] with an [`ErrorDetails`] struct ```rust use tonic::{Code, Status}; use tonic_types::{ErrorDetails, StatusExt}; // ... // Inside a gRPC server endpoint that returns `Result, Status>` // Create empty `ErrorDetails` struct let mut err_details = ErrorDetails::new(); // Add error details conditionally if some_condition { err_details.add_bad_request_violation( "field_a", "description of why the field_a is invalid" ); } if other_condition { err_details.add_bad_request_violation( "field_b", "description of why the field_b is invalid", ); } // Check if any error details were set and return error status if so if err_details.has_bad_request_violations() { // Add additional error details if necessary err_details .add_help_link("description of link", "https://resource.example.local") .set_localized_message("en-US", "message for the user"); let status = Status::with_error_details( Code::InvalidArgument, "bad request", err_details, ); return Err(status); } // Handle valid request // ... ``` ### Client Side: Extracting an [`ErrorDetails`] struct from [`tonic::Status`] ```rust use tonic::{Response, Status}; use tonic_types::StatusExt; // ... // Where `req_result` was returned by a gRPC client endpoint method fn handle_request_result(req_result: Result, Status>) { match req_result { Ok(response) => { // Handle successful response }, Err(status) => { let err_details = status.get_error_details(); if let Some(bad_request) = err_details.bad_request() { // Handle bad_request details } if let Some(help) = err_details.help() { // Handle help details } if let Some(localized_message) = err_details.localized_message() { // Handle localized_message details } } }; } ``` ## Working with different error message types Multiple examples are provided at the [`ErrorDetails`] doc. Instructions about how to use the fields of the standard error message types correctly are provided at [error_details.proto]. ## Alternative `tonic::Status` associated functions and methods In the [`StatusExt`] doc, an alternative way of interacting with [`tonic::Status`] is presented, using vectors of error details structs wrapped with the [`ErrorDetail`] enum. This approach can provide more control over the vector of standard error messages that will be generated or that was received, if necessary. To see how to adopt this approach, please check the [`StatusExt::with_error_details_vec`] and [`StatusExt::get_error_details_vec`] docs, and also the main repo's [Richer Error example] directory. Besides that, multiple examples with alternative error details extraction methods are provided in the [`StatusExt`] doc, which can be specially useful if only one type of standard error message is being handled by the client. For example, using [`StatusExt::get_details_bad_request`] is a more direct way of extracting a [`BadRequest`] error message from [`tonic::Status`]. [`tonic::Status`]: https://docs.rs/tonic/latest/tonic/struct.Status.html [`tonic`]: https://docs.rs/tonic/latest/tonic/ [gRPC Richer Error Model]: https://www.grpc.io/docs/guides/error/ [`pb`]: https://docs.rs/tonic-types/latest/tonic_types/pb/index.html [`StatusExt`]: https://docs.rs/tonic-types/latest/tonic_types/trait.StatusExt.html [examples]: https://github.com/hyperium/tonic/tree/master/examples [`ErrorDetails`]: https://docs.rs/tonic-types/latest/tonic_types/struct.ErrorDetails.html [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto [`ErrorDetail`]: https://docs.rs/tonic-types/latest/tonic_types/enum.ErrorDetail.html [`StatusExt::with_error_details_vec`]: https://docs.rs/tonic-types/latest/tonic_types/trait.StatusExt.html#tymethod.with_error_details_vec [`StatusExt::get_error_details_vec`]: https://docs.rs/tonic-types/latest/tonic_types/trait.StatusExt.html#tymethod.get_error_details_vec [Richer Error example]: https://github.com/hyperium/tonic/tree/master/examples/src/richer-error [`StatusExt::get_details_bad_request`]: https://docs.rs/tonic-types/latest/tonic_types/trait.StatusExt.html#tymethod.get_details_bad_request [`BadRequest`]: https://docs.rs/tonic-types/latest/tonic_types/struct.BadRequest.html ================================================ FILE: tonic-types/proto/error_details.proto ================================================ // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.rpc; import "google/protobuf/duration.proto"; option go_package = "google.golang.org/genproto/googleapis/rpc/errdetails;errdetails"; option java_multiple_files = true; option java_outer_classname = "ErrorDetailsProto"; option java_package = "com.google.rpc"; option objc_class_prefix = "RPC"; // Describes the cause of the error with structured details. // // Example of an error when contacting the "pubsub.googleapis.com" API when it // is not enabled: // // { "reason": "API_DISABLED" // "domain": "googleapis.com" // "metadata": { // "resource": "projects/123", // "service": "pubsub.googleapis.com" // } // } // // This response indicates that the pubsub.googleapis.com API is not enabled. // // Example of an error that is returned when attempting to create a Spanner // instance in a region that is out of stock: // // { "reason": "STOCKOUT" // "domain": "spanner.googleapis.com", // "metadata": { // "availableRegions": "us-central1,us-east2" // } // } message ErrorInfo { // The reason of the error. This is a constant value that identifies the // proximate cause of the error. Error reasons are unique within a particular // domain of errors. This should be at most 63 characters and match a // regular expression of `[A-Z][A-Z0-9_]+[A-Z0-9]`, which represents // UPPER_SNAKE_CASE. string reason = 1; // The logical grouping to which the "reason" belongs. The error domain // is typically the registered service name of the tool or product that // generates the error. Example: "pubsub.googleapis.com". If the error is // generated by some common infrastructure, the error domain must be a // globally unique value that identifies the infrastructure. For Google API // infrastructure, the error domain is "googleapis.com". string domain = 2; // Additional structured details about this error. // // Keys must match a regular expression of `[a-z][a-zA-Z0-9-_]+` but should // ideally be lowerCamelCase. Also, they must be limited to 64 characters in // length. When identifying the current value of an exceeded limit, the units // should be contained in the key, not the value. For example, rather than // `{"instanceLimit": "100/request"}`, should be returned as, // `{"instanceLimitPerRequest": "100"}`, if the client exceeds the number of // instances that can be created in a single (batch) request. map metadata = 3; } // Describes when the clients can retry a failed request. Clients could ignore // the recommendation here or retry when this information is missing from error // responses. // // It's always recommended that clients should use exponential backoff when // retrying. // // Clients should wait until `retry_delay` amount of time has passed since // receiving the error response before retrying. If retrying requests also // fail, clients should use an exponential backoff scheme to gradually increase // the delay between retries based on `retry_delay`, until either a maximum // number of retries have been reached or a maximum retry delay cap has been // reached. message RetryInfo { // Clients should wait at least this long between retrying the same request. google.protobuf.Duration retry_delay = 1; } // Describes additional debugging info. message DebugInfo { // The stack trace entries indicating where the error occurred. repeated string stack_entries = 1; // Additional debugging information provided by the server. string detail = 2; } // Describes how a quota check failed. // // For example if a daily limit was exceeded for the calling project, // a service could respond with a QuotaFailure detail containing the project // id and the description of the quota limit that was exceeded. If the // calling project hasn't enabled the service in the developer console, then // a service could respond with the project id and set `service_disabled` // to true. // // Also see RetryInfo and Help types for other details about handling a // quota failure. message QuotaFailure { // A message type used to describe a single quota violation. For example, a // daily quota or a custom quota that was exceeded. message Violation { // The subject on which the quota check failed. // For example, "clientip:" or "project:". string subject = 1; // A description of how the quota check failed. Clients can use this // description to find more about the quota configuration in the service's // public documentation, or find the relevant quota limit to adjust through // developer console. // // For example: "Service disabled" or "Daily Limit for read operations // exceeded". string description = 2; // The API Service from which the `QuotaFailure.Violation` orginates. In // some cases, Quota issues originate from an API Service other than the one // that was called. In other words, a dependency of the called API Service // could be the cause of the `QuotaFailure`, and this field would have the // dependency API service name. // // For example, if the called API is Kubernetes Engine API // (container.googleapis.com), and a quota violation occurs in the // Kubernetes Engine API itself, this field would be // "container.googleapis.com". On the other hand, if the quota violation // occurs when the Kubernetes Engine API creates VMs in the Compute Engine // API (compute.googleapis.com), this field would be // "compute.googleapis.com". string api_service = 3; // The metric of the violated quota. A quota metric is a named counter to // measure usage, such as API requests or CPUs. When an activity occurs in a // service, such as Virtual Machine allocation, one or more quota metrics // may be affected. // // For example, "compute.googleapis.com/cpus_per_vm_family", // "storage.googleapis.com/internet_egress_bandwidth". string quota_metric = 4; // The id of the violated quota. Also know as "limit name", this is the // unique identifier of a quota in the context of an API service. // // For example, "CPUS-PER-VM-FAMILY-per-project-region". string quota_id = 5; // The dimensions of the violated quota. Every non-global quota is enforced // on a set of dimensions. While quota metric defines what to count, the // dimensions specify for what aspects the counter should be increased. // // For example, the quota "CPUs per region per VM family" enforces a limit // on the metric "compute.googleapis.com/cpus_per_vm_family" on dimensions // "region" and "vm_family". And if the violation occurred in region // "us-central1" and for VM family "n1", the quota_dimensions would be, // // { // "region": "us-central1", // "vm_family": "n1", // } // // When a quota is enforced globally, the quota_dimensions would always be // empty. map quota_dimensions = 6; // The enforced quota value at the time of the `QuotaFailure`. // // For example, if the enforced quota value at the time of the // `QuotaFailure` on the number of CPUs is "10", then the value of this // field would reflect this quantity. int64 quota_value = 7; // The new quota value being rolled out at the time of the violation. At the // completion of the rollout, this value will be enforced in place of // quota_value. If no rollout is in progress at the time of the violation, // this field is not set. // // For example, if at the time of the violation a rollout is in progress // changing the number of CPUs quota from 10 to 20, 20 would be the value of // this field. optional int64 future_quota_value = 8; } // Describes all quota violations. repeated Violation violations = 1; } // Describes what preconditions have failed. // // For example, if an RPC failed because it required the Terms of Service to be // acknowledged, it could list the terms of service violation in the // PreconditionFailure message. message PreconditionFailure { // A message type used to describe a single precondition failure. message Violation { // The type of PreconditionFailure. We recommend using a service-specific // enum type to define the supported precondition violation subjects. For // example, "TOS" for "Terms of Service violation". string type = 1; // The subject, relative to the type, that failed. // For example, "google.com/cloud" relative to the "TOS" type would indicate // which terms of service is being referenced. string subject = 2; // A description of how the precondition failed. Developers can use this // description to understand how to fix the failure. // // For example: "Terms of service not accepted". string description = 3; } // Describes all precondition violations. repeated Violation violations = 1; } // Describes violations in a client request. This error type focuses on the // syntactic aspects of the request. message BadRequest { // A message type used to describe a single bad request field. message FieldViolation { // A path that leads to a field in the request body. The value will be a // sequence of dot-separated identifiers that identify a protocol buffer // field. // // Consider the following: // // message CreateContactRequest { // message EmailAddress { // enum Type { // TYPE_UNSPECIFIED = 0; // HOME = 1; // WORK = 2; // } // // optional string email = 1; // repeated EmailType type = 2; // } // // string full_name = 1; // repeated EmailAddress email_addresses = 2; // } // // In this example, in proto `field` could take one of the following values: // // * `full_name` for a violation in the `full_name` value // * `email_addresses[1].email` for a violation in the `email` field of the // first `email_addresses` message // * `email_addresses[3].type[2]` for a violation in the second `type` // value in the third `email_addresses` message. // // In JSON, the same values are represented as: // // * `fullName` for a violation in the `fullName` value // * `emailAddresses[1].email` for a violation in the `email` field of the // first `emailAddresses` message // * `emailAddresses[3].type[2]` for a violation in the second `type` // value in the third `emailAddresses` message. string field = 1; // A description of why the request element is bad. string description = 2; // The reason of the field-level error. This is a constant value that // identifies the proximate cause of the field-level error. It should // uniquely identify the type of the FieldViolation within the scope of the // google.rpc.ErrorInfo.domain. This should be at most 63 // characters and match a regular expression of `[A-Z][A-Z0-9_]+[A-Z0-9]`, // which represents UPPER_SNAKE_CASE. string reason = 3; // Provides a localized error message for field-level errors that is safe to // return to the API consumer. LocalizedMessage localized_message = 4; } // Describes all violations in a client request. repeated FieldViolation field_violations = 1; } // Contains metadata about the request that clients can attach when filing a bug // or providing other forms of feedback. message RequestInfo { // An opaque string that should only be interpreted by the service generating // it. For example, it can be used to identify requests in the service's logs. string request_id = 1; // Any data that was used to serve this request. For example, an encrypted // stack trace that can be sent back to the service provider for debugging. string serving_data = 2; } // Describes the resource that is being accessed. message ResourceInfo { // A name for the type of resource being accessed, e.g. "sql table", // "cloud storage bucket", "file", "Google calendar"; or the type URL // of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic". string resource_type = 1; // The name of the resource being accessed. For example, a shared calendar // name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current // error is // [google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED]. string resource_name = 2; // The owner of the resource (optional). // For example, "user:" or "project:". string owner = 3; // Describes what error is encountered when accessing this resource. // For example, updating a cloud project may require the `writer` permission // on the developer console project. string description = 4; } // Provides links to documentation or for performing an out of band action. // // For example, if a quota check failed with an error indicating the calling // project hasn't enabled the accessed service, this can contain a URL pointing // directly to the right place in the developer console to flip the bit. message Help { // Describes a URL link. message Link { // Describes what the link offers. string description = 1; // The URL of the link. string url = 2; } // URL(s) pointing to additional information on handling the current error. repeated Link links = 1; } // Provides a localized error message that is safe to return to the user // which can be attached to an RPC error. message LocalizedMessage { // The locale used following the specification defined at // https://www.rfc-editor.org/rfc/bcp/bcp47.txt. // Examples are: "en-US", "fr-CH", "es-MX" string locale = 1; // The localized error message in the above locale. string message = 2; } ================================================ FILE: tonic-types/proto/status.proto ================================================ // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package google.rpc; import "google/protobuf/any.proto"; option cc_enable_arenas = true; option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; option java_multiple_files = true; option java_outer_classname = "StatusProto"; option java_package = "com.google.rpc"; option objc_class_prefix = "RPC"; // The `Status` type defines a logical error model that is suitable for // different programming environments, including REST APIs and RPC APIs. It is // used by [gRPC](https://github.com/grpc). Each `Status` message contains // three pieces of data: error code, error message, and error details. // // You can find out more about this error model and how to work with it in the // [API Design Guide](https://cloud.google.com/apis/design/errors). message Status { // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. int32 code = 1; // A developer-facing error message, which should be in English. Any // user-facing error message should be localized and sent in the // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. string message = 2; // A list of messages that carry the error details. There is a common set of // message types for APIs to use. repeated google.protobuf.Any details = 3; } ================================================ FILE: tonic-types/src/generated/google_rpc.rs ================================================ // This file is @generated by prost-build. /// The `Status` type defines a logical error model that is suitable for /// different programming environments, including REST APIs and RPC APIs. It is /// used by [gRPC](). Each `Status` message contains /// three pieces of data: error code, error message, and error details. /// /// You can find out more about this error model and how to work with it in the /// [API Design Guide](). #[derive(Clone, PartialEq, ::prost::Message)] pub struct Status { /// The status code, which should be an enum value of \[google.rpc.Code\]\[google.rpc.Code\]. #[prost(int32, tag = "1")] pub code: i32, /// A developer-facing error message, which should be in English. Any /// user-facing error message should be localized and sent in the /// \[google.rpc.Status.details\]\[google.rpc.Status.details\] field, or localized by the client. #[prost(string, tag = "2")] pub message: ::prost::alloc::string::String, /// A list of messages that carry the error details. There is a common set of /// message types for APIs to use. #[prost(message, repeated, tag = "3")] pub details: ::prost::alloc::vec::Vec<::prost_types::Any>, } /// Describes the cause of the error with structured details. /// /// Example of an error when contacting the "pubsub.googleapis.com" API when it /// is not enabled: /// /// ```text /// { "reason": "API_DISABLED" /// "domain": "googleapis.com" /// "metadata": { /// "resource": "projects/123", /// "service": "pubsub.googleapis.com" /// } /// } /// ``` /// /// This response indicates that the pubsub.googleapis.com API is not enabled. /// /// Example of an error that is returned when attempting to create a Spanner /// instance in a region that is out of stock: /// /// ```text /// { "reason": "STOCKOUT" /// "domain": "spanner.googleapis.com", /// "metadata": { /// "availableRegions": "us-central1,us-east2" /// } /// } /// ``` #[derive(Clone, PartialEq, ::prost::Message)] pub struct ErrorInfo { /// The reason of the error. This is a constant value that identifies the /// proximate cause of the error. Error reasons are unique within a particular /// domain of errors. This should be at most 63 characters and match a /// regular expression of `[A-Z][A-Z0-9_]+\[A-Z0-9\]`, which represents /// UPPER_SNAKE_CASE. #[prost(string, tag = "1")] pub reason: ::prost::alloc::string::String, /// The logical grouping to which the "reason" belongs. The error domain /// is typically the registered service name of the tool or product that /// generates the error. Example: "pubsub.googleapis.com". If the error is /// generated by some common infrastructure, the error domain must be a /// globally unique value that identifies the infrastructure. For Google API /// infrastructure, the error domain is "googleapis.com". #[prost(string, tag = "2")] pub domain: ::prost::alloc::string::String, /// Additional structured details about this error. /// /// Keys must match a regular expression of `[a-z][a-zA-Z0-9-_]+` but should /// ideally be lowerCamelCase. Also, they must be limited to 64 characters in /// length. When identifying the current value of an exceeded limit, the units /// should be contained in the key, not the value. For example, rather than /// `{"instanceLimit": "100/request"}`, should be returned as, /// `{"instanceLimitPerRequest": "100"}`, if the client exceeds the number of /// instances that can be created in a single (batch) request. #[prost(map = "string, string", tag = "3")] pub metadata: ::std::collections::HashMap< ::prost::alloc::string::String, ::prost::alloc::string::String, >, } /// Describes when the clients can retry a failed request. Clients could ignore /// the recommendation here or retry when this information is missing from error /// responses. /// /// It's always recommended that clients should use exponential backoff when /// retrying. /// /// Clients should wait until `retry_delay` amount of time has passed since /// receiving the error response before retrying. If retrying requests also /// fail, clients should use an exponential backoff scheme to gradually increase /// the delay between retries based on `retry_delay`, until either a maximum /// number of retries have been reached or a maximum retry delay cap has been /// reached. #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct RetryInfo { /// Clients should wait at least this long between retrying the same request. #[prost(message, optional, tag = "1")] pub retry_delay: ::core::option::Option<::prost_types::Duration>, } /// Describes additional debugging info. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct DebugInfo { /// The stack trace entries indicating where the error occurred. #[prost(string, repeated, tag = "1")] pub stack_entries: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// Additional debugging information provided by the server. #[prost(string, tag = "2")] pub detail: ::prost::alloc::string::String, } /// Describes how a quota check failed. /// /// For example if a daily limit was exceeded for the calling project, /// a service could respond with a QuotaFailure detail containing the project /// id and the description of the quota limit that was exceeded. If the /// calling project hasn't enabled the service in the developer console, then /// a service could respond with the project id and set `service_disabled` /// to true. /// /// Also see RetryInfo and Help types for other details about handling a /// quota failure. #[derive(Clone, PartialEq, ::prost::Message)] pub struct QuotaFailure { /// Describes all quota violations. #[prost(message, repeated, tag = "1")] pub violations: ::prost::alloc::vec::Vec, } /// Nested message and enum types in `QuotaFailure`. pub mod quota_failure { /// A message type used to describe a single quota violation. For example, a /// daily quota or a custom quota that was exceeded. #[derive(Clone, PartialEq, ::prost::Message)] pub struct Violation { /// The subject on which the quota check failed. /// For example, "clientip:" or "project:". #[prost(string, tag = "1")] pub subject: ::prost::alloc::string::String, /// A description of how the quota check failed. Clients can use this /// description to find more about the quota configuration in the service's /// public documentation, or find the relevant quota limit to adjust through /// developer console. /// /// For example: "Service disabled" or "Daily Limit for read operations /// exceeded". #[prost(string, tag = "2")] pub description: ::prost::alloc::string::String, /// The API Service from which the `QuotaFailure.Violation` orginates. In /// some cases, Quota issues originate from an API Service other than the one /// that was called. In other words, a dependency of the called API Service /// could be the cause of the `QuotaFailure`, and this field would have the /// dependency API service name. /// /// For example, if the called API is Kubernetes Engine API /// (container.googleapis.com), and a quota violation occurs in the /// Kubernetes Engine API itself, this field would be /// "container.googleapis.com". On the other hand, if the quota violation /// occurs when the Kubernetes Engine API creates VMs in the Compute Engine /// API (compute.googleapis.com), this field would be /// "compute.googleapis.com". #[prost(string, tag = "3")] pub api_service: ::prost::alloc::string::String, /// The metric of the violated quota. A quota metric is a named counter to /// measure usage, such as API requests or CPUs. When an activity occurs in a /// service, such as Virtual Machine allocation, one or more quota metrics /// may be affected. /// /// For example, "compute.googleapis.com/cpus_per_vm_family", /// "storage.googleapis.com/internet_egress_bandwidth". #[prost(string, tag = "4")] pub quota_metric: ::prost::alloc::string::String, /// The id of the violated quota. Also know as "limit name", this is the /// unique identifier of a quota in the context of an API service. /// /// For example, "CPUS-PER-VM-FAMILY-per-project-region". #[prost(string, tag = "5")] pub quota_id: ::prost::alloc::string::String, /// The dimensions of the violated quota. Every non-global quota is enforced /// on a set of dimensions. While quota metric defines what to count, the /// dimensions specify for what aspects the counter should be increased. /// /// For example, the quota "CPUs per region per VM family" enforces a limit /// on the metric "compute.googleapis.com/cpus_per_vm_family" on dimensions /// "region" and "vm_family". And if the violation occurred in region /// "us-central1" and for VM family "n1", the quota_dimensions would be, /// /// { /// "region": "us-central1", /// "vm_family": "n1", /// } /// /// When a quota is enforced globally, the quota_dimensions would always be /// empty. #[prost(map = "string, string", tag = "6")] pub quota_dimensions: ::std::collections::HashMap< ::prost::alloc::string::String, ::prost::alloc::string::String, >, /// The enforced quota value at the time of the `QuotaFailure`. /// /// For example, if the enforced quota value at the time of the /// `QuotaFailure` on the number of CPUs is "10", then the value of this /// field would reflect this quantity. #[prost(int64, tag = "7")] pub quota_value: i64, /// The new quota value being rolled out at the time of the violation. At the /// completion of the rollout, this value will be enforced in place of /// quota_value. If no rollout is in progress at the time of the violation, /// this field is not set. /// /// For example, if at the time of the violation a rollout is in progress /// changing the number of CPUs quota from 10 to 20, 20 would be the value of /// this field. #[prost(int64, optional, tag = "8")] pub future_quota_value: ::core::option::Option, } } /// Describes what preconditions have failed. /// /// For example, if an RPC failed because it required the Terms of Service to be /// acknowledged, it could list the terms of service violation in the /// PreconditionFailure message. #[derive(Clone, PartialEq, ::prost::Message)] pub struct PreconditionFailure { /// Describes all precondition violations. #[prost(message, repeated, tag = "1")] pub violations: ::prost::alloc::vec::Vec, } /// Nested message and enum types in `PreconditionFailure`. pub mod precondition_failure { /// A message type used to describe a single precondition failure. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Violation { /// The type of PreconditionFailure. We recommend using a service-specific /// enum type to define the supported precondition violation subjects. For /// example, "TOS" for "Terms of Service violation". #[prost(string, tag = "1")] pub r#type: ::prost::alloc::string::String, /// The subject, relative to the type, that failed. /// For example, "google.com/cloud" relative to the "TOS" type would indicate /// which terms of service is being referenced. #[prost(string, tag = "2")] pub subject: ::prost::alloc::string::String, /// A description of how the precondition failed. Developers can use this /// description to understand how to fix the failure. /// /// For example: "Terms of service not accepted". #[prost(string, tag = "3")] pub description: ::prost::alloc::string::String, } } /// Describes violations in a client request. This error type focuses on the /// syntactic aspects of the request. #[derive(Clone, PartialEq, ::prost::Message)] pub struct BadRequest { /// Describes all violations in a client request. #[prost(message, repeated, tag = "1")] pub field_violations: ::prost::alloc::vec::Vec, } /// Nested message and enum types in `BadRequest`. pub mod bad_request { /// A message type used to describe a single bad request field. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct FieldViolation { /// A path that leads to a field in the request body. The value will be a /// sequence of dot-separated identifiers that identify a protocol buffer /// field. /// /// Consider the following: /// /// ```text /// message CreateContactRequest { /// message EmailAddress { /// enum Type { /// TYPE_UNSPECIFIED = 0; /// HOME = 1; /// WORK = 2; /// } /// /// optional string email = 1; /// repeated EmailType type = 2; /// } /// /// string full_name = 1; /// repeated EmailAddress email_addresses = 2; /// } /// ``` /// /// In this example, in proto `field` could take one of the following values: /// /// * `full_name` for a violation in the `full_name` value /// * `email_addresses\[1\].email` for a violation in the `email` field of the /// first `email_addresses` message /// * `email_addresses\[3\].type\[2\]` for a violation in the second `type` /// value in the third `email_addresses` message. /// /// In JSON, the same values are represented as: /// /// * `fullName` for a violation in the `fullName` value /// * `emailAddresses\[1\].email` for a violation in the `email` field of the /// first `emailAddresses` message /// * `emailAddresses\[3\].type\[2\]` for a violation in the second `type` /// value in the third `emailAddresses` message. #[prost(string, tag = "1")] pub field: ::prost::alloc::string::String, /// A description of why the request element is bad. #[prost(string, tag = "2")] pub description: ::prost::alloc::string::String, /// The reason of the field-level error. This is a constant value that /// identifies the proximate cause of the field-level error. It should /// uniquely identify the type of the FieldViolation within the scope of the /// google.rpc.ErrorInfo.domain. This should be at most 63 /// characters and match a regular expression of `[A-Z][A-Z0-9_]+\[A-Z0-9\]`, /// which represents UPPER_SNAKE_CASE. #[prost(string, tag = "3")] pub reason: ::prost::alloc::string::String, /// Provides a localized error message for field-level errors that is safe to /// return to the API consumer. #[prost(message, optional, tag = "4")] pub localized_message: ::core::option::Option, } } /// Contains metadata about the request that clients can attach when filing a bug /// or providing other forms of feedback. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RequestInfo { /// An opaque string that should only be interpreted by the service generating /// it. For example, it can be used to identify requests in the service's logs. #[prost(string, tag = "1")] pub request_id: ::prost::alloc::string::String, /// Any data that was used to serve this request. For example, an encrypted /// stack trace that can be sent back to the service provider for debugging. #[prost(string, tag = "2")] pub serving_data: ::prost::alloc::string::String, } /// Describes the resource that is being accessed. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ResourceInfo { /// A name for the type of resource being accessed, e.g. "sql table", /// "cloud storage bucket", "file", "Google calendar"; or the type URL /// of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic". #[prost(string, tag = "1")] pub resource_type: ::prost::alloc::string::String, /// The name of the resource being accessed. For example, a shared calendar /// name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current /// error is /// \[google.rpc.Code.PERMISSION_DENIED\]\[google.rpc.Code.PERMISSION_DENIED\]. #[prost(string, tag = "2")] pub resource_name: ::prost::alloc::string::String, /// The owner of the resource (optional). /// For example, "user:" or "project:". #[prost(string, tag = "3")] pub owner: ::prost::alloc::string::String, /// Describes what error is encountered when accessing this resource. /// For example, updating a cloud project may require the `writer` permission /// on the developer console project. #[prost(string, tag = "4")] pub description: ::prost::alloc::string::String, } /// Provides links to documentation or for performing an out of band action. /// /// For example, if a quota check failed with an error indicating the calling /// project hasn't enabled the accessed service, this can contain a URL pointing /// directly to the right place in the developer console to flip the bit. #[derive(Clone, PartialEq, ::prost::Message)] pub struct Help { /// URL(s) pointing to additional information on handling the current error. #[prost(message, repeated, tag = "1")] pub links: ::prost::alloc::vec::Vec, } /// Nested message and enum types in `Help`. pub mod help { /// Describes a URL link. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Link { /// Describes what the link offers. #[prost(string, tag = "1")] pub description: ::prost::alloc::string::String, /// The URL of the link. #[prost(string, tag = "2")] pub url: ::prost::alloc::string::String, } } /// Provides a localized error message that is safe to return to the user /// which can be attached to an RPC error. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct LocalizedMessage { /// The locale used following the specification defined at /// /// Examples are: "en-US", "fr-CH", "es-MX" #[prost(string, tag = "1")] pub locale: ::prost::alloc::string::String, /// The localized error message in the above locale. #[prost(string, tag = "2")] pub message: ::prost::alloc::string::String, } ================================================ FILE: tonic-types/src/generated/types_fds.rs ================================================ // This file is @generated by codegen. // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // /// Byte encoded FILE_DESCRIPTOR_SET. pub const FILE_DESCRIPTOR_SET: &[u8] = &[ 10u8, 228u8, 1u8, 10u8, 25u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 47u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 47u8, 97u8, 110u8, 121u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 18u8, 15u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 34u8, 54u8, 10u8, 3u8, 65u8, 110u8, 121u8, 18u8, 25u8, 10u8, 8u8, 116u8, 121u8, 112u8, 101u8, 95u8, 117u8, 114u8, 108u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 116u8, 121u8, 112u8, 101u8, 85u8, 114u8, 108u8, 18u8, 20u8, 10u8, 5u8, 118u8, 97u8, 108u8, 117u8, 101u8, 24u8, 2u8, 32u8, 1u8, 40u8, 12u8, 82u8, 5u8, 118u8, 97u8, 108u8, 117u8, 101u8, 66u8, 118u8, 10u8, 19u8, 99u8, 111u8, 109u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 66u8, 8u8, 65u8, 110u8, 121u8, 80u8, 114u8, 111u8, 116u8, 111u8, 80u8, 1u8, 90u8, 44u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 103u8, 111u8, 108u8, 97u8, 110u8, 103u8, 46u8, 111u8, 114u8, 103u8, 47u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 47u8, 116u8, 121u8, 112u8, 101u8, 115u8, 47u8, 107u8, 110u8, 111u8, 119u8, 110u8, 47u8, 97u8, 110u8, 121u8, 112u8, 98u8, 162u8, 2u8, 3u8, 71u8, 80u8, 66u8, 170u8, 2u8, 30u8, 71u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 80u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 46u8, 87u8, 101u8, 108u8, 108u8, 75u8, 110u8, 111u8, 119u8, 110u8, 84u8, 121u8, 112u8, 101u8, 115u8, 98u8, 6u8, 112u8, 114u8, 111u8, 116u8, 111u8, 51u8, 10u8, 136u8, 2u8, 10u8, 12u8, 115u8, 116u8, 97u8, 116u8, 117u8, 115u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 18u8, 10u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 26u8, 25u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 47u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 47u8, 97u8, 110u8, 121u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 34u8, 102u8, 10u8, 6u8, 83u8, 116u8, 97u8, 116u8, 117u8, 115u8, 18u8, 18u8, 10u8, 4u8, 99u8, 111u8, 100u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 5u8, 82u8, 4u8, 99u8, 111u8, 100u8, 101u8, 18u8, 24u8, 10u8, 7u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 18u8, 46u8, 10u8, 7u8, 100u8, 101u8, 116u8, 97u8, 105u8, 108u8, 115u8, 24u8, 3u8, 32u8, 3u8, 40u8, 11u8, 50u8, 20u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 46u8, 65u8, 110u8, 121u8, 82u8, 7u8, 100u8, 101u8, 116u8, 97u8, 105u8, 108u8, 115u8, 66u8, 97u8, 10u8, 14u8, 99u8, 111u8, 109u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 66u8, 11u8, 83u8, 116u8, 97u8, 116u8, 117u8, 115u8, 80u8, 114u8, 111u8, 116u8, 111u8, 80u8, 1u8, 90u8, 55u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 103u8, 111u8, 108u8, 97u8, 110u8, 103u8, 46u8, 111u8, 114u8, 103u8, 47u8, 103u8, 101u8, 110u8, 112u8, 114u8, 111u8, 116u8, 111u8, 47u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 97u8, 112u8, 105u8, 115u8, 47u8, 114u8, 112u8, 99u8, 47u8, 115u8, 116u8, 97u8, 116u8, 117u8, 115u8, 59u8, 115u8, 116u8, 97u8, 116u8, 117u8, 115u8, 248u8, 1u8, 1u8, 162u8, 2u8, 3u8, 82u8, 80u8, 67u8, 98u8, 6u8, 112u8, 114u8, 111u8, 116u8, 111u8, 51u8, 10u8, 251u8, 1u8, 10u8, 30u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 47u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 47u8, 100u8, 117u8, 114u8, 97u8, 116u8, 105u8, 111u8, 110u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 18u8, 15u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 34u8, 58u8, 10u8, 8u8, 68u8, 117u8, 114u8, 97u8, 116u8, 105u8, 111u8, 110u8, 18u8, 24u8, 10u8, 7u8, 115u8, 101u8, 99u8, 111u8, 110u8, 100u8, 115u8, 24u8, 1u8, 32u8, 1u8, 40u8, 3u8, 82u8, 7u8, 115u8, 101u8, 99u8, 111u8, 110u8, 100u8, 115u8, 18u8, 20u8, 10u8, 5u8, 110u8, 97u8, 110u8, 111u8, 115u8, 24u8, 2u8, 32u8, 1u8, 40u8, 5u8, 82u8, 5u8, 110u8, 97u8, 110u8, 111u8, 115u8, 66u8, 131u8, 1u8, 10u8, 19u8, 99u8, 111u8, 109u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 66u8, 13u8, 68u8, 117u8, 114u8, 97u8, 116u8, 105u8, 111u8, 110u8, 80u8, 114u8, 111u8, 116u8, 111u8, 80u8, 1u8, 90u8, 49u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 103u8, 111u8, 108u8, 97u8, 110u8, 103u8, 46u8, 111u8, 114u8, 103u8, 47u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 47u8, 116u8, 121u8, 112u8, 101u8, 115u8, 47u8, 107u8, 110u8, 111u8, 119u8, 110u8, 47u8, 100u8, 117u8, 114u8, 97u8, 116u8, 105u8, 111u8, 110u8, 112u8, 98u8, 248u8, 1u8, 1u8, 162u8, 2u8, 3u8, 71u8, 80u8, 66u8, 170u8, 2u8, 30u8, 71u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 80u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 46u8, 87u8, 101u8, 108u8, 108u8, 75u8, 110u8, 111u8, 119u8, 110u8, 84u8, 121u8, 112u8, 101u8, 115u8, 98u8, 6u8, 112u8, 114u8, 111u8, 116u8, 111u8, 51u8, 10u8, 129u8, 15u8, 10u8, 19u8, 101u8, 114u8, 114u8, 111u8, 114u8, 95u8, 100u8, 101u8, 116u8, 97u8, 105u8, 108u8, 115u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 18u8, 10u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 26u8, 30u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 47u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 47u8, 100u8, 117u8, 114u8, 97u8, 116u8, 105u8, 111u8, 110u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 34u8, 185u8, 1u8, 10u8, 9u8, 69u8, 114u8, 114u8, 111u8, 114u8, 73u8, 110u8, 102u8, 111u8, 18u8, 22u8, 10u8, 6u8, 114u8, 101u8, 97u8, 115u8, 111u8, 110u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 6u8, 114u8, 101u8, 97u8, 115u8, 111u8, 110u8, 18u8, 22u8, 10u8, 6u8, 100u8, 111u8, 109u8, 97u8, 105u8, 110u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 6u8, 100u8, 111u8, 109u8, 97u8, 105u8, 110u8, 18u8, 63u8, 10u8, 8u8, 109u8, 101u8, 116u8, 97u8, 100u8, 97u8, 116u8, 97u8, 24u8, 3u8, 32u8, 3u8, 40u8, 11u8, 50u8, 35u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 46u8, 69u8, 114u8, 114u8, 111u8, 114u8, 73u8, 110u8, 102u8, 111u8, 46u8, 77u8, 101u8, 116u8, 97u8, 100u8, 97u8, 116u8, 97u8, 69u8, 110u8, 116u8, 114u8, 121u8, 82u8, 8u8, 109u8, 101u8, 116u8, 97u8, 100u8, 97u8, 116u8, 97u8, 26u8, 59u8, 10u8, 13u8, 77u8, 101u8, 116u8, 97u8, 100u8, 97u8, 116u8, 97u8, 69u8, 110u8, 116u8, 114u8, 121u8, 18u8, 16u8, 10u8, 3u8, 107u8, 101u8, 121u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 3u8, 107u8, 101u8, 121u8, 18u8, 20u8, 10u8, 5u8, 118u8, 97u8, 108u8, 117u8, 101u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 5u8, 118u8, 97u8, 108u8, 117u8, 101u8, 58u8, 2u8, 56u8, 1u8, 34u8, 71u8, 10u8, 9u8, 82u8, 101u8, 116u8, 114u8, 121u8, 73u8, 110u8, 102u8, 111u8, 18u8, 58u8, 10u8, 11u8, 114u8, 101u8, 116u8, 114u8, 121u8, 95u8, 100u8, 101u8, 108u8, 97u8, 121u8, 24u8, 1u8, 32u8, 1u8, 40u8, 11u8, 50u8, 25u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 112u8, 114u8, 111u8, 116u8, 111u8, 98u8, 117u8, 102u8, 46u8, 68u8, 117u8, 114u8, 97u8, 116u8, 105u8, 111u8, 110u8, 82u8, 10u8, 114u8, 101u8, 116u8, 114u8, 121u8, 68u8, 101u8, 108u8, 97u8, 121u8, 34u8, 72u8, 10u8, 9u8, 68u8, 101u8, 98u8, 117u8, 103u8, 73u8, 110u8, 102u8, 111u8, 18u8, 35u8, 10u8, 13u8, 115u8, 116u8, 97u8, 99u8, 107u8, 95u8, 101u8, 110u8, 116u8, 114u8, 105u8, 101u8, 115u8, 24u8, 1u8, 32u8, 3u8, 40u8, 9u8, 82u8, 12u8, 115u8, 116u8, 97u8, 99u8, 107u8, 69u8, 110u8, 116u8, 114u8, 105u8, 101u8, 115u8, 18u8, 22u8, 10u8, 6u8, 100u8, 101u8, 116u8, 97u8, 105u8, 108u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 6u8, 100u8, 101u8, 116u8, 97u8, 105u8, 108u8, 34u8, 142u8, 4u8, 10u8, 12u8, 81u8, 117u8, 111u8, 116u8, 97u8, 70u8, 97u8, 105u8, 108u8, 117u8, 114u8, 101u8, 18u8, 66u8, 10u8, 10u8, 118u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 115u8, 24u8, 1u8, 32u8, 3u8, 40u8, 11u8, 50u8, 34u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 46u8, 81u8, 117u8, 111u8, 116u8, 97u8, 70u8, 97u8, 105u8, 108u8, 117u8, 114u8, 101u8, 46u8, 86u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 82u8, 10u8, 118u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 115u8, 26u8, 185u8, 3u8, 10u8, 9u8, 86u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 18u8, 24u8, 10u8, 7u8, 115u8, 117u8, 98u8, 106u8, 101u8, 99u8, 116u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 115u8, 117u8, 98u8, 106u8, 101u8, 99u8, 116u8, 18u8, 32u8, 10u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 18u8, 31u8, 10u8, 11u8, 97u8, 112u8, 105u8, 95u8, 115u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 24u8, 3u8, 32u8, 1u8, 40u8, 9u8, 82u8, 10u8, 97u8, 112u8, 105u8, 83u8, 101u8, 114u8, 118u8, 105u8, 99u8, 101u8, 18u8, 33u8, 10u8, 12u8, 113u8, 117u8, 111u8, 116u8, 97u8, 95u8, 109u8, 101u8, 116u8, 114u8, 105u8, 99u8, 24u8, 4u8, 32u8, 1u8, 40u8, 9u8, 82u8, 11u8, 113u8, 117u8, 111u8, 116u8, 97u8, 77u8, 101u8, 116u8, 114u8, 105u8, 99u8, 18u8, 25u8, 10u8, 8u8, 113u8, 117u8, 111u8, 116u8, 97u8, 95u8, 105u8, 100u8, 24u8, 5u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 113u8, 117u8, 111u8, 116u8, 97u8, 73u8, 100u8, 18u8, 98u8, 10u8, 16u8, 113u8, 117u8, 111u8, 116u8, 97u8, 95u8, 100u8, 105u8, 109u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 115u8, 24u8, 6u8, 32u8, 3u8, 40u8, 11u8, 50u8, 55u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 46u8, 81u8, 117u8, 111u8, 116u8, 97u8, 70u8, 97u8, 105u8, 108u8, 117u8, 114u8, 101u8, 46u8, 86u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 46u8, 81u8, 117u8, 111u8, 116u8, 97u8, 68u8, 105u8, 109u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 115u8, 69u8, 110u8, 116u8, 114u8, 121u8, 82u8, 15u8, 113u8, 117u8, 111u8, 116u8, 97u8, 68u8, 105u8, 109u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 115u8, 18u8, 31u8, 10u8, 11u8, 113u8, 117u8, 111u8, 116u8, 97u8, 95u8, 118u8, 97u8, 108u8, 117u8, 101u8, 24u8, 7u8, 32u8, 1u8, 40u8, 3u8, 82u8, 10u8, 113u8, 117u8, 111u8, 116u8, 97u8, 86u8, 97u8, 108u8, 117u8, 101u8, 18u8, 49u8, 10u8, 18u8, 102u8, 117u8, 116u8, 117u8, 114u8, 101u8, 95u8, 113u8, 117u8, 111u8, 116u8, 97u8, 95u8, 118u8, 97u8, 108u8, 117u8, 101u8, 24u8, 8u8, 32u8, 1u8, 40u8, 3u8, 72u8, 0u8, 82u8, 16u8, 102u8, 117u8, 116u8, 117u8, 114u8, 101u8, 81u8, 117u8, 111u8, 116u8, 97u8, 86u8, 97u8, 108u8, 117u8, 101u8, 136u8, 1u8, 1u8, 26u8, 66u8, 10u8, 20u8, 81u8, 117u8, 111u8, 116u8, 97u8, 68u8, 105u8, 109u8, 101u8, 110u8, 115u8, 105u8, 111u8, 110u8, 115u8, 69u8, 110u8, 116u8, 114u8, 121u8, 18u8, 16u8, 10u8, 3u8, 107u8, 101u8, 121u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 3u8, 107u8, 101u8, 121u8, 18u8, 20u8, 10u8, 5u8, 118u8, 97u8, 108u8, 117u8, 101u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 5u8, 118u8, 97u8, 108u8, 117u8, 101u8, 58u8, 2u8, 56u8, 1u8, 66u8, 21u8, 10u8, 19u8, 95u8, 102u8, 117u8, 116u8, 117u8, 114u8, 101u8, 95u8, 113u8, 117u8, 111u8, 116u8, 97u8, 95u8, 118u8, 97u8, 108u8, 117u8, 101u8, 34u8, 189u8, 1u8, 10u8, 19u8, 80u8, 114u8, 101u8, 99u8, 111u8, 110u8, 100u8, 105u8, 116u8, 105u8, 111u8, 110u8, 70u8, 97u8, 105u8, 108u8, 117u8, 114u8, 101u8, 18u8, 73u8, 10u8, 10u8, 118u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 115u8, 24u8, 1u8, 32u8, 3u8, 40u8, 11u8, 50u8, 41u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 46u8, 80u8, 114u8, 101u8, 99u8, 111u8, 110u8, 100u8, 105u8, 116u8, 105u8, 111u8, 110u8, 70u8, 97u8, 105u8, 108u8, 117u8, 114u8, 101u8, 46u8, 86u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 82u8, 10u8, 118u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 115u8, 26u8, 91u8, 10u8, 9u8, 86u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 18u8, 18u8, 10u8, 4u8, 116u8, 121u8, 112u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 4u8, 116u8, 121u8, 112u8, 101u8, 18u8, 24u8, 10u8, 7u8, 115u8, 117u8, 98u8, 106u8, 101u8, 99u8, 116u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 115u8, 117u8, 98u8, 106u8, 101u8, 99u8, 116u8, 18u8, 32u8, 10u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 24u8, 3u8, 32u8, 1u8, 40u8, 9u8, 82u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 34u8, 140u8, 2u8, 10u8, 10u8, 66u8, 97u8, 100u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 80u8, 10u8, 16u8, 102u8, 105u8, 101u8, 108u8, 100u8, 95u8, 118u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 115u8, 24u8, 1u8, 32u8, 3u8, 40u8, 11u8, 50u8, 37u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 46u8, 66u8, 97u8, 100u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 46u8, 70u8, 105u8, 101u8, 108u8, 100u8, 86u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 82u8, 15u8, 102u8, 105u8, 101u8, 108u8, 100u8, 86u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 115u8, 26u8, 171u8, 1u8, 10u8, 14u8, 70u8, 105u8, 101u8, 108u8, 100u8, 86u8, 105u8, 111u8, 108u8, 97u8, 116u8, 105u8, 111u8, 110u8, 18u8, 20u8, 10u8, 5u8, 102u8, 105u8, 101u8, 108u8, 100u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 5u8, 102u8, 105u8, 101u8, 108u8, 100u8, 18u8, 32u8, 10u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 18u8, 22u8, 10u8, 6u8, 114u8, 101u8, 97u8, 115u8, 111u8, 110u8, 24u8, 3u8, 32u8, 1u8, 40u8, 9u8, 82u8, 6u8, 114u8, 101u8, 97u8, 115u8, 111u8, 110u8, 18u8, 73u8, 10u8, 17u8, 108u8, 111u8, 99u8, 97u8, 108u8, 105u8, 122u8, 101u8, 100u8, 95u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 24u8, 4u8, 32u8, 1u8, 40u8, 11u8, 50u8, 28u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 46u8, 76u8, 111u8, 99u8, 97u8, 108u8, 105u8, 122u8, 101u8, 100u8, 77u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 82u8, 16u8, 108u8, 111u8, 99u8, 97u8, 108u8, 105u8, 122u8, 101u8, 100u8, 77u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 34u8, 79u8, 10u8, 11u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 73u8, 110u8, 102u8, 111u8, 18u8, 29u8, 10u8, 10u8, 114u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 95u8, 105u8, 100u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 9u8, 114u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 73u8, 100u8, 18u8, 33u8, 10u8, 12u8, 115u8, 101u8, 114u8, 118u8, 105u8, 110u8, 103u8, 95u8, 100u8, 97u8, 116u8, 97u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 11u8, 115u8, 101u8, 114u8, 118u8, 105u8, 110u8, 103u8, 68u8, 97u8, 116u8, 97u8, 34u8, 144u8, 1u8, 10u8, 12u8, 82u8, 101u8, 115u8, 111u8, 117u8, 114u8, 99u8, 101u8, 73u8, 110u8, 102u8, 111u8, 18u8, 35u8, 10u8, 13u8, 114u8, 101u8, 115u8, 111u8, 117u8, 114u8, 99u8, 101u8, 95u8, 116u8, 121u8, 112u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 12u8, 114u8, 101u8, 115u8, 111u8, 117u8, 114u8, 99u8, 101u8, 84u8, 121u8, 112u8, 101u8, 18u8, 35u8, 10u8, 13u8, 114u8, 101u8, 115u8, 111u8, 117u8, 114u8, 99u8, 101u8, 95u8, 110u8, 97u8, 109u8, 101u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 12u8, 114u8, 101u8, 115u8, 111u8, 117u8, 114u8, 99u8, 101u8, 78u8, 97u8, 109u8, 101u8, 18u8, 20u8, 10u8, 5u8, 111u8, 119u8, 110u8, 101u8, 114u8, 24u8, 3u8, 32u8, 1u8, 40u8, 9u8, 82u8, 5u8, 111u8, 119u8, 110u8, 101u8, 114u8, 18u8, 32u8, 10u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 24u8, 4u8, 32u8, 1u8, 40u8, 9u8, 82u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 34u8, 111u8, 10u8, 4u8, 72u8, 101u8, 108u8, 112u8, 18u8, 43u8, 10u8, 5u8, 108u8, 105u8, 110u8, 107u8, 115u8, 24u8, 1u8, 32u8, 3u8, 40u8, 11u8, 50u8, 21u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 46u8, 72u8, 101u8, 108u8, 112u8, 46u8, 76u8, 105u8, 110u8, 107u8, 82u8, 5u8, 108u8, 105u8, 110u8, 107u8, 115u8, 26u8, 58u8, 10u8, 4u8, 76u8, 105u8, 110u8, 107u8, 18u8, 32u8, 10u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 11u8, 100u8, 101u8, 115u8, 99u8, 114u8, 105u8, 112u8, 116u8, 105u8, 111u8, 110u8, 18u8, 16u8, 10u8, 3u8, 117u8, 114u8, 108u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 3u8, 117u8, 114u8, 108u8, 34u8, 68u8, 10u8, 16u8, 76u8, 111u8, 99u8, 97u8, 108u8, 105u8, 122u8, 101u8, 100u8, 77u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 18u8, 22u8, 10u8, 6u8, 108u8, 111u8, 99u8, 97u8, 108u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 6u8, 108u8, 111u8, 99u8, 97u8, 108u8, 101u8, 18u8, 24u8, 10u8, 7u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 24u8, 2u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 109u8, 101u8, 115u8, 115u8, 97u8, 103u8, 101u8, 66u8, 108u8, 10u8, 14u8, 99u8, 111u8, 109u8, 46u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 114u8, 112u8, 99u8, 66u8, 17u8, 69u8, 114u8, 114u8, 111u8, 114u8, 68u8, 101u8, 116u8, 97u8, 105u8, 108u8, 115u8, 80u8, 114u8, 111u8, 116u8, 111u8, 80u8, 1u8, 90u8, 63u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 46u8, 103u8, 111u8, 108u8, 97u8, 110u8, 103u8, 46u8, 111u8, 114u8, 103u8, 47u8, 103u8, 101u8, 110u8, 112u8, 114u8, 111u8, 116u8, 111u8, 47u8, 103u8, 111u8, 111u8, 103u8, 108u8, 101u8, 97u8, 112u8, 105u8, 115u8, 47u8, 114u8, 112u8, 99u8, 47u8, 101u8, 114u8, 114u8, 100u8, 101u8, 116u8, 97u8, 105u8, 108u8, 115u8, 59u8, 101u8, 114u8, 114u8, 100u8, 101u8, 116u8, 97u8, 105u8, 108u8, 115u8, 162u8, 2u8, 3u8, 82u8, 80u8, 67u8, 98u8, 6u8, 112u8, 114u8, 111u8, 116u8, 111u8, 51u8, ]; ================================================ FILE: tonic-types/src/lib.rs ================================================ //! A collection of useful protobuf types that can be used with `tonic`. //! //! This crate also introduces the [`StatusExt`] trait and implements it in //! [`tonic::Status`], allowing the implementation of the //! [gRPC Richer Error Model] with [`tonic`] in a convenient way. //! //! # Usage //! //! Useful protobuf types are available through the [`pb`] module. They can be //! imported and worked with directly. //! //! The [`StatusExt`] trait adds associated functions to [`tonic::Status`] that //! can be used on the server side to create a status with error details, which //! can then be returned to gRPC clients. Moreover, the trait also adds methods //! to [`tonic::Status`] that can be used by a tonic client to extract error //! details, and handle them with ease. //! //! # Getting Started //! //! ```toml //! [dependencies] //! tonic = //! tonic-types = //! ``` //! //! # Examples //! //! The examples below cover a basic use case of the [gRPC Richer Error Model]. //! More complete server and client implementations are provided in the //! **Richer Error example**, located in the main repo [examples] directory. //! //! ## Server Side: Generating [`tonic::Status`] with an [`ErrorDetails`] struct //! //! ``` //! use tonic::{Code, Status}; //! use tonic_types::{ErrorDetails, StatusExt}; //! //! # async fn endpoint() -> Result, Status> { //! // ... //! // Inside a gRPC server endpoint that returns `Result, Status>` //! //! // Create empty `ErrorDetails` struct //! let mut err_details = ErrorDetails::new(); //! //! // Add error details conditionally //! # let some_condition = true; //! if some_condition { //! err_details.add_bad_request_violation( //! "field_a", //! "description of why the field_a is invalid" //! ); //! } //! //! # let other_condition = true; //! if other_condition { //! err_details.add_bad_request_violation( //! "field_b", //! "description of why the field_b is invalid", //! ); //! } //! //! // Check if any error details were set and return error status if so //! if err_details.has_bad_request_violations() { //! // Add additional error details if necessary //! err_details //! .add_help_link("description of link", "https://resource.example.local") //! .set_localized_message("en-US", "message for the user"); //! //! let status = Status::with_error_details( //! Code::InvalidArgument, //! "bad request", //! err_details, //! ); //! return Err(status); //! } //! //! // Handle valid request //! // ... //! # Ok(tonic::Response::new(())) //! # } //! ``` //! //! ## Client Side: Extracting an [`ErrorDetails`] struct from `tonic::Status` //! //! ``` //! use tonic::{Response, Status}; //! use tonic_types::StatusExt; //! //! // ... //! // Where `req_result` was returned by a gRPC client endpoint method //! fn handle_request_result(req_result: Result, Status>) { //! match req_result { //! Ok(response) => { //! // Handle successful response //! }, //! Err(status) => { //! let err_details = status.get_error_details(); //! if let Some(bad_request) = err_details.bad_request() { //! // Handle bad_request details //! } //! if let Some(help) = err_details.help() { //! // Handle help details //! } //! if let Some(localized_message) = err_details.localized_message() { //! // Handle localized_message details //! } //! } //! }; //! } //! ``` //! //! # Working with different error message types //! //! Multiple examples are provided at the [`ErrorDetails`] doc. Instructions //! about how to use the fields of the standard error message types correctly //! are provided at [error_details.proto]. //! //! # Alternative `tonic::Status` associated functions and methods //! //! In the [`StatusExt`] doc, an alternative way of interacting with //! [`tonic::Status`] is presented, using vectors of error details structs //! wrapped with the [`ErrorDetail`] enum. This approach can provide more //! control over the vector of standard error messages that will be generated or //! that was received, if necessary. To see how to adopt this approach, please //! check the [`StatusExt::with_error_details_vec`] and //! [`StatusExt::get_error_details_vec`] docs, and also the main repo's //! [Richer Error example] directory. //! //! Besides that, multiple examples with alternative error details extraction //! methods are provided in the [`StatusExt`] doc, which can be specially //! useful if only one type of standard error message is being handled by the //! client. For example, using [`StatusExt::get_details_bad_request`] is a //! more direct way of extracting a [`BadRequest`] error message from //! [`tonic::Status`]. //! //! [`tonic::Status`]: https://docs.rs/tonic/latest/tonic/struct.Status.html //! [`tonic`]: https://docs.rs/tonic/latest/tonic/ //! [gRPC Richer Error Model]: https://www.grpc.io/docs/guides/error/ //! [examples]: https://github.com/hyperium/tonic/tree/master/examples //! [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto //! [Richer Error example]: https://github.com/hyperium/tonic/tree/master/examples/src/richer-error #![doc( html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" )] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] mod generated { #![allow(unreachable_pub)] #![allow(rustdoc::invalid_html_tags)] #[rustfmt::skip] pub mod google_rpc; #[rustfmt::skip] pub mod types_fds; pub use types_fds::FILE_DESCRIPTOR_SET; #[cfg(test)] mod tests { use super::FILE_DESCRIPTOR_SET; use prost::Message as _; #[test] fn file_descriptor_set_is_valid() { prost_types::FileDescriptorSet::decode(FILE_DESCRIPTOR_SET).unwrap(); } } } /// Useful protobuf types pub mod pb { pub use crate::generated::{FILE_DESCRIPTOR_SET, google_rpc::*}; } pub use pb::Status; mod richer_error; pub use richer_error::{ BadRequest, DebugInfo, ErrorDetail, ErrorDetails, ErrorInfo, FieldViolation, Help, HelpLink, LocalizedMessage, PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, ResourceInfo, RetryInfo, RpcStatusExt, StatusExt, }; mod sealed { pub trait Sealed {} } ================================================ FILE: tonic-types/src/richer_error/error_details/mod.rs ================================================ use std::{collections::HashMap, time}; use super::std_messages::{ BadRequest, DebugInfo, ErrorInfo, FieldViolation, Help, HelpLink, LocalizedMessage, PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, ResourceInfo, RetryInfo, }; pub(crate) mod vec; /// Groups the standard error messages structs. Provides associated /// functions and methods to setup and edit each error message independently. /// Used when extracting error details from `tonic::Status`, and when /// creating a `tonic::Status` with error details. #[non_exhaustive] #[derive(Clone, Debug, Default)] pub struct ErrorDetails { /// This field stores [`RetryInfo`] data, if any. pub(crate) retry_info: Option, /// This field stores [`DebugInfo`] data, if any. pub(crate) debug_info: Option, /// This field stores [`QuotaFailure`] data, if any. pub(crate) quota_failure: Option, /// This field stores [`ErrorInfo`] data, if any. pub(crate) error_info: Option, /// This field stores [`PreconditionFailure`] data, if any. pub(crate) precondition_failure: Option, /// This field stores [`BadRequest`] data, if any. pub(crate) bad_request: Option, /// This field stores [`RequestInfo`] data, if any. pub(crate) request_info: Option, /// This field stores [`ResourceInfo`] data, if any. pub(crate) resource_info: Option, /// This field stores [`Help`] data, if any. pub(crate) help: Option, /// This field stores [`LocalizedMessage`] data, if any. pub(crate) localized_message: Option, } impl ErrorDetails { /// Generates an [`ErrorDetails`] struct with all fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let err_details = ErrorDetails::new(); /// ``` pub fn new() -> Self { Self::default() } /// Generates an [`ErrorDetails`] struct with [`RetryInfo`] details and /// remaining fields set to `None`. /// /// # Examples /// /// ``` /// use std::time::Duration; /// use tonic_types::ErrorDetails; /// /// let err_details = ErrorDetails::with_retry_info(Some(Duration::from_secs(5))); /// ``` pub fn with_retry_info(retry_delay: Option) -> Self { ErrorDetails { retry_info: Some(RetryInfo::new(retry_delay)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`DebugInfo`] details and /// remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let err_stack = vec!["...".into(), "...".into()]; /// /// let err_details = ErrorDetails::with_debug_info(err_stack, "error details"); /// ``` pub fn with_debug_info( stack_entries: impl Into>, detail: impl Into, ) -> Self { ErrorDetails { debug_info: Some(DebugInfo::new(stack_entries, detail)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`QuotaFailure`] details and /// remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::{ErrorDetails, QuotaViolation}; /// /// let err_details = ErrorDetails::with_quota_failure(vec![ /// QuotaViolation::new("subject 1", "description 1"), /// QuotaViolation::new("subject 2", "description 2"), /// ]); /// ``` pub fn with_quota_failure(violations: impl Into>) -> Self { ErrorDetails { quota_failure: Some(QuotaFailure::new(violations)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`QuotaFailure`] details (one /// [`QuotaViolation`] set) and remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let err_details = ErrorDetails::with_quota_failure_violation("subject", "description"); /// ``` pub fn with_quota_failure_violation( subject: impl Into, description: impl Into, ) -> Self { ErrorDetails { quota_failure: Some(QuotaFailure::with_violation(subject, description)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`ErrorInfo`] details and /// remaining fields set to `None`. /// /// # Examples /// /// ``` /// use std::collections::HashMap; /// use tonic_types::ErrorDetails; /// /// let mut metadata: HashMap = HashMap::new(); /// metadata.insert("instanceLimitPerRequest".into(), "100".into()); /// /// let err_details = ErrorDetails::with_error_info("reason", "domain", metadata); /// ``` pub fn with_error_info( reason: impl Into, domain: impl Into, metadata: impl Into>, ) -> Self { ErrorDetails { error_info: Some(ErrorInfo::new(reason, domain, metadata)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`PreconditionFailure`] /// details and remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::{ErrorDetails, PreconditionViolation}; /// /// let err_details = ErrorDetails::with_precondition_failure(vec![ /// PreconditionViolation::new( /// "violation type 1", /// "subject 1", /// "description 1", /// ), /// PreconditionViolation::new( /// "violation type 2", /// "subject 2", /// "description 2", /// ), /// ]); /// ``` pub fn with_precondition_failure(violations: impl Into>) -> Self { ErrorDetails { precondition_failure: Some(PreconditionFailure::new(violations)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`PreconditionFailure`] /// details (one [`PreconditionViolation`] set) and remaining fields set to /// `None`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let err_details = ErrorDetails::with_precondition_failure_violation( /// "violation type", /// "subject", /// "description", /// ); /// ``` pub fn with_precondition_failure_violation( violation_type: impl Into, subject: impl Into, description: impl Into, ) -> Self { ErrorDetails { precondition_failure: Some(PreconditionFailure::with_violation( violation_type, subject, description, )), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`BadRequest`] details and /// remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::{ErrorDetails, FieldViolation}; /// /// let err_details = ErrorDetails::with_bad_request(vec![ /// FieldViolation::new("field_1", "description 1"), /// FieldViolation::new("field_2", "description 2"), /// ]); /// ``` pub fn with_bad_request(field_violations: impl Into>) -> Self { ErrorDetails { bad_request: Some(BadRequest::new(field_violations)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`BadRequest`] details (one /// [`FieldViolation`] set) and remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let err_details = ErrorDetails::with_bad_request_violation( /// "field", /// "description", /// ); /// ``` pub fn with_bad_request_violation( field: impl Into, description: impl Into, ) -> Self { ErrorDetails { bad_request: Some(BadRequest::with_violation(field, description)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`RequestInfo`] details and /// remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let err_details = ErrorDetails::with_request_info( /// "request_id", /// "serving_data", /// ); /// ``` pub fn with_request_info( request_id: impl Into, serving_data: impl Into, ) -> Self { ErrorDetails { request_info: Some(RequestInfo::new(request_id, serving_data)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`ResourceInfo`] details and /// remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let err_details = ErrorDetails::with_resource_info( /// "res_type", /// "res_name", /// "owner", /// "description", /// ); /// ``` pub fn with_resource_info( resource_type: impl Into, resource_name: impl Into, owner: impl Into, description: impl Into, ) -> Self { ErrorDetails { resource_info: Some(ResourceInfo::new( resource_type, resource_name, owner, description, )), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`Help`] details and /// remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::{ErrorDetails, HelpLink}; /// /// let err_details = ErrorDetails::with_help(vec![ /// HelpLink::new("description of link a", "resource-a.example.local"), /// HelpLink::new("description of link b", "resource-b.example.local"), /// ]); /// ``` pub fn with_help(links: impl Into>) -> Self { ErrorDetails { help: Some(Help::new(links)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`Help`] details (one /// [`HelpLink`] set) and remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let err_details = ErrorDetails::with_help_link( /// "description of link a", /// "resource-a.example.local" /// ); /// ``` pub fn with_help_link(description: impl Into, url: impl Into) -> Self { ErrorDetails { help: Some(Help::with_link(description, url)), ..ErrorDetails::new() } } /// Generates an [`ErrorDetails`] struct with [`LocalizedMessage`] details /// and remaining fields set to `None`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let err_details = ErrorDetails::with_localized_message( /// "en-US", /// "message for the user" /// ); /// ``` pub fn with_localized_message(locale: impl Into, message: impl Into) -> Self { ErrorDetails { localized_message: Some(LocalizedMessage::new(locale, message)), ..ErrorDetails::new() } } /// Get [`RetryInfo`] details, if any. pub fn retry_info(&self) -> Option<&RetryInfo> { self.retry_info.as_ref() } /// Get [`DebugInfo`] details, if any. pub fn debug_info(&self) -> Option<&DebugInfo> { self.debug_info.as_ref() } /// Get [`QuotaFailure`] details, if any. pub fn quota_failure(&self) -> Option<&QuotaFailure> { self.quota_failure.as_ref() } /// Get [`ErrorInfo`] details, if any. pub fn error_info(&self) -> Option<&ErrorInfo> { self.error_info.as_ref() } /// Get [`PreconditionFailure`] details, if any. pub fn precondition_failure(&self) -> Option<&PreconditionFailure> { self.precondition_failure.as_ref() } /// Get [`BadRequest`] details, if any. pub fn bad_request(&self) -> Option<&BadRequest> { self.bad_request.as_ref() } /// Get [`RequestInfo`] details, if any. pub fn request_info(&self) -> Option<&RequestInfo> { self.request_info.as_ref() } /// Get [`ResourceInfo`] details, if any. pub fn resource_info(&self) -> Option<&ResourceInfo> { self.resource_info.as_ref() } /// Get [`Help`] details, if any. pub fn help(&self) -> Option<&Help> { self.help.as_ref() } /// Get [`LocalizedMessage`] details, if any. pub fn localized_message(&self) -> Option<&LocalizedMessage> { self.localized_message.as_ref() } /// Set [`RetryInfo`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use std::time::Duration; /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.set_retry_info(Some(Duration::from_secs(5))); /// ``` pub fn set_retry_info(&mut self, retry_delay: Option) -> &mut Self { self.retry_info = Some(RetryInfo::new(retry_delay)); self } /// Set [`DebugInfo`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// let err_stack = vec!["...".into(), "...".into()]; /// /// err_details.set_debug_info(err_stack, "error details"); /// ``` pub fn set_debug_info( &mut self, stack_entries: impl Into>, detail: impl Into, ) -> &mut Self { self.debug_info = Some(DebugInfo::new(stack_entries, detail)); self } /// Set [`QuotaFailure`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::{ErrorDetails, QuotaViolation}; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.set_quota_failure(vec![ /// QuotaViolation::new("subject 1", "description 1"), /// QuotaViolation::new("subject 2", "description 2"), /// ]); /// ``` pub fn set_quota_failure(&mut self, violations: impl Into>) -> &mut Self { self.quota_failure = Some(QuotaFailure::new(violations)); self } /// Adds a [`QuotaViolation`] to [`QuotaFailure`] details. Sets /// [`QuotaFailure`] details if it is not set yet. Can be chained with /// other `.set_` and `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.add_quota_failure_violation("subject", "description"); /// ``` pub fn add_quota_failure_violation( &mut self, subject: impl Into, description: impl Into, ) -> &mut Self { match &mut self.quota_failure { Some(quota_failure) => { quota_failure.add_violation(subject, description); } None => { self.quota_failure = Some(QuotaFailure::with_violation(subject, description)); } }; self } /// Returns `true` if [`QuotaFailure`] is set and its `violations` vector /// is not empty, otherwise returns `false`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::with_quota_failure(vec![]); /// /// assert_eq!(err_details.has_quota_failure_violations(), false); /// /// err_details.add_quota_failure_violation("subject", "description"); /// /// assert_eq!(err_details.has_quota_failure_violations(), true); /// ``` pub fn has_quota_failure_violations(&self) -> bool { if let Some(quota_failure) = &self.quota_failure { return !quota_failure.violations.is_empty(); } false } /// Set [`ErrorInfo`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use std::collections::HashMap; /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// let mut metadata: HashMap = HashMap::new(); /// metadata.insert("instanceLimitPerRequest".into(), "100".into()); /// /// err_details.set_error_info("reason", "example.local", metadata); /// ``` pub fn set_error_info( &mut self, reason: impl Into, domain: impl Into, metadata: impl Into>, ) -> &mut Self { self.error_info = Some(ErrorInfo::new(reason, domain, metadata)); self } /// Set [`PreconditionFailure`] details. Can be chained with other `.set_` /// and `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::{ErrorDetails, PreconditionViolation}; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.set_precondition_failure(vec![ /// PreconditionViolation::new( /// "violation type 1", /// "subject 1", /// "description 1", /// ), /// PreconditionViolation::new( /// "violation type 2", /// "subject 2", /// "description 2", /// ), /// ]); /// ``` pub fn set_precondition_failure( &mut self, violations: impl Into>, ) -> &mut Self { self.precondition_failure = Some(PreconditionFailure::new(violations)); self } /// Adds a [`PreconditionViolation`] to [`PreconditionFailure`] details. /// Sets [`PreconditionFailure`] details if it is not set yet. Can be /// chained with other `.set_` and `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.add_precondition_failure_violation( /// "violation type", /// "subject", /// "description" /// ); /// ``` pub fn add_precondition_failure_violation( &mut self, violation_type: impl Into, subject: impl Into, description: impl Into, ) -> &mut Self { match &mut self.precondition_failure { Some(precondition_failure) => { precondition_failure.add_violation(violation_type, subject, description); } None => { self.precondition_failure = Some(PreconditionFailure::with_violation( violation_type, subject, description, )); } }; self } /// Returns `true` if [`PreconditionFailure`] is set and its `violations` /// vector is not empty, otherwise returns `false`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::with_precondition_failure(vec![]); /// /// assert_eq!(err_details.has_precondition_failure_violations(), false); /// /// err_details.add_precondition_failure_violation( /// "violation type", /// "subject", /// "description" /// ); /// /// assert_eq!(err_details.has_precondition_failure_violations(), true); /// ``` pub fn has_precondition_failure_violations(&self) -> bool { if let Some(precondition_failure) = &self.precondition_failure { return !precondition_failure.violations.is_empty(); } false } /// Set [`BadRequest`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::{ErrorDetails, FieldViolation}; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.set_bad_request(vec![ /// FieldViolation::new("field_1", "description 1"), /// FieldViolation::new("field_2", "description 2"), /// ]); /// ``` pub fn set_bad_request(&mut self, violations: impl Into>) -> &mut Self { self.bad_request = Some(BadRequest::new(violations)); self } /// Adds a [`FieldViolation`] to [`BadRequest`] details. Sets /// [`BadRequest`] details if it is not set yet. Can be chained with other /// `.set_` and `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.add_bad_request_violation("field", "description"); /// ``` pub fn add_bad_request_violation( &mut self, field: impl Into, description: impl Into, ) -> &mut Self { match &mut self.bad_request { Some(bad_request) => { bad_request.add_violation(field, description); } None => { self.bad_request = Some(BadRequest::with_violation(field, description)); } }; self } /// Returns `true` if [`BadRequest`] is set and its `field_violations` /// vector is not empty, otherwise returns `false`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::with_bad_request(vec![]); /// /// assert_eq!(err_details.has_bad_request_violations(), false); /// /// err_details.add_bad_request_violation("field", "description"); /// /// assert_eq!(err_details.has_bad_request_violations(), true); /// ``` pub fn has_bad_request_violations(&self) -> bool { if let Some(bad_request) = &self.bad_request { return !bad_request.field_violations.is_empty(); } false } /// Set [`RequestInfo`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.set_request_info("request_id", "serving_data"); /// ``` pub fn set_request_info( &mut self, request_id: impl Into, serving_data: impl Into, ) -> &mut Self { self.request_info = Some(RequestInfo::new(request_id, serving_data)); self } /// Set [`ResourceInfo`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.set_resource_info("res_type", "res_name", "owner", "description"); /// ``` pub fn set_resource_info( &mut self, resource_type: impl Into, resource_name: impl Into, owner: impl Into, description: impl Into, ) -> &mut Self { self.resource_info = Some(ResourceInfo::new( resource_type, resource_name, owner, description, )); self } /// Set [`Help`] details. Can be chained with other `.set_` and `.add_` /// [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::{ErrorDetails, HelpLink}; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.set_help(vec![ /// HelpLink::new("description of link a", "resource-a.example.local"), /// HelpLink::new("description of link b", "resource-b.example.local"), /// ]); /// ``` pub fn set_help(&mut self, links: impl Into>) -> &mut Self { self.help = Some(Help::new(links)); self } /// Adds a [`HelpLink`] to [`Help`] details. Sets [`Help`] details if it is /// not set yet. Can be chained with other `.set_` and `.add_` /// [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.add_help_link("description of link", "resource.example.local"); /// ``` pub fn add_help_link( &mut self, description: impl Into, url: impl Into, ) -> &mut Self { match &mut self.help { Some(help) => { help.add_link(description, url); } None => { self.help = Some(Help::with_link(description, url)); } }; self } /// Returns `true` if [`Help`] is set and its `links` vector is not empty, /// otherwise returns `false`. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::with_help(vec![]); /// /// assert_eq!(err_details.has_help_links(), false); /// /// err_details.add_help_link("description of link", "resource.example.local"); /// /// assert_eq!(err_details.has_help_links(), true); /// ``` pub fn has_help_links(&self) -> bool { if let Some(help) = &self.help { return !help.links.is_empty(); } false } /// Set [`LocalizedMessage`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// /// # Examples /// /// ``` /// use tonic_types::ErrorDetails; /// /// let mut err_details = ErrorDetails::new(); /// /// err_details.set_localized_message("en-US", "message for the user"); /// ``` pub fn set_localized_message( &mut self, locale: impl Into, message: impl Into, ) -> &mut Self { self.localized_message = Some(LocalizedMessage::new(locale, message)); self } } ================================================ FILE: tonic-types/src/richer_error/error_details/vec.rs ================================================ use super::super::std_messages::{ BadRequest, DebugInfo, ErrorInfo, Help, LocalizedMessage, PreconditionFailure, QuotaFailure, RequestInfo, ResourceInfo, RetryInfo, }; /// Wraps the structs corresponding to the standard error messages, allowing /// the implementation and handling of vectors containing any of them. #[non_exhaustive] #[derive(Clone, Debug)] pub enum ErrorDetail { /// Wraps the [`RetryInfo`] struct. RetryInfo(RetryInfo), /// Wraps the [`DebugInfo`] struct. DebugInfo(DebugInfo), /// Wraps the [`QuotaFailure`] struct. QuotaFailure(QuotaFailure), /// Wraps the [`ErrorInfo`] struct. ErrorInfo(ErrorInfo), /// Wraps the [`PreconditionFailure`] struct. PreconditionFailure(PreconditionFailure), /// Wraps the [`BadRequest`] struct. BadRequest(BadRequest), /// Wraps the [`RequestInfo`] struct. RequestInfo(RequestInfo), /// Wraps the [`ResourceInfo`] struct. ResourceInfo(ResourceInfo), /// Wraps the [`Help`] struct. Help(Help), /// Wraps the [`LocalizedMessage`] struct. LocalizedMessage(LocalizedMessage), } impl From for ErrorDetail { fn from(err_detail: RetryInfo) -> Self { ErrorDetail::RetryInfo(err_detail) } } impl From for ErrorDetail { fn from(err_detail: DebugInfo) -> Self { ErrorDetail::DebugInfo(err_detail) } } impl From for ErrorDetail { fn from(err_detail: QuotaFailure) -> Self { ErrorDetail::QuotaFailure(err_detail) } } impl From for ErrorDetail { fn from(err_detail: ErrorInfo) -> Self { ErrorDetail::ErrorInfo(err_detail) } } impl From for ErrorDetail { fn from(err_detail: PreconditionFailure) -> Self { ErrorDetail::PreconditionFailure(err_detail) } } impl From for ErrorDetail { fn from(err_detail: BadRequest) -> Self { ErrorDetail::BadRequest(err_detail) } } impl From for ErrorDetail { fn from(err_detail: RequestInfo) -> Self { ErrorDetail::RequestInfo(err_detail) } } impl From for ErrorDetail { fn from(err_detail: ResourceInfo) -> Self { ErrorDetail::ResourceInfo(err_detail) } } impl From for ErrorDetail { fn from(err_detail: Help) -> Self { ErrorDetail::Help(err_detail) } } impl From for ErrorDetail { fn from(err_detail: LocalizedMessage) -> Self { ErrorDetail::LocalizedMessage(err_detail) } } ================================================ FILE: tonic-types/src/richer_error/mod.rs ================================================ use prost::{ DecodeError, Message, bytes::{Bytes, BytesMut}, }; use prost_types::Any; use tonic::{Code, metadata::MetadataMap}; mod error_details; mod std_messages; use super::pb; pub use error_details::{ErrorDetails, vec::ErrorDetail}; pub use std_messages::{ BadRequest, DebugInfo, ErrorInfo, FieldViolation, Help, HelpLink, LocalizedMessage, PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, ResourceInfo, RetryInfo, }; trait IntoAny { fn into_any(self) -> Any; } #[allow(dead_code)] trait FromAny { fn from_any(any: Any) -> Result where Self: Sized; } trait FromAnyRef { fn from_any_ref(any: &Any) -> Result where Self: Sized; } fn gen_details_bytes(code: Code, message: &str, details: Vec) -> Bytes { let status = pb::Status { code: code as i32, message: message.to_owned(), details, }; let mut buf = BytesMut::with_capacity(status.encoded_len()); // Should never panic since `buf` is initialized with sufficient capacity status.encode(&mut buf).unwrap(); buf.freeze() } /// Used to implement associated functions and methods on `tonic::Status`, that /// allow the addition and extraction of standard error details. This trait is /// sealed and not meant to be implemented outside of `tonic-types`. pub trait StatusExt: crate::sealed::Sealed { /// Generates a `tonic::Status` with error details obtained from an /// [`ErrorDetails`] struct, and custom metadata. /// /// # Examples /// /// ``` /// use tonic::{metadata::MetadataMap, Code, Status}; /// use tonic_types::{ErrorDetails, StatusExt}; /// /// let status = Status::with_error_details_and_metadata( /// Code::InvalidArgument, /// "bad request", /// ErrorDetails::with_bad_request_violation("field", "description"), /// MetadataMap::new() /// ); /// ``` fn with_error_details_and_metadata( code: Code, message: impl Into, details: ErrorDetails, metadata: MetadataMap, ) -> tonic::Status; /// Generates a `tonic::Status` with error details obtained from an /// [`ErrorDetails`] struct. /// /// # Examples /// /// ``` /// use tonic::{Code, Status}; /// use tonic_types::{ErrorDetails, StatusExt}; /// /// let status = Status::with_error_details( /// Code::InvalidArgument, /// "bad request", /// ErrorDetails::with_bad_request_violation("field", "description"), /// ); /// ``` fn with_error_details( code: Code, message: impl Into, details: ErrorDetails, ) -> tonic::Status; /// Generates a `tonic::Status` with error details provided in a vector of /// [`ErrorDetail`] enums, and custom metadata. /// /// # Examples /// /// ``` /// use tonic::{metadata::MetadataMap, Code, Status}; /// use tonic_types::{BadRequest, StatusExt}; /// /// let status = Status::with_error_details_vec_and_metadata( /// Code::InvalidArgument, /// "bad request", /// vec![ /// BadRequest::with_violation("field", "description").into(), /// ], /// MetadataMap::new() /// ); /// ``` fn with_error_details_vec_and_metadata( code: Code, message: impl Into, details: impl IntoIterator, metadata: MetadataMap, ) -> tonic::Status; /// Generates a `tonic::Status` with error details provided in a vector of /// [`ErrorDetail`] enums. /// /// # Examples /// /// ``` /// use tonic::{Code, Status}; /// use tonic_types::{BadRequest, StatusExt}; /// /// let status = Status::with_error_details_vec( /// Code::InvalidArgument, /// "bad request", /// vec![ /// BadRequest::with_violation("field", "description").into(), /// ] /// ); /// ``` fn with_error_details_vec( code: Code, message: impl Into, details: impl IntoIterator, ) -> tonic::Status; /// Can be used to check if the error details contained in `tonic::Status` /// are malformed or not. Tries to get an [`ErrorDetails`] struct from a /// `tonic::Status`. If some `prost::DecodeError` occurs, it will be /// returned. If not debugging, consider using /// [`StatusExt::get_error_details`] or /// [`StatusExt::get_error_details_vec`]. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => match status.check_error_details() { /// Ok(err_details) => { /// // Handle extracted details /// } /// Err(decode_error) => { /// // Handle decode_error /// } /// } /// }; /// } /// ``` fn check_error_details(&self) -> Result; /// Get an [`ErrorDetails`] struct from `tonic::Status`. If some /// `prost::DecodeError` occurs, an empty [`ErrorDetails`] struct will be /// returned. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// let err_details = status.get_error_details(); /// if let Some(bad_request) = err_details.bad_request() { /// // Handle bad_request details /// } /// // ... /// } /// }; /// } /// ``` fn get_error_details(&self) -> ErrorDetails; /// Can be used to check if the error details contained in `tonic::Status` /// are malformed or not. Tries to get a vector of [`ErrorDetail`] enums /// from a `tonic::Status`. If some `prost::DecodeError` occurs, it will be /// returned. If not debugging, consider using /// [`StatusExt::get_error_details_vec`] or /// [`StatusExt::get_error_details`]. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => match status.check_error_details_vec() { /// Ok(err_details) => { /// // Handle extracted details /// } /// Err(decode_error) => { /// // Handle decode_error /// } /// } /// }; /// } /// ``` fn check_error_details_vec(&self) -> Result, DecodeError>; /// Get a vector of [`ErrorDetail`] enums from `tonic::Status`. If some /// `prost::DecodeError` occurs, an empty vector will be returned. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::{ErrorDetail, StatusExt}; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// let err_details = status.get_error_details_vec(); /// for err_detail in err_details.iter() { /// match err_detail { /// ErrorDetail::BadRequest(bad_request) => { /// // Handle bad_request details /// } /// // ... /// _ => {} /// } /// } /// } /// }; /// } /// ``` fn get_error_details_vec(&self) -> Vec; /// Get first [`RetryInfo`] details found on `tonic::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(retry_info) = status.get_details_retry_info() { /// // Handle retry_info details /// } /// } /// }; /// } /// ``` fn get_details_retry_info(&self) -> Option; /// Get first [`DebugInfo`] details found on `tonic::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(debug_info) = status.get_details_debug_info() { /// // Handle debug_info details /// } /// } /// }; /// } /// ``` fn get_details_debug_info(&self) -> Option; /// Get first [`QuotaFailure`] details found on `tonic::Status`, if any. /// If some `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(quota_failure) = status.get_details_quota_failure() { /// // Handle quota_failure details /// } /// } /// }; /// } /// ``` fn get_details_quota_failure(&self) -> Option; /// Get first [`ErrorInfo`] details found on `tonic::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(error_info) = status.get_details_error_info() { /// // Handle error_info details /// } /// } /// }; /// } /// ``` fn get_details_error_info(&self) -> Option; /// Get first [`PreconditionFailure`] details found on `tonic::Status`, /// if any. If some `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(precondition_failure) = status.get_details_precondition_failure() { /// // Handle precondition_failure details /// } /// } /// }; /// } /// ``` fn get_details_precondition_failure(&self) -> Option; /// Get first [`BadRequest`] details found on `tonic::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(bad_request) = status.get_details_bad_request() { /// // Handle bad_request details /// } /// } /// }; /// } /// ``` fn get_details_bad_request(&self) -> Option; /// Get first [`RequestInfo`] details found on `tonic::Status`, if any. /// If some `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(request_info) = status.get_details_request_info() { /// // Handle request_info details /// } /// } /// }; /// } /// ``` fn get_details_request_info(&self) -> Option; /// Get first [`ResourceInfo`] details found on `tonic::Status`, if any. /// If some `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(resource_info) = status.get_details_resource_info() { /// // Handle resource_info details /// } /// } /// }; /// } /// ``` fn get_details_resource_info(&self) -> Option; /// Get first [`Help`] details found on `tonic::Status`, if any. If some /// `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(help) = status.get_details_help() { /// // Handle help details /// } /// } /// }; /// } /// ``` fn get_details_help(&self) -> Option; /// Get first [`LocalizedMessage`] details found on `tonic::Status`, if /// any. If some `prost::DecodeError` occurs, returns `None`. /// /// # Examples /// /// ``` /// use tonic::{Status, Response}; /// use tonic_types::StatusExt; /// /// fn handle_request_result(req_result: Result, Status>) { /// match req_result { /// Ok(_) => {}, /// Err(status) => { /// if let Some(localized_message) = status.get_details_localized_message() { /// // Handle localized_message details /// } /// } /// }; /// } /// ``` fn get_details_localized_message(&self) -> Option; } impl crate::sealed::Sealed for tonic::Status {} impl StatusExt for tonic::Status { fn with_error_details_and_metadata( code: Code, message: impl Into, details: ErrorDetails, metadata: MetadataMap, ) -> Self { let message: String = message.into(); let mut conv_details: Vec = Vec::with_capacity(10); if let Some(retry_info) = details.retry_info { conv_details.push(retry_info.into_any()); } if let Some(debug_info) = details.debug_info { conv_details.push(debug_info.into_any()); } if let Some(quota_failure) = details.quota_failure { conv_details.push(quota_failure.into_any()); } if let Some(error_info) = details.error_info { conv_details.push(error_info.into_any()); } if let Some(precondition_failure) = details.precondition_failure { conv_details.push(precondition_failure.into_any()); } if let Some(bad_request) = details.bad_request { conv_details.push(bad_request.into_any()); } if let Some(request_info) = details.request_info { conv_details.push(request_info.into_any()); } if let Some(resource_info) = details.resource_info { conv_details.push(resource_info.into_any()); } if let Some(help) = details.help { conv_details.push(help.into_any()); } if let Some(localized_message) = details.localized_message { conv_details.push(localized_message.into_any()); } let details = gen_details_bytes(code, &message, conv_details); tonic::Status::with_details_and_metadata(code, message, details, metadata) } fn with_error_details(code: Code, message: impl Into, details: ErrorDetails) -> Self { tonic::Status::with_error_details_and_metadata(code, message, details, MetadataMap::new()) } fn with_error_details_vec_and_metadata( code: Code, message: impl Into, details: impl IntoIterator, metadata: MetadataMap, ) -> Self { let message: String = message.into(); let mut conv_details: Vec = Vec::new(); for error_detail in details.into_iter() { match error_detail { ErrorDetail::RetryInfo(retry_info) => { conv_details.push(retry_info.into_any()); } ErrorDetail::DebugInfo(debug_info) => { conv_details.push(debug_info.into_any()); } ErrorDetail::QuotaFailure(quota_failure) => { conv_details.push(quota_failure.into_any()); } ErrorDetail::ErrorInfo(error_info) => { conv_details.push(error_info.into_any()); } ErrorDetail::PreconditionFailure(prec_failure) => { conv_details.push(prec_failure.into_any()); } ErrorDetail::BadRequest(bad_req) => { conv_details.push(bad_req.into_any()); } ErrorDetail::RequestInfo(req_info) => { conv_details.push(req_info.into_any()); } ErrorDetail::ResourceInfo(res_info) => { conv_details.push(res_info.into_any()); } ErrorDetail::Help(help) => { conv_details.push(help.into_any()); } ErrorDetail::LocalizedMessage(loc_message) => { conv_details.push(loc_message.into_any()); } } } let details = gen_details_bytes(code, &message, conv_details); tonic::Status::with_details_and_metadata(code, message, details, metadata) } fn with_error_details_vec( code: Code, message: impl Into, details: impl IntoIterator, ) -> Self { tonic::Status::with_error_details_vec_and_metadata( code, message, details, MetadataMap::new(), ) } fn check_error_details(&self) -> Result { let status = pb::Status::decode(self.details())?; status.check_error_details() } fn get_error_details(&self) -> ErrorDetails { self.check_error_details().unwrap_or_default() } fn check_error_details_vec(&self) -> Result, DecodeError> { let status = pb::Status::decode(self.details())?; status.check_error_details_vec() } fn get_error_details_vec(&self) -> Vec { self.check_error_details_vec().unwrap_or_default() } fn get_details_retry_info(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_retry_info() } fn get_details_debug_info(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_debug_info() } fn get_details_quota_failure(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_quota_failure() } fn get_details_error_info(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_error_info() } fn get_details_precondition_failure(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_precondition_failure() } fn get_details_bad_request(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_bad_request() } fn get_details_request_info(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_request_info() } fn get_details_resource_info(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_resource_info() } fn get_details_help(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_help() } fn get_details_localized_message(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; status.get_details_localized_message() } } impl crate::sealed::Sealed for pb::Status {} /// Used to implement associated functions and methods on `pb::Status`, that /// allow the extraction of standard error details. This trait is /// sealed and not meant to be implemented outside of `tonic-types`. pub trait RpcStatusExt: crate::sealed::Sealed { /// Can be used to check if the error details contained in `pb::Status` /// are malformed or not. Tries to get an [`ErrorDetails`] struct from a /// `pb::Status`. If some `prost::DecodeError` occurs, it will be /// returned. If not debugging, consider using /// [`RpcStatusExt::get_error_details`] or /// [`RpcStatusExt::get_error_details_vec`]. fn check_error_details(&self) -> Result; /// Get an [`ErrorDetails`] struct from `pb::Status`. If some /// `prost::DecodeError` occurs, an empty [`ErrorDetails`] struct will be /// returned. fn get_error_details(&self) -> ErrorDetails; /// Can be used to check if the error details contained in `pb::Status` /// are malformed or not. Tries to get a vector of [`ErrorDetail`] enums /// from a `pb::Status`. If some `prost::DecodeError` occurs, it will be /// returned. If not debugging, consider using /// [`StatusExt::get_error_details_vec`] or /// [`StatusExt::get_error_details`]. fn check_error_details_vec(&self) -> Result, DecodeError>; /// Get a vector of [`ErrorDetail`] enums from `pb::Status`. If some /// `prost::DecodeError` occurs, an empty vector will be returned. fn get_error_details_vec(&self) -> Vec; /// Get first [`RetryInfo`] details found on `pb::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. fn get_details_retry_info(&self) -> Option; /// Get first [`DebugInfo`] details found on `pb::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. fn get_details_debug_info(&self) -> Option; /// Get first [`QuotaFailure`] details found on `pb::Status`, if any. /// If some `prost::DecodeError` occurs, returns `None`. fn get_details_quota_failure(&self) -> Option; /// Get first [`ErrorInfo`] details found on `pb::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. fn get_details_error_info(&self) -> Option; /// Get first [`PreconditionFailure`] details found on `pb::Status`, /// if any. If some `prost::DecodeError` occurs, returns `None`. fn get_details_precondition_failure(&self) -> Option; /// Get first [`BadRequest`] details found on `pb::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. fn get_details_bad_request(&self) -> Option; /// Get first [`RequestInfo`] details found on `pb::Status`, if any. /// If some `prost::DecodeError` occurs, returns `None`. fn get_details_request_info(&self) -> Option; /// Get first [`ResourceInfo`] details found on `pb::Status`, if any. /// If some `prost::DecodeError` occurs, returns `None`. fn get_details_resource_info(&self) -> Option; /// Get first [`Help`] details found on `pb::Status`, if any. If some /// `prost::DecodeError` occurs, returns `None`. fn get_details_help(&self) -> Option; /// Get first [`LocalizedMessage`] details found on `pb::Status`, if /// any. If some `prost::DecodeError` occurs, returns `None`. fn get_details_localized_message(&self) -> Option; } impl RpcStatusExt for pb::Status { fn check_error_details(&self) -> Result { let mut details = ErrorDetails::new(); for any in self.details.iter() { match any.type_url.as_str() { RetryInfo::TYPE_URL => { details.retry_info = Some(RetryInfo::from_any_ref(any)?); } DebugInfo::TYPE_URL => { details.debug_info = Some(DebugInfo::from_any_ref(any)?); } QuotaFailure::TYPE_URL => { details.quota_failure = Some(QuotaFailure::from_any_ref(any)?); } ErrorInfo::TYPE_URL => { details.error_info = Some(ErrorInfo::from_any_ref(any)?); } PreconditionFailure::TYPE_URL => { details.precondition_failure = Some(PreconditionFailure::from_any_ref(any)?); } BadRequest::TYPE_URL => { details.bad_request = Some(BadRequest::from_any_ref(any)?); } RequestInfo::TYPE_URL => { details.request_info = Some(RequestInfo::from_any_ref(any)?); } ResourceInfo::TYPE_URL => { details.resource_info = Some(ResourceInfo::from_any_ref(any)?); } Help::TYPE_URL => { details.help = Some(Help::from_any_ref(any)?); } LocalizedMessage::TYPE_URL => { details.localized_message = Some(LocalizedMessage::from_any_ref(any)?); } _ => {} } } Ok(details) } fn get_error_details(&self) -> ErrorDetails { self.check_error_details().unwrap_or_default() } fn check_error_details_vec(&self) -> Result, DecodeError> { let mut details: Vec = Vec::with_capacity(self.details.len()); for any in self.details.iter() { match any.type_url.as_str() { RetryInfo::TYPE_URL => { details.push(RetryInfo::from_any_ref(any)?.into()); } DebugInfo::TYPE_URL => { details.push(DebugInfo::from_any_ref(any)?.into()); } QuotaFailure::TYPE_URL => { details.push(QuotaFailure::from_any_ref(any)?.into()); } ErrorInfo::TYPE_URL => { details.push(ErrorInfo::from_any_ref(any)?.into()); } PreconditionFailure::TYPE_URL => { details.push(PreconditionFailure::from_any_ref(any)?.into()); } BadRequest::TYPE_URL => { details.push(BadRequest::from_any_ref(any)?.into()); } RequestInfo::TYPE_URL => { details.push(RequestInfo::from_any_ref(any)?.into()); } ResourceInfo::TYPE_URL => { details.push(ResourceInfo::from_any_ref(any)?.into()); } Help::TYPE_URL => { details.push(Help::from_any_ref(any)?.into()); } LocalizedMessage::TYPE_URL => { details.push(LocalizedMessage::from_any_ref(any)?.into()); } _ => {} } } Ok(details) } fn get_error_details_vec(&self) -> Vec { self.check_error_details_vec().unwrap_or_default() } fn get_details_retry_info(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == RetryInfo::TYPE_URL { if let Ok(detail) = RetryInfo::from_any_ref(any) { return Some(detail); } } } None } fn get_details_debug_info(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == DebugInfo::TYPE_URL { if let Ok(detail) = DebugInfo::from_any_ref(any) { return Some(detail); } } } None } fn get_details_quota_failure(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == QuotaFailure::TYPE_URL { if let Ok(detail) = QuotaFailure::from_any_ref(any) { return Some(detail); } } } None } fn get_details_error_info(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == ErrorInfo::TYPE_URL { if let Ok(detail) = ErrorInfo::from_any_ref(any) { return Some(detail); } } } None } fn get_details_precondition_failure(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == PreconditionFailure::TYPE_URL { if let Ok(detail) = PreconditionFailure::from_any_ref(any) { return Some(detail); } } } None } fn get_details_bad_request(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == BadRequest::TYPE_URL { if let Ok(detail) = BadRequest::from_any_ref(any) { return Some(detail); } } } None } fn get_details_request_info(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == RequestInfo::TYPE_URL { if let Ok(detail) = RequestInfo::from_any_ref(any) { return Some(detail); } } } None } fn get_details_resource_info(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == ResourceInfo::TYPE_URL { if let Ok(detail) = ResourceInfo::from_any_ref(any) { return Some(detail); } } } None } fn get_details_help(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == Help::TYPE_URL { if let Ok(detail) = Help::from_any_ref(any) { return Some(detail); } } } None } fn get_details_localized_message(&self) -> Option { for any in self.details.iter() { if any.type_url.as_str() == LocalizedMessage::TYPE_URL { if let Ok(detail) = LocalizedMessage::from_any_ref(any) { return Some(detail); } } } None } } #[cfg(test)] mod tests { use std::{collections::HashMap, time::Duration}; use tonic::{Code, Status}; use super::{ BadRequest, DebugInfo, ErrorDetails, ErrorInfo, Help, LocalizedMessage, PreconditionFailure, QuotaFailure, RequestInfo, ResourceInfo, RetryInfo, StatusExt, }; #[test] fn gen_status_with_details() { let mut metadata = HashMap::new(); metadata.insert("limitPerRequest".into(), "100".into()); let mut err_details = ErrorDetails::new(); err_details .set_retry_info(Some(Duration::from_secs(5))) .set_debug_info( vec!["trace3".into(), "trace2".into(), "trace1".into()], "details", ) .add_quota_failure_violation("clientip:", "description") .set_error_info("SOME_INFO", "example.local", metadata.clone()) .add_precondition_failure_violation("TOS", "example.local", "description") .add_bad_request_violation("field", "description") .set_request_info("request-id", "some-request-data") .set_resource_info("resource-type", "resource-name", "owner", "description") .add_help_link("link to resource", "resource.example.local") .set_localized_message("en-US", "message for the user"); let fmt_details = format!("{err_details:?}"); let err_details_vec = vec![ RetryInfo::new(Some(Duration::from_secs(5))).into(), DebugInfo::new( vec!["trace3".into(), "trace2".into(), "trace1".into()], "details", ) .into(), QuotaFailure::with_violation("clientip:", "description").into(), ErrorInfo::new("SOME_INFO", "example.local", metadata).into(), PreconditionFailure::with_violation("TOS", "example.local", "description").into(), BadRequest::with_violation("field", "description").into(), RequestInfo::new("request-id", "some-request-data").into(), ResourceInfo::new("resource-type", "resource-name", "owner", "description").into(), Help::with_link("link to resource", "resource.example.local").into(), LocalizedMessage::new("en-US", "message for the user").into(), ]; let fmt_details_vec = format!("{err_details_vec:?}"); let status_from_struct = Status::with_error_details( Code::InvalidArgument, "error with bad request details", err_details, ); let status_from_vec = Status::with_error_details_vec( Code::InvalidArgument, "error with bad request details", err_details_vec, ); let ext_details = match status_from_vec.check_error_details() { Ok(ext_details) => ext_details, Err(err) => panic!("Error extracting details struct from status_from_vec: {err:?}"), }; let fmt_ext_details = format!("{ext_details:?}"); assert!( fmt_ext_details.eq(&fmt_details), "Extracted details struct differs from original details struct" ); let ext_details_vec = match status_from_struct.check_error_details_vec() { Ok(ext_details) => ext_details, Err(err) => panic!("Error extracting details_vec from status_from_struct: {err:?}"), }; let fmt_ext_details_vec = format!("{ext_details_vec:?}"); assert!( fmt_ext_details_vec.eq(&fmt_details_vec), "Extracted details vec differs from original details vec" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/bad_request.rs ================================================ use prost::{DecodeError, Message}; use prost_types::Any; use crate::{LocalizedMessage, richer_error::FromAnyRef}; use super::super::{FromAny, IntoAny, pb}; /// Used at the `field_violations` field of the [`BadRequest`] struct. /// Describes a single bad request field. #[derive(Clone, Debug, Default)] pub struct FieldViolation { /// Path leading to a field in the request body. Value should be a /// sequence of dot-separated identifiers that identify a protocol buffer /// field. pub field: String, /// Description of why the field is bad. pub description: String, /// The reason of the field-level error. Value should be a /// SCREAMING_SNAKE_CASE error identifier from the domain of the API /// service. pub reason: String, /// A localized version of the field-level error. pub localized_message: Option, } impl FieldViolation { /// Creates a new [`FieldViolation`] struct. pub fn new(field: impl Into, description: impl Into) -> Self { FieldViolation { field: field.into(), description: description.into(), ..Default::default() } } } impl From for FieldViolation { fn from(value: pb::bad_request::FieldViolation) -> Self { FieldViolation { field: value.field, description: value.description, reason: value.reason, localized_message: value.localized_message.map(Into::into), } } } impl From for pb::bad_request::FieldViolation { fn from(value: FieldViolation) -> Self { pb::bad_request::FieldViolation { field: value.field, description: value.description, ..Default::default() } } } /// Used to encode/decode the `BadRequest` standard error message described in /// [error_details.proto]. Describes violations in a client request. Focuses /// on the syntactic aspects of the request. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug)] pub struct BadRequest { /// Describes all field violations of the request. pub field_violations: Vec, } impl BadRequest { /// Type URL of the `BadRequest` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.BadRequest"; /// Creates a new [`BadRequest`] struct. pub fn new(field_violations: impl Into>) -> Self { BadRequest { field_violations: field_violations.into(), } } /// Creates a new [`BadRequest`] struct with a single [`FieldViolation`] in /// `field_violations`. pub fn with_violation(field: impl Into, description: impl Into) -> Self { BadRequest { field_violations: vec![FieldViolation { field: field.into(), description: description.into(), ..Default::default() }], } } /// Adds a [`FieldViolation`] to [`BadRequest`]'s `field_violations`. pub fn add_violation( &mut self, field: impl Into, description: impl Into, ) -> &mut Self { self.field_violations.append(&mut vec![FieldViolation { field: field.into(), description: description.into(), ..Default::default() }]); self } /// Returns `true` if [`BadRequest`]'s `field_violations` vector is empty, /// and `false` if it is not. pub fn is_empty(&self) -> bool { self.field_violations.is_empty() } } impl IntoAny for BadRequest { fn into_any(self) -> Any { let detail_data: pb::BadRequest = self.into(); Any { type_url: BadRequest::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for BadRequest { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for BadRequest { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let bad_req = pb::BadRequest::decode(buf)?; Ok(bad_req.into()) } } impl From for BadRequest { fn from(value: pb::BadRequest) -> Self { BadRequest { field_violations: value.field_violations.into_iter().map(Into::into).collect(), } } } impl From for pb::BadRequest { fn from(value: BadRequest) -> Self { pb::BadRequest { field_violations: value.field_violations.into_iter().map(Into::into).collect(), } } } #[cfg(test)] mod tests { use super::super::super::{FromAny, IntoAny}; use super::BadRequest; #[test] fn gen_bad_request() { let mut br_details = BadRequest::new(Vec::new()); let formatted = format!("{br_details:?}"); let expected = "BadRequest { field_violations: [] }"; assert!( formatted.eq(expected), "empty BadRequest differs from expected result" ); assert!( br_details.is_empty(), "empty BadRequest returns 'false' from .is_empty()" ); br_details .add_violation("field_a", "description_a") .add_violation("field_b", "description_b"); let formatted = format!("{br_details:?}"); let expected_filled = "BadRequest { field_violations: [FieldViolation { field: \"field_a\", description: \"description_a\", reason: \"\", localized_message: None }, FieldViolation { field: \"field_b\", description: \"description_b\", reason: \"\", localized_message: None }] }"; assert!( formatted.eq(expected_filled), "filled BadRequest differs from expected result" ); assert!( !br_details.is_empty(), "filled BadRequest returns 'true' from .is_empty()" ); let gen_any = br_details.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.BadRequest\", value: [10, 24, 10, 7, 102, 105, 101, 108, 100, 95, 97, 18, 13, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 95, 97, 10, 24, 10, 7, 102, 105, 101, 108, 100, 95, 98, 18, 13, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 95, 98] }"; assert!( formatted.eq(expected), "Any from filled BadRequest differs from expected result" ); let br_details = match BadRequest::from_any(gen_any) { Err(error) => panic!("Error generating BadRequest from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "BadRequest from Any differs from expected result" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/debug_info.rs ================================================ use prost::{DecodeError, Message}; use prost_types::Any; use crate::richer_error::FromAnyRef; use super::super::{FromAny, IntoAny, pb}; /// Used to encode/decode the `DebugInfo` standard error message described in /// [error_details.proto]. Describes additional debugging info. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug)] pub struct DebugInfo { /// Stack trace entries indicating where the error occurred. pub stack_entries: Vec, /// Additional debugging information provided by the server. pub detail: String, } impl DebugInfo { /// Type URL of the `DebugInfo` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.DebugInfo"; /// Creates a new [`DebugInfo`] struct. pub fn new(stack_entries: impl Into>, detail: impl Into) -> Self { DebugInfo { stack_entries: stack_entries.into(), detail: detail.into(), } } /// Returns `true` if [`DebugInfo`] fields are empty, and `false` if they /// are not. pub fn is_empty(&self) -> bool { self.stack_entries.is_empty() && self.detail.is_empty() } } impl IntoAny for DebugInfo { fn into_any(self) -> Any { let detail_data: pb::DebugInfo = self.into(); Any { type_url: DebugInfo::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for DebugInfo { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for DebugInfo { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let debug_info = pb::DebugInfo::decode(buf)?; Ok(debug_info.into()) } } impl From for DebugInfo { fn from(debug_info: pb::DebugInfo) -> Self { DebugInfo { stack_entries: debug_info.stack_entries, detail: debug_info.detail, } } } impl From for pb::DebugInfo { fn from(debug_info: DebugInfo) -> Self { pb::DebugInfo { stack_entries: debug_info.stack_entries, detail: debug_info.detail, } } } #[cfg(test)] mod tests { use super::super::super::{FromAny, IntoAny}; use super::DebugInfo; #[test] fn gen_debug_info() { let debug_info = DebugInfo::new( vec!["trace 3".into(), "trace 2".into(), "trace 1".into()], "details about the error", ); let formatted = format!("{debug_info:?}"); let expected_filled = "DebugInfo { stack_entries: [\"trace 3\", \"trace 2\", \"trace 1\"], detail: \"details about the error\" }"; assert!( formatted.eq(expected_filled), "filled DebugInfo differs from expected result" ); let gen_any = debug_info.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.DebugInfo\", value: [10, 7, 116, 114, 97, 99, 101, 32, 51, 10, 7, 116, 114, 97, 99, 101, 32, 50, 10, 7, 116, 114, 97, 99, 101, 32, 49, 18, 23, 100, 101, 116, 97, 105, 108, 115, 32, 97, 98, 111, 117, 116, 32, 116, 104, 101, 32, 101, 114, 114, 111, 114] }"; assert!( formatted.eq(expected), "Any from filled DebugInfo differs from expected result" ); let br_details = match DebugInfo::from_any(gen_any) { Err(error) => panic!("Error generating DebugInfo from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "DebugInfo from Any differs from expected result" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/error_info.rs ================================================ use std::collections::HashMap; use prost::{DecodeError, Message}; use prost_types::Any; use crate::richer_error::FromAnyRef; use super::super::{FromAny, IntoAny, pb}; /// Used to encode/decode the `ErrorInfo` standard error message described in /// [error_details.proto]. Describes the cause of the error with structured /// details. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug)] pub struct ErrorInfo { /// Reason of the error. Should be a constant value that identifies the /// proximate cause of the error. Error reasons should be unique within a /// particular domain of errors. This should be at most 63 characters and /// match `/[A-Z0-9_]+/`. pub reason: String, /// Logical grouping to which the "reason" belongs. Normally is the /// registered name of the service that generates the error. pub domain: String, /// Additional structured details about this error. Keys should match /// `/[a-zA-Z0-9-_]/` and be limited to 64 characters in length. pub metadata: HashMap, } impl ErrorInfo { /// Type URL of the `ErrorInfo` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.ErrorInfo"; /// Creates a new [`ErrorInfo`] struct. pub fn new( reason: impl Into, domain: impl Into, metadata: impl Into>, ) -> Self { ErrorInfo { reason: reason.into(), domain: domain.into(), metadata: metadata.into(), } } /// Returns `true` if [`ErrorInfo`] fields are empty, and `false` if they /// are not. pub fn is_empty(&self) -> bool { self.reason.is_empty() && self.domain.is_empty() && self.metadata.is_empty() } } impl IntoAny for ErrorInfo { fn into_any(self) -> Any { let detail_data: pb::ErrorInfo = self.into(); Any { type_url: ErrorInfo::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for ErrorInfo { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for ErrorInfo { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let error_info = pb::ErrorInfo::decode(buf)?; Ok(error_info.into()) } } impl From for ErrorInfo { fn from(error_info: pb::ErrorInfo) -> Self { ErrorInfo { reason: error_info.reason, domain: error_info.domain, metadata: error_info.metadata, } } } impl From for pb::ErrorInfo { fn from(error_info: ErrorInfo) -> Self { pb::ErrorInfo { reason: error_info.reason, domain: error_info.domain, metadata: error_info.metadata, } } } #[cfg(test)] mod tests { use std::collections::HashMap; use super::super::super::{FromAny, IntoAny}; use super::ErrorInfo; #[test] fn gen_error_info() { let mut metadata = HashMap::new(); metadata.insert("instanceLimitPerRequest".into(), "100".into()); let error_info = ErrorInfo::new("SOME_INFO", "mydomain.com", metadata); let formatted = format!("{error_info:?}"); let expected_filled = "ErrorInfo { reason: \"SOME_INFO\", domain: \"mydomain.com\", metadata: {\"instanceLimitPerRequest\": \"100\"} }"; assert!( formatted.eq(expected_filled), "filled ErrorInfo differs from expected result" ); let gen_any = error_info.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.ErrorInfo\", value: [10, 9, 83, 79, 77, 69, 95, 73, 78, 70, 79, 18, 12, 109, 121, 100, 111, 109, 97, 105, 110, 46, 99, 111, 109, 26, 30, 10, 23, 105, 110, 115, 116, 97, 110, 99, 101, 76, 105, 109, 105, 116, 80, 101, 114, 82, 101, 113, 117, 101, 115, 116, 18, 3, 49, 48, 48] }"; assert!( formatted.eq(expected), "Any from filled ErrorInfo differs from expected result" ); let br_details = match ErrorInfo::from_any(gen_any) { Err(error) => panic!("Error generating ErrorInfo from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "ErrorInfo from Any differs from expected result" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/help.rs ================================================ use prost::{DecodeError, Message}; use prost_types::Any; use crate::richer_error::FromAnyRef; use super::super::{FromAny, IntoAny, pb}; /// Used at the `links` field of the [`Help`] struct. Describes a URL link. #[derive(Clone, Debug)] pub struct HelpLink { /// Description of what the link offers. pub description: String, /// URL of the link. pub url: String, } impl HelpLink { /// Creates a new [`HelpLink`] struct. pub fn new(description: impl Into, url: impl Into) -> Self { HelpLink { description: description.into(), url: url.into(), } } } impl From for HelpLink { fn from(value: pb::help::Link) -> Self { HelpLink { description: value.description, url: value.url, } } } impl From for pb::help::Link { fn from(value: HelpLink) -> Self { pb::help::Link { description: value.description, url: value.url, } } } /// Used to encode/decode the `Help` standard error message described in /// [error_details.proto]. Provides links to documentation or for performing /// an out-of-band action. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug)] pub struct Help { /// Links pointing to additional information on how to handle the error. pub links: Vec, } impl Help { /// Type URL of the `Help` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.Help"; /// Creates a new [`Help`] struct. pub fn new(links: impl Into>) -> Self { Help { links: links.into(), } } /// Creates a new [`Help`] struct with a single [`HelpLink`] in `links`. pub fn with_link(description: impl Into, url: impl Into) -> Self { Help { links: vec![HelpLink { description: description.into(), url: url.into(), }], } } /// Adds a [`HelpLink`] to [`Help`]'s `links` vector. pub fn add_link( &mut self, description: impl Into, url: impl Into, ) -> &mut Self { self.links.append(&mut vec![HelpLink { description: description.into(), url: url.into(), }]); self } /// Returns `true` if [`Help`]'s `links` vector is empty, and `false` if it /// is not. pub fn is_empty(&self) -> bool { self.links.is_empty() } } impl IntoAny for Help { fn into_any(self) -> Any { let detail_data: pb::Help = self.into(); Any { type_url: Help::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for Help { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for Help { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let help = pb::Help::decode(buf)?; Ok(help.into()) } } impl From for Help { fn from(value: pb::Help) -> Self { Help { links: value.links.into_iter().map(Into::into).collect(), } } } impl From for pb::Help { fn from(value: Help) -> Self { pb::Help { links: value.links.into_iter().map(Into::into).collect(), } } } #[cfg(test)] mod tests { use super::super::super::{FromAny, IntoAny}; use super::Help; #[test] fn gen_help() { let mut help = Help::new(Vec::new()); let formatted = format!("{help:?}"); let expected = "Help { links: [] }"; assert!( formatted.eq(expected), "empty Help differs from expected result" ); assert!( help.is_empty(), "empty Help returns 'false' from .is_empty()" ); help.add_link("link to resource a", "resource-a.example.local") .add_link("link to resource b", "resource-b.example.local"); let formatted = format!("{help:?}"); let expected_filled = "Help { links: [HelpLink { description: \"link to resource a\", url: \"resource-a.example.local\" }, HelpLink { description: \"link to resource b\", url: \"resource-b.example.local\" }] }"; assert!( formatted.eq(expected_filled), "filled Help differs from expected result" ); assert!( !help.is_empty(), "filled Help returns 'true' from .is_empty()" ); let gen_any = help.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.Help\", value: [10, 46, 10, 18, 108, 105, 110, 107, 32, 116, 111, 32, 114, 101, 115, 111, 117, 114, 99, 101, 32, 97, 18, 24, 114, 101, 115, 111, 117, 114, 99, 101, 45, 97, 46, 101, 120, 97, 109, 112, 108, 101, 46, 108, 111, 99, 97, 108, 10, 46, 10, 18, 108, 105, 110, 107, 32, 116, 111, 32, 114, 101, 115, 111, 117, 114, 99, 101, 32, 98, 18, 24, 114, 101, 115, 111, 117, 114, 99, 101, 45, 98, 46, 101, 120, 97, 109, 112, 108, 101, 46, 108, 111, 99, 97, 108] }"; assert!( formatted.eq(expected), "Any from filled Help differs from expected result" ); let br_details = match Help::from_any(gen_any) { Err(error) => panic!("Error generating Help from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "Help from Any differs from expected result" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/loc_message.rs ================================================ use prost::{DecodeError, Message}; use prost_types::Any; use crate::richer_error::FromAnyRef; use super::super::{FromAny, IntoAny, pb}; /// Used to encode/decode the `LocalizedMessage` standard error message /// described in [error_details.proto]. Provides a localized error message /// that is safe to return to the user. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug, Default)] pub struct LocalizedMessage { /// Locale used, following the specification defined in [BCP 47]. For /// example: "en-US", "fr-CH" or "es-MX". /// /// [BCP 47]: http://www.rfc-editor.org/rfc/bcp/bcp47.txt pub locale: String, /// Message corresponding to the locale. pub message: String, } impl LocalizedMessage { /// Type URL of the `LocalizedMessage` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.LocalizedMessage"; /// Creates a new [`LocalizedMessage`] struct. pub fn new(locale: impl Into, message: impl Into) -> Self { LocalizedMessage { locale: locale.into(), message: message.into(), } } /// Returns `true` if [`LocalizedMessage`] fields are empty, and `false` if /// they are not. pub fn is_empty(&self) -> bool { self.locale.is_empty() && self.message.is_empty() } } impl IntoAny for LocalizedMessage { fn into_any(self) -> Any { let detail_data: pb::LocalizedMessage = self.into(); Any { type_url: LocalizedMessage::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for LocalizedMessage { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for LocalizedMessage { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let loc_message = pb::LocalizedMessage::decode(buf)?; Ok(loc_message.into()) } } impl From for LocalizedMessage { fn from(loc_message: pb::LocalizedMessage) -> Self { LocalizedMessage { locale: loc_message.locale, message: loc_message.message, } } } impl From for pb::LocalizedMessage { fn from(loc_message: LocalizedMessage) -> Self { pb::LocalizedMessage { locale: loc_message.locale, message: loc_message.message, } } } #[cfg(test)] mod tests { use super::super::super::{FromAny, IntoAny}; use super::LocalizedMessage; #[test] fn gen_localized_message() { let loc_message = LocalizedMessage::new("en-US", "message for the user"); let formatted = format!("{loc_message:?}"); let expected_filled = "LocalizedMessage { locale: \"en-US\", message: \"message for the user\" }"; assert!( formatted.eq(expected_filled), "filled LocalizedMessage differs from expected result" ); let gen_any = loc_message.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.LocalizedMessage\", value: [10, 5, 101, 110, 45, 85, 83, 18, 20, 109, 101, 115, 115, 97, 103, 101, 32, 102, 111, 114, 32, 116, 104, 101, 32, 117, 115, 101, 114] }"; assert!( formatted.eq(expected), "Any from filled LocalizedMessage differs from expected result" ); let br_details = match LocalizedMessage::from_any(gen_any) { Err(error) => panic!("Error generating LocalizedMessage from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "LocalizedMessage from Any differs from expected result" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/mod.rs ================================================ mod retry_info; pub use retry_info::RetryInfo; mod debug_info; pub use debug_info::DebugInfo; mod quota_failure; pub use quota_failure::{QuotaFailure, QuotaViolation}; mod error_info; pub use error_info::ErrorInfo; mod prec_failure; pub use prec_failure::{PreconditionFailure, PreconditionViolation}; mod bad_request; pub use bad_request::{BadRequest, FieldViolation}; mod request_info; pub use request_info::RequestInfo; mod resource_info; pub use resource_info::ResourceInfo; mod help; pub use help::{Help, HelpLink}; mod loc_message; pub use loc_message::LocalizedMessage; ================================================ FILE: tonic-types/src/richer_error/std_messages/prec_failure.rs ================================================ use prost::{DecodeError, Message}; use prost_types::Any; use crate::richer_error::FromAnyRef; use super::super::{FromAny, IntoAny, pb}; /// Used at the `violations` field of the [`PreconditionFailure`] struct. /// Describes a single precondition failure. #[derive(Clone, Debug)] pub struct PreconditionViolation { /// Type of the PreconditionFailure. At [error_details.proto], the usage /// of a service-specific enum type is recommended. For example, "TOS" for /// a "Terms of Service" violation. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto pub r#type: String, /// Subject, relative to the type, that failed. pub subject: String, /// A description of how the precondition failed. pub description: String, } impl PreconditionViolation { /// Creates a new [`PreconditionViolation`] struct. pub fn new( r#type: impl Into, subject: impl Into, description: impl Into, ) -> Self { PreconditionViolation { r#type: r#type.into(), subject: subject.into(), description: description.into(), } } } impl From for PreconditionViolation { fn from(value: pb::precondition_failure::Violation) -> Self { PreconditionViolation { r#type: value.r#type, subject: value.subject, description: value.description, } } } impl From for pb::precondition_failure::Violation { fn from(value: PreconditionViolation) -> Self { pb::precondition_failure::Violation { r#type: value.r#type, subject: value.subject, description: value.description, } } } /// Used to encode/decode the `PreconditionFailure` standard error message /// described in [error_details.proto]. Describes what preconditions have /// failed. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug)] pub struct PreconditionFailure { /// Describes all precondition violations of the request. pub violations: Vec, } impl PreconditionFailure { /// Type URL of the `PreconditionFailure` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.PreconditionFailure"; /// Creates a new [`PreconditionFailure`] struct. pub fn new(violations: impl Into>) -> Self { PreconditionFailure { violations: violations.into(), } } /// Creates a new [`PreconditionFailure`] struct with a single /// [`PreconditionViolation`] in `violations`. pub fn with_violation( violation_type: impl Into, subject: impl Into, description: impl Into, ) -> Self { PreconditionFailure { violations: vec![PreconditionViolation { r#type: violation_type.into(), subject: subject.into(), description: description.into(), }], } } /// Adds a [`PreconditionViolation`] to [`PreconditionFailure`]'s /// `violations` vector. pub fn add_violation( &mut self, r#type: impl Into, subject: impl Into, description: impl Into, ) -> &mut Self { self.violations.append(&mut vec![PreconditionViolation { r#type: r#type.into(), subject: subject.into(), description: description.into(), }]); self } /// Returns `true` if [`PreconditionFailure`]'s `violations` vector is /// empty, and `false` if it is not. pub fn is_empty(&self) -> bool { self.violations.is_empty() } } impl IntoAny for PreconditionFailure { fn into_any(self) -> Any { let detail_data: pb::PreconditionFailure = self.into(); Any { type_url: PreconditionFailure::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for PreconditionFailure { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for PreconditionFailure { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let precondition_failure = pb::PreconditionFailure::decode(buf)?; Ok(precondition_failure.into()) } } impl From for PreconditionFailure { fn from(value: pb::PreconditionFailure) -> Self { PreconditionFailure { violations: value.violations.into_iter().map(Into::into).collect(), } } } impl From for pb::PreconditionFailure { fn from(value: PreconditionFailure) -> Self { pb::PreconditionFailure { violations: value.violations.into_iter().map(Into::into).collect(), } } } #[cfg(test)] mod tests { use super::super::super::{FromAny, IntoAny}; use super::PreconditionFailure; #[test] fn gen_prec_failure() { let mut prec_failure = PreconditionFailure::new(Vec::new()); let formatted = format!("{prec_failure:?}"); let expected = "PreconditionFailure { violations: [] }"; assert!( formatted.eq(expected), "empty PreconditionFailure differs from expected result" ); assert!( prec_failure.is_empty(), "empty PreconditionFailure returns 'false' from .is_empty()" ); prec_failure .add_violation("TOS", "example.local", "Terms of service not accepted") .add_violation("FNF", "example.local", "File not found"); let formatted = format!("{prec_failure:?}"); let expected_filled = "PreconditionFailure { violations: [PreconditionViolation { type: \"TOS\", subject: \"example.local\", description: \"Terms of service not accepted\" }, PreconditionViolation { type: \"FNF\", subject: \"example.local\", description: \"File not found\" }] }"; assert!( formatted.eq(expected_filled), "filled PreconditionFailure differs from expected result" ); assert!( !prec_failure.is_empty(), "filled PreconditionFailure returns 'true' from .is_empty()" ); let gen_any = prec_failure.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.PreconditionFailure\", value: [10, 51, 10, 3, 84, 79, 83, 18, 13, 101, 120, 97, 109, 112, 108, 101, 46, 108, 111, 99, 97, 108, 26, 29, 84, 101, 114, 109, 115, 32, 111, 102, 32, 115, 101, 114, 118, 105, 99, 101, 32, 110, 111, 116, 32, 97, 99, 99, 101, 112, 116, 101, 100, 10, 36, 10, 3, 70, 78, 70, 18, 13, 101, 120, 97, 109, 112, 108, 101, 46, 108, 111, 99, 97, 108, 26, 14, 70, 105, 108, 101, 32, 110, 111, 116, 32, 102, 111, 117, 110, 100] }"; assert!( formatted.eq(expected), "Any from filled PreconditionFailure differs from expected result" ); let br_details = match PreconditionFailure::from_any(gen_any) { Err(error) => panic!("Error generating PreconditionFailure from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "PreconditionFailure from Any differs from expected result" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/quota_failure.rs ================================================ use std::collections::HashMap; use prost::{DecodeError, Message}; use prost_types::Any; use crate::richer_error::FromAnyRef; use super::super::{FromAny, IntoAny, pb}; /// Used at the `violations` field of the [`QuotaFailure`] struct. Describes a /// single quota violation. #[derive(Clone, Debug, Default)] pub struct QuotaViolation { /// Subject on which the quota check failed. pub subject: String, /// Description of why the quota check failed. pub description: String, /// The API service from which the quota check originates. pub api_service: String, /// The quota check that was violated. pub quota_metric: String, /// The ID of the violated quota check. pub quota_id: String, /// The dimensions of the violated quota check. pub quota_dimensions: HashMap, /// The quota check value at the time of violation. pub quota_value: i64, /// The future value of the quota check value when a quota check rollout is /// in progress. pub futura_quota_value: Option, } impl QuotaViolation { /// Creates a new [`QuotaViolation`] struct. pub fn new(subject: impl Into, description: impl Into) -> Self { QuotaViolation { subject: subject.into(), description: description.into(), ..Default::default() } } } impl From for QuotaViolation { fn from(value: pb::quota_failure::Violation) -> Self { QuotaViolation { subject: value.subject, description: value.description, api_service: value.api_service, quota_metric: value.quota_metric, quota_id: value.quota_id, quota_dimensions: value.quota_dimensions, quota_value: value.quota_value, futura_quota_value: value.future_quota_value, } } } impl From for pb::quota_failure::Violation { fn from(value: QuotaViolation) -> Self { pb::quota_failure::Violation { subject: value.subject, description: value.description, api_service: value.api_service, quota_metric: value.quota_metric, quota_id: value.quota_id, quota_dimensions: value.quota_dimensions, quota_value: value.quota_value, future_quota_value: value.futura_quota_value, } } } /// Used to encode/decode the `QuotaFailure` standard error message described /// in [error_details.proto]. Describes how a quota check failed. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug)] pub struct QuotaFailure { /// Describes all quota violations. pub violations: Vec, } impl QuotaFailure { /// Type URL of the `QuotaFailure` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.QuotaFailure"; /// Creates a new [`QuotaFailure`] struct. pub fn new(violations: impl Into>) -> Self { QuotaFailure { violations: violations.into(), } } /// Creates a new [`QuotaFailure`] struct with a single [`QuotaViolation`] /// in `violations`. pub fn with_violation(subject: impl Into, description: impl Into) -> Self { QuotaFailure { violations: vec![QuotaViolation { subject: subject.into(), description: description.into(), ..Default::default() }], } } /// Adds a [`QuotaViolation`] to [`QuotaFailure`]'s `violations`. pub fn add_violation( &mut self, subject: impl Into, description: impl Into, ) -> &mut Self { self.violations.append(&mut vec![QuotaViolation { subject: subject.into(), description: description.into(), ..Default::default() }]); self } /// Returns `true` if [`QuotaFailure`]'s `violations` vector is empty, and /// `false` if it is not. pub fn is_empty(&self) -> bool { self.violations.is_empty() } } impl IntoAny for QuotaFailure { fn into_any(self) -> Any { let detail_data: pb::QuotaFailure = self.into(); Any { type_url: QuotaFailure::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for QuotaFailure { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for QuotaFailure { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let quota_failure = pb::QuotaFailure::decode(buf)?; Ok(quota_failure.into()) } } impl From for QuotaFailure { fn from(value: pb::QuotaFailure) -> Self { QuotaFailure { violations: value.violations.into_iter().map(Into::into).collect(), } } } impl From for pb::QuotaFailure { fn from(value: QuotaFailure) -> Self { pb::QuotaFailure { violations: value.violations.into_iter().map(Into::into).collect(), } } } #[cfg(test)] mod tests { use super::super::super::{FromAny, IntoAny}; use super::QuotaFailure; #[test] fn gen_quota_failure() { let mut quota_failure = QuotaFailure::new(Vec::new()); let formatted = format!("{quota_failure:?}"); let expected = "QuotaFailure { violations: [] }"; assert!( formatted.eq(expected), "empty QuotaFailure differs from expected result" ); assert!( quota_failure.is_empty(), "empty QuotaFailure returns 'false' from .is_empty()" ); quota_failure .add_violation("clientip:", "description a") .add_violation("project:", "description b"); let formatted = format!("{quota_failure:?}"); let expected_filled = "QuotaFailure { violations: [QuotaViolation { subject: \"clientip:\", description: \"description a\", api_service: \"\", quota_metric: \"\", quota_id: \"\", quota_dimensions: {}, quota_value: 0, futura_quota_value: None }, QuotaViolation { subject: \"project:\", description: \"description b\", api_service: \"\", quota_metric: \"\", quota_id: \"\", quota_dimensions: {}, quota_value: 0, futura_quota_value: None }] }"; assert!( formatted.eq(expected_filled), "filled QuotaFailure differs from expected result" ); assert!( !quota_failure.is_empty(), "filled QuotaFailure returns 'true' from .is_empty()" ); let gen_any = quota_failure.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.QuotaFailure\", value: [10, 38, 10, 21, 99, 108, 105, 101, 110, 116, 105, 112, 58, 60, 105, 112, 32, 97, 100, 100, 114, 101, 115, 115, 62, 18, 13, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 32, 97, 10, 37, 10, 20, 112, 114, 111, 106, 101, 99, 116, 58, 60, 112, 114, 111, 106, 101, 99, 116, 32, 105, 100, 62, 18, 13, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 32, 98] }"; assert!( formatted.eq(expected), "Any from filled QuotaFailure differs from expected result" ); let br_details = match QuotaFailure::from_any(gen_any) { Err(error) => panic!("Error generating QuotaFailure from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "QuotaFailure from Any differs from expected result" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/request_info.rs ================================================ use prost::{DecodeError, Message}; use prost_types::Any; use crate::richer_error::FromAnyRef; use super::super::{FromAny, IntoAny, pb}; /// Used to encode/decode the `RequestInfo` standard error message described /// in [error_details.proto]. Contains metadata about the request that /// clients can attach when providing feedback. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug)] pub struct RequestInfo { /// An opaque string that should only be interpreted by the service that /// generated it. For example, an id used to identify requests in the logs. pub request_id: String, /// Any data used to serve this request. For example, an encrypted stack /// trace that can be sent back to the service provider for debugging. pub serving_data: String, } impl RequestInfo { /// Type URL of the `RequestInfo` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.RequestInfo"; /// Creates a new [`RequestInfo`] struct. pub fn new(request_id: impl Into, serving_data: impl Into) -> Self { RequestInfo { request_id: request_id.into(), serving_data: serving_data.into(), } } /// Returns `true` if [`RequestInfo`] fields are empty, and `false` if they /// are not. pub fn is_empty(&self) -> bool { self.request_id.is_empty() && self.serving_data.is_empty() } } impl IntoAny for RequestInfo { fn into_any(self) -> Any { let detail_data: pb::RequestInfo = self.into(); Any { type_url: RequestInfo::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for RequestInfo { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for RequestInfo { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let req_info = pb::RequestInfo::decode(buf)?; Ok(req_info.into()) } } impl From for RequestInfo { fn from(req_info: pb::RequestInfo) -> Self { RequestInfo { request_id: req_info.request_id, serving_data: req_info.serving_data, } } } impl From for pb::RequestInfo { fn from(req_info: RequestInfo) -> Self { pb::RequestInfo { request_id: req_info.request_id, serving_data: req_info.serving_data, } } } #[cfg(test)] mod tests { use super::super::super::{FromAny, IntoAny}; use super::RequestInfo; #[test] fn gen_request_info() { let req_info = RequestInfo::new("some-id", "some-data"); let formatted = format!("{req_info:?}"); let expected_filled = "RequestInfo { request_id: \"some-id\", serving_data: \"some-data\" }"; assert!( formatted.eq(expected_filled), "filled RequestInfo differs from expected result" ); let gen_any = req_info.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.RequestInfo\", value: [10, 7, 115, 111, 109, 101, 45, 105, 100, 18, 9, 115, 111, 109, 101, 45, 100, 97, 116, 97] }"; assert!( formatted.eq(expected), "Any from filled RequestInfo differs from expected result" ); let br_details = match RequestInfo::from_any(gen_any) { Err(error) => panic!("Error generating RequestInfo from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "RequestInfo from Any differs from expected result" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/resource_info.rs ================================================ use prost::{DecodeError, Message}; use prost_types::Any; use crate::richer_error::FromAnyRef; use super::super::{FromAny, IntoAny, pb}; /// Used to encode/decode the `ResourceInfo` standard error message described /// in [error_details.proto]. Describes the resource that is being accessed. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug)] pub struct ResourceInfo { /// Type of resource being accessed. pub resource_type: String, /// Name of the resource being accessed. pub resource_name: String, /// The owner of the resource (optional). pub owner: String, /// Describes the error encountered when accessing the resource. pub description: String, } impl ResourceInfo { /// Type URL of the `ResourceInfo` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.ResourceInfo"; /// Creates a new [`ResourceInfo`] struct. pub fn new( resource_type: impl Into, resource_name: impl Into, owner: impl Into, description: impl Into, ) -> Self { ResourceInfo { resource_type: resource_type.into(), resource_name: resource_name.into(), owner: owner.into(), description: description.into(), } } /// Returns `true` if [`ResourceInfo`] fields are empty, and `false` if /// they are not. pub fn is_empty(&self) -> bool { self.resource_type.is_empty() && self.resource_name.is_empty() && self.owner.is_empty() && self.description.is_empty() } } impl IntoAny for ResourceInfo { fn into_any(self) -> Any { let detail_data: pb::ResourceInfo = self.into(); Any { type_url: ResourceInfo::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for ResourceInfo { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for ResourceInfo { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let res_info = pb::ResourceInfo::decode(buf)?; Ok(res_info.into()) } } impl From for ResourceInfo { fn from(res_info: pb::ResourceInfo) -> Self { ResourceInfo { resource_type: res_info.resource_type, resource_name: res_info.resource_name, owner: res_info.owner, description: res_info.description, } } } impl From for pb::ResourceInfo { fn from(res_info: ResourceInfo) -> Self { pb::ResourceInfo { resource_type: res_info.resource_type, resource_name: res_info.resource_name, owner: res_info.owner, description: res_info.description, } } } #[cfg(test)] mod tests { use super::super::super::{FromAny, IntoAny}; use super::ResourceInfo; #[test] fn gen_resource_info() { let res_info = ResourceInfo::new("resource-type", "resource-name", "owner", "description"); let formatted = format!("{res_info:?}"); let expected_filled = "ResourceInfo { resource_type: \"resource-type\", resource_name: \"resource-name\", owner: \"owner\", description: \"description\" }"; assert!( formatted.eq(expected_filled), "filled ResourceInfo differs from expected result" ); let gen_any = res_info.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.ResourceInfo\", value: [10, 13, 114, 101, 115, 111, 117, 114, 99, 101, 45, 116, 121, 112, 101, 18, 13, 114, 101, 115, 111, 117, 114, 99, 101, 45, 110, 97, 109, 101, 26, 5, 111, 119, 110, 101, 114, 34, 11, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110] }"; assert!( formatted.eq(expected), "Any from filled ResourceInfo differs from expected result" ); let br_details = match ResourceInfo::from_any(gen_any) { Err(error) => panic!("Error generating ResourceInfo from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "ResourceInfo from Any differs from expected result" ); } } ================================================ FILE: tonic-types/src/richer_error/std_messages/retry_info.rs ================================================ use std::time; use prost::{DecodeError, Message}; use prost_types::Any; use crate::richer_error::FromAnyRef; use super::super::{FromAny, IntoAny, pb}; /// Used to encode/decode the `RetryInfo` standard error message described in /// [error_details.proto]. Describes when the clients can retry a failed /// request. /// Note: When obtained from decoding `RetryInfo` messages, negative /// `retry_delay`'s become 0. /// /// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto #[derive(Clone, Debug)] pub struct RetryInfo { /// Informs the amount of time that clients should wait before retrying. pub retry_delay: Option, } impl RetryInfo { /// Type URL of the `RetryInfo` standard error message type. pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.RetryInfo"; /// Should not exceed `prost_types::Duration` range. Limited to /// approximately 10,000 years. pub const MAX_RETRY_DELAY: time::Duration = time::Duration::new(315_576_000_000, 999_999_999); /// Creates a new [`RetryInfo`] struct. If `retry_delay` exceeds /// [`RetryInfo::MAX_RETRY_DELAY`], [`RetryInfo::MAX_RETRY_DELAY`] will /// be used instead. pub fn new(retry_delay: Option) -> Self { let retry_delay = match retry_delay { Some(mut delay) => { if delay > RetryInfo::MAX_RETRY_DELAY { delay = RetryInfo::MAX_RETRY_DELAY } Some(delay) } None => None, }; RetryInfo { retry_delay } } /// Returns `true` if [`RetryInfo`]'s `retry_delay` is set as `None`, and /// `false` if it is not. pub fn is_empty(&self) -> bool { self.retry_delay.is_none() } } impl IntoAny for RetryInfo { fn into_any(self) -> Any { let detail_data: pb::RetryInfo = self.into(); Any { type_url: RetryInfo::TYPE_URL.to_string(), value: detail_data.encode_to_vec(), } } } impl FromAny for RetryInfo { #[inline] fn from_any(any: Any) -> Result { FromAnyRef::from_any_ref(&any) } } impl FromAnyRef for RetryInfo { fn from_any_ref(any: &Any) -> Result { let buf: &[u8] = &any.value; let retry_info = pb::RetryInfo::decode(buf)?; Ok(retry_info.into()) } } impl From for RetryInfo { fn from(retry_info: pb::RetryInfo) -> Self { let retry_delay = match retry_info.retry_delay { Some(duration) => { // Negative retry_delays become 0 let duration = time::Duration::try_from(duration).unwrap_or(time::Duration::ZERO); Some(duration) } None => None, }; RetryInfo { retry_delay } } } impl From for pb::RetryInfo { fn from(value: RetryInfo) -> Self { let retry_delay = match value.retry_delay { Some(duration) => { // If duration is too large, uses max `prost_types::Duration` let duration = match prost_types::Duration::try_from(duration) { Ok(duration) => duration, Err(_) => prost_types::Duration { seconds: 315_576_000_000, nanos: 999_999_999, }, }; Some(duration) } None => None, }; pb::RetryInfo { retry_delay } } } #[cfg(test)] mod tests { use core::time::Duration; use super::super::super::{FromAny, IntoAny}; use super::RetryInfo; #[test] fn gen_retry_info() { let retry_info = RetryInfo::new(Some(Duration::from_secs(u64::MAX))); let formatted = format!("{retry_info:?}"); let expected_filled = "RetryInfo { retry_delay: Some(315576000000.999999999s) }"; assert!( formatted.eq(expected_filled), "filled RetryInfo differs from expected result" ); assert!( !retry_info.is_empty(), "filled RetryInfo returns 'false' from .has_retry_delay()" ); let gen_any = retry_info.into_any(); let formatted = format!("{gen_any:?}"); let expected = "Any { type_url: \"type.googleapis.com/google.rpc.RetryInfo\", value: [10, 13, 8, 128, 188, 174, 206, 151, 9, 16, 255, 147, 235, 220, 3] }"; assert!( formatted.eq(expected), "Any from filled RetryInfo differs from expected result" ); let br_details = match RetryInfo::from_any(gen_any) { Err(error) => panic!("Error generating RetryInfo from Any: {error:?}"), Ok(from_any) => from_any, }; let formatted = format!("{br_details:?}"); assert!( formatted.eq(expected_filled), "RetryInfo from Any differs from expected result" ); } } ================================================ FILE: tonic-web/Cargo.toml ================================================ [package] authors = ["Juan Alvarez "] categories = ["network-programming", "asynchronous"] description = """ grpc-web protocol translation for tonic services. """ edition = "2024" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "grpc-web"] license = "MIT" name = "tonic-web" readme = "README.md" repository = "https://github.com/hyperium/tonic" version = "0.14.5" rust-version = { workspace = true } [dependencies] base64 = "0.22" bytes = "1" tokio-stream = { version = "0.1", default-features = false } http = "1" http-body = "1" pin-project = "1" tonic = { version = "0.14.0", path = "../tonic", default-features = false } tower-service = "0.3" tower-layer = "0.3" tracing = "0.1" [dev-dependencies] tokio = { version = "1", features = ["macros", "rt"] } tower-http = { version = "0.6", features = ["cors"] } axum = { version = "0.8", default-features = false } [lints] workspace = true [package.metadata.cargo_check_external_types] allowed_external_types = [ "tonic::*", # major released "bytes::*", "http::*", "http_body::*", # not major released "futures_core::stream::Stream", "tower_layer::Layer", "tower_service::Service", ] ================================================ FILE: tonic-web/README.md ================================================ # tonic-web Enables tonic servers to handle requests from `grpc-web` clients directly, without the need of an external proxy. ## Enabling tonic services The easiest way to get started, is to call the function with your tonic service and allow the tonic server to accept HTTP/1.1 requests: ```rust #[tokio::main] async fn main() -> Result<(), Box> { let addr = "[::1]:50051".parse().unwrap(); let greeter = GreeterServer::new(MyGreeter::default()); Server::builder() .accept_http1(true) .layer(GrpcWebLayer::new()) .add_service(greeter) .serve(addr) .await?; Ok(()) } ``` ## Examples See [the examples folder][example] for a server and client example. [example]: https://github.com/hyperium/tonic/tree/master/examples/src/grpc-web ================================================ FILE: tonic-web/src/call.rs ================================================ use std::fmt; use std::pin::Pin; use std::task::{Context, Poll, ready}; use base64::Engine as _; use bytes::{Buf, BufMut, Bytes, BytesMut}; use http::{HeaderMap, HeaderName, HeaderValue, header}; use http_body::{Body, Frame, SizeHint}; use pin_project::pin_project; use tokio_stream::Stream; use tonic::Status; use self::content_types::*; // A grpc header is u8 (flag) + u32 (msg len) const GRPC_HEADER_SIZE: usize = 1 + 4; pub(crate) mod content_types { use http::{HeaderMap, header::CONTENT_TYPE}; pub(crate) const GRPC_WEB: &str = "application/grpc-web"; pub(crate) const GRPC_WEB_PROTO: &str = "application/grpc-web+proto"; pub(crate) const GRPC_WEB_TEXT: &str = "application/grpc-web-text"; pub(crate) const GRPC_WEB_TEXT_PROTO: &str = "application/grpc-web-text+proto"; pub(crate) fn is_grpc_web(headers: &HeaderMap) -> bool { matches!( content_type(headers), Some(GRPC_WEB) | Some(GRPC_WEB_PROTO) | Some(GRPC_WEB_TEXT) | Some(GRPC_WEB_TEXT_PROTO) ) } fn content_type(headers: &HeaderMap) -> Option<&str> { headers.get(CONTENT_TYPE).and_then(|val| val.to_str().ok()) } } const BUFFER_SIZE: usize = 8 * 1024; const FRAME_HEADER_SIZE: usize = 5; // 8th (MSB) bit of the 1st gRPC frame byte // denotes an uncompressed trailer (as part of the body) const GRPC_WEB_TRAILERS_BIT: u8 = 0b10000000; #[derive(Copy, Clone, PartialEq, Debug)] enum Direction { Decode, Encode, Empty, } #[derive(Copy, Clone, PartialEq, Debug)] pub(crate) enum Encoding { Base64, None, } /// HttpBody adapter for the grpc web based services. #[derive(Debug)] #[pin_project] pub struct GrpcWebCall { #[pin] inner: B, buf: BytesMut, decoded: BytesMut, direction: Direction, encoding: Encoding, client: bool, trailers: Option, } impl Default for GrpcWebCall { fn default() -> Self { Self { inner: Default::default(), buf: Default::default(), decoded: Default::default(), direction: Direction::Empty, encoding: Encoding::None, client: Default::default(), trailers: Default::default(), } } } impl GrpcWebCall { pub(crate) fn request(inner: B, encoding: Encoding) -> Self { Self::new(inner, Direction::Decode, encoding) } pub(crate) fn response(inner: B, encoding: Encoding) -> Self { Self::new(inner, Direction::Encode, encoding) } pub(crate) fn client_request(inner: B) -> Self { Self::new_client(inner, Direction::Encode, Encoding::None) } pub(crate) fn client_response(inner: B) -> Self { Self::new_client(inner, Direction::Decode, Encoding::None) } fn new_client(inner: B, direction: Direction, encoding: Encoding) -> Self { GrpcWebCall { inner, buf: BytesMut::with_capacity(match (direction, encoding) { (Direction::Encode, Encoding::Base64) => BUFFER_SIZE, _ => 0, }), decoded: BytesMut::with_capacity(match direction { Direction::Decode => BUFFER_SIZE, _ => 0, }), direction, encoding, client: true, trailers: None, } } fn new(inner: B, direction: Direction, encoding: Encoding) -> Self { GrpcWebCall { inner, buf: BytesMut::with_capacity(match (direction, encoding) { (Direction::Encode, Encoding::Base64) => BUFFER_SIZE, _ => 0, }), decoded: BytesMut::with_capacity(0), direction, encoding, client: false, trailers: None, } } // This is to avoid passing a slice of bytes with a length that the base64 // decoder would consider invalid. #[inline] fn max_decodable(&self) -> usize { (self.buf.len() / 4) * 4 } fn decode_chunk(mut self: Pin<&mut Self>) -> Result, Status> { // not enough bytes to decode if self.buf.is_empty() || self.buf.len() < 4 { return Ok(None); } // Split `buf` at the largest index that is multiple of 4. Decode the // returned `Bytes`, keeping the rest for the next attempt to decode. let index = self.max_decodable(); crate::util::base64::STANDARD .decode(self.as_mut().project().buf.split_to(index)) .map(|decoded| Some(Bytes::from(decoded))) .map_err(internal_error) } } impl GrpcWebCall where B: Body, B::Data: Buf, B::Error: fmt::Display, { // Poll body for data, decoding (e.g. via Base64 if necessary) and returning frames // to the caller. If the caller is a client, it should look for trailers before // returning these frames. fn poll_decode( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Status>>> { match self.encoding { Encoding::Base64 => loop { if let Some(bytes) = self.as_mut().decode_chunk()? { return Poll::Ready(Some(Ok(Frame::data(bytes)))); } let this = self.as_mut().project(); match ready!(this.inner.poll_frame(cx)) { Some(Ok(frame)) if frame.is_data() => this .buf .put(frame.into_data().unwrap_or_else(|_| unreachable!())), Some(Ok(frame)) if frame.is_trailers() => { return Poll::Ready(Some(Err(internal_error( "malformed base64 request has unencoded trailers", )))); } Some(Ok(_)) => { return Poll::Ready(Some(Err(internal_error("unexpected frame type")))); } Some(Err(e)) => return Poll::Ready(Some(Err(internal_error(e)))), None => { return if this.buf.has_remaining() { Poll::Ready(Some(Err(internal_error("malformed base64 request")))) } else if let Some(trailers) = this.trailers.take() { Poll::Ready(Some(Ok(Frame::trailers(trailers)))) } else { Poll::Ready(None) }; } } }, Encoding::None => self .project() .inner .poll_frame(cx) .map_ok(|f| f.map_data(|mut d| d.copy_to_bytes(d.remaining()))) .map_err(internal_error), } } fn poll_encode( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Status>>> { let this = self.as_mut().project(); match ready!(this.inner.poll_frame(cx)) { Some(Ok(frame)) if frame.is_data() => { let mut data = frame.into_data().unwrap_or_else(|_| unreachable!()); let mut res = data.copy_to_bytes(data.remaining()); if *this.encoding == Encoding::Base64 { res = crate::util::base64::STANDARD.encode(res).into(); } Poll::Ready(Some(Ok(Frame::data(res)))) } Some(Ok(frame)) if frame.is_trailers() => { let trailers = frame.into_trailers().unwrap_or_else(|_| unreachable!()); let mut res = make_trailers_frame(trailers); if *this.encoding == Encoding::Base64 { res = crate::util::base64::STANDARD.encode(res).into(); } Poll::Ready(Some(Ok(Frame::data(res)))) } Some(Ok(_)) => Poll::Ready(Some(Err(internal_error("unexpected frame type")))), Some(Err(e)) => Poll::Ready(Some(Err(internal_error(e)))), None => Poll::Ready(None), } } } impl Body for GrpcWebCall where B: Body, B::Error: fmt::Display, { type Data = Bytes; type Error = Status; fn poll_frame( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll, Self::Error>>> { if self.client && self.direction == Direction::Decode { let mut me = self.as_mut(); loop { match ready!(me.as_mut().poll_decode(cx)) { Some(Ok(incoming_buf)) if incoming_buf.is_data() => { me.as_mut() .project() .decoded .put(incoming_buf.into_data().unwrap()); } Some(Ok(incoming_buf)) if incoming_buf.is_trailers() => { let trailers = incoming_buf.into_trailers().unwrap(); match me.as_mut().project().trailers { Some(current_trailers) => { current_trailers.extend(trailers); } None => { me.as_mut().project().trailers.replace(trailers); } } continue; } Some(Ok(_)) => unreachable!("unexpected frame type"), None => {} // No more data to decode, time to look for trailers Some(Err(e)) => return Poll::Ready(Some(Err(e))), }; // Hold the incoming, decoded data until we have a full message // or trailers to return. let buf = me.as_mut().project().decoded; return match find_trailers(&buf[..])? { FindTrailers::Trailer(len) => { // Extract up to len of where the trailers are at let msg_buf = buf.copy_to_bytes(len); match decode_trailers_frame(buf.split().freeze()) { Ok(Some(trailers)) => { me.as_mut().project().trailers.replace(trailers); } Err(e) => return Poll::Ready(Some(Err(e))), _ => {} } if msg_buf.has_remaining() { Poll::Ready(Some(Ok(Frame::data(msg_buf)))) } else if let Some(trailers) = me.as_mut().project().trailers.take() { Poll::Ready(Some(Ok(Frame::trailers(trailers)))) } else { Poll::Ready(None) } } FindTrailers::IncompleteBuf => continue, FindTrailers::Done(len) => Poll::Ready(match len { 0 => None, _ => Some(Ok(Frame::data(buf.split_to(len).freeze()))), }), }; } } match self.direction { Direction::Decode => self.poll_decode(cx), Direction::Encode => self.poll_encode(cx), Direction::Empty => Poll::Ready(None), } } fn is_end_stream(&self) -> bool { self.inner.is_end_stream() } fn size_hint(&self) -> SizeHint { self.inner.size_hint() } } impl Stream for GrpcWebCall where B: Body, B::Error: fmt::Display, { type Item = Result, Status>; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.poll_frame(cx) } } impl Encoding { pub(crate) fn from_content_type(headers: &HeaderMap) -> Encoding { Self::from_header(headers.get(header::CONTENT_TYPE)) } pub(crate) fn from_accept(headers: &HeaderMap) -> Encoding { Self::from_header(headers.get(header::ACCEPT)) } pub(crate) fn to_content_type(self) -> &'static str { match self { Encoding::Base64 => GRPC_WEB_TEXT_PROTO, Encoding::None => GRPC_WEB_PROTO, } } fn from_header(value: Option<&HeaderValue>) -> Encoding { match value.and_then(|val| val.to_str().ok()) { Some(GRPC_WEB_TEXT_PROTO) | Some(GRPC_WEB_TEXT) => Encoding::Base64, _ => Encoding::None, } } } fn internal_error(e: impl std::fmt::Display) -> Status { Status::internal(format!("tonic-web: {e}")) } // Key-value pairs encoded as a HTTP/1 headers block (without the terminating newline) fn encode_trailers(trailers: HeaderMap) -> Vec { trailers.iter().fold(Vec::new(), |mut acc, (key, value)| { acc.put_slice(key.as_ref()); acc.push(b':'); acc.put_slice(value.as_bytes()); acc.put_slice(b"\r\n"); acc }) } fn decode_trailers_frame(mut buf: Bytes) -> Result, Status> { if buf.remaining() < GRPC_HEADER_SIZE { return Ok(None); } buf.get_u8(); buf.get_u32(); let mut map = HeaderMap::new(); let mut temp_buf = buf.clone(); let mut trailers = Vec::new(); let mut cursor_pos = 0; for (i, b) in buf.iter().enumerate() { // if we are at a trailer delimiter (\r\n) if b == &b'\r' && buf.get(i + 1) == Some(&b'\n') { // read the bytes of the trailer passed so far let trailer = temp_buf.copy_to_bytes(i - cursor_pos); // increment cursor beyond the delimiter cursor_pos = i + 2; trailers.push(trailer); if temp_buf.has_remaining() { // advance buf beyond the delimiters temp_buf.get_u8(); temp_buf.get_u8(); } } } for trailer in trailers { let Some((key, value)) = trailer .iter() .position(|b| *b == b':') .map(|pos| trailer.split_at(pos)) else { return Err(Status::internal("trailers couldn't parse key/value")); }; // Skip the ':' separator and trim leading OWS (spaces/tabs) from the value let value = &value[1..]; // skip ':' let value = trim_ascii_start(value); let header_key = HeaderName::try_from(key) .map_err(|e| Status::internal(format!("Unable to parse HeaderName: {e}")))?; let header_value = HeaderValue::try_from(value) .map_err(|e| Status::internal(format!("Unable to parse HeaderValue: {e}")))?; map.insert(header_key, header_value); } Ok(Some(map)) } fn trim_ascii_start(bytes: &[u8]) -> &[u8] { let start = bytes .iter() .position(|b| !b.is_ascii_whitespace()) .unwrap_or(bytes.len()); &bytes[start..] } fn make_trailers_frame(trailers: HeaderMap) -> Bytes { let trailers = encode_trailers(trailers); let len = trailers.len(); assert!(len <= u32::MAX as usize); let mut frame = BytesMut::with_capacity(len + FRAME_HEADER_SIZE); frame.put_u8(GRPC_WEB_TRAILERS_BIT); frame.put_u32(len as u32); frame.put_slice(&trailers); frame.freeze() } /// Search some buffer for grpc-web trailers headers and return /// its location in the original buf. If `None` is returned we did /// not find a trailers in this buffer either because its incomplete /// or the buffer just contained grpc message frames. fn find_trailers(buf: &[u8]) -> Result { let mut len = 0; let mut temp_buf = buf; loop { // To check each frame, there must be at least GRPC_HEADER_SIZE // amount of bytes available otherwise the buffer is incomplete. if temp_buf.is_empty() || temp_buf.len() < GRPC_HEADER_SIZE { return Ok(FindTrailers::Done(len)); } let header = temp_buf.get_u8(); if header == GRPC_WEB_TRAILERS_BIT { return Ok(FindTrailers::Trailer(len)); } if !(header == 0 || header == 1) { return Err(Status::internal(format!( "Invalid header bit {header} expected 0 or 1" ))); } let msg_len = temp_buf.get_u32(); len += msg_len as usize + 4 + 1; // If the msg len of a non-grpc-web trailer frame is larger than // the overall buffer we know within that buffer there are no trailers. if len > buf.len() { return Ok(FindTrailers::IncompleteBuf); } temp_buf = &buf[len..]; } } #[derive(Debug, PartialEq, Eq)] enum FindTrailers { Trailer(usize), IncompleteBuf, Done(usize), } #[cfg(test)] mod tests { use tonic::Code; use super::*; #[test] fn encoding_constructors() { let cases = &[ (GRPC_WEB, Encoding::None), (GRPC_WEB_PROTO, Encoding::None), (GRPC_WEB_TEXT, Encoding::Base64), (GRPC_WEB_TEXT_PROTO, Encoding::Base64), ("foo", Encoding::None), ]; let mut headers = HeaderMap::new(); for case in cases { headers.insert(header::CONTENT_TYPE, case.0.parse().unwrap()); headers.insert(header::ACCEPT, case.0.parse().unwrap()); assert_eq!(Encoding::from_content_type(&headers), case.1, "{}", case.0); assert_eq!(Encoding::from_accept(&headers), case.1, "{}", case.0); } } #[test] fn decode_trailers() { let mut headers = HeaderMap::new(); headers.insert(Status::GRPC_STATUS, 0.into()); headers.insert( Status::GRPC_MESSAGE, "this is a message".try_into().unwrap(), ); let trailers = make_trailers_frame(headers.clone()); let map = decode_trailers_frame(trailers).unwrap().unwrap(); assert_eq!(headers, map); } #[test] fn find_trailers_non_buffered() { // Byte version of this: // b"\x80\0\0\0\x0fgrpc-status:0\r\n" let buf = [ 128, 0, 0, 0, 15, 103, 114, 112, 99, 45, 115, 116, 97, 116, 117, 115, 58, 48, 13, 10, ]; let out = find_trailers(&buf[..]).unwrap(); assert_eq!(out, FindTrailers::Trailer(0)); } #[test] fn find_trailers_buffered() { // Byte version of this: // b"\0\0\0\0L\n$975738af-1a17-4aea-b887-ed0bbced6093\x1a$da609e9b-f470-4cc0-a691-3fd6a005a436\x80\0\0\0\x0fgrpc-status:0\r\n" let buf = [ 0, 0, 0, 0, 76, 10, 36, 57, 55, 53, 55, 51, 56, 97, 102, 45, 49, 97, 49, 55, 45, 52, 97, 101, 97, 45, 98, 56, 56, 55, 45, 101, 100, 48, 98, 98, 99, 101, 100, 54, 48, 57, 51, 26, 36, 100, 97, 54, 48, 57, 101, 57, 98, 45, 102, 52, 55, 48, 45, 52, 99, 99, 48, 45, 97, 54, 57, 49, 45, 51, 102, 100, 54, 97, 48, 48, 53, 97, 52, 51, 54, 128, 0, 0, 0, 15, 103, 114, 112, 99, 45, 115, 116, 97, 116, 117, 115, 58, 48, 13, 10, ]; let out = find_trailers(&buf[..]).unwrap(); assert_eq!(out, FindTrailers::Trailer(81)); let trailers = decode_trailers_frame(Bytes::copy_from_slice(&buf[81..])) .unwrap() .unwrap(); let status = trailers.get(Status::GRPC_STATUS).unwrap(); assert_eq!(status.to_str().unwrap(), "0") } #[test] fn find_trailers_buffered_incomplete_message() { let buf = vec![ 0, 0, 0, 9, 238, 10, 233, 19, 18, 230, 19, 10, 9, 10, 1, 120, 26, 4, 84, 69, 88, 84, 18, 60, 10, 58, 10, 56, 3, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 118, 105, 97, 32, 119, 114, 105, 116, 101, 32, 100, 101, 108, 101, 103, 97, 116, 105, 111, 110, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, 121, 32, ]; let out = find_trailers(&buf[..]).unwrap(); assert_eq!(out, FindTrailers::IncompleteBuf); } #[test] #[ignore] fn find_trailers_buffered_incomplete_buf_bug() { let buf = std::fs::read("tests/incomplete-buf-bug.bin").unwrap(); let out = find_trailers(&buf[..]).unwrap_err(); assert_eq!(out.code(), Code::Internal); } #[test] fn decode_multiple_trailers() { let buf = b"\x80\0\0\0\x0fgrpc-status:0\r\ngrpc-message:\r\na:1\r\nb:2\r\n"; let trailers = decode_trailers_frame(Bytes::copy_from_slice(&buf[..])) .unwrap() .unwrap(); let mut expected = HeaderMap::new(); expected.insert(Status::GRPC_STATUS, "0".parse().unwrap()); expected.insert(Status::GRPC_MESSAGE, "".parse().unwrap()); expected.insert("a", "1".parse().unwrap()); expected.insert("b", "2".parse().unwrap()); assert_eq!(trailers, expected); } #[test] fn decode_trailers_with_space_after_colon() { let buf = b"\x80\0\0\0\x0fgrpc-status: 0\r\ngrpc-message: \r\n"; let trailers = decode_trailers_frame(Bytes::copy_from_slice(&buf[..])) .unwrap() .unwrap(); let mut expected = HeaderMap::new(); expected.insert(Status::GRPC_STATUS, "0".parse().unwrap()); expected.insert(Status::GRPC_MESSAGE, "".parse().unwrap()); assert_eq!(trailers, expected); } #[test] fn decode_trailers_space_after_colon() { // connect-rpc and standard HTTP use "key: value" (space after colon) let trailers_bytes = b"grpc-status: 0\r\ngrpc-message: this is a message\r\n"; let len = trailers_bytes.len(); let mut frame = BytesMut::new(); frame.put_u8(GRPC_WEB_TRAILERS_BIT); frame.put_u32(len as u32); frame.put_slice(&trailers_bytes[..]); let map = decode_trailers_frame(frame.freeze()).unwrap().unwrap(); let mut expected = HeaderMap::new(); expected.insert(Status::GRPC_STATUS, HeaderValue::from_static("0")); expected.insert( Status::GRPC_MESSAGE, HeaderValue::from_static("this is a message"), ); assert_eq!(map, expected); } #[test] fn decode_trailers_value_with_colons() { let trailers_bytes = b"grpc-status: 0\r\ngrpc-message: error: something: went wrong\r\n"; let len = trailers_bytes.len(); let mut frame = BytesMut::new(); frame.put_u8(GRPC_WEB_TRAILERS_BIT); frame.put_u32(len as u32); frame.put_slice(&trailers_bytes[..]); let map = decode_trailers_frame(frame.freeze()).unwrap().unwrap(); assert_eq!(map.get("grpc-status").unwrap(), "0"); assert_eq!( map.get("grpc-message").unwrap(), "error: something: went wrong" ); } } ================================================ FILE: tonic-web/src/client.rs ================================================ use http::header::CONTENT_TYPE; use http::{Request, Response, Version}; use pin_project::pin_project; use std::fmt; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll, ready}; use tower_layer::Layer; use tower_service::Service; use tracing::debug; use crate::call::GrpcWebCall; use crate::call::content_types::GRPC_WEB; /// Layer implementing the grpc-web protocol for clients. #[derive(Debug, Default, Clone)] pub struct GrpcWebClientLayer { _priv: (), } impl GrpcWebClientLayer { /// Create a new grpc-web for clients layer. pub fn new() -> GrpcWebClientLayer { Self::default() } } impl Layer for GrpcWebClientLayer { type Service = GrpcWebClientService; fn layer(&self, inner: S) -> Self::Service { GrpcWebClientService::new(inner) } } /// A [`Service`] that wraps some inner http service that will /// coerce requests coming from [`tonic::client::Grpc`] into proper /// `grpc-web` requests. #[derive(Debug, Clone)] pub struct GrpcWebClientService { inner: S, } impl GrpcWebClientService { /// Create a new grpc-web for clients service. pub fn new(inner: S) -> Self { Self { inner } } } impl Service> for GrpcWebClientService where S: Service>, Response = Response>, { type Response = Response>; type Error = S::Error; type Future = ResponseFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, mut req: Request) -> Self::Future { if req.version() == Version::HTTP_2 { debug!("coercing HTTP2 request to HTTP1.1"); *req.version_mut() = Version::HTTP_11; } req.headers_mut() .insert(CONTENT_TYPE, GRPC_WEB.try_into().unwrap()); let req = req.map(GrpcWebCall::client_request); let fut = self.inner.call(req); ResponseFuture { inner: fut } } } /// Response future for the [`GrpcWebService`](crate::GrpcWebService). #[pin_project] #[must_use = "futures do nothing unless polled"] pub struct ResponseFuture { #[pin] inner: F, } impl Future for ResponseFuture where F: Future, E>>, { type Output = Result>, E>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let res = ready!(self.project().inner.poll(cx)); Poll::Ready(res.map(|r| r.map(GrpcWebCall::client_response))) } } impl fmt::Debug for ResponseFuture { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ResponseFuture").finish() } } ================================================ FILE: tonic-web/src/layer.rs ================================================ use super::GrpcWebService; use tower_layer::Layer; /// Layer implementing the grpc-web protocol. #[derive(Debug, Default, Clone)] pub struct GrpcWebLayer { _priv: (), } impl GrpcWebLayer { /// Create a new grpc-web layer. pub fn new() -> GrpcWebLayer { Self::default() } } impl Layer for GrpcWebLayer { type Service = GrpcWebService; fn layer(&self, inner: S) -> Self::Service { GrpcWebService::new(inner) } } ================================================ FILE: tonic-web/src/lib.rs ================================================ //! grpc-web protocol translation for [`tonic`] services. //! //! [`tonic_web`] enables tonic servers to handle requests from [grpc-web] clients directly, //! without the need of an external proxy. It achieves this by wrapping individual tonic services //! with a [tower] service that performs the translation between protocols and handles `cors` //! requests. //! //! ## Enabling tonic services //! //! You can customize the CORS configuration composing the [`GrpcWebLayer`] with the cors layer of your choice. //! //! ```ignore //! #[tokio::main] //! async fn main() -> Result<(), Box> { //! let addr = "[::1]:50051".parse().unwrap(); //! let greeter = GreeterServer::new(MyGreeter::default()); //! //! Server::builder() //! .accept_http1(true) //! // This will apply the gRPC-Web translation layer //! .layer(GrpcWebLayer::new()) //! .add_service(greeter) //! .serve(addr) //! .await?; //! //! Ok(()) //! } //! ``` //! //! Alternatively, if you have a tls enabled server, you could skip setting `accept_http1` to `true`. //! This works because the browser will handle `ALPN`. //! //! ```ignore //! #[tokio::main] //! async fn main() -> Result<(), Box> { //! let cert = tokio::fs::read("server.pem").await?; //! let key = tokio::fs::read("server.key").await?; //! let identity = Identity::from_pem(cert, key); //! //! let addr = "[::1]:50051".parse().unwrap(); //! let greeter = GreeterServer::new(MyGreeter::default()); //! //! // No need to enable HTTP/1 //! Server::builder() //! .tls_config(ServerTlsConfig::new().identity(identity))? //! .layer(GrpcWebLayer::new()) //! .add_service(greeter) //! .serve(addr) //! .await?; //! //! Ok(()) //! } //! ``` //! //! ## Limitations //! //! * `tonic_web` is designed to work with grpc-web-compliant clients only. It is not expected to //! handle arbitrary HTTP/x.x requests or bespoke protocols. //! * Similarly, the cors support implemented by this crate will *only* handle grpc-web and //! grpc-web preflight requests. //! * Currently, grpc-web clients can only perform `unary` and `server-streaming` calls. These //! are the only requests this crate is designed to handle. Support for client and bi-directional //! streaming will be officially supported when clients do. //! * There is no support for web socket transports. //! //! //! [`tonic`]: https://github.com/hyperium/tonic //! [`tonic_web`]: https://github.com/hyperium/tonic //! [grpc-web]: https://github.com/grpc/grpc-web //! [tower]: https://github.com/tower-rs/tower #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] pub use call::GrpcWebCall; pub use client::{GrpcWebClientLayer, GrpcWebClientService}; pub use layer::GrpcWebLayer; pub use service::{GrpcWebService, ResponseFuture}; mod call; mod client; mod layer; mod service; type BoxError = Box; pub(crate) mod util { pub(crate) mod base64 { use base64::{ alphabet, engine::{ DecodePaddingMode, general_purpose::{GeneralPurpose, GeneralPurposeConfig}, }, }; pub(crate) const STANDARD: GeneralPurpose = GeneralPurpose::new( &alphabet::STANDARD, GeneralPurposeConfig::new() .with_encode_padding(true) .with_decode_padding_mode(DecodePaddingMode::Indifferent), ); } } ================================================ FILE: tonic-web/src/service.rs ================================================ use core::fmt; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll, ready}; use http::{HeaderMap, HeaderValue, Method, Request, Response, StatusCode, Version, header}; use pin_project::pin_project; use tonic::metadata::GRPC_CONTENT_TYPE; use tonic::{body::Body, server::NamedService}; use tower_service::Service; use tracing::{debug, trace}; use crate::call::content_types::is_grpc_web; use crate::call::{Encoding, GrpcWebCall}; /// Service implementing the grpc-web protocol. #[derive(Debug, Clone)] pub struct GrpcWebService { inner: S, } #[derive(Debug, PartialEq)] enum RequestKind<'a> { // The request is considered a grpc-web request if its `content-type` // header is exactly one of: // // - "application/grpc-web" // - "application/grpc-web+proto" // - "application/grpc-web-text" // - "application/grpc-web-text+proto" GrpcWeb { method: &'a Method, encoding: Encoding, accept: Encoding, }, // All other requests, including `application/grpc` Other(http::Version), } impl GrpcWebService { pub(crate) fn new(inner: S) -> Self { GrpcWebService { inner } } } impl Service> for GrpcWebService where S: Service, Response = Response>, ReqBody: http_body::Body + Send + 'static, ReqBody::Error: Into + fmt::Display, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into + fmt::Display, { type Response = Response; type Error = S::Error; type Future = ResponseFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, req: Request) -> Self::Future { match RequestKind::new(req.headers(), req.method(), req.version()) { // A valid grpc-web request, regardless of HTTP version. // // If the request includes an `origin` header, we verify it is allowed // to access the resource, an HTTP 403 response is returned otherwise. // // If the origin is allowed to access the resource or there is no // `origin` header present, translate the request into a grpc request, // call the inner service, and translate the response back to // grpc-web. RequestKind::GrpcWeb { method: &Method::POST, encoding, accept, } => { trace!(kind = "simple", path = ?req.uri().path(), ?encoding, ?accept); ResponseFuture { case: Case::GrpcWeb { future: self.inner.call(coerce_request(req, encoding)), accept, }, } } // The request's content-type matches one of the 4 supported grpc-web // content-types, but the request method is not `POST`. // This is not a valid grpc-web request, return HTTP 405. RequestKind::GrpcWeb { .. } => { debug!(kind = "simple", error="method not allowed", method = ?req.method()); ResponseFuture { case: Case::immediate(StatusCode::METHOD_NOT_ALLOWED), } } // All http/2 requests that are not grpc-web are passed through to the inner service, // whatever they are. RequestKind::Other(Version::HTTP_2) => { debug!(kind = "other h2", content_type = ?req.headers().get(header::CONTENT_TYPE)); ResponseFuture { case: Case::Other { future: self.inner.call(req.map(Body::new)), }, } } // Return HTTP 400 for all other requests. RequestKind::Other(_) => { debug!(kind = "other h1", content_type = ?req.headers().get(header::CONTENT_TYPE)); ResponseFuture { case: Case::immediate(StatusCode::BAD_REQUEST), } } } } } /// Response future for the [`GrpcWebService`]. #[pin_project] #[must_use = "futures do nothing unless polled"] pub struct ResponseFuture { #[pin] case: Case, } #[pin_project(project = CaseProj)] enum Case { GrpcWeb { #[pin] future: F, accept: Encoding, }, Other { #[pin] future: F, }, ImmediateResponse { res: Option, }, } impl Case { fn immediate(status: StatusCode) -> Self { let (res, ()) = Response::builder() .status(status) .body(()) .unwrap() .into_parts(); Self::ImmediateResponse { res: Some(res) } } } impl Future for ResponseFuture where F: Future, E>>, B: http_body::Body + Send + 'static, B::Error: Into + fmt::Display, { type Output = Result, E>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); match this.case.project() { CaseProj::GrpcWeb { future, accept } => { let res = ready!(future.poll(cx))?; Poll::Ready(Ok(coerce_response(res, *accept))) } CaseProj::Other { future } => future.poll(cx).map_ok(|res| res.map(Body::new)), CaseProj::ImmediateResponse { res } => { let res = Response::from_parts(res.take().unwrap(), Body::empty()); Poll::Ready(Ok(res)) } } } } impl NamedService for GrpcWebService { const NAME: &'static str = S::NAME; } impl fmt::Debug for ResponseFuture { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ResponseFuture").finish() } } impl<'a> RequestKind<'a> { fn new(headers: &'a HeaderMap, method: &'a Method, version: Version) -> Self { if is_grpc_web(headers) { return RequestKind::GrpcWeb { method, encoding: Encoding::from_content_type(headers), accept: Encoding::from_accept(headers), }; } RequestKind::Other(version) } } // Mutating request headers to conform to a gRPC request is not really // necessary for us at this point. We could remove most of these except // maybe for inserting `header::TE`, which tonic should check? fn coerce_request(mut req: Request, encoding: Encoding) -> Request where B: http_body::Body + Send + 'static, B::Error: Into + fmt::Display, { req.headers_mut().remove(header::CONTENT_LENGTH); req.headers_mut() .insert(header::CONTENT_TYPE, GRPC_CONTENT_TYPE); req.headers_mut() .insert(header::TE, HeaderValue::from_static("trailers")); req.headers_mut().insert( header::ACCEPT_ENCODING, HeaderValue::from_static("identity,deflate,gzip"), ); req.map(|b| Body::new(GrpcWebCall::request(b, encoding))) } fn coerce_response(res: Response, encoding: Encoding) -> Response where B: http_body::Body + Send + 'static, B::Error: Into + fmt::Display, { let mut res = res .map(|b| GrpcWebCall::response(b, encoding)) .map(Body::new); res.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_static(encoding.to_content_type()), ); res } #[cfg(test)] mod tests { use super::*; use crate::call::content_types::*; use http::header::{ ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, CONTENT_TYPE, ORIGIN, }; use tower_layer::Layer as _; type BoxFuture = Pin> + Send>>; #[derive(Debug, Clone)] struct Svc; impl tower_service::Service> for Svc { type Response = Response; type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, _: Request) -> Self::Future { Box::pin(async { Ok(Response::new(Body::default())) }) } } impl NamedService for Svc { const NAME: &'static str = "test"; } fn enable(service: S) -> tower_http::cors::Cors> where S: Service, Response = http::Response>, { tower_layer::Stack::new( crate::GrpcWebLayer::new(), tower_http::cors::CorsLayer::new(), ) .layer(service) } mod grpc_web { use super::*; use tower_layer::Layer; fn request() -> Request { Request::builder() .method(Method::POST) .header(CONTENT_TYPE, GRPC_WEB) .header(ORIGIN, "http://example.com") .body(Body::default()) .unwrap() } #[tokio::test] async fn default_cors_config() { let mut svc = enable(Svc); let res = svc.call(request()).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } #[tokio::test] async fn web_layer() { let mut svc = crate::GrpcWebLayer::new().layer(Svc); let res = svc.call(request()).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } #[tokio::test] async fn web_layer_with_axum() { let mut svc = axum::routing::Router::new() .route("/", axum::routing::post_service(Svc)) .layer(crate::GrpcWebLayer::new()); let res = svc.call(request()).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } #[tokio::test] async fn without_origin() { let mut svc = enable(Svc); let mut req = request(); req.headers_mut().remove(ORIGIN); let res = svc.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } #[tokio::test] async fn only_post_and_options_allowed() { let mut svc = enable(Svc); for method in &[ Method::GET, Method::PUT, Method::DELETE, Method::HEAD, Method::PATCH, ] { let mut req = request(); *req.method_mut() = method.clone(); let res = svc.call(req).await.unwrap(); assert_eq!( res.status(), StatusCode::METHOD_NOT_ALLOWED, "{method} should not be allowed" ); } } #[tokio::test] async fn grpc_web_content_types() { let mut svc = enable(Svc); for ct in &[GRPC_WEB_TEXT, GRPC_WEB_PROTO, GRPC_WEB_TEXT_PROTO, GRPC_WEB] { let mut req = request(); req.headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static(ct)); let res = svc.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } } } mod options { use super::*; fn request() -> Request { Request::builder() .method(Method::OPTIONS) .header(ORIGIN, "http://example.com") .header(ACCESS_CONTROL_REQUEST_HEADERS, "x-grpc-web") .header(ACCESS_CONTROL_REQUEST_METHOD, "POST") .body(Body::default()) .unwrap() } #[tokio::test] async fn valid_grpc_web_preflight() { let mut svc = enable(Svc); let res = svc.call(request()).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } } mod grpc { use super::*; fn request() -> Request { Request::builder() .version(Version::HTTP_2) .header(CONTENT_TYPE, GRPC_CONTENT_TYPE) .body(Body::default()) .unwrap() } #[tokio::test] async fn h2_is_ok() { let mut svc = enable(Svc); let req = request(); let res = svc.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK) } #[tokio::test] async fn h1_is_err() { let mut svc = enable(Svc); let req = Request::builder() .header(CONTENT_TYPE, GRPC_CONTENT_TYPE) .body(Body::default()) .unwrap(); let res = svc.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST) } #[tokio::test] async fn content_type_variants() { let mut svc = enable(Svc); for variant in &["grpc", "grpc+proto", "grpc+thrift", "grpc+foo"] { let mut req = request(); req.headers_mut().insert( CONTENT_TYPE, HeaderValue::from_maybe_shared(format!("application/{variant}")).unwrap(), ); let res = svc.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK) } } } mod other { use super::*; fn request() -> Request { Request::builder() .header(CONTENT_TYPE, "application/text") .body(Body::default()) .unwrap() } #[tokio::test] async fn h1_is_err() { let mut svc = enable(Svc); let res = svc.call(request()).await.unwrap(); assert_eq!(res.status(), StatusCode::BAD_REQUEST) } #[tokio::test] async fn h2_is_ok() { let mut svc = enable(Svc); let mut req = request(); *req.version_mut() = Version::HTTP_2; let res = svc.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK) } } } ================================================ FILE: tonic-xds/Cargo.toml ================================================ [package] name = "tonic-xds" version = "0.1.0-alpha.1" edition = "2024" rust-version.workspace = true homepage = "https://github.com/hyperium/tonic" repository = "https://github.com/hyperium/tonic" authors = [ "Ankur Mittal ", "Jeff Jiang ", "Lucio Franco ", "Yutao Ma " ] categories = ["web-programming", "network-programming", "asynchronous"] description = """ xDS routing and load balancing implementation for Tonic and Tower services """ keywords = ["grpc", "xds"] license = "MIT" publish = false exclude = ["proto/test/*"] [dependencies] tonic = "0.14" http = "1" tower = { version = "0.5", default-features = false, features = ["discover"] } dashmap = "6.1" thiserror = "2.0.17" url = "2.5.8" futures-core = "0.3.31" bytes = "1" xds-client = { version = "0.1.0-alpha.1", path = "../xds-client" } serde = { version = "1", features = ["derive"] } serde_json = "1" envoy-types = "0.7" prost = "0.14" regex = "1" [lints] workspace = true [dev-dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] } tonic = { version = "0.14", features = [ "server", "channel", "tls-ring" ] } tonic-prost = "0.14" tokio-stream = "0.1" tonic-prost-build = "0.14" ================================================ FILE: tonic-xds/examples/gen_test_proto.rs ================================================ //! Build script for the protobufs used for tests. //! To invoke, run: //! ``` //! cargo run -p tonic-xds --example gen_test_proto //! ``` use std::path::PathBuf; fn main() { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let proto_dir = manifest_dir.join("proto/test"); let proto_file = proto_dir.join("helloworld.proto"); let out_dir = manifest_dir.join("src/testutil/proto"); println!("Writing generated test protos to {}", out_dir.display()); tonic_prost_build::configure() .out_dir(proto_dir.clone()) .compile_protos( &[proto_file.to_str().unwrap()], &[proto_dir.to_str().unwrap()], ) .unwrap(); } ================================================ FILE: tonic-xds/proto/test/helloworld.proto ================================================ // Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; package helloworld; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } ================================================ FILE: tonic-xds/src/client/channel.rs ================================================ use crate::XdsUri; use crate::client::endpoint::{EndpointAddress, EndpointChannel}; use crate::client::lb::XdsLbService; use crate::client::route::XdsRoutingService; use crate::common::async_util::BoxFuture; use http::Request; use std::fmt::Debug; use std::sync::Arc; use std::task::{Context, Poll}; use tonic::{body::Body as TonicBody, client::GrpcService, transport::channel::Channel}; use tower::{BoxError, Service, load::Load, util::BoxCloneService}; #[cfg(test)] use { crate::client::cluster::ClusterClientRegistryGrpc, crate::client::route::XdsRoutingLayer, crate::xds::xds_manager::XdsManager, tower::ServiceBuilder, }; /// Configuration for building [`XdsChannel`] / [`XdsChannelGrpc`]. /// Currently, only support specifying the xDS URI for the target service. /// In the future, more configurations such as xDS management server address will be added. #[derive(Clone, Debug, Default)] pub struct XdsChannelConfig { target_uri: Option, } impl XdsChannelConfig { /// Sets the xDS URI for the channel. #[must_use] pub fn with_target_uri(mut self, target_uri: XdsUri) -> Self { self.target_uri = Some(target_uri); self } } /// `XdsChannel` is an xDS-capable [`tower::Service`] implementation. /// /// It routes requests according to the xDS configuration that it fetches from the xDS management server. /// The routing implementation is based on the [Google gRPC xDS features](https://grpc.github.io/grpc/core/md_doc_grpc_xds_features.html). /// /// # Type Parameters /// /// * `Req` - The request type that this channel accepts, as an example: `http::Request`. /// * `Endpoint` - The endpoint identifier type used for load balancing (e.g., socket address). /// * `S` - The underlying [`tower::Service`] implementation that handles individual endpoint connections. pub struct XdsChannel where Req: Send + 'static, S: Service, S::Response: Send + 'static, { config: Arc, // Currently the routing decision is directly executed by the XdsLbService. // In the future, we will add more layers in between for retries, request mirroring, etc. inner: XdsRoutingService>, } #[allow(clippy::missing_fields_in_debug)] impl Debug for XdsChannel where Req: Send + 'static, S: Service, S::Response: Send + 'static, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("XdsChannel") .field("config", &self.config) .finish() } } impl Clone for XdsChannel where Req: Send + 'static, S: Service, S::Response: Send + 'static, XdsRoutingService>: Clone, { fn clone(&self) -> Self { Self { config: self.config.clone(), inner: self.inner.clone(), } } } impl Service> for XdsChannel, Endpoint, S> where B: Send + 'static, Request: Send + 'static, Endpoint: std::hash::Hash + Eq + Clone + Send + 'static, S: Service> + Load + Send + 'static, S::Response: Send + 'static, S::Error: Into, S::Future: Send, ::Metric: std::fmt::Debug, { type Response = S::Response; type Error = BoxError; type Future = BoxFuture>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, request: Request) -> Self::Future { self.inner.call(request) } } /// A type alias for an `XdsChannel` that uses Tonic's Channel as the underlying transport. pub(crate) type XdsChannelTonicGrpc = XdsChannel, EndpointAddress, EndpointChannel>; /// A [`tonic::client::GrpcService`] implementation that can route and load-balance /// gRPC requests based on xDS configuration. pub type XdsChannelGrpc = BoxCloneService, http::Response, BoxError>; // Static assertion that XdsChannelGrpc and XdsChannelTonicGrpc implement GrpcService const _: fn() = || { fn assert_grpc_service>() {} assert_grpc_service::(); assert_grpc_service::(); }; /// Builder for creating an [`XdsChannel`] or [`XdsChannelGrpc`]. #[derive(Clone, Debug)] pub struct XdsChannelBuilder { #[allow(dead_code)] config: Arc, } impl XdsChannelBuilder { /// Create a builder from an channel configurations. #[must_use] pub fn with_config(config: XdsChannelConfig) -> Self { Self { config: Arc::new(config), } } /// Builds an `XdsChannel`, which takes generic request, endpoint, and service types and can be /// used for generic HTTP services. #[must_use] pub fn build_channel(&self) -> XdsChannel where Req: Send + 'static, S: Service, S::Response: Send + 'static, { todo!("Implement XdsChannel building logic"); } pub(crate) fn build_tonic_grpc_channel(&self) -> XdsChannelTonicGrpc { todo!("Implement XdsChannel building logic"); } /// Builds an `XdsChannelGrpc`, which is a type-erased gRPC channel. #[must_use] pub fn build_grpc_channel(&self) -> XdsChannelGrpc { BoxCloneService::new(self.build_tonic_grpc_channel()) } /// Builds an `XdsChannelGrpc` from the given xDS manager dependencies. /// This is primarily intended for testing purposes for now. /// [`XdsChannelBuilder::build_grpc_channel`] should build [`XdsManager`](crate::xds::xds_manager::XdsManager) /// as part of constructing `XdsChannelGrpc`. #[cfg(test)] pub(crate) fn build_grpc_channel_from_xds_manager( &self, xds_manager: Arc>>, ) -> XdsChannelGrpc { let routing_layer = XdsRoutingLayer::new(xds_manager.clone()); let cluster_registry = Arc::new(ClusterClientRegistryGrpc::new()); let lb_service = XdsLbService::new(cluster_registry, xds_manager.clone()); let service = ServiceBuilder::new() .layer(routing_layer) .service(lb_service); BoxCloneService::new(XdsChannelTonicGrpc { config: self.config.clone(), inner: service, }) } } #[cfg(test)] mod tests { use super::XdsChannelBuilder; use super::XdsChannelConfig; use crate::client::channel::XdsChannelGrpc; use crate::client::endpoint::EndpointAddress; use crate::client::endpoint::EndpointChannel; use crate::client::route::RouteDecision; use crate::client::route::RouteInput; use crate::common::async_util::BoxFuture; use crate::testutil::grpc::GreeterClient; use crate::testutil::grpc::HelloRequest; use crate::testutil::grpc::TestServer; use crate::xds::xds_manager::BoxDiscover; use crate::xds::xds_manager::{XdsClusterDiscovery, XdsRouter}; use std::sync::Arc; use tokio::sync::mpsc; use tonic::transport::Channel; use tower::discover::Change; /// Sets up multiple gRPC test servers and returns their addresses, clients and shutdown handles. async fn setup_grpc_servers( count: usize, ) -> (Vec, Vec) { use crate::testutil::grpc::spawn_greeter_server; let mut servers = Vec::new(); let mut server_addrs = Vec::new(); for i in 0..count { let server_name = format!("server-{i}"); let server = spawn_greeter_server(&server_name, None, None) .await .expect("Failed to spawn gRPC server"); server_addrs.push(server.addr.to_string()); servers.push(server); } (server_addrs, servers) } /// A mock XdsManager that provides pre-configured endpoints for testing. struct MockXdsManager { endpoints: Vec<(EndpointAddress, Channel)>, } impl MockXdsManager { /// Creates a new MockXdsManager from test servers. fn from_test_servers(servers: &[TestServer]) -> Self { let endpoints = servers .iter() .map(|s| { let addr = EndpointAddress::from(s.addr); (addr, s.channel.clone()) }) .collect(); Self { endpoints } } } impl XdsRouter for MockXdsManager { fn route(&self, _input: &RouteInput<'_>) -> BoxFuture { Box::pin(async move { RouteDecision { cluster: "test-cluster".to_string(), } }) } } impl XdsClusterDiscovery> for MockXdsManager { fn discover_cluster( &self, _cluster_name: &str, ) -> BoxDiscover> { let endpoints = self.endpoints.clone(); let (tx, rx) = mpsc::channel(16); tokio::spawn(async move { for (addr, channel) in endpoints { let endpoint_channel = EndpointChannel::new(channel); let change = Change::Insert(addr, endpoint_channel); tx.send(Ok(change)).await.expect("Failed to send SD change"); } }); Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx)) } } /// Sends multiple gRPC requests using the provided client and returns statistics about the requests. async fn send_grpc_requests( mut grpc_client: crate::testutil::grpc::GreeterClient, num_requests: usize, ) -> ( usize, std::collections::HashMap, std::collections::HashMap, ) { let mut successful_requests = 0; let mut error_types = std::collections::HashMap::new(); let mut server_counts = std::collections::HashMap::new(); for i in 0..num_requests { let request_timeout = tokio::time::Duration::from_secs(3); let request_future = grpc_client.say_hello(HelloRequest { name: format!("test-request-{i}"), }); match tokio::time::timeout(request_timeout, request_future).await { Ok(Ok(response)) => { successful_requests += 1; // Extract server name from response message (format: "server-X: test-request-Y") let message = response.into_inner().message; if let Some(server_name) = message.split(':').next() { *server_counts.entry(server_name.to_string()).or_insert(0) += 1; } } Ok(Err(e)) => { let error_type = format!("{e:?}").chars().take(80).collect::(); *error_types.entry(error_type).or_insert(0) += 1; } Err(_) => { *error_types.entry("Timeout".to_string()).or_insert(0) += 1; if error_types.get("Timeout").unwrap_or(&0) > &2 { break; } } } } (successful_requests, error_types, server_counts) } #[tokio::test] /// Tests the `XdsChannelGrpc` with a power-of-two-choices load balancer. async fn test_xds_channel_grpc_with_p2c_lb() { let num_requests = 1000; let num_servers = 5; let (_, servers) = setup_grpc_servers(num_servers).await; // Create a mock XdsManager with the test servers let xds_manager = Arc::new(MockXdsManager::from_test_servers(&servers)); let xds_channel_builder = XdsChannelBuilder::with_config(XdsChannelConfig::default()); let xds_channel = xds_channel_builder.build_grpc_channel_from_xds_manager(xds_manager.clone()); let client = GreeterClient::new(xds_channel); let (successful_requests, error_types, server_counts) = send_grpc_requests(client, num_requests).await; println!("Successful requests: {successful_requests}"); println!("Error types: {error_types:?}"); println!("Per-server call counts: {server_counts:?}"); assert_eq!( successful_requests, num_requests, "Expected 100% success rate. Got {successful_requests} successful out of {num_requests} requests. Errors: {error_types:?}", ); assert!( error_types.is_empty(), "Expected no errors but got: {error_types:?}", ); let actual_server_count = server_counts.len(); assert_eq!( actual_server_count, num_servers, "Expected all {num_servers} servers to receive requests, but only {actual_server_count} servers received traffic. Server counts: {server_counts:?}", ); let expected_per_server = num_requests / num_servers; let min_requests_per_server = (expected_per_server as f64 / 1.5) as usize; let max_requests_per_server = (expected_per_server as f64 * 1.5) as usize; for (server_name, count) in &server_counts { assert!( *count >= min_requests_per_server, "Server {server_name} received only {count} requests, expected at least {min_requests_per_server} (expected ~{expected_per_server} per server with 1.5x variance)", ); assert!( *count <= max_requests_per_server, "Server {server_name} received {count} requests, expected at most {max_requests_per_server} (expected ~{expected_per_server} per server with 1.5x variance)", ); } let total_server_requests: usize = server_counts.values().sum(); assert_eq!( total_server_requests, successful_requests, "Total server requests ({total_server_requests}) should equal successful requests ({successful_requests}). Server counts: {server_counts:?}", ); for server in servers { let _ = server.shutdown.send(()); let _ = server.handle.await; } } } ================================================ FILE: tonic-xds/src/client/cluster.rs ================================================ use crate::common::async_util::BoxFuture; use dashmap::DashMap; use http::{Request, Response}; use std::fmt::Debug; use std::future::Future; use std::hash::Hash; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use tonic::body::Body as TonicBody; use tower::{ BoxError, Service, balance::p2c::Balance, buffer::Buffer, discover::Discover, load::Load, }; type RespFut = BoxFuture>; const DEFAULT_BUFFER_CAPACITY: usize = 1024; /// `ClusterBalancer` is responsible for managing load balancing requests across multiple channels. /// Currently, `ClusterBalancer` leverges `tower::balance::p2c` for doing P2C load balancing. In the future, we will /// support more load balancing strategies as needed. pub(crate) struct ClusterBalancer where D: Discover, D::Key: Hash, { balancer: Balance, } impl ClusterBalancer where D: Discover, D::Key: Hash, D::Service: Service, >::Error: Into, { /// Creates a new `ClusterBalancer` with provided service discovery. pub(crate) fn new(discover: D) -> Self { Self { balancer: Balance::new(discover), } } /// Returns the number of endpoints currently tracked by the balancer. /// This can be useful for monitoring and debugging purposes. #[allow(dead_code)] pub(crate) fn len(&self) -> usize { self.balancer.len() } } impl Service for ClusterBalancer where D: Discover + Unpin, D::Key: Hash + Clone, D::Error: Into, D::Service: Service + Load, ::Metric: std::fmt::Debug, >::Error: Into + 'static, >::Future: Send + 'static, { type Response = as Service>::Response; type Error = as Service>::Error; type Future = RespFut; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.balancer.poll_ready(cx) } fn call(&mut self, req: Req) -> Self::Future { Box::pin(self.balancer.call(req)) } } /// `ClusterChannel` is similar to `tonic::transport::Channel`, but is for load-balancing across all /// the channels for a xDS Cluster. /// `ClusterChannel` should be cloned to be used in multi-threaded environment. It leverages a `tower::Buffer` to /// queue requests from multiple callers and behind the queue, it load-balances the requests across all /// available channels by leveraging the inner `ClusterBalancer` object. pub(crate) struct ClusterChannel where Req: Send + 'static, Resp: 'static, { // The mpsc channel between callers and the actual pool of channels. svc: Buffer>>, } impl Clone for ClusterChannel where Req: Send + 'static, Resp: 'static, { fn clone(&self) -> Self { Self { svc: self.svc.clone(), } } } impl ClusterChannel where Req: Send + 'static, Resp: 'static, { /// Creates a new `ClusterChannel` with the given service and picker. pub(crate) fn from_balancer(balancer: B, buffer_cap: usize) -> Self where B: Service> + Send + 'static, { let svc = Buffer::new(balancer, buffer_cap); Self { svc } } } impl Service for ClusterChannel where Req: Send + 'static, Resp: 'static, { type Response = Resp; type Error = BoxError; type Future = Pin> + Send>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { Service::poll_ready(&mut self.svc, cx).map_err(BoxError::from) } fn call(&mut self, request: Req) -> Self::Future { Box::pin(self.svc.call(request)) } } /// `ClusterClient` manages channels that load-balance for a xDS cluster. pub(crate) struct ClusterClient where Req: Send + 'static, Resp: 'static, { name: String, channel: ClusterChannel, } impl Debug for ClusterClient<(), ()> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClusterClient") .field("name", &self.name) .finish() } } impl ClusterClient where Req: Send + 'static, Resp: 'static, { /// Creates a new `ClusterClient` with the given cluster name and service discovery implementation. /// Currently, `tower::discover::Discover` is used for service discovery. pub(crate) fn new(name: String, discover: D) -> Self where D: Discover + Unpin + Send + 'static, D::Key: std::hash::Hash + Clone + Send, D::Error: Into, D::Service: Service + Load + Send + 'static, ::Metric: std::fmt::Debug, >::Error: Into, >::Future: Send + 'static, { let balancer = ClusterBalancer::new(discover); let channel = ClusterChannel::from_balancer(balancer, DEFAULT_BUFFER_CAPACITY); Self { name, channel } } /// Returns a channel that can be used to send RPCs to the cluster. pub(crate) fn channel(&self) -> ClusterChannel { self.channel.clone() } /// Returns the name of the cluster. #[allow(dead_code)] pub(crate) fn name(&self) -> &str { &self.name } } /// `ClusterRegistry` is the client registry for all xDS clusters. /// The xDS Tower service implementations uses this to get the client for a specific cluster. pub(crate) struct ClusterClientRegistry where Req: Send + 'static, Resp: 'static, { registry: DashMap>>, } impl ClusterClientRegistry where Req: Send + 'static, Resp: 'static, { /// Creates a new `ClusterClientRegistry`. pub(crate) fn new() -> Self { Self { registry: DashMap::new(), } } /// Get the client of a cluster with lazy discovery. pub(crate) fn get_cluster( &self, key: &str, discover_fn: F, ) -> Arc> where F: FnOnce() -> D, D: Discover + Unpin + Send + 'static, D::Key: std::hash::Hash + Clone + Send, D::Error: Into, D::Service: Service + Load + Send + 'static, ::Metric: std::fmt::Debug, >::Error: Into, >::Future: Send + 'static, { self.registry .entry(key.to_string()) .or_insert_with(|| { let name = key.to_string(); let discover = discover_fn(); Arc::new(ClusterClient::new(name, discover)) }) .clone() } } impl Default for ClusterClientRegistry where Req: Send + 'static, Resp: 'static, { fn default() -> Self { Self::new() } } /// A type erased registry for Tonic clients. /// This will be used by the xDS Tower Service implementations to get the client for a specific Tonic xDS cluster. #[allow(dead_code)] pub(crate) type ClusterClientRegistryGrpc = ClusterClientRegistry, Response>; ================================================ FILE: tonic-xds/src/client/endpoint.rs ================================================ use crate::common::async_util::BoxFuture; use std::net::SocketAddr; use std::sync::{Arc, atomic::AtomicU64, atomic::Ordering}; use std::task::{Context, Poll}; use tower::{Service, load::Load}; /// Represents the host part of an endpoint address #[derive(Debug, Clone, PartialEq, Eq, Hash)] enum EndpointHost { Ipv4(std::net::Ipv4Addr), Ipv6(std::net::Ipv6Addr), Hostname(String), } impl From for EndpointHost { fn from(s: String) -> Self { if let Ok(ipv4) = s.parse::() { EndpointHost::Ipv4(ipv4) } else if let Ok(ipv6) = s.parse::() { EndpointHost::Ipv6(ipv6) } else { EndpointHost::Hostname(s) } } } /// Represents a validated endpoint address extracted from xDS #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct EndpointAddress { /// The IP address or hostname host: EndpointHost, /// The port number port: u16, } impl EndpointAddress { /// Creates a new `EndpointAddress` from a host string and port. /// /// Attempts to parse the host as an IP address; falls back to hostname. #[allow(dead_code)] pub(crate) fn new(host: impl Into, port: u16) -> Self { Self { host: EndpointHost::from(host.into()), port, } } } impl std::fmt::Display for EndpointAddress { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.host { EndpointHost::Ipv4(ip) => write!(f, "{ip}:{}", self.port), EndpointHost::Ipv6(ip) => write!(f, "[{ip}]:{}", self.port), EndpointHost::Hostname(h) => write!(f, "{h}:{}", self.port), } } } impl From for EndpointAddress { fn from(addr: SocketAddr) -> Self { match addr { SocketAddr::V4(v4_addr) => Self { host: EndpointHost::Ipv4(*v4_addr.ip()), port: v4_addr.port(), }, SocketAddr::V6(v6_addr) => Self { host: EndpointHost::Ipv6(*v6_addr.ip()), port: v6_addr.port(), }, } } } /// RAII tracker for in-flight requests. /// This is mainly used to implement endpoint load reporting for load balancing purposes. #[derive(Clone, Debug, Default)] struct InFlightTracker { in_flight: Arc, } impl InFlightTracker { fn new(in_flight: Arc) -> Self { in_flight.fetch_add(1, Ordering::Relaxed); Self { in_flight } } } impl Drop for InFlightTracker { fn drop(&mut self) { self.in_flight.fetch_sub(1, Ordering::Relaxed); } } /// An endpoint channel for communicating with a single gRPC endpoint, with load reporting support for load balancing. pub(crate) struct EndpointChannel { inner: S, in_flight: Arc, } impl EndpointChannel { /// Creates a new `EndpointChannel`. /// This should be used by xDS implementations to construct channels to individual endpoints. #[allow(dead_code)] pub(crate) fn new(inner: S) -> Self { Self { inner, in_flight: Arc::new(AtomicU64::new(0)), } } } impl Clone for EndpointChannel where S: Clone, { fn clone(&self) -> Self { Self { inner: self.inner.clone(), in_flight: self.in_flight.clone(), } } } impl Service for EndpointChannel where S: Service + Send + 'static, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = BoxFuture>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, req: Req) -> Self::Future { let in_flight = InFlightTracker::new(self.in_flight.clone()); let fut = self.inner.call(req); // -1 when the inner future completes Box::pin(async move { let _in_flight_guard = in_flight; fut.await }) } } impl Load for EndpointChannel { type Metric = u64; fn load(&self) -> Self::Metric { self.in_flight.load(Ordering::Relaxed) } } ================================================ FILE: tonic-xds/src/client/lb.rs ================================================ use crate::client::cluster::ClusterClientRegistry; use crate::client::route::RouteDecision; use crate::common::async_util::BoxFuture; use crate::xds::xds_manager::XdsClusterDiscovery; use http::Request; use std::sync::Arc; use std::task::{Context, Poll}; use tower::ServiceExt; use tower::{BoxError, Service, load::Load}; /// Errors that can occur during load balancing. #[derive(Debug, Clone, thiserror::Error)] pub(crate) enum LoadBalancingError { #[error("No routing decision extension from the routing layer available")] NoRoutingDecision, } /// A Tower Service that performs load balancing based on routing decisions and xDS configuration. pub(crate) struct XdsLbService where Req: Send + 'static, S: Service, S::Response: Send + 'static, { cluster_registry: Arc>, cluster_discovery: Arc>, } impl XdsLbService where Req: Send + 'static, S: Service, S::Response: Send + 'static, { /// Creates a new `XdsLbService` with the given cluster client registry and xDS cluster discovery. #[allow(dead_code)] pub(crate) fn new( cluster_registry: Arc>, cluster_discovery: Arc>, ) -> Self { Self { cluster_registry, cluster_discovery, } } } impl Clone for XdsLbService where Req: Send + 'static, S: Service, S::Response: Send + 'static, { fn clone(&self) -> Self { Self { cluster_registry: self.cluster_registry.clone(), cluster_discovery: self.cluster_discovery.clone(), } } } impl Service> for XdsLbService, Endpoint, S> where Request: Send + 'static, S::Response: Send + 'static, Endpoint: std::hash::Hash + Eq + Clone + Send + 'static, S: Service> + Load + Send + 'static, S::Response: Send + 'static, S::Error: Into, S::Future: Send, ::Metric: std::fmt::Debug, { type Response = S::Response; type Error = BoxError; type Future = BoxFuture>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { // Under xDS, the destination cluster is decided by the routing layer, which takes // the request as an input. Therefore, we cannot determine readiness without // knowing the target cluster, which is tied to the request. Poll::Ready(Ok(())) } fn call(&mut self, request: Request) -> Self::Future { // Extract the routing decision from the request extensions. let Some(routing_decision) = request.extensions().get::().cloned() else { return Box::pin(async move { Err(LoadBalancingError::NoRoutingDecision.into()) }); }; // Get or create the cluster client for the target xDS cluster. let cluster_client = self .cluster_registry .get_cluster(&routing_decision.cluster, || { self.cluster_discovery .discover_cluster(&routing_decision.cluster) }); // Get the transport channel for the target xDS cluster. // The actual load-balancing will be performeed by the channel. let mut channel = cluster_client.channel(); Box::pin(async move { // This will block until the first endpoint is available. channel.ready().await?; channel.call(request).await }) } } ================================================ FILE: tonic-xds/src/client/mod.rs ================================================ pub(crate) mod channel; pub(crate) mod cluster; pub(crate) mod endpoint; pub(crate) mod lb; pub(crate) mod route; ================================================ FILE: tonic-xds/src/client/route.rs ================================================ use crate::common::async_util::BoxFuture; use crate::xds::xds_manager::XdsRouter; use http::Request; use std::sync::Arc; use std::task::{Context, Poll}; use tower::{Layer, Service}; /// Represents the input for xDS routing decisions. #[allow(dead_code)] pub(crate) struct RouteInput<'a> { /// The authority (host) of the request URI. pub authority: &'a str, /// The HTTP headers of the request. These can be used for header-based routing decisions. pub headers: &'a http::HeaderMap, } /// Represents the routing decision made by the xDS routing layer. #[derive(Clone)] pub(crate) struct RouteDecision { /// The name of the cluster to which the request should be routed. pub cluster: String, } /// Tower service for routing requests to the appropriate cluster based on the xDS routing configurations. /// Attaches routing decision as `RoutingDecision` to the request extensions. /// The `RoutingDecision` will be used by the `XdsLbService` to identify the xDS cluster to which the request should be routed. #[derive(Clone)] pub(crate) struct XdsRoutingService { /// The inner Tower service to which the request will be forwarded after routing decision is made. inner: S, /// The xDS router used to make routing decisions based on the request and the xDS routing configurations. xds_router: Arc, } impl Service> for XdsRoutingService where S: Service> + Clone + Send + 'static, B: Send + 'static, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = BoxFuture>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner.poll_ready(cx) } fn call(&mut self, mut request: Request) -> Self::Future { let xds_router = self.xds_router.clone(); let mut inner_service = self.inner.clone(); Box::pin(async move { let authority = request .uri() .authority() .map_or("", http::uri::Authority::as_str); let headers = &request.headers(); let route_input = RouteInput { authority, headers }; let route_decision = xds_router.route(&route_input).await; request.extensions_mut().insert(route_decision); inner_service.call(request).await }) } } /// Tower layer for routing requests to the appropriate cluster based on the `RouteConfiguration`. #[derive(Clone)] #[allow(dead_code)] pub(crate) struct XdsRoutingLayer { xds_router: Arc, } impl XdsRoutingLayer { /// Creates a new `XdsRoutingLayer` with the given `XdsRouter`. #[allow(dead_code)] pub(crate) fn new(xds_router: Arc) -> Self { Self { xds_router } } } impl Layer for XdsRoutingLayer { type Service = XdsRoutingService; fn layer(&self, service: S) -> Self::Service { XdsRoutingService { inner: service, xds_router: self.xds_router.clone(), } } } ================================================ FILE: tonic-xds/src/common/async_util.rs ================================================ //! Utilities for async operations. use std::future::Future; use std::pin::Pin; pub(crate) type BoxFuture = Pin + Send + 'static>>; ================================================ FILE: tonic-xds/src/common/mod.rs ================================================ pub(crate) mod async_util; ================================================ FILE: tonic-xds/src/lib.rs ================================================ //! # tonic-xds //! //! xDS (discovery service) support for [Tonic](https://docs.rs/tonic) gRPC clients as well as //! general [`tower::Service`]. //! //! This crate provides an xDS-enabled [`tonic::client::GrpcService`] implementation ([`XdsChannelGrpc`]) //! that automatically discovers, routes and load-balances across endpoints using the xDS protocol. //! The implementation will align with the //! [gRPC xDS features](https://github.com/grpc/grpc/blob/master/doc/grpc_xds_features.md). //! //! In addition to gRPC, this crate also provides a generic [`tower::Service`] implementation ([`XdsChannel`]) //! for enabling xDS features for generic Http clients. This can be used to support both gRPC and Http //! clients by the same xDS management server. //! //! ## Current Planned Features: //! //! - LDS / RDS / CDS / EDS subscriptions via ADS stream. //! - Client-side P2C load balancing //! - Other features will be added in future releases. //! //! ## Example //! //! ```rust,no_run //! use tonic_xds::{XdsChannelBuilder, XdsChannelConfig, XdsChannelGrpc, XdsUri}; //! //! let target_uri = XdsUri::parse( //! "xds:///myservice:50051" //! ).expect("fail to parse valid target URI"); //! //! let xds_channel = XdsChannelBuilder::with_config( //! XdsChannelConfig::default().with_target_uri(target_uri) //! ).build_grpc_channel(); //! //! // Use with your generated gRPC client //! // let client = MyServiceClient::new(xds_channel); //! // client.my_rpc_method(...).await; //! ``` //! //! ## How it works //! //! [`XdsChannelGrpc`] connects to an xDS management server and subscribes to resource updates for //! listeners, routes, clusters, and endpoints. Requests are automatically routed and load-balanced //! in stacked [`tower::Service`]s that implement the [gRPC xDS features](https://github.com/grpc/grpc/blob/master/doc/grpc_xds_features.md). pub(crate) mod client; pub(crate) mod common; pub(crate) mod xds; pub use client::channel::{XdsChannel, XdsChannelBuilder, XdsChannelConfig, XdsChannelGrpc}; pub use xds::uri::{XdsUri, XdsUriError}; #[cfg(test)] pub(crate) mod testutil; ================================================ FILE: tonic-xds/src/testutil/grpc.rs ================================================ //! Test utilities for gRPC servers and clients. use std::error::Error; use std::net::SocketAddr; use tokio::{net::TcpListener, sync::oneshot}; use tonic::server::NamedService; use tonic::transport::{Channel, ClientTlsConfig, Endpoint, Server, ServerTlsConfig}; use tonic::{Request, Response, Status}; pub(crate) use crate::testutil::proto::helloworld::{ HelloReply, HelloRequest, greeter_client::GreeterClient, greeter_server::{Greeter, GreeterServer}, }; #[derive(Default)] struct MyGreeter { msg: String, } #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello(&self, req: Request) -> Result, Status> { Ok(Response::new(HelloReply { message: format!("{}: {}", self.msg, req.into_inner().name), })) } } /// A test server that runs a gRPC service and provides a channel for clients to connect. pub(crate) struct TestServer { /// The gRPC channel for talking to the test server. pub channel: Channel, /// Signal the server to shutdown. pub shutdown: oneshot::Sender<()>, /// Handle to wait for server to exit. pub handle: tokio::task::JoinHandle>, /// Server address. pub addr: SocketAddr, } impl NamedService for TestServer { const NAME: &'static str = "TestServer"; } /// Spawns a gRPC greeter server for testing purposes. pub(crate) async fn spawn_greeter_server( msg: &str, server_tls: Option, client_tls: Option, ) -> Result> { // Bind to an ephemeral port (random free port assigned by OS) let listener = TcpListener::bind("127.0.0.1:0").await?; let addr = listener.local_addr()?; let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener); let (tx, rx) = oneshot::channel(); let svc = GreeterServer::new(MyGreeter { msg: msg.to_string(), }); let handle = tokio::spawn(async move { let mut builder = if let Some(tls) = server_tls { Server::builder().tls_config(tls)? } else { Server::builder() }; let res = builder .add_service(svc) .serve_with_incoming_shutdown(incoming, async { let _ = rx.await; }) .await; match res { Ok(_) => println!("Server exited cleanly"), Err(e) => eprintln!("Server error: {e}"), } Ok(()) }); let channel = if let Some(client_tls) = client_tls { let endpoint_str = format!("https://{addr}"); Endpoint::from_shared(endpoint_str)? .tls_config(client_tls)? .connect() .await? } else { let endpoint_str = format!("http://{addr}"); Endpoint::from_shared(endpoint_str)?.connect().await? }; Ok(TestServer { channel, shutdown: tx, handle, addr, }) } ================================================ FILE: tonic-xds/src/testutil/mod.rs ================================================ #[cfg(test)] pub(crate) mod grpc; #[cfg(test)] pub(crate) mod proto; ================================================ FILE: tonic-xds/src/testutil/proto/helloworld.rs ================================================ // This file is @generated by prost-build. /// The request message containing the user's name. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct HelloRequest { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, } /// The response message containing the greetings #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct HelloReply { #[prost(string, tag = "1")] pub message: ::prost::alloc::string::String, } /// Generated client implementations. pub mod greeter_client { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value )] use tonic::codegen::http::Uri; use tonic::codegen::*; /// The greeting service definition. #[derive(Debug, Clone)] pub struct GreeterClient { inner: tonic::client::Grpc, } impl GreeterClient { /// Attempt to create a new client by connecting to a given endpoint. pub async fn connect(dst: D) -> Result where D: TryInto, D::Error: Into, { let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; Ok(Self::new(conn)) } } impl GreeterClient where T: tonic::client::GrpcService, T::Error: Into, T::ResponseBody: Body + std::marker::Send + 'static, ::Error: Into + std::marker::Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } pub fn with_origin(inner: T, origin: Uri) -> Self { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } pub fn with_interceptor( inner: T, interceptor: F, ) -> GreeterClient> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response< >::ResponseBody, >, >, >>::Error: Into + std::marker::Send + std::marker::Sync, { GreeterClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); self } /// Enable decompressing responses. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.accept_compressed(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_decoding_message_size(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.inner = self.inner.max_encoding_message_size(limit); self } /// Sends a greeting pub async fn say_hello( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { self.inner.ready().await.map_err(|e| { tonic::Status::unknown(format!("Service was not ready: {}", e.into())) })?; let codec = tonic_prost::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/helloworld.Greeter/SayHello"); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("helloworld.Greeter", "SayHello")); self.inner.unary(req, path, codec).await } } } /// Generated server implementations. pub mod greeter_server { #![allow( unused_variables, dead_code, missing_docs, clippy::wildcard_imports, clippy::let_unit_value )] use tonic::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with GreeterServer. #[async_trait] pub trait Greeter: std::marker::Send + std::marker::Sync + 'static { /// Sends a greeting async fn say_hello( &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; } /// The greeting service definition. #[derive(Debug)] pub struct GreeterServer { inner: Arc, accept_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings, max_decoding_message_size: Option, max_encoding_message_size: Option, } impl GreeterServer { pub fn new(inner: T) -> Self { Self::from_arc(Arc::new(inner)) } pub fn from_arc(inner: Arc) -> Self { Self { inner, accept_compression_encodings: Default::default(), send_compression_encodings: Default::default(), max_decoding_message_size: None, max_encoding_message_size: None, } } pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService where F: tonic::service::Interceptor, { InterceptedService::new(Self::new(inner), interceptor) } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.accept_compression_encodings.enable(encoding); self } /// Compress responses with the given encoding, if the client supports it. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.send_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. /// /// Default: `4MB` #[must_use] pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. /// /// Default: `usize::MAX` #[must_use] pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.max_encoding_message_size = Some(limit); self } } impl tonic::codegen::Service> for GreeterServer where T: Greeter, B: Body + std::marker::Send + 'static, B::Error: Into + std::marker::Send + 'static, { type Response = http::Response; type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready( &mut self, _cx: &mut Context<'_>, ) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, req: http::Request) -> Self::Future { match req.uri().path() { "/helloworld.Greeter/SayHello" => { #[allow(non_camel_case_types)] struct SayHelloSvc(pub Arc); impl tonic::server::UnaryService for SayHelloSvc { type Response = super::HelloReply; type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { ::say_hello(&inner, request).await }; Box::pin(fut) } } let accept_compression_encodings = self.accept_compression_encodings; let send_compression_encodings = self.send_compression_encodings; let max_decoding_message_size = self.max_decoding_message_size; let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { let method = SayHelloSvc(inner); let codec = tonic_prost::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( accept_compression_encodings, send_compression_encodings, ) .apply_max_message_size_config( max_decoding_message_size, max_encoding_message_size, ); let res = grpc.unary(method, req).await; Ok(res) }; Box::pin(fut) } _ => Box::pin(async move { let mut response = http::Response::new(tonic::body::Body::default()); let headers = response.headers_mut(); headers.insert( tonic::Status::GRPC_STATUS, (tonic::Code::Unimplemented as i32).into(), ); headers.insert( http::header::CONTENT_TYPE, tonic::metadata::GRPC_CONTENT_TYPE, ); Ok(response) }), } } } impl Clone for GreeterServer { fn clone(&self) -> Self { let inner = self.inner.clone(); Self { inner, accept_compression_encodings: self.accept_compression_encodings, send_compression_encodings: self.send_compression_encodings, max_decoding_message_size: self.max_decoding_message_size, max_encoding_message_size: self.max_encoding_message_size, } } } /// Generated gRPC service name pub const SERVICE_NAME: &str = "helloworld.Greeter"; impl tonic::server::NamedService for GreeterServer { const NAME: &'static str = SERVICE_NAME; } } ================================================ FILE: tonic-xds/src/testutil/proto/mod.rs ================================================ //! This module contains Protobuf definitions for tests. //! To regenerate, run `cargo run -p tonic-xds --example gen_test_proto`. #[cfg(test)] #[allow(unreachable_pub, missing_docs)] pub(crate) mod helloworld; ================================================ FILE: tonic-xds/src/xds/bootstrap.rs ================================================ #![allow(dead_code)] //! xDS bootstrap configuration. //! //! Parses the bootstrap JSON from `GRPC_XDS_BOOTSTRAP` (file path) or //! `GRPC_XDS_BOOTSTRAP_CONFIG` (inline JSON) environment variables, //! per gRFC A27. use serde::Deserialize; use xds_client::message::{Locality, Node}; /// Environment variable pointing to a bootstrap JSON file path. const ENV_BOOTSTRAP_FILE: &str = "GRPC_XDS_BOOTSTRAP"; /// Environment variable containing inline bootstrap JSON. const ENV_BOOTSTRAP_CONFIG: &str = "GRPC_XDS_BOOTSTRAP_CONFIG"; /// Parsed xDS bootstrap configuration. #[derive(Debug, Clone, Deserialize)] #[non_exhaustive] pub(crate) struct BootstrapConfig { /// xDS management servers to connect to. pub xds_servers: Vec, /// Node identity sent to the xDS server. #[serde(default)] pub node: NodeConfig, } /// Configuration for a single xDS management server. #[derive(Debug, Clone, Deserialize)] pub(crate) struct XdsServerConfig { /// URI of the xDS server (e.g., `"xds.example.com:443"`). pub server_uri: String, /// Ordered list of channel credentials. The client uses the first supported type. #[serde(default)] pub channel_creds: Vec, /// Server features (e.g., `["xds_v3"]`). #[serde(default)] pub server_features: Vec, } /// A channel credential entry from the bootstrap config. #[derive(Debug, Clone, Deserialize)] pub(crate) struct ChannelCredentialConfig { /// Credential type (e.g., `"insecure"`, `"tls"`, `"google_default"`). #[serde(rename = "type")] pub cred_type: ChannelCredentialType, } /// Channel credential type from the bootstrap config. /// /// Known types are deserialized into specific variants; unrecognized types /// are captured as `Unsupported(String)` so they can be skipped gracefully. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(rename_all = "snake_case")] pub(crate) enum ChannelCredentialType { Insecure, Tls, #[serde(untagged)] Unsupported(String), } /// Node identity configuration from bootstrap JSON. #[derive(Debug, Clone, Default, Deserialize)] pub(crate) struct NodeConfig { /// Opaque node identifier. #[serde(default)] pub id: String, /// Cluster the node belongs to. pub cluster: Option, /// Locality where the node is running. pub locality: Option, } /// Locality configuration from bootstrap JSON. #[derive(Debug, Clone, Deserialize)] pub(crate) struct LocalityConfig { #[serde(default)] pub region: String, #[serde(default)] pub zone: String, #[serde(default)] pub sub_zone: String, } /// Errors that can occur when loading bootstrap configuration. #[derive(Debug, thiserror::Error)] pub(crate) enum BootstrapError { #[error("neither {ENV_BOOTSTRAP_FILE} nor {ENV_BOOTSTRAP_CONFIG} environment variable is set")] NotConfigured, #[error("failed to read bootstrap file '{path}': {source}")] ReadFile { path: String, source: std::io::Error, }, #[error("failed to parse bootstrap JSON: {0}")] InvalidJson(#[from] serde_json::Error), #[error("bootstrap config validation failed: {0}")] Validation(String), } impl BootstrapConfig { /// Create a bootstrap configuration directly from struct fields. pub(crate) fn new( xds_servers: Vec, node: NodeConfig, ) -> Result { let config = Self { xds_servers, node }; config.validate()?; Ok(config) } /// Load bootstrap configuration from environment variables. /// /// Checks `GRPC_XDS_BOOTSTRAP` (file path) first, then falls back to /// `GRPC_XDS_BOOTSTRAP_CONFIG` (inline JSON). pub(crate) fn from_env() -> Result { if let Ok(path) = std::env::var(ENV_BOOTSTRAP_FILE) { let json = std::fs::read_to_string(&path) .map_err(|e| BootstrapError::ReadFile { path, source: e })?; return Self::from_json(&json); } if let Ok(json) = std::env::var(ENV_BOOTSTRAP_CONFIG) { return Self::from_json(&json); } Err(BootstrapError::NotConfigured) } /// Parse bootstrap configuration from a JSON string. pub(crate) fn from_json(json: &str) -> Result { let config: BootstrapConfig = serde_json::from_str(json)?; config.validate()?; Ok(config) } fn validate(&self) -> Result<(), BootstrapError> { if self.xds_servers.is_empty() { return Err(BootstrapError::Validation( "xds_servers must not be empty".into(), )); } for (i, server) in self.xds_servers.iter().enumerate() { if server.server_uri.is_empty() { return Err(BootstrapError::Validation(format!( "xds_servers[{i}].server_uri must not be empty" ))); } } Ok(()) } /// Returns the URI of the first xDS server. pub(crate) fn server_uri(&self) -> &str { self.xds_servers .first() .map(|s| s.server_uri.as_str()) .expect("xds_servers validated non-empty") } /// Select the first supported channel credential type from the first server's config. /// /// Per gRFC A27, the client stops at the first credential type it supports. /// Returns `None` if no supported credential type is found. pub(crate) fn selected_credential(&self) -> Option<&ChannelCredentialType> { self.xds_servers .first()? .channel_creds .iter() .map(|c| &c.cred_type) .find(|t| { matches!( t, ChannelCredentialType::Insecure | ChannelCredentialType::Tls ) }) } } impl From for Node { fn from(config: NodeConfig) -> Self { let mut node = Node::new("tonic-xds", env!("CARGO_PKG_VERSION")); if !config.id.is_empty() { node = node.with_id(config.id); } if let Some(cluster) = config.cluster { node = node.with_cluster(cluster); } if let Some(locality) = config.locality { node = node.with_locality(Locality { region: locality.region, zone: locality.zone, sub_zone: locality.sub_zone, }); } node } } #[cfg(test)] mod tests { use super::*; fn minimal_json() -> &'static str { r#"{ "xds_servers": [{"server_uri": "xds.example.com:443"}], "node": {"id": "test-node"} }"# } fn full_json() -> &'static str { r#"{ "xds_servers": [{ "server_uri": "xds.example.com:443", "channel_creds": [ {"type": "google_default"}, {"type": "tls"}, {"type": "insecure"} ], "server_features": ["xds_v3"] }], "node": { "id": "projects/123/nodes/456", "cluster": "test-cluster", "locality": { "region": "us-east1", "zone": "us-east1-b", "sub_zone": "rack1" } } }"# } #[test] fn parse_minimal() { let config = BootstrapConfig::from_json(minimal_json()).unwrap(); assert_eq!(config.xds_servers.len(), 1); assert_eq!(config.server_uri(), "xds.example.com:443"); assert_eq!(config.node.id, "test-node"); assert!(config.node.cluster.is_none()); assert!(config.node.locality.is_none()); } #[test] fn parse_full() { let config = BootstrapConfig::from_json(full_json()).unwrap(); assert_eq!(config.xds_servers[0].server_uri, "xds.example.com:443"); assert_eq!(config.xds_servers[0].channel_creds.len(), 3); assert!(matches!( config.xds_servers[0].channel_creds[0].cred_type, ChannelCredentialType::Unsupported(_) )); assert_eq!(config.xds_servers[0].server_features, vec!["xds_v3"]); assert_eq!(config.node.id, "projects/123/nodes/456"); assert_eq!(config.node.cluster.as_deref(), Some("test-cluster")); let locality = config.node.locality.as_ref().unwrap(); assert_eq!(locality.region, "us-east1"); assert_eq!(locality.zone, "us-east1-b"); assert_eq!(locality.sub_zone, "rack1"); } #[test] fn node_from_full_config() { let config = BootstrapConfig::from_json(full_json()).unwrap(); let node = Node::from(config.node); assert_eq!(node.id.as_deref(), Some("projects/123/nodes/456")); assert_eq!(node.cluster.as_deref(), Some("test-cluster")); assert_eq!(node.user_agent_name, "tonic-xds"); let locality = node.locality.unwrap(); assert_eq!(locality.region, "us-east1"); assert_eq!(locality.zone, "us-east1-b"); assert_eq!(locality.sub_zone, "rack1"); } #[test] fn node_from_minimal_config() { let config = BootstrapConfig::from_json(minimal_json()).unwrap(); let node = Node::from(config.node); assert_eq!(node.id.as_deref(), Some("test-node")); assert!(node.cluster.is_none()); assert!(node.locality.is_none()); } #[test] fn selected_credential_first_supported_wins() { let config = BootstrapConfig::from_json(full_json()).unwrap(); // google_default skipped, tls is first supported assert_eq!( config.selected_credential(), Some(&ChannelCredentialType::Tls) ); } #[test] fn selected_credential_insecure() { let json = r#"{ "xds_servers": [{ "server_uri": "localhost:5000", "channel_creds": [{"type": "insecure"}] }], "node": {"id": "n1"} }"#; let config = BootstrapConfig::from_json(json).unwrap(); assert_eq!( config.selected_credential(), Some(&ChannelCredentialType::Insecure) ); } #[test] fn selected_credential_none_supported() { let json = r#"{ "xds_servers": [{ "server_uri": "localhost:5000", "channel_creds": [{"type": "google_default"}] }], "node": {"id": "n1"} }"#; let config = BootstrapConfig::from_json(json).unwrap(); assert_eq!(config.selected_credential(), None); } #[test] fn selected_credential_empty_creds() { let config = BootstrapConfig::from_json(minimal_json()).unwrap(); assert_eq!(config.selected_credential(), None); } #[test] fn empty_xds_servers_fails() { let json = r#"{"xds_servers": [], "node": {"id": "n1"}}"#; let err = BootstrapConfig::from_json(json).unwrap_err(); assert!(err.to_string().contains("xds_servers must not be empty")); } #[test] fn empty_server_uri_fails() { let json = r#"{"xds_servers": [{"server_uri": ""}], "node": {"id": "n1"}}"#; let err = BootstrapConfig::from_json(json).unwrap_err(); assert!(err.to_string().contains("server_uri must not be empty")); } #[test] fn invalid_json_fails() { let err = BootstrapConfig::from_json("not json").unwrap_err(); assert!(matches!(err, BootstrapError::InvalidJson(_))); } #[test] fn missing_required_field_fails() { let json = r#"{"node": {"id": "n1"}}"#; let err = BootstrapConfig::from_json(json).unwrap_err(); assert!(err.to_string().contains("xds_servers")); } #[test] fn node_without_id() { let json = r#"{ "xds_servers": [{"server_uri": "localhost:5000"}] }"#; let config = BootstrapConfig::from_json(json).unwrap(); let node = Node::from(config.node); assert!(node.id.is_none()); } } ================================================ FILE: tonic-xds/src/xds/mod.rs ================================================ pub(crate) mod bootstrap; pub(crate) mod resource; // TODO: remove dead_code once routing is wired into the client layer #[allow(dead_code)] pub(crate) mod routing; pub(crate) mod uri; pub(crate) mod xds_manager; ================================================ FILE: tonic-xds/src/xds/resource/cluster.rs ================================================ //! Validated Cluster resource (CDS). use bytes::Bytes; use envoy_types::pb::envoy::config::cluster::v3::{Cluster, cluster}; use prost::Message; use xds_client::resource::TypeUrl; use xds_client::{Error, Resource}; /// Validated Cluster resource. #[derive(Debug, Clone)] pub(crate) struct ClusterResource { pub name: String, /// The EDS service name for endpoint discovery. /// If not set, the cluster name is used. pub eds_service_name: Option, /// The load balancing policy for this cluster. pub lb_policy: LbPolicy, } /// Load balancing policies. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum LbPolicy { RoundRobin, LeastRequest, } impl Resource for ClusterResource { type Message = Cluster; const TYPE_URL: TypeUrl = TypeUrl::new("type.googleapis.com/envoy.config.cluster.v3.Cluster"); const ALL_RESOURCES_REQUIRED_IN_SOTW: bool = true; fn deserialize(bytes: Bytes) -> xds_client::Result { Cluster::decode(bytes).map_err(Into::into) } fn name(message: &Self::Message) -> &str { &message.name } fn validate(message: Self::Message) -> xds_client::Result { let name = message.name; if name.is_empty() { return Err(Error::Validation("cluster name is empty".into())); } let eds_service_name = message .eds_cluster_config .map(|eds| eds.service_name) .filter(|s| !s.is_empty()); let lb_policy = match cluster::LbPolicy::try_from(message.lb_policy) { Ok(cluster::LbPolicy::RoundRobin) => LbPolicy::RoundRobin, Ok(cluster::LbPolicy::LeastRequest) => LbPolicy::LeastRequest, _ => { return Err(Error::Validation(format!( "unsupported load balancing policy: {}", message.lb_policy ))); } }; Ok(ClusterResource { name, eds_service_name, lb_policy, }) } } impl ClusterResource { /// Returns the EDS service name for cascading EDS subscriptions. /// Falls back to the cluster name if no EDS service name is set. pub(crate) fn eds_service_name(&self) -> &str { self.eds_service_name.as_deref().unwrap_or(&self.name) } } #[cfg(test)] mod tests { use super::*; use envoy_types::pb::envoy::config::cluster::v3::cluster::EdsClusterConfig; fn make_cluster(name: &str) -> Cluster { Cluster { name: name.to_string(), lb_policy: cluster::LbPolicy::RoundRobin as i32, ..Default::default() } } #[test] fn test_validate_basic() { let cluster = make_cluster("my-cluster"); let validated = ClusterResource::validate(cluster).expect("should validate"); assert_eq!(validated.name, "my-cluster"); assert_eq!(validated.lb_policy, LbPolicy::RoundRobin); assert!(validated.eds_service_name.is_none()); } #[test] fn test_eds_service_name_defaults_to_cluster_name() { let cluster = make_cluster("my-cluster"); let validated = ClusterResource::validate(cluster).unwrap(); assert_eq!(validated.eds_service_name(), "my-cluster"); } #[test] fn test_eds_service_name() { let cluster = Cluster { name: "my-cluster".to_string(), eds_cluster_config: Some(EdsClusterConfig { service_name: "eds-svc".to_string(), ..Default::default() }), lb_policy: cluster::LbPolicy::RoundRobin as i32, ..Default::default() }; let validated = ClusterResource::validate(cluster).unwrap(); assert_eq!(validated.eds_service_name.as_deref(), Some("eds-svc")); assert_eq!(validated.eds_service_name(), "eds-svc"); } #[test] fn test_least_request_lb_policy() { let cluster = Cluster { name: "lr-cluster".to_string(), lb_policy: cluster::LbPolicy::LeastRequest as i32, ..Default::default() }; let validated = ClusterResource::validate(cluster).unwrap(); assert_eq!(validated.lb_policy, LbPolicy::LeastRequest); } #[test] fn test_unsupported_lb_policy_is_rejected() { let cluster = Cluster { name: "rh-cluster".to_string(), lb_policy: cluster::LbPolicy::RingHash as i32, ..Default::default() }; let err = ClusterResource::validate(cluster).unwrap_err(); assert!( err.to_string() .contains("unsupported load balancing policy") ); } #[test] fn test_validate_empty_name() { let cluster = make_cluster(""); let err = ClusterResource::validate(cluster).unwrap_err(); assert!(err.to_string().contains("cluster name is empty")); } #[test] fn test_all_resources_required() { assert!(ClusterResource::ALL_RESOURCES_REQUIRED_IN_SOTW); } #[test] fn test_deserialize_roundtrip() { let cluster = make_cluster("test"); let bytes = cluster.encode_to_vec(); let deserialized = ClusterResource::deserialize(Bytes::from(bytes)).unwrap(); assert_eq!(ClusterResource::name(&deserialized), "test"); } } ================================================ FILE: tonic-xds/src/xds/resource/endpoints.rs ================================================ //! Validated ClusterLoadAssignment resource (EDS). use bytes::Bytes; use envoy_types::pb::envoy::config::core::v3::{ HealthStatus as EnvoyHealthStatus, address, socket_address, }; use envoy_types::pb::envoy::config::endpoint::v3::{ ClusterLoadAssignment, LbEndpoint, lb_endpoint, }; use prost::Message; use xds_client::resource::TypeUrl; use xds_client::{Error, Resource}; use crate::client::endpoint::EndpointAddress; /// Validated ClusterLoadAssignment (EDS resource). #[derive(Debug)] pub(crate) struct EndpointsResource { pub cluster_name: String, pub localities: Vec, } /// Endpoints within a locality. #[derive(Debug)] pub(crate) struct LocalityEndpoints { pub locality: Option, pub endpoints: Vec, pub load_balancing_weight: u32, pub priority: u32, } /// Locality information for a set of endpoints. #[derive(Debug, PartialEq, Eq, Hash)] pub(crate) struct Locality { pub region: String, pub zone: String, pub sub_zone: String, } /// A single validated endpoint. #[derive(Debug)] pub(crate) struct ResolvedEndpoint { pub address: EndpointAddress, pub health_status: HealthStatus, pub load_balancing_weight: u32, } /// Health status of an endpoint. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum HealthStatus { Unknown, Healthy, Unhealthy, Draining, } impl From for HealthStatus { fn from(value: i32) -> Self { match EnvoyHealthStatus::try_from(value) { Ok(EnvoyHealthStatus::Healthy) => Self::Healthy, // Envoy's health_check.proto defines TIMEOUT as "interpreted by Envoy as // UNHEALTHY". Per gRFC A27, only HEALTHY and UNKNOWN are considered usable. Ok(EnvoyHealthStatus::Unhealthy) | Ok(EnvoyHealthStatus::Timeout) => Self::Unhealthy, Ok(EnvoyHealthStatus::Draining) => Self::Draining, _ => Self::Unknown, } } } impl Resource for EndpointsResource { type Message = ClusterLoadAssignment; const TYPE_URL: TypeUrl = TypeUrl::new("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"); const ALL_RESOURCES_REQUIRED_IN_SOTW: bool = false; fn deserialize(bytes: Bytes) -> xds_client::Result { ClusterLoadAssignment::decode(bytes).map_err(Into::into) } fn name(message: &Self::Message) -> &str { &message.cluster_name } fn validate(message: Self::Message) -> xds_client::Result { let cluster_name = message.cluster_name; if cluster_name.is_empty() { return Err(Error::Validation( "ClusterLoadAssignment missing cluster_name".into(), )); } let mut localities = Vec::with_capacity(message.endpoints.len()); for locality_endpoints in message.endpoints { let mut endpoints = Vec::with_capacity(locality_endpoints.lb_endpoints.len()); for lb_ep in locality_endpoints.lb_endpoints { if let Some(ep) = validate_lb_endpoint(lb_ep)? { endpoints.push(ep); } } let weight = locality_endpoints .load_balancing_weight .map(|w| w.value) .unwrap_or(0); let locality = locality_endpoints.locality.map(|l| Locality { region: l.region, zone: l.zone, sub_zone: l.sub_zone, }); localities.push(LocalityEndpoints { locality, endpoints, load_balancing_weight: weight, priority: locality_endpoints.priority, }); } Ok(EndpointsResource { cluster_name, localities, }) } } fn validate_lb_endpoint(lb_ep: LbEndpoint) -> xds_client::Result> { let health_status = HealthStatus::from(lb_ep.health_status); let host_identifier = match lb_ep.host_identifier { Some(lb_endpoint::HostIdentifier::Endpoint(ep)) => ep, // Skip unsupported or missing host_identifier variants (e.g. named // endpoints). These are not relevant to gRPC and should not cause the // entire EDS resource to be NACKed — the control plane may be serving // both envoy proxies and gRPC clients. _ => return Ok(None), }; let addr = host_identifier .address .ok_or_else(|| Error::Validation("endpoint missing address".into()))?; let addr = match addr.address { Some(address::Address::SocketAddress(sa)) => { let port = match sa.port_specifier { Some(socket_address::PortSpecifier::PortValue(p)) => p as u16, _ => { return Err(Error::Validation( "endpoint address missing numeric port".into(), )); } }; EndpointAddress::new(sa.address, port) } _ => { return Err(Error::Validation( "only socket addresses are supported for gRPC endpoints".into(), )); } }; let weight = lb_ep.load_balancing_weight.map(|w| w.value).unwrap_or(1); Ok(Some(ResolvedEndpoint { address: addr, health_status, load_balancing_weight: weight, })) } impl EndpointsResource { /// Returns all healthy endpoints (Unknown and Healthy status). pub(crate) fn healthy_endpoints(&self) -> impl Iterator { self.localities .iter() .flat_map(|l| &l.endpoints) .filter(|e| { matches!( e.health_status, HealthStatus::Unknown | HealthStatus::Healthy ) }) } } #[cfg(test)] mod tests { use super::*; use envoy_types::pb::envoy::config::core::v3::{Address, SocketAddress}; use envoy_types::pb::envoy::config::endpoint::v3::{Endpoint, LocalityLbEndpoints}; use envoy_types::pb::google::protobuf::UInt32Value; fn make_lb_endpoint(ip: &str, port: u32, health: i32) -> LbEndpoint { LbEndpoint { host_identifier: Some(lb_endpoint::HostIdentifier::Endpoint(Endpoint { address: Some(Address { address: Some(address::Address::SocketAddress(SocketAddress { address: ip.to_string(), port_specifier: Some(socket_address::PortSpecifier::PortValue(port)), ..Default::default() })), }), ..Default::default() })), health_status: health, load_balancing_weight: Some(UInt32Value { value: 1 }), ..Default::default() } } fn make_cla(cluster_name: &str) -> ClusterLoadAssignment { ClusterLoadAssignment { cluster_name: cluster_name.to_string(), endpoints: vec![LocalityLbEndpoints { lb_endpoints: vec![ make_lb_endpoint("10.0.0.1", 8080, EnvoyHealthStatus::Healthy as i32), make_lb_endpoint("10.0.0.2", 8080, EnvoyHealthStatus::Unknown as i32), make_lb_endpoint("10.0.0.3", 8080, EnvoyHealthStatus::Unhealthy as i32), ], load_balancing_weight: Some(UInt32Value { value: 100 }), priority: 0, ..Default::default() }], ..Default::default() } } #[test] fn test_validate_basic() { let cla = make_cla("my-cluster"); let validated = EndpointsResource::validate(cla).expect("should validate"); assert_eq!(validated.cluster_name, "my-cluster"); assert_eq!(validated.localities.len(), 1); assert_eq!(validated.localities[0].endpoints.len(), 3); } #[test] fn test_healthy_endpoints() { let cla = make_cla("my-cluster"); let validated = EndpointsResource::validate(cla).unwrap(); let healthy: Vec<_> = validated.healthy_endpoints().collect(); // Healthy + Unknown = 2 (Unhealthy excluded) assert_eq!(healthy.len(), 2); } #[test] fn test_validate_empty_cluster_name() { let cla = ClusterLoadAssignment { cluster_name: String::new(), ..Default::default() }; let err = EndpointsResource::validate(cla).unwrap_err(); assert!(err.to_string().contains("cluster_name")); } #[test] fn test_eds_allows_partial_responses_in_sotw() { // EDS resources are per-cluster, so SotW responses may contain only a subset. // Unlike LDS/CDS which require all resources in every SotW response. assert!(!EndpointsResource::ALL_RESOURCES_REQUIRED_IN_SOTW); } #[test] fn test_deserialize_roundtrip() { let cla = make_cla("test"); let bytes = cla.encode_to_vec(); let deserialized = EndpointsResource::deserialize(Bytes::from(bytes)).unwrap(); assert_eq!(EndpointsResource::name(&deserialized), "test"); } #[test] fn test_endpoint_with_weight() { let cla = make_cla("c1"); let validated = EndpointsResource::validate(cla).unwrap(); for ep in &validated.localities[0].endpoints { assert_eq!(ep.load_balancing_weight, 1); } } } ================================================ FILE: tonic-xds/src/xds/resource/listener.rs ================================================ //! Validated Listener resource (LDS). use bytes::Bytes; use envoy_types::pb::envoy::config::listener::v3::Listener; use envoy_types::pb::envoy::extensions::filters::network::http_connection_manager::v3::{ HttpConnectionManager, http_connection_manager::RouteSpecifier, }; use prost::Message; use xds_client::resource::TypeUrl; use xds_client::{Error, Resource}; use super::route_config::RouteConfigResource; /// How the listener obtains its route configuration. #[derive(Debug)] pub(crate) enum RouteSource { /// Route configuration fetched dynamically via RDS. Rds(String), /// Route configuration embedded inline in the listener. Inline(RouteConfigResource), } /// Validated Listener resource. /// /// Extracts the route source from the /// `ApiListener` -> `HttpConnectionManager` -> `route_specifier` chain. #[derive(Debug)] pub(crate) struct ListenerResource { pub name: String, pub route_source: RouteSource, } impl Resource for ListenerResource { type Message = Listener; const TYPE_URL: TypeUrl = TypeUrl::new("type.googleapis.com/envoy.config.listener.v3.Listener"); const ALL_RESOURCES_REQUIRED_IN_SOTW: bool = true; fn deserialize(bytes: Bytes) -> xds_client::Result { Listener::decode(bytes).map_err(Into::into) } fn name(message: &Self::Message) -> &str { &message.name } fn validate(message: Self::Message) -> xds_client::Result { let name = message.name; // gRPC listeners must have an ApiListener. let api_listener = message .api_listener .ok_or_else(|| Error::Validation("listener missing api_listener field".into()))?; // The ApiListener contains an Any that should be HttpConnectionManager. let any = api_listener.api_listener.ok_or_else(|| { Error::Validation("api_listener missing inner api_listener Any field".into()) })?; let hcm = HttpConnectionManager::decode(Bytes::from(any.value)).map_err(|e| { Error::Validation(format!("failed to decode HttpConnectionManager: {e}")) })?; let route_specifier = hcm.route_specifier.ok_or_else(|| { Error::Validation("HttpConnectionManager missing route_specifier".into()) })?; match route_specifier { RouteSpecifier::Rds(rds) => { if rds.route_config_name.is_empty() { return Err(Error::Validation("RDS route_config_name is empty".into())); } Ok(ListenerResource { name, route_source: RouteSource::Rds(rds.route_config_name), }) } RouteSpecifier::RouteConfig(route_config) => { let validated = RouteConfigResource::validate(route_config)?; Ok(ListenerResource { name, route_source: RouteSource::Inline(validated), }) } RouteSpecifier::ScopedRoutes(_) => Err(Error::Validation( "scoped_routes not supported for gRPC".into(), )), } } } impl ListenerResource { /// Returns the RDS route config name for cascading subscriptions. pub(crate) fn route_config_name(&self) -> Option<&str> { match &self.route_source { RouteSource::Rds(name) => Some(name), RouteSource::Inline(_) => None, } } } #[cfg(test)] mod tests { use super::*; use envoy_types::pb::envoy::config::listener::v3::ApiListener; use envoy_types::pb::envoy::extensions::filters::network::http_connection_manager::v3::Rds; use envoy_types::pb::google::protobuf::Any; fn make_rds_listener(name: &str, route_config_name: &str) -> Listener { let rds = Rds { route_config_name: route_config_name.to_string(), ..Default::default() }; let hcm = HttpConnectionManager { route_specifier: Some(RouteSpecifier::Rds(rds)), ..Default::default() }; let hcm_any = Any { type_url: "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager".to_string(), value: hcm.encode_to_vec().into(), }; Listener { name: name.to_string(), api_listener: Some(ApiListener { api_listener: Some(hcm_any), }), ..Default::default() } } #[test] fn test_validate_rds_listener() { let listener = make_rds_listener("test-listener", "route-config-1"); let validated = ListenerResource::validate(listener).expect("should validate"); assert_eq!(validated.name, "test-listener"); assert!( matches!(&validated.route_source, RouteSource::Rds(name) if name == "route-config-1") ); assert_eq!(validated.route_config_name(), Some("route-config-1")); } #[test] fn test_validate_missing_api_listener() { let listener = Listener { name: "test-listener".to_string(), ..Default::default() }; let err = ListenerResource::validate(listener).unwrap_err(); assert!(err.to_string().contains("api_listener")); } #[test] fn test_validate_empty_rds_name() { let listener = make_rds_listener("test-listener", ""); let err = ListenerResource::validate(listener).unwrap_err(); assert!(err.to_string().contains("route_config_name is empty")); } #[test] fn test_deserialize_valid() { let listener = make_rds_listener("test", "rc1"); let bytes = listener.encode_to_vec(); let deserialized = ListenerResource::deserialize(Bytes::from(bytes)).expect("should deserialize"); assert_eq!(ListenerResource::name(&deserialized), "test"); } #[test] fn test_deserialize_invalid_bytes() { let result = ListenerResource::deserialize(Bytes::from_static(b"invalid")); assert!(result.is_err()); } #[test] fn test_type_url() { assert_eq!( ListenerResource::TYPE_URL.as_str(), "type.googleapis.com/envoy.config.listener.v3.Listener" ); } #[test] fn test_all_resources_required() { assert!(ListenerResource::ALL_RESOURCES_REQUIRED_IN_SOTW); } #[test] fn test_validate_inline_route_config() { use envoy_types::pb::envoy::config::route::v3::route_match::PathSpecifier; use envoy_types::pb::envoy::config::route::v3::{ RouteAction, RouteConfiguration, RouteMatch, VirtualHost, route::Action, }; let route_config = RouteConfiguration { name: "inline-rc".to_string(), virtual_hosts: vec![VirtualHost { name: "vh1".to_string(), domains: vec!["*".to_string()], routes: vec![envoy_types::pb::envoy::config::route::v3::Route { r#match: Some(RouteMatch { path_specifier: Some(PathSpecifier::Prefix("/".to_string())), ..Default::default() }), action: Some(Action::Route(RouteAction { cluster_specifier: Some( envoy_types::pb::envoy::config::route::v3::route_action::ClusterSpecifier::Cluster( "cluster-1".to_string(), ), ), ..Default::default() })), ..Default::default() }], ..Default::default() }], ..Default::default() }; let hcm = HttpConnectionManager { route_specifier: Some(RouteSpecifier::RouteConfig(route_config)), ..Default::default() }; let hcm_any = Any { type_url: "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager".to_string(), value: hcm.encode_to_vec().into(), }; let listener = Listener { name: "inline-listener".to_string(), api_listener: Some(ApiListener { api_listener: Some(hcm_any), }), ..Default::default() }; let validated = ListenerResource::validate(listener).expect("should validate"); assert_eq!(validated.name, "inline-listener"); assert!(matches!(&validated.route_source, RouteSource::Inline(_))); assert!(validated.route_config_name().is_none()); } } ================================================ FILE: tonic-xds/src/xds/resource/mod.rs ================================================ #![allow(dead_code, unused_imports)] //! xDS resource type implementations. //! //! Each module implements [`xds_client::Resource`] for one of the four resource types: //! - [`ListenerResource`] (LDS) //! - [`RouteConfigResource`] (RDS) //! - [`ClusterResource`] (CDS) //! - [`EndpointsResource`] (EDS) //! //! These are *validated* types containing only the fields relevant to gRPC pub(crate) mod cluster; pub(crate) mod endpoints; pub(crate) mod listener; pub(crate) mod route_config; pub(crate) use cluster::{ClusterResource, LbPolicy}; pub(crate) use endpoints::{EndpointsResource, LocalityEndpoints, ResolvedEndpoint}; pub(crate) use listener::ListenerResource; pub(crate) use route_config::{RouteConfigResource, VirtualHostConfig}; ================================================ FILE: tonic-xds/src/xds/resource/route_config.rs ================================================ //! Validated RouteConfiguration resource (RDS). use std::collections::HashSet; use bytes::Bytes; use envoy_types::pb::envoy::config::route::v3::{ RouteConfiguration, RouteMatch, route, route_action, route_match, }; use prost::Message; use regex::Regex; use xds_client::resource::TypeUrl; use xds_client::{Error, Resource}; /// Validated RouteConfiguration. #[derive(Debug, Clone)] pub(crate) struct RouteConfigResource { pub name: String, pub virtual_hosts: Vec, } /// Validated virtual host with domain matching and routes. #[derive(Debug, Clone)] pub(crate) struct VirtualHostConfig { pub name: String, pub domains: Vec, pub routes: Vec, } /// A validated route with match criteria and action. #[derive(Debug, Clone)] pub(crate) struct RouteConfig { pub match_criteria: RouteConfigMatch, pub action: RouteConfigAction, } /// Validated route match criteria. #[derive(Debug, Clone)] pub(crate) struct RouteConfigMatch { pub path_specifier: PathSpecifierConfig, pub headers: Vec, pub case_sensitive: bool, } /// Path matching specifier. #[derive(Debug, Clone)] pub(crate) enum PathSpecifierConfig { Prefix(String), Path(String), SafeRegex(Regex), } /// Header matching criteria. #[derive(Debug, Clone)] pub(crate) struct HeaderMatcherConfig { pub name: String, pub match_specifier: HeaderMatchSpecifierConfig, pub invert_match: bool, } /// Header match specifier variants. #[derive(Debug, Clone)] pub(crate) enum HeaderMatchSpecifierConfig { Exact(String), SafeRegex(Regex), Prefix(String), Suffix(String), Contains(String), /// Match if header is present (any value). Present, } /// Route action deciding where to send traffic. #[derive(Debug, Clone)] pub(crate) enum RouteConfigAction { Cluster(String), WeightedClusters(Vec), } /// A cluster with an associated weight for traffic splitting. #[derive(Debug, Clone)] pub(crate) struct WeightedCluster { pub name: String, pub weight: u32, } impl Resource for RouteConfigResource { type Message = RouteConfiguration; const TYPE_URL: TypeUrl = TypeUrl::new("type.googleapis.com/envoy.config.route.v3.RouteConfiguration"); const ALL_RESOURCES_REQUIRED_IN_SOTW: bool = false; fn deserialize(bytes: Bytes) -> xds_client::Result { RouteConfiguration::decode(bytes).map_err(Into::into) } fn name(message: &Self::Message) -> &str { &message.name } fn validate(message: Self::Message) -> xds_client::Result { let name = message.name; if message.virtual_hosts.is_empty() { return Err(Error::Validation(format!( "route configuration '{name}' has no virtual hosts" ))); } let mut virtual_hosts = Vec::with_capacity(message.virtual_hosts.len()); for vh in message.virtual_hosts { if vh.domains.is_empty() { return Err(Error::Validation(format!( "virtual host '{}' has no domains", vh.name ))); } let mut routes = Vec::with_capacity(vh.routes.len()); for route in vh.routes { let validated_route = validate_route(route)?; routes.push(validated_route); } virtual_hosts.push(VirtualHostConfig { name: vh.name, domains: vh.domains, routes, }); } Ok(RouteConfigResource { name, virtual_hosts, }) } } fn validate_route( route: envoy_types::pb::envoy::config::route::v3::Route, ) -> xds_client::Result { let route_match = route .r#match .ok_or_else(|| Error::Validation("route missing match field".into()))?; let match_criteria = validate_route_match(route_match)?; let action = route .action .ok_or_else(|| Error::Validation("route missing action field".into()))?; let validated_action = match action { route::Action::Route(route_action) => validate_route_action(route_action)?, route::Action::NonForwardingAction(_) => { // Non-forwarding actions are used in xDS server-side, not client-side. return Err(Error::Validation( "non_forwarding_action not supported for client-side routing".into(), )); } _ => { return Err(Error::Validation( "only route action is supported for client routing".into(), )); } }; Ok(RouteConfig { match_criteria, action: validated_action, }) } fn validate_route_match(rm: RouteMatch) -> xds_client::Result { let path_specifier = match rm.path_specifier { Some(route_match::PathSpecifier::Prefix(p)) => PathSpecifierConfig::Prefix(p), Some(route_match::PathSpecifier::Path(p)) => PathSpecifierConfig::Path(p), Some(route_match::PathSpecifier::SafeRegex(r)) => { let re = Regex::new(&r.regex) .map_err(|e| Error::Validation(format!("invalid path regex '{}': {e}", r.regex)))?; PathSpecifierConfig::SafeRegex(re) } None => { // Default: empty prefix matches everything. PathSpecifierConfig::Prefix(String::new()) } _ => { return Err(Error::Validation( "unsupported path specifier variant".into(), )); } }; let case_sensitive = rm.case_sensitive.map(|v| v.value).unwrap_or(true); let mut headers = Vec::with_capacity(rm.headers.len()); for hm in rm.headers { let validated_hm = validate_header_matcher(hm)?; headers.push(validated_hm); } Ok(RouteConfigMatch { path_specifier, headers, case_sensitive, }) } fn validate_header_matcher( hm: envoy_types::pb::envoy::config::route::v3::HeaderMatcher, ) -> xds_client::Result { use envoy_types::pb::envoy::config::route::v3::header_matcher::HeaderMatchSpecifier; use envoy_types::pb::envoy::r#type::matcher::v3::string_matcher::MatchPattern; let match_specifier = match hm.header_match_specifier { Some(HeaderMatchSpecifier::ExactMatch(v)) => HeaderMatchSpecifierConfig::Exact(v), Some(HeaderMatchSpecifier::SafeRegexMatch(r)) => { let re = Regex::new(&r.regex).map_err(|e| { Error::Validation(format!("invalid header regex '{}': {e}", r.regex)) })?; HeaderMatchSpecifierConfig::SafeRegex(re) } Some(HeaderMatchSpecifier::PresentMatch(_)) => HeaderMatchSpecifierConfig::Present, Some(HeaderMatchSpecifier::StringMatch(sm)) => match sm.match_pattern { Some(MatchPattern::Exact(v)) => HeaderMatchSpecifierConfig::Exact(v), Some(MatchPattern::Prefix(v)) => HeaderMatchSpecifierConfig::Prefix(v), Some(MatchPattern::Suffix(v)) => HeaderMatchSpecifierConfig::Suffix(v), Some(MatchPattern::Contains(v)) => HeaderMatchSpecifierConfig::Contains(v), Some(MatchPattern::SafeRegex(r)) => { let re = Regex::new(&r.regex).map_err(|e| { Error::Validation(format!("invalid header regex '{}': {e}", r.regex)) })?; HeaderMatchSpecifierConfig::SafeRegex(re) } _ => { return Err(Error::Validation( "unsupported StringMatcher pattern".into(), )); } }, None => HeaderMatchSpecifierConfig::Present, _ => { return Err(Error::Validation( "unsupported header match specifier".into(), )); } }; Ok(HeaderMatcherConfig { name: hm.name, match_specifier, invert_match: hm.invert_match, }) } fn validate_route_action( ra: envoy_types::pb::envoy::config::route::v3::RouteAction, ) -> xds_client::Result { match ra.cluster_specifier { Some(route_action::ClusterSpecifier::Cluster(name)) => { if name.is_empty() { return Err(Error::Validation("cluster name is empty".into())); } Ok(RouteConfigAction::Cluster(name)) } Some(route_action::ClusterSpecifier::WeightedClusters(wc)) => { if wc.clusters.is_empty() { return Err(Error::Validation("weighted_clusters is empty".into())); } let clusters: Vec = wc .clusters .into_iter() .map(|c| WeightedCluster { name: c.name, weight: c.weight.map(|w| w.value).unwrap_or(0), }) .collect(); Ok(RouteConfigAction::WeightedClusters(clusters)) } Some(_) => Err(Error::Validation( "unsupported cluster specifier variant".into(), )), None => Err(Error::Validation( "route action missing cluster specifier".into(), )), } } impl RouteConfigResource { /// Returns cluster names referenced by this route configuration for cascading CDS subscriptions. pub(crate) fn cluster_names(&self) -> HashSet { let mut clusters = HashSet::new(); for vh in &self.virtual_hosts { for route in &vh.routes { match &route.action { RouteConfigAction::Cluster(name) => { clusters.insert(name.clone()); } RouteConfigAction::WeightedClusters(wcs) => { for wc in wcs { clusters.insert(wc.name.clone()); } } } } } clusters } } #[cfg(test)] mod tests { use super::*; use envoy_types::pb::envoy::config::route::v3::{ RouteAction, VirtualHost, route::Action, route_action::ClusterSpecifier, }; fn make_route(prefix: &str, cluster: &str) -> envoy_types::pb::envoy::config::route::v3::Route { envoy_types::pb::envoy::config::route::v3::Route { r#match: Some(RouteMatch { path_specifier: Some(route_match::PathSpecifier::Prefix(prefix.to_string())), ..Default::default() }), action: Some(Action::Route(RouteAction { cluster_specifier: Some(ClusterSpecifier::Cluster(cluster.to_string())), ..Default::default() })), ..Default::default() } } fn make_route_config(name: &str) -> RouteConfiguration { RouteConfiguration { name: name.to_string(), virtual_hosts: vec![VirtualHost { name: "vh1".to_string(), domains: vec!["*".to_string()], routes: vec![make_route("/", "cluster-1")], ..Default::default() }], ..Default::default() } } #[test] fn test_validate_basic() { let rc = make_route_config("rc-1"); let validated = RouteConfigResource::validate(rc).expect("should validate"); assert_eq!(validated.name, "rc-1"); assert_eq!(validated.virtual_hosts.len(), 1); assert_eq!(validated.virtual_hosts[0].routes.len(), 1); } #[test] fn test_cluster_names() { let rc = make_route_config("rc-1"); let validated = RouteConfigResource::validate(rc).unwrap(); let clusters = validated.cluster_names(); assert_eq!(clusters.len(), 1); assert!(clusters.contains("cluster-1")); } #[test] fn test_validate_empty_domains() { let rc = RouteConfiguration { name: "rc".to_string(), virtual_hosts: vec![VirtualHost { name: "vh-no-domains".to_string(), domains: vec![], routes: vec![], ..Default::default() }], ..Default::default() }; let err = RouteConfigResource::validate(rc).unwrap_err(); assert!(err.to_string().contains("no domains")); } #[test] fn test_validate_empty_cluster_name() { let rc = RouteConfiguration { name: "rc".to_string(), virtual_hosts: vec![VirtualHost { name: "vh1".to_string(), domains: vec!["*".to_string()], routes: vec![make_route("/", "")], ..Default::default() }], ..Default::default() }; let err = RouteConfigResource::validate(rc).unwrap_err(); assert!(err.to_string().contains("cluster name is empty")); } #[test] fn test_validate_exact_path() { let route = envoy_types::pb::envoy::config::route::v3::Route { r#match: Some(RouteMatch { path_specifier: Some(route_match::PathSpecifier::Path( "/service/Method".to_string(), )), ..Default::default() }), action: Some(Action::Route(RouteAction { cluster_specifier: Some(ClusterSpecifier::Cluster("c1".to_string())), ..Default::default() })), ..Default::default() }; let rc = RouteConfiguration { name: "rc".to_string(), virtual_hosts: vec![VirtualHost { name: "vh1".to_string(), domains: vec!["*".to_string()], routes: vec![route], ..Default::default() }], ..Default::default() }; let validated = RouteConfigResource::validate(rc).unwrap(); assert!(matches!( &validated.virtual_hosts[0].routes[0] .match_criteria .path_specifier, PathSpecifierConfig::Path(p) if p == "/service/Method" )); } #[test] fn test_cascade_weighted_clusters() { use envoy_types::pb::envoy::config::route::v3::{ WeightedCluster, weighted_cluster::ClusterWeight, }; use envoy_types::pb::google::protobuf::UInt32Value; let route = envoy_types::pb::envoy::config::route::v3::Route { r#match: Some(RouteMatch { path_specifier: Some(route_match::PathSpecifier::Prefix("/".to_string())), ..Default::default() }), action: Some(Action::Route(RouteAction { cluster_specifier: Some(route_action::ClusterSpecifier::WeightedClusters( WeightedCluster { clusters: vec![ ClusterWeight { name: "c1".to_string(), weight: Some(UInt32Value { value: 70 }), ..Default::default() }, ClusterWeight { name: "c2".to_string(), weight: Some(UInt32Value { value: 30 }), ..Default::default() }, ], ..Default::default() }, )), ..Default::default() })), ..Default::default() }; let rc = RouteConfiguration { name: "rc".to_string(), virtual_hosts: vec![VirtualHost { name: "vh1".to_string(), domains: vec!["*".to_string()], routes: vec![route], ..Default::default() }], ..Default::default() }; let validated = RouteConfigResource::validate(rc).unwrap(); let clusters = validated.cluster_names(); assert_eq!(clusters.len(), 2); assert!(clusters.contains("c1")); assert!(clusters.contains("c2")); } #[test] fn test_not_all_resources_required() { assert!(!RouteConfigResource::ALL_RESOURCES_REQUIRED_IN_SOTW); } #[test] fn test_deserialize_roundtrip() { let rc = make_route_config("rc-1"); let bytes = rc.encode_to_vec(); let deserialized = RouteConfigResource::deserialize(Bytes::from(bytes)).unwrap(); assert_eq!(RouteConfigResource::name(&deserialized), "rc-1"); } #[test] fn test_invalid_regex_fails_validation() { use envoy_types::pb::envoy::config::route::v3::{ RouteAction, VirtualHost, route::Action, route_action::ClusterSpecifier, }; use envoy_types::pb::envoy::r#type::matcher::v3::RegexMatcher; let route = envoy_types::pb::envoy::config::route::v3::Route { r#match: Some(RouteMatch { path_specifier: Some(route_match::PathSpecifier::SafeRegex(RegexMatcher { regex: "[invalid".to_string(), ..Default::default() })), ..Default::default() }), action: Some(Action::Route(RouteAction { cluster_specifier: Some(ClusterSpecifier::Cluster("c1".to_string())), ..Default::default() })), ..Default::default() }; let rc = RouteConfiguration { name: "rc".to_string(), virtual_hosts: vec![VirtualHost { name: "vh1".to_string(), domains: vec!["*".to_string()], routes: vec![route], ..Default::default() }], ..Default::default() }; let err = RouteConfigResource::validate(rc).unwrap_err(); assert!(err.to_string().contains("invalid path regex")); } #[test] fn test_empty_virtual_hosts_fails() { let rc = RouteConfiguration { name: "rc".to_string(), virtual_hosts: vec![], ..Default::default() }; let err = RouteConfigResource::validate(rc).unwrap_err(); assert!(err.to_string().contains("no virtual hosts")); } } ================================================ FILE: tonic-xds/src/xds/routing.rs ================================================ //! Per-request route matching on validated resource types. //! //! Operates directly on [`RouteConfigResource`] and its sub-types. //! The matching pipeline: domain → path → headers. //! //! Domain matching follows gRFC A27 priority: //! 1. Exact match //! 2. Suffix wildcard (`*.foo.com`) //! 3. Prefix wildcard (`foo.*`) //! 4. Universal wildcard `*` //! //! Within each category, the most specific (longest non-wildcard part) wins. use std::cmp::Reverse; use crate::xds::resource::route_config::{ HeaderMatchSpecifierConfig, HeaderMatcherConfig, PathSpecifierConfig, RouteConfig, RouteConfigAction, RouteConfigMatch, RouteConfigResource, VirtualHostConfig, }; /// Error returned when route matching fails. #[derive(Debug, Clone, thiserror::Error)] pub(crate) enum RoutingError { #[error("no matching virtual host for authority '{0}'")] NoMatchingVirtualHost(String), #[error("no matching route in virtual host for path '{0}'")] NoMatchingRoute(String), } impl RouteConfigResource { /// Match a request and return the target cluster action. /// /// Performs domain matching on the authority, then walks routes in order /// to find the first match. pub(crate) fn route( &self, authority: &str, path: &str, headers: &http::HeaderMap, ) -> Result<&RouteConfigAction, RoutingError> { let vh = find_best_matching_virtual_host(authority, &self.virtual_hosts) .ok_or_else(|| RoutingError::NoMatchingVirtualHost(authority.to_string()))?; for route in &vh.routes { if route_matches(route, path, headers) { return Ok(&route.action); } } Err(RoutingError::NoMatchingRoute(path.to_string())) } } const WILDCARD: &str = "*"; /// Finds the best-matching virtual host for the given authority. fn find_best_matching_virtual_host<'a>( authority: &str, virtual_hosts: &'a [VirtualHostConfig], ) -> Option<&'a VirtualHostConfig> { virtual_hosts .iter() .filter_map(|vh| { let best_score = vh .domains .iter() .filter_map(|d| match_domain(authority, d)) .min()?; Some((best_score, vh)) }) .min_by_key(|(score, _)| *score) .map(|(_, vh)| vh) } /// How well a domain pattern matched an authority. /// /// Sorts naturally so that better matches are smaller: /// match type (Exact < Suffix < Prefix < Universal), then higher /// specificity (more non-wildcard characters) breaks ties. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct DomainMatchScore(DomainMatchType, Reverse); /// Domain match types ordered by priority (lower is better). #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] enum DomainMatchType { Exact = 0, Suffix = 1, Prefix = 2, Universal = 3, } fn match_domain(authority: &str, pattern: &str) -> Option { if pattern == WILDCARD { return Some(DomainMatchScore(DomainMatchType::Universal, Reverse(0))); } let authority_lower = authority.to_ascii_lowercase(); let pattern_lower = pattern.to_ascii_lowercase(); if authority_lower == pattern_lower { return Some(DomainMatchScore( DomainMatchType::Exact, Reverse(pattern.len()), )); } if let Some(suffix) = pattern_lower.strip_prefix(WILDCARD) && authority_lower.ends_with(suffix) && authority_lower.len() > suffix.len() { return Some(DomainMatchScore( DomainMatchType::Suffix, Reverse(suffix.len()), )); } if let Some(prefix) = pattern_lower.strip_suffix(WILDCARD) && authority_lower.starts_with(prefix) && authority_lower.len() > prefix.len() { return Some(DomainMatchScore( DomainMatchType::Prefix, Reverse(prefix.len()), )); } None } fn route_matches(route: &RouteConfig, path: &str, headers: &http::HeaderMap) -> bool { match_path(&route.match_criteria, path) && match_headers(&route.match_criteria, headers) } fn match_path(criteria: &RouteConfigMatch, path: &str) -> bool { match &criteria.path_specifier { PathSpecifierConfig::Prefix(prefix) => { if prefix.is_empty() { return true; } if criteria.case_sensitive { path.starts_with(prefix.as_str()) } else { path.to_ascii_lowercase() .starts_with(&prefix.to_ascii_lowercase()) } } PathSpecifierConfig::Path(exact) => { if criteria.case_sensitive { path == exact } else { path.eq_ignore_ascii_case(exact) } } PathSpecifierConfig::SafeRegex(re) => re.is_match(path), } } fn match_headers(criteria: &RouteConfigMatch, headers: &http::HeaderMap) -> bool { criteria.headers.iter().all(|m| { let result = match_header(m, headers); if m.invert_match { !result } else { result } }) } fn match_header(hm: &HeaderMatcherConfig, headers: &http::HeaderMap) -> bool { let value = headers.get(&hm.name).and_then(|v| v.to_str().ok()); match &hm.match_specifier { HeaderMatchSpecifierConfig::Present => value.is_some(), HeaderMatchSpecifierConfig::Exact(e) => value.is_some_and(|v| v == e), HeaderMatchSpecifierConfig::Prefix(p) => value.is_some_and(|v| v.starts_with(p.as_str())), HeaderMatchSpecifierConfig::Suffix(s) => value.is_some_and(|v| v.ends_with(s.as_str())), HeaderMatchSpecifierConfig::Contains(c) => value.is_some_and(|v| v.contains(c.as_str())), HeaderMatchSpecifierConfig::SafeRegex(re) => value.is_some_and(|v| re.is_match(v)), } } #[cfg(test)] mod tests { use super::*; use crate::xds::resource::route_config::{ RouteConfig, RouteConfigAction, RouteConfigMatch, VirtualHostConfig, }; fn simple_route(prefix: &str, cluster: &str) -> RouteConfig { RouteConfig { match_criteria: RouteConfigMatch { path_specifier: PathSpecifierConfig::Prefix(prefix.into()), headers: vec![], case_sensitive: true, }, action: RouteConfigAction::Cluster(cluster.into()), } } fn simple_rc(virtual_hosts: Vec) -> RouteConfigResource { RouteConfigResource { name: "test-rc".into(), virtual_hosts, } } #[test] fn domain_exact() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["foo.com".into()], routes: vec![simple_route("/", "c1")], }]); assert!(rc.route("foo.com", "/", &http::HeaderMap::new()).is_ok()); } #[test] fn domain_case_insensitive() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["FOO.COM".into()], routes: vec![simple_route("/", "c1")], }]); assert!(rc.route("foo.com", "/", &http::HeaderMap::new()).is_ok()); } #[test] fn domain_suffix_wildcard() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["*.foo.com".into()], routes: vec![simple_route("/", "c1")], }]); let h = http::HeaderMap::new(); assert!(rc.route("bar.foo.com", "/", &h).is_ok()); assert!(rc.route("foo.com", "/", &h).is_err()); } #[test] fn domain_prefix_wildcard() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["foo.*".into()], routes: vec![simple_route("/", "c1")], }]); let h = http::HeaderMap::new(); assert!(rc.route("foo.bar", "/", &h).is_ok()); assert!(rc.route("bar.foo", "/", &h).is_err()); } #[test] fn domain_universal() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["*".into()], routes: vec![simple_route("/", "c1")], }]); assert!( rc.route("anything.com", "/", &http::HeaderMap::new()) .is_ok() ); } #[test] fn domain_exact_beats_suffix() { let rc = simple_rc(vec![ VirtualHostConfig { name: "vh-suffix".into(), domains: vec!["*.foo.com".into()], routes: vec![simple_route("/", "cluster-suffix")], }, VirtualHostConfig { name: "vh-exact".into(), domains: vec!["bar.foo.com".into()], routes: vec![simple_route("/", "cluster-exact")], }, ]); let action = rc .route("bar.foo.com", "/", &http::HeaderMap::new()) .unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-exact")); } #[test] fn domain_suffix_beats_universal() { let rc = simple_rc(vec![ VirtualHostConfig { name: "vh-universal".into(), domains: vec!["*".into()], routes: vec![simple_route("/", "cluster-universal")], }, VirtualHostConfig { name: "vh-suffix".into(), domains: vec!["*.foo.com".into()], routes: vec![simple_route("/", "cluster-suffix")], }, ]); let action = rc .route("bar.foo.com", "/", &http::HeaderMap::new()) .unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-suffix")); } #[test] fn domain_longer_suffix_wins() { let rc = simple_rc(vec![ VirtualHostConfig { name: "vh-short".into(), domains: vec!["*.com".into()], routes: vec![simple_route("/", "cluster-short")], }, VirtualHostConfig { name: "vh-long".into(), domains: vec!["*.foo.com".into()], routes: vec![simple_route("/", "cluster-long")], }, ]); let action = rc .route("bar.foo.com", "/", &http::HeaderMap::new()) .unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-long")); } #[test] fn domain_no_match() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["foo.com".into()], routes: vec![simple_route("/", "c1")], }]); assert!(rc.route("bar.com", "/", &http::HeaderMap::new()).is_err()); } #[test] fn basic_routing() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["*".into()], routes: vec![simple_route("/", "cluster-1")], }]); let headers = http::HeaderMap::new(); let action = rc.route("any.host", "/foo", &headers).unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-1")); } #[test] fn domain_selects_virtual_host() { let rc = simple_rc(vec![ VirtualHostConfig { name: "vh-foo".into(), domains: vec!["foo.com".into()], routes: vec![simple_route("/", "cluster-foo")], }, VirtualHostConfig { name: "vh-bar".into(), domains: vec!["bar.com".into()], routes: vec![simple_route("/", "cluster-bar")], }, ]); let headers = http::HeaderMap::new(); let action = rc.route("foo.com", "/x", &headers).unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-foo")); let action = rc.route("bar.com", "/x", &headers).unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-bar")); } #[test] fn no_matching_virtual_host() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["foo.com".into()], routes: vec![simple_route("/", "c1")], }]); let headers = http::HeaderMap::new(); let err = rc.route("unknown.com", "/", &headers).unwrap_err(); assert!(matches!(err, RoutingError::NoMatchingVirtualHost(_))); } #[test] fn first_matching_route_wins() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["*".into()], routes: vec![ simple_route("/svc/", "cluster-svc"), simple_route("/", "cluster-default"), ], }]); let headers = http::HeaderMap::new(); let action = rc.route("host", "/svc/Method", &headers).unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-svc")); let action = rc.route("host", "/other", &headers).unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-default")); } #[test] fn no_matching_route() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["*".into()], routes: vec![simple_route("/svc/", "c1")], }]); let headers = http::HeaderMap::new(); let err = rc.route("host", "/other", &headers).unwrap_err(); assert!(matches!(err, RoutingError::NoMatchingRoute(_))); } #[test] fn exact_path_match() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["*".into()], routes: vec![RouteConfig { match_criteria: RouteConfigMatch { path_specifier: PathSpecifierConfig::Path("/svc/Method".into()), headers: vec![], case_sensitive: true, }, action: RouteConfigAction::Cluster("c1".into()), }], }]); let headers = http::HeaderMap::new(); assert!(rc.route("host", "/svc/Method", &headers).is_ok()); assert!(rc.route("host", "/svc/Other", &headers).is_err()); } #[test] fn regex_path_match() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["*".into()], routes: vec![RouteConfig { match_criteria: RouteConfigMatch { path_specifier: PathSpecifierConfig::SafeRegex( regex::Regex::new("^/svc/.*").unwrap(), ), headers: vec![], case_sensitive: true, }, action: RouteConfigAction::Cluster("c1".into()), }], }]); let headers = http::HeaderMap::new(); assert!(rc.route("host", "/svc/Anything", &headers).is_ok()); assert!(rc.route("host", "/other", &headers).is_err()); } #[test] fn header_matcher_filters_routes() { let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["*".into()], routes: vec![ RouteConfig { match_criteria: RouteConfigMatch { path_specifier: PathSpecifierConfig::Prefix("/".into()), headers: vec![HeaderMatcherConfig { name: "x-env".into(), match_specifier: HeaderMatchSpecifierConfig::Exact("prod".into()), invert_match: false, }], case_sensitive: true, }, action: RouteConfigAction::Cluster("cluster-prod".into()), }, simple_route("/", "cluster-default"), ], }]); let mut prod_headers = http::HeaderMap::new(); prod_headers.insert("x-env", "prod".parse().unwrap()); let action = rc.route("host", "/", &prod_headers).unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-prod")); let action = rc.route("host", "/", &http::HeaderMap::new()).unwrap(); assert!(matches!(action, RouteConfigAction::Cluster(c) if c == "cluster-default")); } #[test] fn weighted_clusters_passed_through() { use crate::xds::resource::route_config::WeightedCluster; let rc = simple_rc(vec![VirtualHostConfig { name: "vh1".into(), domains: vec!["*".into()], routes: vec![RouteConfig { match_criteria: RouteConfigMatch { path_specifier: PathSpecifierConfig::Prefix("/".into()), headers: vec![], case_sensitive: true, }, action: RouteConfigAction::WeightedClusters(vec![ WeightedCluster { name: "c1".into(), weight: 70, }, WeightedCluster { name: "c2".into(), weight: 30, }, ]), }], }]); let action = rc.route("host", "/", &http::HeaderMap::new()).unwrap(); assert!(matches!(action, RouteConfigAction::WeightedClusters(wcs) if wcs.len() == 2)); } } ================================================ FILE: tonic-xds/src/xds/uri.rs ================================================ use thiserror::Error; use url::Url; /// Error type for parsing xDS URIs. #[derive(Debug, Error)] pub enum XdsUriError { /// The URI scheme is not "xds". #[error("URI scheme must be 'xds', got '{0}'")] InvalidScheme(String), /// The URI could not be parsed. #[error("invalid URI: {0}")] InvalidUri(#[from] url::ParseError), } /// An xDS target URI (e.g., `xds:///my-service`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct XdsUri { /// The target service name extracted from the URI. pub target: String, } const XDS_SCHEME: &str = "xds"; impl XdsUri { /// Parses an xDS URI from a string. /// /// # Errors /// /// Returns an error if: /// - The URI cannot be parsed as a valid URI ([`XdsUriError::InvalidUri`]) /// - The URI scheme is not `xds` ([`XdsUriError::InvalidScheme`]) /// /// # Examples /// /// ``` /// use tonic_xds::XdsUri; /// /// let uri = XdsUri::parse("xds:///my-service").expect("Failed to parse valid xDS URI"); /// assert_eq!(uri.target, "my-service"); /// /// let invalid_uri = XdsUri::parse("http:///my-service"); /// assert!(invalid_uri.is_err()); /// assert_eq!(invalid_uri.unwrap_err().to_string(), "URI scheme must be 'xds', got 'http'"); /// ``` pub fn parse(uri: &str) -> Result { let uri = Url::parse(uri)?; if uri.scheme() != XDS_SCHEME { return Err(XdsUriError::InvalidScheme(uri.scheme().to_string())); } let target = uri.path().trim_start_matches('/').to_string(); Ok(Self { target }) } } ================================================ FILE: tonic-xds/src/xds/xds_manager.rs ================================================ use crate::common::async_util::BoxFuture; use std::pin::Pin; use tower::{BoxError, discover::Change}; use crate::client::route::{RouteDecision, RouteInput}; pub(crate) type BoxDiscover = Pin, BoxError>> + Send>>; /// Trait for routing requests to clusters based on xDS routing configurations. pub(crate) trait XdsRouter: Send + Sync + 'static { fn route(&self, input: &RouteInput<'_>) -> BoxFuture; } /// Trait for discovering cluster endpoints based on xDS cluster configurations. pub(crate) trait XdsClusterDiscovery: Send + Sync + 'static { fn discover_cluster(&self, cluster_name: &str) -> BoxDiscover; } /// Combined trait for xDS management (routing + load balancing). /// Automatically implemented for any type that implements both `XdsRouter` and `XdsClusterDiscovery`. #[allow(dead_code)] pub(crate) trait XdsManager: XdsRouter + XdsClusterDiscovery { } impl XdsManager for T where T: XdsRouter + XdsClusterDiscovery { } ================================================ FILE: xds-client/Cargo.toml ================================================ [package] name = "xds-client" description = "An xDS client implementation in Rust" version = "0.1.0-alpha.1" edition = "2024" homepage = "https://github.com/hyperium/tonic" repository = "https://github.com/hyperium/tonic" license = "MIT" rust-version = { workspace = true } publish = false [lints] workspace = true [dependencies] bytes = "1.11.0" thiserror = "2" tokio = { version = "1", features = ["sync", "macros"] } # Optional dependencies for tonic transport tonic = { version = "0.14", optional = true } tokio-stream = { version = "0.1", optional = true } http = { version = "1", optional = true } # Optional dependencies for prost codec envoy-types = { version = "0.7", optional = true } prost = { version = "0.14", optional = true } [features] default = ["transport-tonic", "codegen-prost"] transport-tonic = [ "rt-tokio", "dep:tonic", "dep:tokio-stream", "dep:http", ] rt-tokio = ["tokio/rt", "tokio/time"] codegen-prost = ["dep:envoy-types", "dep:prost"] [dev-dependencies] tokio = { version = "1", features = [ "rt-multi-thread", "macros", "net", ] } tonic = { version = "0.14", features = ["tls-ring"] } async-stream = "0.3" envoy-types = "0.7" prost = "0.14" [[example]] name = "basic" path = "examples/basic.rs" [package.metadata.cargo_check_external_types] allowed_external_types = [ # major released "bytes::*", ] ================================================ FILE: xds-client/examples/basic.rs ================================================ //! Example demonstrating xds-client usage. //! //! This example shows: //! - How to implement the `Resource` trait for Envoy Listener //! - How to create an `XdsClient` with tonic transport and prost codec //! - How to watch for resources and handle events //! //! # Configuration (environment variables) //! //! - `XDS_SERVER` — URI of the xDS management server (default: `http://localhost:18000`) //! - `XDS_LISTENERS` — Comma-separated listener names to watch (required) //! - `XDS_CA_CERT` — Path to PEM-encoded CA certificate (enables TLS) //! - `XDS_CLIENT_CERT` — Path to PEM-encoded client certificate (for mTLS, requires `XDS_CA_CERT`) //! - `XDS_CLIENT_KEY` — Path to PEM-encoded client key (for mTLS, requires `XDS_CLIENT_CERT`) //! //! # Usage //! //! ```sh //! # Basic usage //! XDS_LISTENERS=my-listener cargo run -p xds-client --example basic //! //! # Multiple listeners //! XDS_LISTENERS=listener-1,listener-2 cargo run -p xds-client --example basic //! //! # Custom server //! XDS_SERVER=http://xds.example.com:18000 XDS_LISTENERS=foo cargo run -p xds-client --example basic //! //! # With TLS //! XDS_CA_CERT=/path/to/ca.pem \ //! XDS_CLIENT_CERT=/path/to/client.pem \ //! XDS_CLIENT_KEY=/path/to/client.key \ //! XDS_LISTENERS=my-listener \ //! cargo run -p xds-client --example basic //! ``` use bytes::Bytes; use envoy_types::pb::envoy::config::listener::v3::Listener as ListenerProto; use envoy_types::pb::envoy::extensions::filters::network::http_connection_manager::v3::{ HttpConnectionManager, http_connection_manager::RouteSpecifier, }; use prost::Message; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use xds_client::resource::TypeUrl; use xds_client::{ ClientConfig, Node, ProstCodec, Resource, ResourceEvent, Result as XdsResult, ServerConfig, TokioRuntime, TonicTransport, TonicTransportBuilder, TransportBuilder, XdsClient, }; struct Args { server: String, ca_cert: Option, client_cert: Option, client_key: Option, listeners: Vec, } fn parse_args() -> Args { let server = std::env::var("XDS_SERVER").unwrap_or_else(|_| "http://localhost:18000".to_string()); let listeners: Vec = std::env::var("XDS_LISTENERS") .expect("XDS_LISTENERS env var is required (comma-separated listener names)") .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); if listeners.is_empty() { panic!("XDS_LISTENERS must contain at least one listener name"); } let ca_cert = std::env::var("XDS_CA_CERT").ok(); let client_cert = std::env::var("XDS_CLIENT_CERT").ok(); let client_key = std::env::var("XDS_CLIENT_KEY").ok(); if client_cert.is_some() && ca_cert.is_none() { panic!("XDS_CLIENT_CERT requires XDS_CA_CERT to be set"); } if client_key.is_some() && client_cert.is_none() { panic!("XDS_CLIENT_KEY requires XDS_CLIENT_CERT to be set"); } Args { server, ca_cert, client_cert, client_key, listeners, } } /// A simplified Listener resource for gRPC xDS. /// /// Extracts the RDS route config name from the ApiListener's HttpConnectionManager. #[derive(Debug, Clone)] pub struct Listener { /// The listener name. pub name: String, /// The RDS route config name (from HttpConnectionManager). pub rds_route_config_name: Option, } /// Custom transport builder that configures TLS on the channel. /// /// This demonstrates how to implement a custom [`TransportBuilder`] when you need /// TLS or other custom channel configuration. The default [`TonicTransportBuilder`] /// creates plain (non-TLS) connections. struct TlsTransportBuilder { tls_config: ClientTlsConfig, } impl TransportBuilder for TlsTransportBuilder { type Transport = TonicTransport; async fn build(&self, server: &ServerConfig) -> XdsResult { let channel = Channel::from_shared(server.uri().to_string()) .map_err(|e| xds_client::Error::Connection(e.to_string()))? .tls_config(self.tls_config.clone()) .map_err(|e| xds_client::Error::Connection(e.to_string()))? .connect() .await .map_err(|e| xds_client::Error::Connection(e.to_string()))?; Ok(TonicTransport::from_channel(channel)) } } impl Resource for Listener { type Message = ListenerProto; const TYPE_URL: TypeUrl = TypeUrl::new("type.googleapis.com/envoy.config.listener.v3.Listener"); fn deserialize(bytes: Bytes) -> xds_client::Result { ListenerProto::decode(bytes).map_err(Into::into) } fn name(message: &Self::Message) -> &str { &message.name } fn validate(message: Self::Message) -> xds_client::Result { let hcm = message .api_listener .and_then(|api| api.api_listener) .and_then(|any| HttpConnectionManager::decode(Bytes::from(any.value)).ok()); let rds_route_config_name = hcm.and_then(|hcm| match hcm.route_specifier { Some(RouteSpecifier::Rds(rds)) => Some(rds.route_config_name), _ => None, }); Ok(Self { name: message.name, rds_route_config_name, }) } } #[tokio::main] async fn main() -> Result<(), Box> { let args = parse_args(); println!("xds-client Example\n"); println!("Connecting to xDS server: {}", args.server); let node = Node::new("grpc", "1.0").with_id("example-node"); let config = ClientConfig::new(node, &args.server); let client = match &args.ca_cert { Some(ca_path) => { let ca_cert = std::fs::read_to_string(ca_path)?; let mut tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(&ca_cert)); if let (Some(cert_path), Some(key_path)) = (&args.client_cert, &args.client_key) { let client_cert = std::fs::read_to_string(cert_path)?; let client_key = std::fs::read_to_string(key_path)?; tls = tls.identity(Identity::from_pem(client_cert, client_key)); } let tls_builder = TlsTransportBuilder { tls_config: tls }; XdsClient::builder(config, tls_builder, ProstCodec, TokioRuntime).build() } None => XdsClient::builder( config, TonicTransportBuilder::new(), ProstCodec, TokioRuntime, ) .build(), }; println!("Starting watchers...\n"); let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel::>(); // Start watchers for each listener from args for name in &args.listeners { println!("Watching for Listener: '{name}'"); let mut watcher = client.watch::(name).await; let tx = event_tx.clone(); tokio::spawn(async move { while let Some(event) = watcher.next().await { if tx.send(event).is_err() { eprintln!("Event channel closed, stopping watcher"); break; } } }); } // Drop the original sender so the loop exits when all watchers complete drop(event_tx); while let Some(event) = event_rx.recv().await { match event { ResourceEvent::ResourceChanged { result: Ok(resource), done, } => { println!("Listener received:"); println!(" name: {}", resource.name); if let Some(ref rds) = resource.rds_route_config_name { println!(" rds_config: {rds}"); } println!(); // In gRPC xDS, you would cascadingly subscribe to RDS, CDS, EDS, etc. // The done signal is sent automatically when it's dropped. drop(done); } ResourceEvent::ResourceChanged { result: Err(error), .. } => { // Resource was invalidated (validation error, deleted, etc.) println!("Resource invalidated: {error}"); } ResourceEvent::AmbientError { error, .. } => { // Non-fatal error, continue using cached resource if available println!("Ambient error: {error}"); } } } println!("Exiting"); Ok(()) } ================================================ FILE: xds-client/src/client/config.rs ================================================ //! Configuration for the xDS client. use std::time::Duration; use crate::client::retry::RetryPolicy; use crate::message::Node; /// Configuration for an xDS management server. #[derive(Debug, Clone)] #[non_exhaustive] pub struct ServerConfig { uri: String, // Future extensions per gRFC: // - `ignore_resource_deletion: bool` (gRFC A53) // - Server features / capabilities // - Per-server channel credentials config } impl ServerConfig { /// Create a new server configuration with the given URI. pub fn new(uri: impl Into) -> Self { Self { uri: uri.into() } } /// Returns the URI of the management server. pub fn uri(&self) -> &str { &self.uri } } /// Default timeout for initial resource response (30 seconds per gRFC A57). pub const DEFAULT_RESOURCE_INITIAL_TIMEOUT: Duration = Duration::from_secs(30); /// Configuration for the xDS client. #[derive(Debug, Clone)] #[non_exhaustive] pub struct ClientConfig { /// Node identification sent to the xDS server. pub(crate) node: Node, /// Retry policy for connection attempts. /// /// Controls the backoff behavior when reconnecting to the xDS server. pub(crate) retry_policy: RetryPolicy, /// Priority-ordered list of xDS management servers. /// /// The client will attempt to connect to servers in order, falling back /// to the next server if the current one is unavailable (per gRFC A71). /// Index 0 has the highest priority. pub(crate) servers: Vec, /// Timeout for initial resource response (gRFC A57). /// /// If a watched resource is not received within this duration after the watch /// is registered, watchers receive a `ResourceDoesNotExist` error. /// /// Default: 30 seconds. Set to `None` to disable the timeout. pub(crate) resource_initial_timeout: Option, // Future extensions: // - `authorities: HashMap` for xDS federation (gRFC A47) // - Locality / zone information for locality-aware routing } impl ClientConfig { /// Create a new configuration with a single server. /// /// Uses the default retry policy. /// /// # Example /// /// ``` /// use xds_client::{ClientConfig, Node}; /// /// let node = Node::new("grpc", "1.0") /// .with_id("my-node") /// .with_cluster("my-cluster"); /// /// let config = ClientConfig::new(node, "https://xds.example.com:443"); /// ``` pub fn new(node: Node, server_uri: impl Into) -> Self { Self { node, retry_policy: RetryPolicy::default(), servers: vec![ServerConfig::new(server_uri)], resource_initial_timeout: Some(DEFAULT_RESOURCE_INITIAL_TIMEOUT), } } /// Create a new configuration with multiple servers for fallback. /// /// Servers are tried in order; index 0 has the highest priority. /// /// # Example /// /// ``` /// use xds_client::{ClientConfig, Node, ServerConfig}; /// /// let node = Node::new("grpc", "1.0"); /// let config = ClientConfig::with_servers(node, vec![ /// ServerConfig::new("https://primary.xds.example.com:443"), /// ServerConfig::new("https://backup.xds.example.com:443"), /// ]); /// ``` pub fn with_servers(node: Node, servers: Vec) -> Self { Self { node, retry_policy: RetryPolicy::default(), servers, resource_initial_timeout: Some(DEFAULT_RESOURCE_INITIAL_TIMEOUT), } } /// Set the retry policy. /// /// # Example /// /// ``` /// use xds_client::{ClientConfig, Node, RetryPolicy}; /// use std::time::Duration; /// /// let node = Node::new("grpc", "1.0"); /// let policy = RetryPolicy::default() /// .with_initial_backoff(Duration::from_millis(500)).unwrap() /// .with_max_backoff(Duration::from_secs(60)).unwrap(); /// /// let config = ClientConfig::new(node, "https://xds.example.com:443") /// .with_retry_policy(policy); /// ``` pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self { self.retry_policy = policy; self } /// Set the timeout for initial resource response (gRFC A57). /// /// If a watched resource is not received within this duration after the watch /// is registered, watchers receive a `ResourceDoesNotExist` error. /// /// Set to `None` to disable the timeout. /// /// # Example /// /// ``` /// use xds_client::{ClientConfig, Node}; /// use std::time::Duration; /// /// let node = Node::new("grpc", "1.0"); /// /// // Use a custom timeout /// let config = ClientConfig::new(node.clone(), "https://xds.example.com:443") /// .with_resource_initial_timeout(Some(Duration::from_secs(60))); /// /// // Disable the timeout /// let config = ClientConfig::new(node, "https://xds.example.com:443") /// .with_resource_initial_timeout(None); /// ``` pub fn with_resource_initial_timeout(mut self, timeout: Option) -> Self { self.resource_initial_timeout = timeout; self } } ================================================ FILE: xds-client/src/client/mod.rs ================================================ //! Client interface through which the user can watch and receive updates for xDS resources. use tokio::sync::mpsc; use crate::client::config::ClientConfig; use crate::client::watch::ResourceWatcher; use crate::client::worker::{AdsWorker, WatcherId, WorkerCommand}; use crate::codec::XdsCodec; use crate::resource::{DecodedResource, DecoderFn, Resource}; use crate::runtime::Runtime; use crate::transport::TransportBuilder; pub mod config; pub mod retry; pub mod watch; pub mod worker; /// Builder for [`XdsClient`]. #[derive(Debug)] pub struct XdsClientBuilder { config: ClientConfig, transport_builder: TB, codec: C, runtime: R, } impl XdsClientBuilder where TB: TransportBuilder, C: XdsCodec, R: Runtime, { /// Create a new builder with the given configuration, transport builder, codec, and runtime. pub fn new(config: ClientConfig, transport_builder: TB, codec: C, runtime: R) -> Self { Self { config, transport_builder, codec, runtime, } } /// Build the client and start the background worker. /// /// This spawns a background task that manages the ADS stream. /// The task runs until all `XdsClient` handles are dropped. pub fn build(self) -> XdsClient { let (command_tx, command_rx) = mpsc::channel(COMMAND_CHANNEL_BUFFER_SIZE); let worker = AdsWorker::new( self.transport_builder, self.codec, self.runtime.clone(), self.config, command_tx.clone(), command_rx, ); self.runtime.spawn(async move { worker.run().await; }); XdsClient { command_tx } } } /// The xDS client. /// /// This is a handle to the background worker that manages the ADS stream. /// Cloning this handle creates a new reference to the same worker. /// /// When all `XdsClient` handles are dropped, the background worker shuts down. #[derive(Clone, Debug)] pub struct XdsClient { /// Channel to send commands to the worker. command_tx: mpsc::Sender, } /// Buffer size for the command channel between [`XdsClient`] handles and the worker. /// /// Commands are lightweight (watch/unwatch/timer), so a modest buffer suffices. /// The channel provides backpressure if the worker is temporarily busy processing /// a response. const COMMAND_CHANNEL_BUFFER_SIZE: usize = 64; /// Default buffer size for watcher event channels. /// /// This provides backpressure when watchers are slow to process events. const WATCHER_CHANNEL_BUFFER_SIZE: usize = 16; impl XdsClient { /// Create a new builder with the given configuration, transport builder, codec, and runtime. pub fn builder( config: ClientConfig, transport_builder: TB, codec: C, runtime: R, ) -> XdsClientBuilder where TB: TransportBuilder, C: XdsCodec, R: Runtime, { XdsClientBuilder::new(config, transport_builder, codec, runtime) } /// Watch a resource by name. /// /// Returns a [`ResourceWatcher`] that receives events for this resource. /// Dropping the watcher automatically unsubscribes. /// /// # Arguments /// /// * `name` - The resource name to watch. Use an empty string for wildcard /// subscriptions (receive all resources of this type). /// /// # Example /// /// ```ignore /// let mut watcher = client.watch::("my-listener").await; /// while let Some(event) = watcher.next().await { /// match event { /// ResourceEvent::ResourceChanged { result: Ok(resource), done } => { /// println!("Listener changed: {}", resource.name()); /// // Signal is sent automatically when done is dropped /// } /// ResourceEvent::ResourceChanged { result: Err(error), .. } => { /// println!("Error watching listener: {}", error); /// } /// ResourceEvent::AmbientError { error, .. } => { /// println!("Ambient error: {}", error); /// } /// } /// } /// ``` pub async fn watch(&self, name: impl Into) -> ResourceWatcher { let name = name.into(); let watcher_id = WatcherId::new(); let (event_tx, event_rx) = mpsc::channel(WATCHER_CHANNEL_BUFFER_SIZE); let decoder: DecoderFn = Box::new(|bytes| match crate::resource::decode::(bytes) { crate::resource::DecodeResult::Success { name, resource } => { crate::resource::DecodeResult::Success { name: name.clone(), resource: DecodedResource::new(name, resource), } } crate::resource::DecodeResult::ResourceError { name, error } => { crate::resource::DecodeResult::ResourceError { name, error } } crate::resource::DecodeResult::TopLevelError(error) => { crate::resource::DecodeResult::TopLevelError(error) } }); let _ = self .command_tx .send(WorkerCommand::Watch { type_url: T::TYPE_URL.as_str(), name, watcher_id, event_tx, decoder, all_resources_required_in_sotw: T::ALL_RESOURCES_REQUIRED_IN_SOTW, }) .await; ResourceWatcher::new(event_rx, watcher_id, self.command_tx.clone()) } } ================================================ FILE: xds-client/src/client/retry.rs ================================================ //! Retry policy configuration based on gRFC A6. use std::time::Duration; use crate::error::{Error, Result}; /// Retry policy for xDS client connection attempts. /// /// This configuration follows the gRFC A6 proposal for client retries, /// using exponential backoff with jitter for reconnection attempts. /// /// # Example /// /// ``` /// use xds_client::RetryPolicy; /// use std::time::Duration; /// /// let policy = RetryPolicy::default() /// .with_initial_backoff(Duration::from_secs(1)).unwrap() /// .with_max_backoff(Duration::from_secs(30)).unwrap() /// .with_backoff_multiplier(2.0).unwrap(); /// ``` #[derive(Debug, Clone)] pub struct RetryPolicy { /// Initial backoff duration for the first retry attempt. /// /// Default: 1 second. pub initial_backoff: Duration, /// Maximum backoff duration. /// /// The backoff will not grow beyond this value, regardless of how many /// retry attempts have been made. /// /// Default: 30 seconds. pub max_backoff: Duration, /// Multiplier for exponential backoff. /// /// After each failed attempt, the current backoff duration is multiplied /// by this value (up to `max_backoff`). /// /// Default: 2.0 (exponential backoff). pub backoff_multiplier: f64, /// Maximum number of retry attempts. /// /// If `None`, retries indefinitely. If `Some(n)`, stops after `n` attempts. /// /// Default: None (infinite retries). pub max_attempts: Option, } impl RetryPolicy { /// Create a new retry policy with custom parameters. /// /// # Errors /// /// Returns an error if: /// - `backoff_multiplier` is less than 1.0 /// - `max_backoff` is less than `initial_backoff` /// - `initial_backoff` is zero /// /// # Example /// /// ``` /// use xds_client::RetryPolicy; /// use std::time::Duration; /// /// let policy = RetryPolicy::new( /// Duration::from_millis(500), // initial_backoff /// Duration::from_secs(60), // max_backoff /// 1.5, // backoff_multiplier /// )?; /// # Ok::<(), xds_client::Error>(()) /// ``` pub fn new( initial_backoff: Duration, max_backoff: Duration, backoff_multiplier: f64, ) -> Result { if initial_backoff.is_zero() { return Err(Error::Validation( "initial_backoff must be greater than zero".into(), )); } if backoff_multiplier < 1.0 { return Err(Error::Validation(format!( "backoff_multiplier must be >= 1.0, got {backoff_multiplier}" ))); } if max_backoff < initial_backoff { return Err(Error::Validation(format!( "max_backoff ({max_backoff:?}) must be >= initial_backoff ({initial_backoff:?})" ))); } Ok(Self { initial_backoff, max_backoff, backoff_multiplier, max_attempts: None, }) } /// Set the initial backoff duration. /// /// # Errors /// /// Returns an error if `duration` is zero or greater than `max_backoff`. pub fn with_initial_backoff(mut self, duration: Duration) -> Result { if duration.is_zero() { return Err(Error::Validation( "initial_backoff must be greater than zero".into(), )); } if duration > self.max_backoff { let max_backoff = self.max_backoff; return Err(Error::Validation(format!( "initial_backoff ({duration:?}) must be <= max_backoff ({max_backoff:?})" ))); } self.initial_backoff = duration; Ok(self) } /// Set the maximum backoff duration. /// /// # Errors /// /// Returns an error if `duration` is less than `initial_backoff`. pub fn with_max_backoff(mut self, duration: Duration) -> Result { if duration < self.initial_backoff { let initial_backoff = self.initial_backoff; return Err(Error::Validation(format!( "max_backoff ({duration:?}) must be >= initial_backoff ({initial_backoff:?})" ))); } self.max_backoff = duration; Ok(self) } /// Set the backoff multiplier. /// /// # Errors /// /// Returns an error if `multiplier` is less than 1.0. pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Result { if multiplier < 1.0 { return Err(Error::Validation(format!( "backoff_multiplier must be >= 1.0, got {multiplier}" ))); } self.backoff_multiplier = multiplier; Ok(self) } /// Set the maximum number of retry attempts. /// /// If set to `None`, retries indefinitely. pub fn with_max_attempts(mut self, max_attempts: Option) -> Self { self.max_attempts = max_attempts; self } /// Calculate the backoff duration for a given attempt number. /// /// Returns `None` if `max_attempts` is set and the attempt exceeds it. /// /// # Arguments /// /// * `attempt` - The retry attempt number (0-indexed). /// /// # Example /// /// ``` /// use xds_client::RetryPolicy; /// use std::time::Duration; /// /// let policy = RetryPolicy::default(); /// assert_eq!(policy.backoff_duration(0), Some(Duration::from_secs(1))); /// assert_eq!(policy.backoff_duration(1), Some(Duration::from_secs(2))); /// assert_eq!(policy.backoff_duration(2), Some(Duration::from_secs(4))); /// ``` pub fn backoff_duration(&self, attempt: usize) -> Option { // Check if we've exceeded max attempts if let Some(max) = self.max_attempts && attempt >= max { return None; } // Calculate exponential backoff (saturate to i32::MAX to avoid overflow in powi) let exponent = i32::try_from(attempt).unwrap_or(i32::MAX); let multiplier = self.backoff_multiplier.powi(exponent); let backoff = self.initial_backoff.mul_f64(multiplier); // Cap at max_backoff Some(backoff.min(self.max_backoff)) } } impl Default for RetryPolicy { /// Create a retry policy with default values based on gRFC A6. /// /// Defaults: /// - `initial_backoff`: 1 second /// - `max_backoff`: 30 seconds /// - `backoff_multiplier`: 2.0 /// - `max_attempts`: None (infinite retries) fn default() -> Self { Self { initial_backoff: Duration::from_secs(1), max_backoff: Duration::from_secs(30), backoff_multiplier: 2.0, max_attempts: None, } } } /// Stateful backoff calculator based on a [`RetryPolicy`]. /// /// This struct tracks the current attempt number and provides methods to /// get the next backoff duration and reset after successful operations. /// /// # Example /// /// ``` /// use xds_client::{Backoff, RetryPolicy}; /// use std::time::Duration; /// /// let mut backoff = Backoff::new(RetryPolicy::default()); /// /// // First failure: get initial backoff /// assert_eq!(backoff.next_backoff(), Some(Duration::from_secs(1))); /// /// // Second failure: backoff doubles /// assert_eq!(backoff.next_backoff(), Some(Duration::from_secs(2))); /// /// // Success: reset for next failure sequence /// backoff.reset(); /// assert_eq!(backoff.next_backoff(), Some(Duration::from_secs(1))); /// ``` #[derive(Debug, Clone)] pub struct Backoff { policy: RetryPolicy, attempt: usize, } impl Backoff { /// Create a new backoff calculator from a retry policy. pub fn new(policy: RetryPolicy) -> Self { Self { policy, attempt: 0 } } /// Get the next backoff duration and advance the attempt counter. /// /// Returns `None` if `max_attempts` is set and has been exceeded. pub fn next_backoff(&mut self) -> Option { let duration = self.policy.backoff_duration(self.attempt)?; self.attempt += 1; Some(duration) } /// Reset the backoff after a successful operation. /// /// This resets the attempt counter to 0, so the next failure will /// use the initial backoff duration. pub fn reset(&mut self) { self.attempt = 0; } } ================================================ FILE: xds-client/src/client/watch.rs ================================================ //! Resource watcher types. use std::marker::PhantomData; use std::sync::Arc; use tokio::sync::{mpsc, oneshot}; use crate::client::worker::{WatcherId, WorkerCommand}; use crate::error::Error; use crate::resource::{DecodedResource, Resource}; /// A signal to indicate that processing of a resource event is complete. /// /// The xDS client waits for this signal before sending ACK/NACK to the server. /// This allows watchers to add cascading subscriptions (e.g. LDS -> RDS -> CDS -> EDS) /// that will be included in the same ACK. /// /// # Automatic Signaling /// /// Signals automatically when dropped. If you have cascading watches to add, simply /// add them before dropping the `ProcessingDone`. /// /// # Example /// /// ```ignore /// match event { /// ResourceEvent::ResourceChanged { result: Ok(resource), done } => { /// // Process the new resource, possibly add cascading watches. /// client.watch::(&resource.route_name()).await; /// // Signal is sent automatically when done is dropped /// } /// ResourceEvent::ResourceChanged { result: Err(error), done } => { /// // Resource was invalidated (validation error or deleted) /// eprintln!("Resource invalidated: {}", error); /// // Stop using the previously cached resource /// } /// ResourceEvent::AmbientError { error, .. } => { /// // Non-fatal error, continue using cached resource /// eprintln!("Ambient error: {}", error); /// } /// } /// ``` #[derive(Debug)] pub struct ProcessingDone(Option>); impl ProcessingDone { /// Create a channel pair for signaling. /// /// Returns the `ProcessingDone` sender and a receiver future that resolves /// when the sender is dropped. pub(crate) fn channel() -> (Self, oneshot::Receiver<()>) { let (tx, rx) = oneshot::channel(); (Self(Some(tx)), rx) } } impl Drop for ProcessingDone { fn drop(&mut self) { // Auto-signal on drop to prevent deadlocks. if let Some(tx) = self.0.take() { let _ = tx.send(()); } } } /// Events delivered to resource watchers. /// /// Per gRFC A88, there are two types of events: /// - `ResourceChanged`: Indicates a change in the resource's cached state /// - `AmbientError`: Non-fatal errors that don't affect the cached resource #[derive(Debug)] pub enum ResourceEvent { /// Indicates a change in the resource's cached state. /// /// This event is sent when: /// - A new valid resource is received (`Ok(resource)`) /// - A validation error occurred (`Err(Error::Validation(...))`) /// - The resource was deleted or doesn't exist (`Err(Error::ResourceDoesNotExist)`) /// /// When `result` is `Err`, the previously cached resource (if any) should be /// invalidated. The watcher should stop using the old resource data. /// /// The resource is wrapped in `Arc` because multiple watchers may /// subscribe to the same resource and share the same data. ResourceChanged { /// The result of the resource update. /// - `Ok(resource)`: New valid resource received /// - `Err(error)`: Cache-invalidating error (validation failure, does not exist) result: Result, Error>, /// Signal when processing is complete. done: ProcessingDone, }, /// Indicates a non-fatal error that doesn't affect the cached resource. /// /// This is sent for transient errors like temporary connectivity issues /// with the xDS management server. The previously cached resource (if any) /// should continue to be used. /// /// Per gRFC A88, ambient errors should not cause the client to stop using /// a previously valid resource. AmbientError { /// The error that occurred. error: Error, /// Signal when processing is complete. done: ProcessingDone, }, } /// A watcher for resources of type `T`. /// /// Call [`next()`](Self::next) to receive resource events. /// Dropping the watcher unsubscribes from the resource. #[derive(Debug)] pub struct ResourceWatcher { /// Channel to receive events from the worker. event_rx: mpsc::Receiver>, /// Unique identifier for this watcher. watcher_id: WatcherId, /// Channel to send commands to the worker (for unwatch on drop). command_tx: mpsc::Sender, /// Marker for the resource type. _marker: PhantomData, } impl ResourceWatcher { /// Create a new resource watcher. pub(crate) fn new( event_rx: mpsc::Receiver>, watcher_id: WatcherId, command_tx: mpsc::Sender, ) -> Self { Self { event_rx, watcher_id, command_tx, _marker: PhantomData, } } /// Returns the next resource event. /// /// Returns `None` when the subscription is closed (worker shut down). /// /// # Example /// /// ```ignore /// while let Some(event) = watcher.next().await { /// match event { /// ResourceEvent::ResourceChanged { result: Ok(resource), done } => { /// // Process the new resource, possibly add cascading watches. /// client.watch::(&resource.route_name()).await; /// // Signal is sent automatically when done is dropped /// } /// ResourceEvent::ResourceChanged { result: Err(error), done } => { /// // Resource was invalidated (validation error or deleted) /// eprintln!("Resource invalidated: {}", error); /// } /// ResourceEvent::AmbientError { error, .. } => { /// // Non-fatal error, continue using cached resource /// eprintln!("Ambient error: {}", error); /// } /// } /// } /// ``` pub async fn next(&mut self) -> Option> { let event = self.event_rx.recv().await?; Some(match event { ResourceEvent::ResourceChanged { result, done } => { let typed_result = match result { Ok(resource) => match resource.downcast::() { Some(typed_resource) => Ok(typed_resource), None => Err(Error::Validation(format!( "resource type mismatch (expected: {}, actual: {})", std::any::type_name::(), resource.type_url() ))), }, Err(e) => Err(e), }; ResourceEvent::ResourceChanged { result: typed_result, done, } } ResourceEvent::AmbientError { error, done } => { ResourceEvent::AmbientError { error, done } } }) } } impl Drop for ResourceWatcher { fn drop(&mut self) { // Best-effort: if the channel is full or closed, the worker will // detect the closed event channel and clean up the watcher eventually. let _ = self.command_tx.try_send(WorkerCommand::Unwatch { watcher_id: self.watcher_id, }); } } ================================================ FILE: xds-client/src/client/worker.rs ================================================ //! ADS worker that manages the xDS stream. //! //! The worker runs as a background task, managing: //! - The ADS stream lifecycle (connection, reconnection) //! - Resource subscriptions and version/nonce tracking //! - Dispatching resources to watchers //! - ACK/NACK protocol use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; use bytes::Bytes; use tokio::sync::{mpsc, oneshot}; use crate::client::config::{ClientConfig, ServerConfig}; use crate::client::retry::Backoff; use crate::client::watch::{ProcessingDone, ResourceEvent}; use crate::codec::XdsCodec; use crate::error::{Error, Result}; use crate::message::{DiscoveryRequest, DiscoveryResponse, ErrorDetail, Node}; use crate::resource::{DecodedResource, DecoderFn}; use crate::runtime::Runtime; use crate::transport::{Transport, TransportBuilder, TransportStream}; /// Global counter for generating unique watcher IDs. static NEXT_WATCHER_ID: AtomicU64 = AtomicU64::new(1); /// Unique identifier for a watcher. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct WatcherId(u64); impl WatcherId { /// Create a new unique watcher ID. pub fn new() -> Self { Self(NEXT_WATCHER_ID.fetch_add(1, Ordering::Relaxed)) } } impl Default for WatcherId { fn default() -> Self { Self::new() } } /// Commands sent from `XdsClient` to the worker. pub(crate) enum WorkerCommand { /// Subscribe to a resource. Watch { /// The type URL of the resource. type_url: &'static str, /// The resource name (empty string for wildcard subscription). name: String, /// Unique identifier for this watcher. watcher_id: WatcherId, /// Channel to send resource events to the watcher. event_tx: mpsc::Sender>, /// Decoder function for this resource type. decoder: DecoderFn, /// Whether all resources must be present in SotW responses (per A53). all_resources_required_in_sotw: bool, }, /// Unsubscribe a watcher. Unwatch { /// The watcher to remove. watcher_id: WatcherId, }, /// Timer expired for a resource that was never received (gRFC A57). ResourceTimerExpired { /// The type URL of the resource. type_url: String, /// The resource name. name: String, }, } /// Represents the subscription mode for a resource type. /// /// This enum captures the mutually exclusive subscription states: /// - Wildcard: receive all resources of this type /// - Named: receive only specific resources by name #[derive(Debug, Clone, PartialEq, Eq)] enum SubscriptionMode { /// Wildcard subscription - receive all resources of this type. /// In xDS protocol, this is represented by an empty resource_names list. Wildcard, /// Named subscription - receive only specific resources. /// Contains the set of resource names to subscribe to. Named(HashSet), } impl SubscriptionMode { /// Get resource names for DiscoveryRequest. /// Returns empty vec for wildcard (xDS spec: empty = all resources). fn resource_names_for_request(&self) -> Vec { match self { Self::Wildcard => Vec::new(), Self::Named(names) => names.iter().cloned().collect(), } } } /// State of a cached resource per gRFC A88. #[derive(Debug, Clone)] enum ResourceState { /// Resource has been requested but not yet received. Requested, /// Resource has been successfully received and validated. Received, /// Resource validation failed. Contains the error message. NACKed(String), /// Resource does not exist (server indicated deletion or absence). DoesNotExist, } /// A cached resource entry. #[derive(Debug, Clone)] struct CachedResource { /// Current state of the resource. state: ResourceState, /// The decoded resource, if successfully received. /// None if state is Requested, NACKed, or DoesNotExist. resource: Option>, } impl CachedResource { /// Create a new cached resource in Requested state. fn requested() -> Self { Self { state: ResourceState::Requested, resource: None, } } /// Create a cached resource in Received state. fn received(resource: Arc) -> Self { Self { state: ResourceState::Received, resource: Some(resource), } } /// Create a cached resource in DoesNotExist state. fn does_not_exist() -> Self { Self { state: ResourceState::DoesNotExist, resource: None, } } /// Create a cached resource in NACKed state. fn nacked(error: String) -> Self { Self { state: ResourceState::NACKed(error), resource: None, } } /// Returns true if the resource is in Requested state (waiting for server response). fn is_requested(&self) -> bool { matches!(self.state, ResourceState::Requested) } /// Convert cached state to a ResourceEvent for notifying watchers. /// Returns None if state is Requested (nothing to notify yet). fn to_event(&self) -> Option> { let (done, _rx) = ProcessingDone::channel(); match &self.state { ResourceState::Received => { self.resource .as_ref() .map(|r| ResourceEvent::ResourceChanged { result: Ok(Arc::clone(r)), done, }) } ResourceState::DoesNotExist => Some(ResourceEvent::ResourceChanged { result: Err(Error::ResourceDoesNotExist), done, }), ResourceState::NACKed(error) => Some(ResourceEvent::ResourceChanged { result: Err(Error::Validation(error.clone())), done, }), ResourceState::Requested => None, } } } /// Per-type_url state tracking. struct TypeState { /// Decoder function for this resource type. decoder: DecoderFn, /// Version from last successful response. version_info: String, /// Nonce from last response (for ACK/NACK). nonce: String, /// Active watchers for this type. watchers: HashMap, /// Current subscription mode (wildcard or named resources). subscription: SubscriptionMode, /// Resource cache: name -> cached resource. cache: HashMap, /// Whether missing resources in SotW should be treated as deleted (per A53). all_resources_required_in_sotw: bool, } impl std::fmt::Debug for TypeState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TypeState") .field("decoder", &"") .field("version_info", &self.version_info) .field("nonce", &self.nonce) .field("watchers", &self.watchers) .field("subscription", &self.subscription) .field("cache", &format!("<{} entries>", self.cache.len())) .field( "all_resources_required_in_sotw", &self.all_resources_required_in_sotw, ) .finish() } } impl TypeState { fn new(decoder: DecoderFn, all_resources_required_in_sotw: bool) -> Self { Self { decoder, version_info: String::new(), nonce: String::new(), watchers: HashMap::new(), subscription: SubscriptionMode::Named(HashSet::new()), cache: HashMap::new(), all_resources_required_in_sotw, } } /// Recalculate subscription mode from watchers. fn recalculate_subscriptions(&mut self) { let has_wildcard = self .watchers .values() .any(|entry| entry.subscription.is_wildcard()); if has_wildcard { self.subscription = SubscriptionMode::Wildcard; } else { let names: HashSet = self .watchers .values() .filter_map(|entry| match &entry.subscription { WatcherSubscription::Named(name) => Some(name.clone()), WatcherSubscription::Wildcard => None, }) .collect(); self.subscription = SubscriptionMode::Named(names); } } /// Get resource names to send in DiscoveryRequest. fn resource_names_for_request(&self) -> Vec { self.subscription.resource_names_for_request() } /// Get senders for all watchers interested in a specific resource. fn matching_watchers(&self, name: &str) -> Vec>> { self.watchers .values() .filter(|e| e.subscription.matches(name)) .map(|e| e.event_tx.clone()) .collect() } } /// Specifies which resources a watcher is interested in. #[derive(Debug, Clone, PartialEq, Eq)] enum WatcherSubscription { /// Wildcard subscription - receive all resources of this type. Wildcard, /// Named subscription - receive only the specified resource. Named(String), } impl WatcherSubscription { /// Create a subscription from a resource name. /// Empty string is treated as wildcard. fn from_name(name: String) -> Self { if name.is_empty() { Self::Wildcard } else { Self::Named(name) } } /// Check if this subscription matches a resource name. fn matches(&self, resource_name: &str) -> bool { match self { Self::Wildcard => true, Self::Named(name) => name == resource_name, } } /// Returns true if this is a wildcard subscription. fn is_wildcard(&self) -> bool { matches!(self, Self::Wildcard) } } /// Per-watcher state. #[derive(Debug)] struct WatcherEntry { /// Channel to send events to this watcher. event_tx: mpsc::Sender>, /// What resources this watcher is subscribed to. subscription: WatcherSubscription, } /// The ADS worker manages the xDS stream and dispatches resources to watchers. pub(crate) struct AdsWorker { /// Transport builder for creating transports to xDS servers. transport_builder: TB, /// Codec for encoding/decoding messages. codec: C, /// Runtime for spawning tasks and sleeping. runtime: R, /// Node identification. node: Node, /// Backoff calculator for reconnection attempts. backoff: Backoff, /// Priority-ordered list of xDS servers. /// Index 0 has the highest priority. servers: Vec, /// Timeout for initial resource response (gRFC A57). None = disabled. resource_initial_timeout: Option, /// Sender for timer callback commands. command_tx: mpsc::Sender, /// Receiver for commands from XdsClient. command_rx: mpsc::Receiver, /// Per-type_url state. type_states: HashMap, /// Cancellation handles for resource timers (gRFC A57). /// Key is (type_url, resource_name). Dropping the sender cancels the timer. resource_timers: HashMap<(String, String), oneshot::Sender<()>>, } impl AdsWorker where TB: TransportBuilder, C: XdsCodec, R: Runtime, { /// Create a new worker. pub(crate) fn new( transport_builder: TB, codec: C, runtime: R, config: ClientConfig, command_tx: mpsc::Sender, command_rx: mpsc::Receiver, ) -> Self { Self { transport_builder, codec, runtime, node: config.node, backoff: Backoff::new(config.retry_policy), servers: config.servers, resource_initial_timeout: config.resource_initial_timeout, command_tx, command_rx, type_states: HashMap::new(), resource_timers: HashMap::new(), } } /// Run the worker event loop. /// /// This method runs until all `XdsClient` handles are dropped /// (which closes the command channel). pub(crate) async fn run(mut self) { loop { // Wait for at least one subscription before connecting. // This prevents deadlock with servers that require a message before // sending response headers - we need something to send. while self.type_states.is_empty() { match self.command_rx.recv().await { Some(cmd) => { let _ = self .handle_command::<::Stream>(None, cmd) .await; } None => return, } } // Nonces are tied to the stream for type_state in self.type_states.values_mut() { type_state.nonce.clear(); } // Connect to server. // Future extension (gRFC A71): Try servers in priority order with fallback. let server = match self.servers.first() { Some(s) => s, None => return, // No servers configured }; let transport = match self.transport_builder.build(server).await { Ok(t) => t, Err(_) => { match self.backoff.next_backoff() { Some(backoff) => self.runtime.sleep(backoff).await, None => return, // Max attempts exceeded } continue; } }; let stream = match transport.new_stream(self.build_initial_requests()).await { Ok(s) => { self.backoff.reset(); s } Err(_) => { match self.backoff.next_backoff() { Some(backoff) => self.runtime.sleep(backoff).await, None => return, // Max attempts exceeded } continue; } }; match self.run_connected(stream).await { Ok(()) => return, // shutdown Err(_e) => { match self.backoff.next_backoff() { Some(backoff) => self.runtime.sleep(backoff).await, None => return, // Max attempts exceeded } continue; } } } } /// Build initial DiscoveryRequests for all active subscriptions. /// /// These are sent when establishing the stream to prevent deadlock with /// servers that don't send response headers until they receive a request. fn build_initial_requests(&self) -> Vec { let mut requests = Vec::new(); for (type_url, type_state) in &self.type_states { if type_state.watchers.is_empty() { continue; } let resource_names = type_state.resource_names_for_request(); let request = DiscoveryRequest { node: &self.node, type_url, resource_names: &resource_names, version_info: &type_state.version_info, response_nonce: "", // Initial request has empty nonce error_detail: None, }; if let Ok(bytes) = self.codec.encode_request(&request) { requests.push(bytes); } } requests } /// Run the main event loop while connected. /// /// Returns `Ok(())` if the worker should shut down (command channel closed). /// Returns `Err` if an error occurred and the worker should reconnect. async fn run_connected(&mut self, mut stream: S) -> Result<()> { loop { tokio::select! { result = stream.recv() => { match result { Ok(Some(bytes)) => { self.handle_response(&mut stream, bytes).await?; } // Stream closed by server; return Err to trigger reconnection Ok(None) => return Err(Error::StreamClosed), Err(e) => return Err(e), } } cmd = self.command_rx.recv() => { match cmd { Some(cmd) => { self.handle_command(Some(&mut stream), cmd).await?; } None => return Ok(()), } } } } } /// Handle a command, optionally sending network requests if connected. /// /// When `stream` is `None`, only state updates are performed (disconnected mode). /// When `stream` is `Some`, subscription changes trigger network requests. async fn handle_command( &mut self, stream: Option<&mut S>, cmd: WorkerCommand, ) -> Result<()> { match cmd { WorkerCommand::Watch { type_url, name, watcher_id, event_tx, decoder, all_resources_required_in_sotw, } => { if self.add_watcher( type_url, name, watcher_id, event_tx, decoder, all_resources_required_in_sotw, ) && let Some(stream) = stream { self.send_request(stream, type_url).await?; } } WorkerCommand::Unwatch { watcher_id } => { if let Some((type_url, true)) = self.remove_watcher(watcher_id) && let Some(stream) = stream { self.send_request(stream, &type_url).await?; } } WorkerCommand::ResourceTimerExpired { type_url, name } => { self.handle_resource_timeout(&type_url, &name).await; } } Ok(()) } /// Add a watcher to the state. /// /// If the resource is already cached, the watcher receives the cached state immediately. /// Returns true if subscriptions changed (need to send new request to server). fn add_watcher( &mut self, type_url: &'static str, name: String, watcher_id: WatcherId, event_tx: mpsc::Sender>, decoder: DecoderFn, all_resources_required_in_sotw: bool, ) -> bool { let type_url_string = type_url.to_string(); let type_state = self .type_states .entry(type_url_string.clone()) .or_insert_with(|| TypeState::new(decoder, all_resources_required_in_sotw)); let old_subscription = type_state.subscription.clone(); let watcher_subscription = WatcherSubscription::from_name(name.clone()); // Track if we need to start a timer (resource in Requested state) let mut start_timer_for: Option = None; // For named subscriptions, check cache and send cached state to new watcher. // For wildcard subscriptions, watchers receive updates as they come in. if let WatcherSubscription::Named(ref resource_name) = watcher_subscription { let cached = type_state .cache .entry(resource_name.clone()) .or_insert_with(CachedResource::requested); if let Some(event) = cached.to_event() { // Send cached state to watcher (non-blocking, ignore if full) let _ = event_tx.try_send(event); } if cached.is_requested() { // Resource pending - start a timer (gRFC A57) start_timer_for = Some(resource_name.clone()); } } type_state.watchers.insert( watcher_id, WatcherEntry { event_tx, subscription: watcher_subscription, }, ); type_state.recalculate_subscriptions(); let subscriptions_changed = type_state.subscription != old_subscription; // Start timer if resource is in Requested state if let (Some(resource_name), Some(timeout)) = (start_timer_for, self.resource_initial_timeout) { self.start_resource_timer(&type_url_string, resource_name, timeout); } subscriptions_changed } /// Remove a watcher from the state. /// Returns the type_url and whether subscriptions changed. fn remove_watcher(&mut self, watcher_id: WatcherId) -> Option<(String, bool)> { let type_url = self .type_states .iter() .find(|(_, state)| state.watchers.contains_key(&watcher_id)) .map(|(url, _)| url.clone())?; let type_state = self.type_states.get_mut(&type_url)?; let old_subscription = type_state.subscription.clone(); type_state.watchers.remove(&watcher_id); type_state.recalculate_subscriptions(); let subscriptions_changed = type_state.subscription != old_subscription; if type_state.watchers.is_empty() { self.type_states.remove(&type_url); // Cancel all pending resource timers for this type. self.resource_timers.retain(|key, _| key.0 != type_url); } Some((type_url, subscriptions_changed)) } /// Send a DiscoveryRequest for a type. async fn send_request(&self, stream: &mut S, type_url: &str) -> Result<()> { let type_state = match self.type_states.get(type_url) { Some(s) => s, None => return Ok(()), }; let resource_names = type_state.resource_names_for_request(); let request = DiscoveryRequest { node: &self.node, type_url, resource_names: &resource_names, version_info: &type_state.version_info, response_nonce: &type_state.nonce, error_detail: None, }; let bytes = self.codec.encode_request(&request)?; stream.send(bytes).await } /// Handle a response from the server. /// /// Implements partial success per gRFC A46: valid resources are accepted even /// if some resources in the response fail validation. Each resource is processed /// independently: /// - Valid resources are cached and dispatched to watchers /// - Invalid resources are cached as NACKed and errors sent to specific watchers /// - Missing resources (for types with ALL_RESOURCES_REQUIRED_IN_SOTW) are marked deleted async fn handle_response( &mut self, stream: &mut S, bytes: Bytes, ) -> Result<()> { let response = self.codec.decode_response(bytes)?; let type_url = response.type_url.clone(); let decoder = match self.type_states.get(&type_url) { Some(s) => &s.decoder, None => { return Ok(()); } }; // Decode all resources, tracking valid and invalid separately. // Per A46, we accept valid resources even if some fail validation. // Per A88, we categorize errors: // - top_level_errors: deserialization failures where name cannot be extracted // - per_resource_errors: validation failures where name is known let mut valid_resources: Vec = Vec::new(); let mut top_level_errors: Vec = Vec::new(); let mut per_resource_errors: Vec<(String, String)> = Vec::new(); // (name, error) for resource_any in &response.resources { match decoder(resource_any.value.clone()) { crate::resource::DecodeResult::Success { resource, .. } => { valid_resources.push(resource); } crate::resource::DecodeResult::ResourceError { name, error } => { per_resource_errors.push((name, error.to_string())); } crate::resource::DecodeResult::TopLevelError(error) => { top_level_errors.push(error.to_string()); } } } if let Some(type_state) = self.type_states.get_mut(&type_url) { type_state.nonce = response.nonce.clone(); } let received_names: HashSet = valid_resources .iter() .map(|r| r.name().to_string()) .collect(); let mut processing_done_futures = self.dispatch_resources(&type_url, valid_resources).await; // Only notify watchers for per-resource errors (where we know the name). // Top-level errors have no associated name, so no watcher to notify. for (resource_name, error) in &per_resource_errors { self.notify_resource_error(&type_url, resource_name, error) .await; } // Detect deleted resources (per A53): // For resource types with ALL_RESOURCES_REQUIRED_IN_SOTW = true, // any previously-received resource not in this response is deleted. let deleted_futures = self .detect_deleted_resources(&type_url, &received_names) .await; processing_done_futures.extend(deleted_futures); // Wait for all watchers to finish processing. for rx in processing_done_futures { let _ = rx.await; } let has_errors = !top_level_errors.is_empty() || !per_resource_errors.is_empty(); if !has_errors { // Only update version on ACK; NACK must keep the old version so the // server knows which version the client is still running. if let Some(ts) = self.type_states.get_mut(&type_url) { ts.version_info = response.version_info.clone(); } self.send_ack(stream, &response).await } else { // Build NACK message combining both error categories let mut error_parts = Vec::new(); if !top_level_errors.is_empty() { error_parts.push(format!("top level errors: {}", top_level_errors.join("; "))); } if !per_resource_errors.is_empty() { let per_resource_msg = per_resource_errors .iter() .map(|(name, err)| format!("{name}: {err}")) .collect::>() .join("; "); error_parts.push(per_resource_msg); } self.send_nack(stream, &response, error_parts.join("; ")) .await } } /// Dispatch decoded resources to watchers and update cache. /// /// Returns futures that resolve when watchers signal ProcessingDone. /// Uses backpressure: waits if a watcher's channel is full. async fn dispatch_resources( &mut self, type_url: &str, resources: Vec, ) -> Vec> { let mut processing_done_futures = Vec::new(); let watcher_info: Vec<_> = match self.type_states.get_mut(type_url) { Some(s) => { for resource in &resources { let resource_name = resource.name().to_string(); s.cache.insert( resource_name, CachedResource::received(Arc::new(resource.clone())), ); } s.watchers .iter() .map(|(id, entry)| (*id, entry.event_tx.clone(), entry.subscription.clone())) .collect() } None => return processing_done_futures, }; // Cancel resource timers for received resources (gRFC A57). for resource in &resources { self.resource_timers .remove(&(type_url.to_string(), resource.name().to_string())); } for resource in resources { let resource_name = resource.name().to_string(); let resource = Arc::new(resource); for (_watcher_id, event_tx, subscription) in watcher_info.clone() { if subscription.matches(&resource_name) { let (done, rx) = ProcessingDone::channel(); let event = ResourceEvent::ResourceChanged { result: Ok(Arc::clone(&resource)), done, }; // Use backpressure: await if channel is full. // Ignore send errors (watcher dropped). let _ = event_tx.send(event).await; processing_done_futures.push(rx); } } } processing_done_futures } /// Notify watchers of a validation error for a specific resource. /// /// Per gRFC A46/A88, errors are routed only to watchers interested in /// that specific resource (plus wildcard watchers). async fn notify_resource_error(&mut self, type_url: &str, resource_name: &str, error: &str) { let type_state = match self.type_states.get_mut(type_url) { Some(s) => s, None => return, }; type_state.cache.insert( resource_name.to_string(), CachedResource::nacked(error.to_string()), ); // Cancel the resource timer (gRFC A57). self.resource_timers .remove(&(type_url.to_string(), resource_name.to_string())); for event_tx in type_state.matching_watchers(resource_name) { let (done, _rx) = ProcessingDone::channel(); let event = ResourceEvent::ResourceChanged { result: Err(Error::Validation(error.to_string())), done, }; let _ = event_tx.send(event).await; } } /// Detect resources that were deleted (present in cache but not in response). /// /// Per gRFC A53, for resource types with ALL_RESOURCES_REQUIRED_IN_SOTW = true, /// if a previously-received resource is absent from a new SotW response, /// it is treated as deleted. async fn detect_deleted_resources( &mut self, type_url: &str, received_names: &HashSet, ) -> Vec> { let mut processing_done_futures = Vec::new(); let type_state = match self.type_states.get_mut(type_url) { Some(s) => s, None => return processing_done_futures, }; if !type_state.all_resources_required_in_sotw { return processing_done_futures; } let deleted_names: Vec = type_state .cache .iter() .filter(|(name, cached)| { matches!(cached.state, ResourceState::Received) && !received_names.contains(*name) }) .map(|(name, _)| name.clone()) .collect(); for name in deleted_names { type_state .cache .insert(name.clone(), CachedResource::does_not_exist()); for event_tx in type_state.matching_watchers(&name) { let (done, rx) = ProcessingDone::channel(); let event = ResourceEvent::ResourceChanged { result: Err(Error::ResourceDoesNotExist), done, }; let _ = event_tx.send(event).await; processing_done_futures.push(rx); } } processing_done_futures } /// Send an ACK for a response. async fn send_ack( &self, stream: &mut S, response: &DiscoveryResponse, ) -> Result<()> { let type_state = match self.type_states.get(&response.type_url) { Some(s) => s, None => return Ok(()), }; let resource_names = type_state.resource_names_for_request(); let request = DiscoveryRequest { node: &self.node, type_url: &response.type_url, resource_names: &resource_names, version_info: &response.version_info, response_nonce: &response.nonce, error_detail: None, }; let bytes = self.codec.encode_request(&request)?; stream.send(bytes).await } /// Send a NACK for a response. async fn send_nack( &self, stream: &mut S, response: &DiscoveryResponse, error_message: String, ) -> Result<()> { let type_state = match self.type_states.get(&response.type_url) { Some(s) => s, None => return Ok(()), }; let resource_names = type_state.resource_names_for_request(); let request = DiscoveryRequest { node: &self.node, type_url: &response.type_url, resource_names: &resource_names, version_info: &type_state.version_info, // Keep old version for NACK response_nonce: &response.nonce, error_detail: Some(ErrorDetail { code: 3, // INVALID_ARGUMENT message: error_message, }), }; let bytes = self.codec.encode_request(&request)?; stream.send(bytes).await } /// Start a timer for a resource in Requested state (gRFC A57). /// /// If a timer is already running for this resource, this is a no-op to /// preserve the original timeout deadline per A57. /// /// When the timer fires, it sends a `ResourceTimerExpired` command. /// The handler checks if the resource is still in Requested state before acting. fn start_resource_timer(&mut self, type_url: &str, name: String, timeout: Duration) { let key = (type_url.to_string(), name.clone()); // Don't reset an existing timer — A57 says timeout starts on first request. if self.resource_timers.contains_key(&key) { return; } let (cancel_tx, cancel_rx) = oneshot::channel::<()>(); let type_url_owned = type_url.to_string(); let command_tx = self.command_tx.clone(); let runtime = self.runtime.clone(); self.runtime.spawn(async move { tokio::select! { _ = runtime.sleep(timeout) => { let _ = command_tx.send(WorkerCommand::ResourceTimerExpired { type_url: type_url_owned, name, }).await; } _ = cancel_rx => {} } }); self.resource_timers.insert(key, cancel_tx); } /// Handle a resource timer expiration (gRFC A57). /// /// If the resource is still in Requested state, marks it as DoesNotExist /// and notifies all watchers interested in this resource. async fn handle_resource_timeout(&mut self, type_url: &str, name: &str) { self.resource_timers .remove(&(type_url.to_string(), name.to_string())); let type_state = match self.type_states.get_mut(type_url) { Some(s) => s, None => return, }; let is_pending = type_state .cache .get(name) .map(|c| c.is_requested()) .unwrap_or(true); if !is_pending { return; } type_state .cache .insert(name.to_string(), CachedResource::does_not_exist()); for event_tx in type_state.matching_watchers(name) { let (done, _rx) = ProcessingDone::channel(); let event = ResourceEvent::ResourceChanged { result: Err(Error::ResourceDoesNotExist), done, }; let _ = event_tx.send(event).await; } } } ================================================ FILE: xds-client/src/codec/mod.rs ================================================ //! Codec for encoding/decoding xDS messages. //! //! The codec layer converts between crate-owned message types //! ([`DiscoveryRequest`], [`DiscoveryResponse`]) and serialized bytes. //! This abstraction allows different protobuf implementations //! (prost, google-protobuf) to be used with the same xDS client logic. use crate::error::Result; use crate::message::{DiscoveryRequest, DiscoveryResponse}; use bytes::Bytes; #[cfg(feature = "codegen-prost")] pub mod prost; /// Trait for encoding/decoding xDS discovery messages. /// /// Implementations convert between the crate-owned message types /// and their serialized wire format. pub trait XdsCodec: Send + Sync + 'static { /// Encode a [`DiscoveryRequest`] to bytes. fn encode_request(&self, request: &DiscoveryRequest<'_>) -> Result; /// Decode bytes into a [`DiscoveryResponse`]. fn decode_response(&self, bytes: Bytes) -> Result; } ================================================ FILE: xds-client/src/codec/prost.rs ================================================ //! Prost-based codec using envoy-types. use crate::codec::XdsCodec; use crate::error::{Error, Result}; use crate::message::{DiscoveryRequest, DiscoveryResponse, ResourceAny}; use bytes::Bytes; use prost::Message; /// A codec that uses prost/envoy-types for serialization. #[derive(Debug, Clone, Copy, Default)] pub struct ProstCodec; impl XdsCodec for ProstCodec { fn encode_request(&self, request: &DiscoveryRequest<'_>) -> Result { use envoy_types::pb::envoy::config::core::v3 as core; use envoy_types::pb::envoy::service::discovery::v3 as discovery; use envoy_types::pb::google::rpc::Status; let proto_request = discovery::DiscoveryRequest { version_info: request.version_info.to_owned(), node: Some(core::Node { id: request.node.id.clone().unwrap_or_default(), cluster: request.node.cluster.clone().unwrap_or_default(), user_agent_name: request.node.user_agent_name.clone(), user_agent_version_type: Some(core::node::UserAgentVersionType::UserAgentVersion( request.node.user_agent_version.clone(), )), locality: request.node.locality.as_ref().map(|l| core::Locality { region: l.region.clone(), zone: l.zone.clone(), sub_zone: l.sub_zone.clone(), }), ..Default::default() }), resource_names: request.resource_names.to_vec(), type_url: request.type_url.to_owned(), response_nonce: request.response_nonce.to_owned(), error_detail: request.error_detail.as_ref().map(|e| Status { code: e.code, message: e.message.clone(), details: vec![], }), ..Default::default() }; Ok(proto_request.encode_to_vec().into()) } fn decode_response(&self, bytes: Bytes) -> Result { use envoy_types::pb::envoy::service::discovery::v3 as discovery; let proto_response = discovery::DiscoveryResponse::decode(bytes).map_err(Error::Decode)?; Ok(DiscoveryResponse { version_info: proto_response.version_info, resources: proto_response .resources .into_iter() .map(|any| ResourceAny { type_url: any.type_url, value: any.value.into(), }) .collect(), type_url: proto_response.type_url, nonce: proto_response.nonce, }) } } #[cfg(test)] mod tests { use super::*; use crate::message::{ErrorDetail, Locality, Node}; #[test] fn test_encode_request_minimal() { let codec = ProstCodec; let node = Node::new("grpc", "1.0"); let resource_names = vec!["listener-1".to_string()]; let request = DiscoveryRequest { version_info: "", node: &node, type_url: "type.googleapis.com/envoy.config.listener.v3.Listener", resource_names: &resource_names, response_nonce: "", error_detail: None, }; let bytes = codec.encode_request(&request).unwrap(); assert!(!bytes.is_empty()); // Verify we can decode it back with prost use envoy_types::pb::envoy::service::discovery::v3 as discovery; let decoded = discovery::DiscoveryRequest::decode(bytes).unwrap(); assert_eq!(decoded.type_url, request.type_url); assert_eq!(decoded.resource_names, request.resource_names); } #[test] fn test_encode_request_with_node() { let codec = ProstCodec; let node = Node::new("grpc", "1.0") .with_id("node-1") .with_cluster("cluster-1") .with_locality(Locality { region: "us-west".to_string(), zone: "us-west-1a".to_string(), sub_zone: "rack-1".to_string(), }); let resource_names: Vec = Vec::new(); let request = DiscoveryRequest { version_info: "", node: &node, type_url: "type.googleapis.com/envoy.config.cluster.v3.Cluster", resource_names: &resource_names, response_nonce: "", error_detail: None, }; let bytes = codec.encode_request(&request).unwrap(); use envoy_types::pb::envoy::config::core::v3 as core; use envoy_types::pb::envoy::service::discovery::v3 as discovery; let decoded = discovery::DiscoveryRequest::decode(bytes).unwrap(); let node = decoded.node.unwrap(); assert_eq!(node.id, "node-1"); assert_eq!(node.cluster, "cluster-1"); assert_eq!(node.user_agent_name, "grpc"); // Verify user_agent_version is properly encoded match node.user_agent_version_type { Some(core::node::UserAgentVersionType::UserAgentVersion(version)) => { assert_eq!(version, "1.0"); } _ => panic!("Expected UserAgentVersion to be set"), } let locality = node.locality.unwrap(); assert_eq!(locality.region, "us-west"); assert_eq!(locality.zone, "us-west-1a"); assert_eq!(locality.sub_zone, "rack-1"); } #[test] fn test_decode_response() { use envoy_types::pb::envoy::service::discovery::v3 as discovery; use envoy_types::pb::google::protobuf::Any; let proto_response = discovery::DiscoveryResponse { version_info: "1".to_string(), type_url: "type.googleapis.com/envoy.config.listener.v3.Listener".to_string(), nonce: "nonce-1".to_string(), resources: vec![Any { type_url: "type.googleapis.com/envoy.config.listener.v3.Listener".to_string(), value: b"fake-listener-bytes".to_vec(), }], ..Default::default() }; let bytes: Bytes = proto_response.encode_to_vec().into(); let codec = ProstCodec; let response = codec.decode_response(bytes).unwrap(); assert_eq!(response.version_info, "1"); assert_eq!( response.type_url, "type.googleapis.com/envoy.config.listener.v3.Listener" ); assert_eq!(response.nonce, "nonce-1"); assert_eq!(response.resources.len(), 1); assert_eq!( response.resources[0].type_url, "type.googleapis.com/envoy.config.listener.v3.Listener" ); assert_eq!(response.resources[0].value.as_ref(), b"fake-listener-bytes"); } #[test] fn test_roundtrip() { use envoy_types::pb::envoy::service::discovery::v3 as discovery; let codec = ProstCodec; let node = Node::new("grpc", "1.0"); let resource_names = vec!["res-1".to_string(), "res-2".to_string()]; let request = DiscoveryRequest { version_info: "42", node: &node, type_url: "type.googleapis.com/test.Resource", resource_names: &resource_names, response_nonce: "nonce-abc", error_detail: Some(ErrorDetail { code: 3, // INVALID_ARGUMENT message: "validation failed".to_string(), }), }; let request_bytes = codec.encode_request(&request).unwrap(); let proto_request = discovery::DiscoveryRequest::decode(request_bytes).unwrap(); assert_eq!(proto_request.version_info, "42"); assert_eq!(proto_request.response_nonce, "nonce-abc"); let error = proto_request.error_detail.unwrap(); assert_eq!(error.code, 3); assert_eq!(error.message, "validation failed"); } } ================================================ FILE: xds-client/src/error.rs ================================================ //! Error types for the xDS client. use thiserror::Error; /// Error type for the xDS client. #[derive(Debug, Error)] pub enum Error { /// Failed to connect to the xDS server. #[error("failed to connect: {0}")] Connection(String), /// Error on the ADS stream. #[cfg(feature = "transport-tonic")] #[error("stream error: {0}")] Stream(#[from] tonic::Status), /// The stream was closed unexpectedly. #[error("stream closed unexpectedly")] StreamClosed, /// Failed to decode a protobuf message. #[cfg(feature = "codegen-prost")] #[error("decode error: {0}")] Decode(#[from] prost::DecodeError), /// Resource validation failed. #[error("resource validation failed: {0}")] Validation(String), /// Resource does not exist. /// /// This indicates the resource has been deleted or was never created. #[error("resource does not exist")] ResourceDoesNotExist, } /// Result type alias for xDS client operations. pub type Result = std::result::Result; ================================================ FILE: xds-client/src/lib.rs ================================================ //! A Rust implementation of [xDS](https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol) client. //! //! This crate provides a protocol-agnostic xDS client. It handles: //! - ADS stream management (connection, reconnection, etc.) //! - Resource subscription and watching //! - Version/nonce tracking and ACK/NACK //! //! It does NOT contain gRPC-specific logic such as: //! - LDS -> RDS -> CDS -> EDS cascading //! - gRPC-specific resource validation //! - Service config generation //! //! Instead a gRPC library can use this crate to build these features. //! //! # Example //! //! ```ignore //! use xds_client::{XdsClient, ClientConfig, Node, ResourceEvent}; //! //! // Create node and configuration //! let node = Node::new("grpc", "1.0").with_id("my-node"); //! let config = ClientConfig::new(node, "https://xds.example.com:443"); //! //! // Build client with transport, codec, and runtime //! let client = XdsClient::builder(config, transport, codec, runtime).build(); //! //! // Watch for Listener resources //! let mut watcher = client.watch::("my-listener").await; //! while let Some(event) = watcher.next().await { //! match event { //! ResourceEvent::ResourceChanged { result: Ok(resource), done } => { //! // Process the resource, possibly add cascading watches. //! client.watch::(&resource.route_name()).await; //! // Signal is sent automatically when done is dropped //! } //! ResourceEvent::ResourceChanged { result: Err(error), .. } => { //! // Resource was invalidated (validation error or deleted) //! eprintln!("Resource invalidated: {}", error); //! } //! ResourceEvent::AmbientError { error, .. } => { //! // Non-fatal error, continue using cached resource //! eprintln!("Ambient error: {}", error); //! } //! } //! } //! ``` //! //! # Feature Flags //! //! - `transport-tonic`: Enables the use of the `tonic` transport. This enables `rt-tokio` and `codegen-prost` features. Enabled by default. //! - `rt-tokio`: Enables the use of the `tokio` runtime. Enabled by default. //! - `codegen-prost`: Enables the use of the `prost` codec generated resources. Enabled by default. pub mod client; pub mod codec; pub mod error; pub mod message; pub mod resource; pub mod runtime; pub mod transport; pub use client::config::{ClientConfig, ServerConfig}; pub use client::retry::{Backoff, RetryPolicy}; pub use client::watch::{ProcessingDone, ResourceEvent, ResourceWatcher}; pub use client::{XdsClient, XdsClientBuilder}; pub use codec::XdsCodec; pub use error::{Error, Result}; pub use message::{DiscoveryRequest, DiscoveryResponse, ErrorDetail, Locality, Node, ResourceAny}; pub use resource::{DecodeResult, DecodedResource, Resource}; pub use runtime::Runtime; pub use transport::{Transport, TransportBuilder, TransportStream}; // Tokio runtime #[cfg(feature = "rt-tokio")] pub use runtime::tokio::TokioRuntime; // Tonic transport #[cfg(feature = "transport-tonic")] pub use transport::tonic::{TonicTransport, TonicTransportBuilder}; // Prost codec #[cfg(feature = "codegen-prost")] pub use codec::prost::ProstCodec; ================================================ FILE: xds-client/src/message.rs ================================================ //! Crate-owned xDS message types. //! //! These types are codegen-agnostic and serve as the interface between //! the xDS client logic and the codec layer. The codec converts these //! to/from the wire format (e.g., prost/envoy-types or google-protobuf). use bytes::Bytes; /// A discovery request to send to the xDS server. /// /// This struct borrows data to avoid unnecessary allocations when encoding. /// The data only needs to live long enough for the codec to encode it. #[derive(Debug, Clone)] pub struct DiscoveryRequest<'a> { /// The version_info provided in the most recent successfully processed /// response for this type, or empty for the first request. pub version_info: &'a str, /// The node making the request. pub node: &'a Node, /// List of resource names to subscribe to. pub resource_names: &'a [String], /// Type URL of the resource being requested. pub type_url: &'a str, /// The nonce from the most recent successfully processed response, /// or empty for the first request. pub response_nonce: &'a str, /// Error details if this is a NACK (negative acknowledgment). pub error_detail: Option, } /// A discovery response from the xDS server. #[derive(Debug, Clone, Default)] pub struct DiscoveryResponse { /// The version of the response data. pub version_info: String, /// The response resources wrapped as Any protos. pub resources: Vec, /// Type URL of the resources. pub type_url: String, /// Nonce for this response, to be echoed back in the next request. pub nonce: String, } /// A resource wrapped as google.protobuf.Any. #[derive(Debug, Clone)] pub struct ResourceAny { /// Type URL of the resource. pub type_url: String, /// Serialized resource bytes. pub value: Bytes, } /// Node identification for the client. #[derive(Debug, Clone)] pub struct Node { /// An opaque node identifier. pub id: Option, /// The cluster the node belongs to. pub cluster: Option, /// Locality specifying where the node is running. pub locality: Option, /// Free-form string identifying the client type (e.g., "envoy", "grpc"). pub user_agent_name: String, /// Version of the client. pub user_agent_version: String, } impl Node { /// Create a new Node with the required user agent fields. /// /// Other fields (id, cluster, locality) can be set using builder methods. pub fn new(user_agent_name: impl Into, user_agent_version: impl Into) -> Self { Self { id: None, cluster: None, locality: None, user_agent_name: user_agent_name.into(), user_agent_version: user_agent_version.into(), } } /// Set the node ID. pub fn with_id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } /// Set the cluster. pub fn with_cluster(mut self, cluster: impl Into) -> Self { self.cluster = Some(cluster.into()); self } /// Set the locality. pub fn with_locality(mut self, locality: Locality) -> Self { self.locality = Some(locality); self } } /// Locality information identifying where a node is running. #[derive(Debug, Clone, Default)] pub struct Locality { /// Region the node is in. pub region: String, /// Zone within the region. pub zone: String, /// Sub-zone within the zone. pub sub_zone: String, } /// Error details for NACK responses. #[derive(Debug, Clone)] pub struct ErrorDetail { /// gRPC status code. pub code: i32, /// Error message. pub message: String, } ================================================ FILE: xds-client/src/resource/mod.rs ================================================ //! Provides abstraction for xDS resources. use crate::error::Error; use bytes::Bytes; use std::any::Any; use std::sync::Arc; #[cfg(feature = "codegen-prost")] pub mod prost; /// A type URL identifying an xDS resource type. /// /// Format: `type.googleapis.com/` /// e.g. `type.googleapis.com/envoy.config.listener.v3.Listener` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct TypeUrl(&'static str); impl TypeUrl { /// Create a new type URL from a static string. pub const fn new(url: &'static str) -> Self { Self(url) } /// Returns the type URL as a string slice. pub const fn as_str(&self) -> &'static str { self.0 } } /// Result of decoding a resource. /// /// This enum represents the three possible outcomes of decoding, following /// the pattern used in grpc-go's xDS client (gRFC A46/A88): /// /// - [`Success`](DecodeResult::Success): Resource decoded and validated successfully. /// - [`ResourceError`](DecodeResult::ResourceError): Decoding failed but the resource /// name was identified. The error can be routed to the specific watcher. /// - [`TopLevelError`](DecodeResult::TopLevelError): Decoding failed and the resource /// name could not be identified. No specific watcher can be notified. #[derive(Debug)] pub enum DecodeResult { /// Resource decoded and validated successfully. Success { /// The resource name. name: String, /// The decoded resource. resource: T, }, /// Error decoding a resource whose name could be identified. /// /// This typically occurs when deserialization succeeded (allowing the name /// to be extracted) but subsequent validation failed. /// The error will be reported to watchers interested in this specific resource. ResourceError { /// The resource name that was extracted before the error occurred. name: String, /// The error that occurred during validation. error: Error, }, /// Error decoding a resource whose name could not be identified. /// /// This typically occurs when deserialization fails early, before the resource /// name can be extracted. Since we don't know which resource this was meant to be, /// no specific watcher can be notified. The error is included in the NACK /// message sent back to the server. TopLevelError(Error), } /// Trait for xDS resources. /// /// # Two-Phase Decoding /// /// Resource decoding is split into two phases to support per-resource error /// reporting (gRFC A46/A88): /// /// 1. **Deserialization**: Parse bytes into the [`Message`](Self::Message) type. /// If this fails, no resource name is available ([`DecodeResult::TopLevelError`]). /// /// 2. **Validation**: Transform the message into the final resource type. /// If this fails, the resource name is known ([`DecodeResult::ResourceError`]). /// /// The provided [`decode`](Self::decode) method orchestrates these phases and /// returns the appropriate [`DecodeResult`]. /// /// # Resource Deletion in State of the World (SotW) /// /// In SotW xDS, the server sends all resources in each response. The client must /// determine whether a previously-seen resource that's absent from a new response /// has been deleted or is simply not included. /// /// Per gRFC A53, the behavior depends on the resource type: /// /// - **`ALL_RESOURCES_REQUIRED_IN_SOTW = true`** (default): The server always /// sends all resources of this type in each response. If a subscribed /// resource is missing, it's treated as deleted. Watchers receive `ResourceDoesNotExist`. /// Examples: Listener (LDS), Cluster (CDS). /// /// - **`ALL_RESOURCES_REQUIRED_IN_SOTW = false`**: The resource type allows partial /// responses. Missing resources are not treated as deleted; the client continues /// using the cached version. Examples: RouteConfiguration (RDS), ClusterLoadAssignment (EDS). /// /// # Example /// /// ```ignore /// impl Resource for Listener { /// type Message = ListenerProto; /// /// const TYPE_URL: TypeUrl = TypeUrl::new("type.googleapis.com/envoy.config.listener.v3.Listener"); /// /// fn deserialize(bytes: Bytes) -> Result { /// ListenerProto::decode(bytes).map_err(Into::into) /// } /// /// fn name(message: &Self::Message) -> &str { /// &message.name /// } /// /// fn validate(message: Self::Message) -> Result { /// // Validation and transformation logic... /// Ok(Self { name: message.name, /* ... */ }) /// } /// } /// ``` pub trait Resource: Sized + Send + Sync + 'static { /// The deserialized message type (e.g., protobuf-generated struct). type Message; /// The xDS type URL for this resource type. const TYPE_URL: TypeUrl; /// Whether all subscribed resources must be present in each SotW response. /// /// When `true` (default), if a previously-received resource is absent from a new /// response, it is treated as deleted. Watchers are notified with `ResourceDoesNotExist`. /// /// When `false`, missing resources are not treated as deleted. The client continues /// using the cached version until explicitly removed or updated. /// /// Per gRFC A53: /// - LDS (Listener) and CDS (Cluster): `true` /// - RDS (RouteConfiguration) and EDS (ClusterLoadAssignment): `false` const ALL_RESOURCES_REQUIRED_IN_SOTW: bool = true; /// Deserialize bytes into the message type. /// /// This is the first phase of decoding. If this fails, no resource name /// is available and the error becomes a [`DecodeResult::TopLevelError`]. fn deserialize(bytes: Bytes) -> Result; /// Extract the resource name from the deserialized message. fn name(message: &Self::Message) -> &str; /// Validate and transform the message into the final resource type. /// /// This is the second phase of decoding. If this fails, the resource name /// is known (from [`name`](Self::name)) and the error becomes /// a [`DecodeResult::ResourceError`]. fn validate(message: Self::Message) -> Result; } /// Decode and validate a resource from its serialized bytes. /// /// This function orchestrates the two-phase decoding process: /// 1. Deserialize bytes into [`Resource::Message`] /// 2. Validate and transform into `T` /// /// Returns the appropriate [`DecodeResult`] based on where (if anywhere) the error occurred. pub(crate) fn decode(bytes: Bytes) -> DecodeResult { let message = match T::deserialize(bytes) { Ok(m) => m, Err(e) => return DecodeResult::TopLevelError(e), }; let name = T::name(&message).to_string(); match T::validate(message) { Ok(resource) => DecodeResult::Success { name, resource }, Err(e) => DecodeResult::ResourceError { name, error: e }, } } /// A decoded resource with type-erased value. /// /// Created by the decoder function when a response is received from the xDS server. /// The worker stores and dispatches these to watchers, which downcast to the concrete type. /// /// This type is cheaply cloneable (via `Arc`) to support multiple watchers /// for the same resource. #[derive(Debug, Clone)] pub struct DecodedResource { type_url: &'static str, name: String, value: Arc, } impl DecodedResource { /// Create a new decoded resource from a concrete resource type. pub fn new(name: String, resource: T) -> Self { Self { type_url: T::TYPE_URL.as_str(), name, value: Arc::new(resource), } } /// Returns the type URL of the resource. pub fn type_url(&self) -> &'static str { self.type_url } /// Returns the name of the resource. pub fn name(&self) -> &str { &self.name } /// Downcast to a concrete type wrapped in `Arc`. /// /// Returns `None` if the type does not match. /// /// This returns `Arc` because `DecodedResource` may be cloned and shared /// across multiple watchers. Each watcher receives a reference to the same /// underlying resource data. /// /// This method clones the internal `Arc` (cheap refcount increment) and /// attempts to downcast it to the concrete type. pub fn downcast(&self) -> Option> { Arc::clone(&self.value).downcast().ok() } /// Borrow the decoded resource and downcast to a concrete type reference. /// /// Returns `None` if the type does not match. pub fn downcast_ref(&self) -> Option<&T> { self.value.downcast_ref() } } /// Type-erased decoder function. /// /// Created by `XdsClient::watch()` capturing the resource type `T`. /// The worker stores this per type_url and uses it to decode incoming resources. /// /// Returns a [`DecodeResult`] indicating success or the type of failure. pub type DecoderFn = Box DecodeResult + Send + Sync>; ================================================ FILE: xds-client/src/resource/prost.rs ================================================ //! `prost` codec-specific resources. ================================================ FILE: xds-client/src/runtime/mod.rs ================================================ //! Provides abstraction for async runtimes. use std::future::Future; use std::time::Duration; #[cfg(feature = "rt-tokio")] pub mod tokio; /// Trait for async runtime operations. /// /// This abstraction allows the xDS client to be runtime-agnostic. // TODO: unify with the grpc-rust runtime trait pub trait Runtime: Send + Sync + Clone + 'static { /// Spawn a future to run in the background. fn spawn(&self, future: F) where F: Future + Send + 'static; /// Sleep for the given duration. fn sleep(&self, duration: Duration) -> impl Future + Send; } ================================================ FILE: xds-client/src/runtime/tokio.rs ================================================ //! `tokio` based runtime implementation. use crate::runtime::Runtime; use std::future::Future; use std::time::Duration; /// Tokio-based runtime implementation. #[derive(Clone, Debug, Default)] pub struct TokioRuntime; impl Runtime for TokioRuntime { fn spawn(&self, future: F) where F: Future + Send + 'static, { tokio::spawn(future); } async fn sleep(&self, duration: Duration) { tokio::time::sleep(duration).await; } } ================================================ FILE: xds-client/src/transport/mod.rs ================================================ //! Provides abstraction for transport layers. use crate::client::config::ServerConfig; use crate::error::Result; use bytes::Bytes; use std::future::Future; #[cfg(feature = "transport-tonic")] pub mod tonic; mod sealed { pub trait Sealed {} } /// Factory for creating xDS transport streams. /// /// This abstraction allows for different transport implementations: /// - Tonic-based gRPC transport /// - The upcoming gRPC Rust transport /// - Mock transport for testing /// - Other custom transports pub trait Transport: Send + Sync + 'static { /// The stream type produced by this transport. type Stream: TransportStream; /// Creates a new bidirectional ADS stream to the xDS server. /// /// # Arguments /// /// * `initial_requests` - Requests to send immediately when establishing the stream. /// This is critical for xDS servers that don't send response headers until /// they receive the first request (prevents deadlock). /// /// This may be called multiple times for reconnection. fn new_stream( &self, initial_requests: Vec, ) -> impl Future> + Send; } /// A bidirectional byte stream for xDS ADS communication. /// /// Raw byte transport where the bytes are serialized DiscoveryRequest/DiscoveryResponse /// (de)serialization is handled at the xDS client worker layer. // Sealed for now to limit API surface. pub trait TransportStream: sealed::Sealed + Send + 'static { /// Send serialized DiscoveryRequest bytes to the server. fn send(&mut self, request: Bytes) -> impl Future> + Send; /// Receive serialized DiscoveryResponse bytes from the server. /// /// Returns: /// - `Ok(Some(bytes))` - Received a response. /// - `Ok(None)` - Stream closed normally. /// - `Err(_)` - Stream error (connection dropped, etc.) fn recv(&mut self) -> impl Future>> + Send; } #[cfg(feature = "transport-tonic")] impl sealed::Sealed for tonic::TonicAdsStream {} /// Factory for creating transports to xDS servers. /// /// This abstraction allows the client to create transports on-demand, /// enabling features like: /// - Server fallback (gRFC A71): Try backup servers when primary fails /// - Connection pooling: Reuse connections to the same server /// /// Implementations may hold configuration (e.g., TLS settings) that applies /// to all servers. /// /// # Example /// /// ```ignore /// use xds_client::{ServerConfig, TransportBuilder}; /// /// struct MyTransportBuilder { /* ... */ } /// /// impl TransportBuilder for MyTransportBuilder { /// type Transport = MyTransport; /// /// async fn build(&self, server: &ServerConfig) -> Result { /// // Create transport connected to server.uri() /// } /// } /// ``` pub trait TransportBuilder: Send + Sync + 'static { /// The transport type produced by this builder. type Transport: Transport; /// Build a transport connected to the given server. /// /// This may be called multiple times for reconnection or fallback. /// Implementations may cache/pool connections internally. fn build(&self, server: &ServerConfig) -> impl Future> + Send; // Future extensions: // - `fn close(&self, server: &ServerConfig)` for explicit connection cleanup // - Metrics/observability hooks } ================================================ FILE: xds-client/src/transport/tonic.rs ================================================ //! `tonic` based transport implementation. //! //! This transport uses tonic's low-level `Grpc` client with a `BytesCodec` //! to send and receive raw bytes, allowing the xDS client layer to handle //! serialization/deserialization independently. use crate::client::config::ServerConfig; use crate::error::{Error, Result}; use crate::transport::{Transport, TransportBuilder, TransportStream}; use bytes::{Buf, BufMut, Bytes}; use http::uri::PathAndQuery; use tokio::sync::mpsc; use tokio_stream::StreamExt as _; use tonic::client::Grpc; use tonic::codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}; use tonic::transport::Channel; use tonic::{Status, Streaming}; /// The gRPC path for the ADS StreamAggregatedResources RPC. const ADS_PATH: &str = "/envoy.service.discovery.v3.AggregatedDiscoveryService/StreamAggregatedResources"; const ADS_CHANNEL_BUFFER_SIZE: usize = 16; /// A codec that passes bytes through without serialization. /// /// This allows us to handle serialization in the xDS client layer /// rather than in the transport layer. #[derive(Debug, Clone, Copy)] struct BytesCodec; impl Codec for BytesCodec { type Encode = Bytes; type Decode = Bytes; type Encoder = BytesEncoder; type Decoder = BytesDecoder; fn encoder(&mut self) -> Self::Encoder { BytesEncoder } fn decoder(&mut self) -> Self::Decoder { BytesDecoder } } #[derive(Debug)] struct BytesEncoder; impl Encoder for BytesEncoder { type Item = Bytes; type Error = Status; fn encode( &mut self, item: Self::Item, dst: &mut EncodeBuf<'_>, ) -> std::result::Result<(), Self::Error> { dst.put_slice(&item); Ok(()) } } #[derive(Debug)] struct BytesDecoder; impl Decoder for BytesDecoder { type Item = Bytes; type Error = Status; fn decode( &mut self, src: &mut DecodeBuf<'_>, ) -> std::result::Result, Self::Error> { Ok(Some(src.copy_to_bytes(src.remaining()))) } } /// Factory for creating ADS streams using tonic. #[derive(Clone, Debug)] pub struct TonicTransport { channel: Channel, } impl TonicTransport { /// Create a transport from an existing tonic [`Channel`]. /// /// Use this when you need custom channel configuration (e.g., TLS, timeouts). /// /// # Example /// /// ```ignore /// use tonic::transport::{Certificate, Channel, ClientTlsConfig}; /// /// let tls = ClientTlsConfig::new() /// .ca_certificate(Certificate::from_pem(ca_cert)) /// .domain_name("xds.example.com"); /// /// let channel = Channel::from_static("https://xds.example.com:443") /// .tls_config(tls)? /// .connect() /// .await?; /// /// let transport = TonicTransport::from_channel(channel); /// ``` pub fn from_channel(channel: Channel) -> Self { Self { channel } } /// Connect to an xDS server with default settings. /// /// For custom configuration (TLS, timeouts, etc.), use [`from_channel`](Self::from_channel). pub async fn connect(uri: impl Into) -> Result { let uri: String = uri.into(); let channel = Channel::from_shared(uri) .map_err(|e| Error::Connection(e.to_string()))? .connect() .await .map_err(|e| Error::Connection(e.to_string()))?; Ok(Self { channel }) } } /// Builder for creating [`TonicTransport`] instances. /// /// This implements [`TransportBuilder`] and can be used with [`XdsClientBuilder`] /// to enable server fallback support. /// /// For connections requiring TLS or custom channel configuration, see the /// example in [`TonicTransport::from_channel`]. /// /// # Example /// /// ```ignore /// use xds_client::{ClientConfig, Node, TonicTransportBuilder, XdsClient}; /// /// let transport_builder = TonicTransportBuilder::new(); /// let config = ClientConfig::new(node, "http://xds.example.com:18000"); /// let client = XdsClient::builder(config, transport_builder, codec, runtime).build(); /// ``` #[derive(Debug, Clone, Default)] pub struct TonicTransportBuilder { // Future extensions: // - TLS configuration (requires tonic TLS feature) // - Connection timeout settings // - Keep-alive configuration // - Connection pooling settings // - Per-server credential overrides (via ServerConfig.extensions) } impl TonicTransportBuilder { /// Create a new transport builder with default settings. pub fn new() -> Self { Self::default() } } impl TransportBuilder for TonicTransportBuilder { type Transport = TonicTransport; async fn build(&self, server: &ServerConfig) -> Result { let channel = Channel::from_shared(server.uri().to_string()) .map_err(|e| Error::Connection(e.to_string()))? .connect() .await .map_err(|e| Error::Connection(e.to_string()))?; Ok(TonicTransport::from_channel(channel)) } } impl Transport for TonicTransport { type Stream = TonicAdsStream; async fn new_stream(&self, initial_requests: Vec) -> Result { let mut grpc = Grpc::new(self.channel.clone()); grpc.ready() .await .map_err(|e| Error::Connection(e.to_string()))?; let (tx, rx) = mpsc::channel::(ADS_CHANNEL_BUFFER_SIZE); // Create a stream that first yields initial requests, then reads from the channel. // This ensures data is available immediately when the stream is polled, // preventing deadlock with servers that don't send response headers // until they receive the first request message. let initial_stream = tokio_stream::iter(initial_requests); let channel_stream = tokio_stream::wrappers::ReceiverStream::new(rx); let request_stream = initial_stream.chain(channel_stream); let path = PathAndQuery::from_static(ADS_PATH); let response = grpc .streaming(tonic::Request::new(request_stream), path, BytesCodec) .await .map_err(Error::Stream)?; Ok(TonicAdsStream { sender: tx, receiver: response.into_inner(), }) } } /// A bidirectional ADS stream backed by tonic. #[derive(Debug)] pub struct TonicAdsStream { sender: mpsc::Sender, receiver: Streaming, } impl TransportStream for TonicAdsStream { async fn send(&mut self, request: Bytes) -> Result<()> { self.sender .send(request) .await .map_err(|_| Error::StreamClosed)?; Ok(()) } async fn recv(&mut self) -> Result> { match self.receiver.message().await { Ok(msg) => Ok(msg), Err(status) => Err(Error::Stream(status)), } } } #[cfg(test)] mod tests { use super::*; use envoy_types::pb::envoy::service::discovery::v3::{ DeltaDiscoveryRequest, DeltaDiscoveryResponse, DiscoveryRequest, DiscoveryResponse, aggregated_discovery_service_server::{ AggregatedDiscoveryService, AggregatedDiscoveryServiceServer, }, }; use prost::Message; use std::net::SocketAddr; use std::pin::Pin; use tokio::net::TcpListener; use tokio_stream::Stream; use tokio_stream::wrappers::TcpListenerStream; use tonic::{Request, Response, Status}; /// Mock ADS server that echoes back a response for each request. struct MockAdsServer; #[tonic::async_trait] impl AggregatedDiscoveryService for MockAdsServer { type StreamAggregatedResourcesStream = Pin> + Send>>; async fn stream_aggregated_resources( &self, request: Request>, ) -> std::result::Result, Status> { let mut inbound = request.into_inner(); let outbound = async_stream::try_stream! { while let Some(req) = inbound.next().await { let req = req?; let response = DiscoveryResponse { version_info: "1".to_string(), type_url: req.type_url.clone(), nonce: "nonce-1".to_string(), resources: vec![], ..Default::default() }; yield response; } }; Ok(Response::new(Box::pin(outbound))) } type DeltaAggregatedResourcesStream = Pin> + Send>>; async fn delta_aggregated_resources( &self, _request: Request>, ) -> std::result::Result, Status> { Err(Status::unimplemented("delta not supported in mock")) } } async fn start_mock_server() -> SocketAddr { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { tonic::transport::Server::builder() .add_service(AggregatedDiscoveryServiceServer::new(MockAdsServer)) .serve_with_incoming(TcpListenerStream::new(listener)) .await .unwrap(); }); // Give the server a moment to start tokio::time::sleep(std::time::Duration::from_millis(50)).await; addr } #[tokio::test] async fn test_tonic_transport_connect_and_stream() { let addr = start_mock_server().await; let uri = format!("http://{addr}"); let transport = TonicTransport::connect(&uri).await.unwrap(); let request = DiscoveryRequest { type_url: "type.googleapis.com/envoy.config.listener.v3.Listener".to_string(), resource_names: vec!["listener-1".to_string()], ..Default::default() }; let request_bytes: Bytes = request.encode_to_vec().into(); let mut stream = transport.new_stream(vec![request_bytes]).await.unwrap(); let response_bytes = stream.recv().await.unwrap().unwrap(); let response = DiscoveryResponse::decode(response_bytes).unwrap(); assert_eq!(response.version_info, "1"); assert_eq!( response.type_url, "type.googleapis.com/envoy.config.listener.v3.Listener" ); assert_eq!(response.nonce, "nonce-1"); } }