From 1904ae6b9a0dbaecd4cc7238271a235b2cf99f6b Mon Sep 17 00:00:00 2001 From: Joe Dahlquist Date: Fri, 11 Feb 2022 11:09:14 -0800 Subject: [PATCH 1/3] tonic: introduce async_interceptor --- examples/src/tower/server.rs | 6 + tests/integration_tests/tests/extensions.rs | 95 +++++++ tonic/src/service/interceptor.rs | 287 ++++++++++++++++++++ tonic/src/service/mod.rs | 4 +- 4 files changed, 391 insertions(+), 1 deletion(-) diff --git a/examples/src/tower/server.rs b/examples/src/tower/server.rs index 59d884ad8..d1e27cf30 100644 --- a/examples/src/tower/server.rs +++ b/examples/src/tower/server.rs @@ -48,6 +48,7 @@ async fn main() -> Result<(), Box> { .layer(MyMiddlewareLayer::default()) // Interceptors can be also be applied as middleware .layer(tonic::service::interceptor(intercept)) + .layer(tonic::service::async_interceptor(async_intercept)) .into_inner(); Server::builder() @@ -65,6 +66,11 @@ fn intercept(req: Request<()>) -> Result, Status> { Ok(req) } +// An async interceptor function. +async fn async_intercept(req: Request<()>) -> Result, Status> { + Ok(req) +} + #[derive(Debug, Clone, Default)] struct MyMiddlewareLayer; diff --git a/tests/integration_tests/tests/extensions.rs b/tests/integration_tests/tests/extensions.rs index c60c98414..e41f87d02 100644 --- a/tests/integration_tests/tests/extensions.rs +++ b/tests/integration_tests/tests/extensions.rs @@ -8,6 +8,7 @@ use std::{ use tokio::sync::oneshot; use tonic::{ body::BoxBody, + service::{async_interceptor, interceptor}, transport::{Endpoint, NamedService, Server}, Request, Response, Status, }; @@ -60,6 +61,100 @@ async fn setting_extension_from_interceptor() { jh.await.unwrap(); } +#[tokio::test] +async fn setting_extension_from_interceptor_layer() { + 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::new(Svc); + let interceptor_layer = interceptor(|mut req: Request<()>| { + req.extensions_mut().insert(ExtensionValue(42)); + Ok(req) + }); + + let (tx, rx) = oneshot::channel::<()>(); + + let jh = tokio::spawn(async move { + Server::builder() + .layer(interceptor_layer) + .add_service(svc) + .serve_with_shutdown("127.0.0.1:1325".parse().unwrap(), rx.map(drop)) + .await + .unwrap(); + }); + + tokio::time::sleep(Duration::from_millis(100)).await; + + let channel = Endpoint::from_static("http://127.0.0.1:1325") + .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_async_interceptor_layer() { + 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::new(Svc); + let interceptor_layer = async_interceptor(|mut req: Request<()>| { + req.extensions_mut().insert(ExtensionValue(42)); + futures::future::ok(req) + }); + + let (tx, rx) = oneshot::channel::<()>(); + + let jh = tokio::spawn(async move { + Server::builder() + .layer(interceptor_layer) + .add_service(svc) + .serve_with_shutdown("127.0.0.1:1326".parse().unwrap(), rx.map(drop)) + .await + .unwrap(); + }); + + tokio::time::sleep(Duration::from_millis(100)).await; + + let channel = Endpoint::from_static("http://127.0.0.1:1326") + .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; diff --git a/tonic/src/service/interceptor.rs b/tonic/src/service/interceptor.rs index 85477e432..b27d6be7a 100644 --- a/tonic/src/service/interceptor.rs +++ b/tonic/src/service/interceptor.rs @@ -3,10 +3,12 @@ //! See [`Interceptor`] for more details. use crate::{request::SanitizeHeaders, Status}; +use http::Uri; use pin_project::pin_project; use std::{ fmt, future::Future, + mem, pin::Pin, task::{Context, Poll}, }; @@ -52,6 +54,97 @@ where } } +/// Async version of `Interceptor`. +pub trait AsyncInterceptor { + /// The Future returned by the interceptor. + type Future: Future, Status>>; + /// Call the underlying async function that transforms a body-less gRPC request. + fn call_underlying(&mut self, request: crate::Request<()>) -> Self::Future; + /// Intercept a request before it is sent, optionally cancelling it. + fn call( + &mut self, + request: http::Request, + ) -> AsyncInterceptorFuture; +} + +impl AsyncInterceptor for F +where + F: FnMut(crate::Request<()>) -> U, + U: Future, Status>>, +{ + type Future = U; + + fn call_underlying(&mut self, request: crate::Request<()>) -> Self::Future { + self(request) + } + + fn call( + &mut self, + request: http::Request, + ) -> AsyncInterceptorFuture { + AsyncInterceptorFuture::new(self, request) + } +} + +/// Wrapper that hides the gRPC body from the underlying [`AsyncInterceptor`] function. +#[pin_project] +#[derive(Debug)] +pub struct AsyncInterceptorFuture +where + I: Future, Status>>, +{ + #[pin] + interceptor_fut: I, + uri: Uri, + msg: ReqBody, +} + +impl AsyncInterceptorFuture +where + F: Future, Status>>, +{ + fn new>( + interceptor: &mut A, + req: http::Request, + ) -> Self { + let uri = req.uri().clone(); + let grpc_req = crate::Request::from_http(req); + let (metadata, extensions, msg) = grpc_req.into_parts(); + + let req_without_body = crate::Request::from_parts(metadata, extensions, ()); + AsyncInterceptorFuture { + interceptor_fut: interceptor.call_underlying(req_without_body), + uri, + msg, + } + } +} + +impl Future for AsyncInterceptorFuture +where + F: Future, Status>>, + ReqBody: Default, +{ + type Output = Result, crate::Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + match this.interceptor_fut.poll(cx) { + Poll::Ready(intercepted_req) => match intercepted_req { + Ok(r) => { + let (metadata, extensions, _) = r.into_parts(); + let msg = mem::replace(this.msg, ReqBody::default()); + let req = crate::Request::from_parts(metadata, extensions, msg); + let req = req.into_http(this.uri.clone(), SanitizeHeaders::No); + Poll::Ready(Ok(req)) + } + Err(status) => Poll::Ready(Err(status.into())), + }, + Poll::Pending => return Poll::Pending, + } + } +} + /// Create a new interceptor layer. /// /// See [`Interceptor`] for more details. @@ -62,6 +155,16 @@ where InterceptorLayer { f } } +/// Create a new async interceptor layer. +/// +/// See [`AsyncInterceptor`] and [`Interceptor`] for more details. +pub fn async_interceptor(f: F) -> AsyncInterceptorLayer +where + F: AsyncInterceptor, +{ + AsyncInterceptorLayer { f } +} + #[deprecated( since = "0.5.1", note = "Please use the `interceptor` function instead" @@ -96,6 +199,27 @@ where } } +/// A gRPC async interceptor that can be used as a [`Layer`], +/// created by calling [`async_interceptor`]. +/// +/// See [`AsyncInterceptor`] for more details. +#[derive(Debug, Clone, Copy)] +pub struct AsyncInterceptorLayer { + f: F, +} + +impl Layer for AsyncInterceptorLayer +where + S: Clone, + F: AsyncInterceptor + Clone, +{ + type Service = AsyncInterceptedService; + + fn layer(&self, service: S) -> Self::Service { + AsyncInterceptedService::new(service, self.f.clone()) + } +} + #[deprecated( since = "0.5.1", note = "Please use the `InterceptorLayer` type instead" @@ -173,6 +297,56 @@ where } } +/// A service wrapped in an async interceptor middleware. +/// +/// See [`AsyncInterceptor`] for more details. +#[derive(Clone, Copy)] +pub struct AsyncInterceptedService { + inner: S, + f: F, +} + +impl AsyncInterceptedService { + /// Create a new `AsyncInterceptedService` that wraps `S` and intercepts each request with the + /// function `F`. + pub fn new(service: S, f: F) -> Self { + Self { inner: service, f } + } +} + +impl fmt::Debug for AsyncInterceptedService +where + S: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AsyncInterceptedService") + .field("inner", &self.inner) + .field("f", &format_args!("{}", std::any::type_name::())) + .finish() + } +} + +impl Service> for AsyncInterceptedService +where + F: AsyncInterceptor + Clone, + S: Service, Response = http::Response> + Clone, + S::Error: Into, + ReqBody: Default, +{ + type Response = S::Response; + type Error = crate::Error; + type Future = AsyncResponseFuture, ReqBody>; + + #[inline] + 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 { + AsyncResponseFuture::new(self.f.call(req), self.inner.clone()) + } +} + // required to use `InterceptedService` with `Router` #[cfg(feature = "transport")] impl crate::transport::NamedService for InterceptedService @@ -182,6 +356,15 @@ where const NAME: &'static str = S::NAME; } +// required to use `AsyncInterceptedService` with `Router` +#[cfg(feature = "transport")] +impl crate::transport::NamedService for AsyncInterceptedService +where + S: crate::transport::NamedService, +{ + const NAME: &'static str = S::NAME; +} + /// Response future for [`InterceptedService`]. #[pin_project] #[derive(Debug)] @@ -229,6 +412,77 @@ where } } +#[pin_project(project = PinnedOptionProj)] +#[derive(Debug)] +enum PinnedOption { + Some(#[pin] F), + None, +} + +/// Response future for [`AsyncInterceptedService`]. +#[pin_project] +#[derive(Debug)] +pub struct AsyncResponseFuture +where + S: Service>, + S::Error: Into, + I: Future, crate::Error>>, +{ + #[pin] + interceptor_fut: PinnedOption, + #[pin] + inner_fut: PinnedOption>, + inner: S, +} + +impl AsyncResponseFuture +where + S: Service>, + S::Error: Into, + I: Future, crate::Error>>, +{ + fn new(interceptor_fut: I, inner: S) -> Self { + AsyncResponseFuture { + interceptor_fut: PinnedOption::Some(interceptor_fut), + inner_fut: PinnedOption::None, + inner, + } + } +} + +impl Future for AsyncResponseFuture +where + S: Service, Response = http::Response>, + I: Future, crate::Error>>, + S::Error: Into, + ReqBody: Default, +{ + type Output = Result, crate::Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + if let PinnedOptionProj::Some(f) = this.interceptor_fut.as_mut().project() { + match f.poll(cx) { + Poll::Ready(intercepted_req) => match intercepted_req { + Ok(req) => { + this.inner_fut + .set(PinnedOption::Some(ResponseFuture::future( + this.inner.call(req), + ))); + this.interceptor_fut.set(PinnedOption::None); + } + Err(e) => return Poll::Ready(Err(e)), + }, + Poll::Pending => return Poll::Pending, + } + } + if let PinnedOptionProj::Some(inner_fut) = this.inner_fut.project() { + return inner_fut.poll(cx); + } + panic!() + } +} + #[cfg(test)] mod tests { #[allow(unused_imports)] @@ -267,4 +521,37 @@ mod tests { svc.oneshot(request).await.unwrap(); } + + #[tokio::test] + async fn async_interceptor_doesnt_remove_headers() { + 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::<_, hyper::Error>(hyper::Response::new(hyper::Body::empty())) + }); + + let svc = AsyncInterceptedService::new(svc, |request: crate::Request<()>| { + assert_eq!( + request + .metadata() + .get("user-agent") + .expect("missing in interceptor"), + "test-tonic" + ); + std::future::ready(Ok(request)) + }); + + let request = http::Request::builder() + .header("user-agent", "test-tonic") + .body(hyper::Body::empty()) + .unwrap(); + + svc.oneshot(request).await.unwrap(); + } } diff --git a/tonic/src/service/mod.rs b/tonic/src/service/mod.rs index 125ac7980..f464d0670 100644 --- a/tonic/src/service/mod.rs +++ b/tonic/src/service/mod.rs @@ -4,4 +4,6 @@ pub mod interceptor; #[doc(inline)] #[allow(deprecated)] -pub use self::interceptor::{interceptor, interceptor_fn, Interceptor}; +pub use self::interceptor::{ + async_interceptor, interceptor, interceptor_fn, AsyncInterceptor, Interceptor, +}; From 6769a870987329ef2a5c6915402f0358814e3c93 Mon Sep 17 00:00:00 2001 From: Joe Dahlquist Date: Tue, 10 May 2022 12:40:49 -0700 Subject: [PATCH 2/3] tonic: merge recent commits and refactor (async_)interceptor --- .github/workflows/CI.yml | 3 + CHANGELOG.md | 62 +- README.md | 6 +- examples/Cargo.toml | 52 +- examples/LICENSE | 19 + examples/build.rs | 29 +- examples/helloworld-tutorial.md | 6 +- examples/routeguide-tutorial.md | 6 +- examples/src/authentication/client.rs | 2 +- examples/src/authentication/server.rs | 2 +- examples/src/gcp/client.rs | 2 +- examples/src/json-codec/client.rs | 28 + examples/src/json-codec/common.rs | 80 +++ examples/src/json-codec/server.rs | 48 ++ examples/src/mock/mock.rs | 70 +-- examples/src/streaming/client.rs | 75 ++- examples/src/streaming/server.rs | 115 +++- examples/src/tls/client_rustls.rs | 85 +++ examples/src/tls/server_rustls.rs | 143 +++++ examples/src/tower/server.rs | 8 +- examples/src/uds/server.rs | 85 +-- interop/Cargo.toml | 6 +- interop/LICENSE | 19 + interop/src/bin/client.rs | 92 ++- interop/src/bin/server.rs | 8 +- interop/src/client.rs | 2 +- tests/ambiguous_methods/Cargo.toml | 2 +- tests/ambiguous_methods/LICENSE | 19 + tests/compression/Cargo.toml | 4 +- tests/compression/LICENSE | 19 + tests/compression/src/bidirectional_stream.rs | 4 +- tests/compression/src/client_stream.rs | 16 +- tests/compression/src/compressing_request.rs | 12 +- tests/compression/src/compressing_response.rs | 28 +- tests/compression/src/lib.rs | 2 +- tests/compression/src/server_stream.rs | 12 +- tests/compression/src/util.rs | 39 +- tests/extern_path/my_application/Cargo.toml | 4 +- tests/extern_path/my_application/LICENSE | 19 + tests/extern_path/uuid/Cargo.toml | 4 +- tests/extern_path/uuid/LICENSE | 19 + tests/included_service/Cargo.toml | 2 +- tests/included_service/LICENSE | 19 + tests/integration_tests/Cargo.toml | 3 +- tests/integration_tests/LICENSE | 19 + tests/integration_tests/tests/client_layer.rs | 58 ++ tests/integration_tests/tests/connect_info.rs | 74 +++ tests/integration_tests/tests/extensions.rs | 8 +- tests/integration_tests/tests/status.rs | 3 +- tests/root-crate-path/Cargo.toml | 2 +- tests/root-crate-path/LICENSE | 19 + tests/same_name/Cargo.toml | 2 +- tests/same_name/LICENSE | 19 + tests/service_named_service/Cargo.toml | 2 +- tests/service_named_service/LICENSE | 19 + tests/stream_conflict/Cargo.toml | 7 +- tests/stream_conflict/LICENSE | 19 + tests/wellknown-compiled/Cargo.toml | 4 +- tests/wellknown-compiled/LICENSE | 19 + tests/wellknown/Cargo.toml | 4 +- tests/wellknown/LICENSE | 19 + tonic-build/Cargo.toml | 6 +- tonic-build/LICENSE | 19 + tonic-build/src/client.rs | 11 +- tonic-build/src/lib.rs | 11 +- tonic-build/src/manual.rs | 482 ++++++++++++++++ tonic-build/src/prost.rs | 25 +- tonic-build/src/server.rs | 10 +- tonic-health/Cargo.toml | 10 +- tonic-health/LICENSE | 19 + tonic-health/src/lib.rs | 2 +- tonic-health/src/server.rs | 4 +- tonic-reflection/Cargo.toml | 11 +- tonic-reflection/LICENSE | 19 + tonic-reflection/src/lib.rs | 2 +- tonic-types/Cargo.toml | 10 +- tonic-types/LICENSE | 19 + tonic-types/src/lib.rs | 2 +- tonic-web/Cargo.toml | 6 +- tonic-web/LICENSE | 19 + tonic-web/README.md | 6 +- tonic-web/src/lib.rs | 2 +- tonic-web/tests/integration/Cargo.toml | 13 +- tonic-web/tests/integration/LICENSE | 19 + tonic/Cargo.toml | 21 +- tonic/LICENSE | 19 + tonic/src/body.rs | 9 + tonic/src/client/grpc.rs | 7 +- tonic/src/codec/decode.rs | 6 + tonic/src/codegen.rs | 12 +- tonic/src/lib.rs | 6 +- tonic/src/metadata/encoding.rs | 20 +- tonic/src/metadata/value.rs | 209 ++++++- tonic/src/request.rs | 19 +- tonic/src/service/interceptor.rs | 539 +++++++++++------- tonic/src/status.rs | 2 +- tonic/src/transport/channel/endpoint.rs | 39 +- tonic/src/transport/channel/mod.rs | 37 +- tonic/src/transport/mod.rs | 6 +- tonic/src/transport/server/conn.rs | 5 +- tonic/src/transport/server/mod.rs | 285 ++++----- tonic/src/transport/server/unix.rs | 31 + tonic/src/transport/service/connection.rs | 1 + tonic/src/transport/service/connector.rs | 18 + tonic/src/transport/service/executor.rs | 43 ++ tonic/src/transport/service/mod.rs | 7 +- tonic/src/transport/service/router.rs | 169 +++--- 107 files changed, 2810 insertions(+), 1004 deletions(-) create mode 100644 examples/LICENSE create mode 100644 examples/src/json-codec/client.rs create mode 100644 examples/src/json-codec/common.rs create mode 100644 examples/src/json-codec/server.rs create mode 100644 examples/src/tls/client_rustls.rs create mode 100644 examples/src/tls/server_rustls.rs create mode 100644 interop/LICENSE create mode 100644 tests/ambiguous_methods/LICENSE create mode 100644 tests/compression/LICENSE create mode 100644 tests/extern_path/my_application/LICENSE create mode 100644 tests/extern_path/uuid/LICENSE create mode 100644 tests/included_service/LICENSE create mode 100644 tests/integration_tests/LICENSE create mode 100644 tests/integration_tests/tests/client_layer.rs create mode 100644 tests/root-crate-path/LICENSE create mode 100644 tests/same_name/LICENSE create mode 100644 tests/service_named_service/LICENSE create mode 100644 tests/stream_conflict/LICENSE create mode 100644 tests/wellknown-compiled/LICENSE create mode 100644 tests/wellknown/LICENSE create mode 100644 tonic-build/LICENSE create mode 100644 tonic-build/src/manual.rs create mode 100644 tonic-health/LICENSE create mode 100644 tonic-reflection/LICENSE create mode 100644 tonic-types/LICENSE create mode 100644 tonic-web/LICENSE create mode 100644 tonic-web/tests/integration/LICENSE create mode 100644 tonic/LICENSE create mode 100644 tonic/src/transport/server/unix.rs create mode 100644 tonic/src/transport/service/executor.rs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b14a0f1d9..f0e82be9e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -26,6 +26,7 @@ jobs: run: rustup component add rustfmt - name: Install cargo-hack run: cargo install cargo-hack + - uses: Swatinem/rust-cache@v1 - name: Check fmt run: cargo fmt -- --check - name: Check features @@ -58,6 +59,7 @@ jobs: rust-version: ${{ matrix.rust }} - name: Install rustfmt run: rustup component add rustfmt + - uses: Swatinem/rust-cache@v1 - uses: actions/checkout@master - name: Run tests run: cargo test --all --all-features @@ -80,6 +82,7 @@ jobs: - name: Install rustfmt run: rustup component add rustfmt - uses: actions/checkout@master + - uses: Swatinem/rust-cache@v1 - name: Run interop tests run: ./interop/test.sh shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a64b362..d5bb220d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,63 @@ +# [v0.7.2](https://github.com/hyperium/tonic/compare/v0.7.1...v0.7.2) (2022-05-04) + + +### 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 with tls feature enabled ([#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-03-31) + +### Breaking Changes + +* Update prost to 0.10 ([#948](https://github.com/hyperium/tonic/issues/948)) ([c78274e](https://github.com/hyperium/tonic/commit/c78274e3fe5763cba291a605979cd7175ad6c38f)) +* **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)) +* **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:** port router to axum ([#830](https://github.com/hyperium/tonic/issues/830)) ([6dfc20e](https://github.com/hyperium/tonic/commit/6dfc20e1db455be12b0a647533c65bbfd6ae78f2)) +* **build:** add must_use to gzip methods ([#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)) +* **tonic:** Expose h2 error instead of reason ([#883](https://github.com/hyperium/tonic/issues/883)) ([a33e15a](https://github.com/hyperium/tonic/commit/a33e15a387a6ca1844748346904d28cb4caae84b)) +* **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)) +* 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)) + + +### Bug Fixes + +* **codec:** Return None after poll_data error ([#921](https://github.com/hyperium/tonic/issues/921)) ([d7cae70](https://github.com/hyperium/tonic/commit/d7cae702fc2284473846db7c946baf87977b7b48)) +* **health:** Correctly implement spec for overall health ([#897](https://github.com/hyperium/tonic/issues/897)) ([2b0ffee](https://github.com/hyperium/tonic/commit/2b0ffee62034f5983f8d6dcdafccd66f780559f2)) +* **tonic:** Preserve HTTP method in interceptor ([#912](https://github.com/hyperium/tonic/issues/912)) ([e623562](https://github.com/hyperium/tonic/commit/e6235623c4707f97e9b9f7c3ba88745050a884e5)) +* **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 consistent ([#901](https://github.com/hyperium/tonic/issues/901)) ([6763d19](https://github.com/hyperium/tonic/commit/6763d191d267c1b9f861b96ad0f4b850e0264f4d)) +* 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)) + + +### Features + +* **build:** Expose Prost generation plugin ([#947](https://github.com/hyperium/tonic/issues/947)) ([d4bd475](https://github.com/hyperium/tonic/commit/d4bd4758dd80135f89d3e559c5d7f42ccbbab504)) +* **build:** add constructor `from_arc` for gRPC servers ([#875](https://github.com/hyperium/tonic/issues/875)) ([7179f7a](https://github.com/hyperium/tonic/commit/7179f7ae6a5186bb64e4c120302084f56c053206)) +* **health:** Expose `HealthService` publically ([#930](https://github.com/hyperium/tonic/issues/930)) ([097e7e8](https://github.com/hyperium/tonic/commit/097e7e85a9079bb76bef54921f03c6f7e0ee0744)) +* **transport:** add unix socket support in server ([#861](https://github.com/hyperium/tonic/issues/861)) ([dee2ab5](https://github.com/hyperium/tonic/commit/dee2ab52ff4a2995156a3baf5ea916b479fd1d14)) +* **transport:** support customizing `Channel`'s async executor ([#935](https://github.com/hyperium/tonic/issues/935)) ([0859d82](https://github.com/hyperium/tonic/commit/0859d82e577fb024e39ce9b5b7356b95dcb66562)) +* Implement hash for `Code` ([#917](https://github.com/hyperium/tonic/issues/917)) ([6bc7dab](https://github.com/hyperium/tonic/commit/6bc7dab8e099c8ce226a6261e545d8d131c604f0)) + + + # [0.6.2](https://github.com/hyperium/tonic/compare/v0.6.1...v0.6.2) (2021-12-07) @@ -28,7 +88,7 @@ * **build:** Correctly convert `Empty` to `()` ([#734](https://github.com/hyperium/tonic/issues/734)) ([ff6a690](https://github.com/hyperium/tonic/commit/ff6a690cec9daca33984cabea66f9d370ac63462)) * **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:** 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)) +* **tonic:** Status code to set correct source on unknown 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)) * **tonic:** compression: handle compression flag but no header (#763) diff --git a/README.md b/README.md index 26f136798..cacbd7217 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,13 @@ contains the tools to build clients and servers from [`protobuf`] definitions. 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. -If you're using [rust-analyzer] we recommend you set `"rust-analyzer.cargo.loadOutDirsFromCheck": true` to correctly load +If you're using [rust-analyzer] we recommend you set `"rust-analyzer.cargo.runBuildScripts": true` to correctly load the generated code. +For IntelliJ IDEA users, please refer to [this](https://github.com/intellij-rust/intellij-rust/pull/8056) and enable +`org.rust.cargo.evaluate.build.scripts` +[experimental feature](https://plugins.jetbrains.com/plugin/8182-rust/docs/rust-faq.html#experimental-features). + ### Rust Version `tonic` currently works on Rust `1.56` and above as it requires support for the 2018 edition. diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 3857d6952..35911625c 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -54,10 +54,18 @@ path = "src/dynamic_load_balance/server.rs" name = "tls-client" path = "src/tls/client.rs" +[[bin]] +name = "tls-client-rustls" +path = "src/tls/client_rustls.rs" + [[bin]] name = "tls-server" path = "src/tls/server.rs" +[[bin]] +name = "tls-server-rustls" +path = "src/tls/server_rustls.rs" + [[bin]] name = "tls-client-auth-server" path = "src/tls_client_auth/server.rs" @@ -178,39 +186,55 @@ path = "src/streaming/client.rs" name = "streaming-server" path = "src/streaming/server.rs" +[[bin]] +name = "json-codec-client" +path = "src/json-codec/client.rs" + +[[bin]] +name = "json-codec-server" +path = "src/json-codec/server.rs" + [dependencies] async-stream = "0.3" -futures = {version = "0.3", default-features = false, features = ["alloc"]} -prost = "0.9" -tokio = {version = "1.0", features = ["rt-multi-thread", "time", "fs", "macros", "net"]} -tokio-stream = {version = "0.1", features = ["net"]} -tonic = {path = "../tonic", features = ["tls", "compression"]} -tower = {version = "0.4"} +futures = { version = "0.3", default-features = false, features = ["alloc"] } +prost = "0.10" +tokio = { version = "1.0", features = [ "rt-multi-thread", "time", "fs", "macros", "net",] } +tokio-stream = { version = "0.1", features = ["net"] } +tonic = { path = "../tonic", features = ["tls", "compression"] } +tower = { version = "0.4" } # Required for routeguide rand = "0.8" -serde = {version = "1.0", features = ["derive"]} +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # Tracing tracing = "0.1.16" tracing-attributes = "0.1" tracing-futures = "0.2" -tracing-subscriber = {version = "0.3", features = ["tracing-log"]} +tracing-subscriber = { version = "0.3", features = ["tracing-log"] } # Required for wellknown types -prost-types = "0.9" +prost-types = "0.10" # Hyper example http = "0.2" http-body = "0.4.2" -hyper = {version = "0.14", features = ["full"]} +hyper = { version = "0.14", features = ["full"] } pin-project = "1.0" warp = "0.3" # Health example -tonic-health = {path = "../tonic-health"} +tonic-health = { path = "../tonic-health" } # Reflection example listenfd = "0.3" -tonic-reflection = {path = "../tonic-reflection"} +tonic-reflection = { path = "../tonic-reflection" } # grpc-web example bytes = "1" -tonic-web = {path = "../tonic-web"} +tonic-web = { path = "../tonic-web" } +# streaming example +h2 = "0.3" + +tokio-rustls = "*" +hyper-rustls = { version = "0.23", features = ["http2"] } +rustls-pemfile = "*" +tower-http = { version = "0.3", features = ["add-extension", "util"] } + [build-dependencies] -tonic-build = {path = "../tonic-build", features = ["prost", "compression"]} +tonic-build = { path = "../tonic-build", features = ["prost", "compression"] } diff --git a/examples/LICENSE b/examples/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/examples/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/examples/build.rs b/examples/build.rs index 8e4f64ff1..392d5a6be 100644 --- a/examples/build.rs +++ b/examples/build.rs @@ -1,5 +1,4 @@ -use std::env; -use std::path::PathBuf; +use std::{env, path::PathBuf}; fn main() { tonic_build::configure() @@ -30,4 +29,30 @@ fn main() { &["proto/googleapis"], ) .unwrap(); + + build_json_codec_service(); +} + +// 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_build::manual::Service::builder() + .name("Greeter") + .package("json.helloworld") + .method( + tonic_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_build::manual::Builder::new().compile(&[greeter_service]); } diff --git a/examples/helloworld-tutorial.md b/examples/helloworld-tutorial.md index 8442dad2d..e6930fe4c 100644 --- a/examples/helloworld-tutorial.md +++ b/examples/helloworld-tutorial.md @@ -112,12 +112,12 @@ name = "helloworld-client" path = "src/client.rs" [dependencies] -tonic = "0.6" -prost = "0.9" +tonic = "0.7" +prost = "0.10" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } [build-dependencies] -tonic-build = "0.6" +tonic-build = "0.7" ``` 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: diff --git a/examples/routeguide-tutorial.md b/examples/routeguide-tutorial.md index a6f14db11..9b619d73c 100644 --- a/examples/routeguide-tutorial.md +++ b/examples/routeguide-tutorial.md @@ -174,8 +174,8 @@ Edit `Cargo.toml` and add all the dependencies we'll need for this example: ```toml [dependencies] -tonic = "0.6" -prost = "0.9" +tonic = "0.7" +prost = "0.10" futures-core = "0.3" futures-util = "0.3" tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "sync", "time"] } @@ -187,7 +187,7 @@ serde_json = "1.0" rand = "0.7" [build-dependencies] -tonic-build = "0.6" +tonic-build = "0.7" ``` Create a `build.rs` file at the root of your crate: diff --git a/examples/src/authentication/client.rs b/examples/src/authentication/client.rs index 7297c4716..2fa83ebd2 100644 --- a/examples/src/authentication/client.rs +++ b/examples/src/authentication/client.rs @@ -9,7 +9,7 @@ use tonic::{metadata::MetadataValue, transport::Channel, Request}; async fn main() -> Result<(), Box> { let channel = Channel::from_static("http://[::1]:50051").connect().await?; - let token = MetadataValue::from_str("Bearer some-auth-token")?; + 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()); diff --git a/examples/src/authentication/server.rs b/examples/src/authentication/server.rs index 951154079..289eaf80d 100644 --- a/examples/src/authentication/server.rs +++ b/examples/src/authentication/server.rs @@ -59,7 +59,7 @@ async fn main() -> Result<(), Box> { } fn check_auth(req: Request<()>) -> Result, Status> { - let token = MetadataValue::from_str("Bearer some-secret-token").unwrap(); + let token: MetadataValue<_> = "Bearer some-secret-token".parse().unwrap(); match req.metadata().get("authorization") { Some(t) if token == t => Ok(req), diff --git a/examples/src/gcp/client.rs b/examples/src/gcp/client.rs index b434ed112..57b78aea0 100644 --- a/examples/src/gcp/client.rs +++ b/examples/src/gcp/client.rs @@ -22,7 +22,7 @@ async fn main() -> Result<(), Box> { .ok_or_else(|| "Expected a project name as the first argument.".to_string())?; let bearer_token = format!("Bearer {}", token); - let header_value = MetadataValue::from_str(&bearer_token)?; + let header_value: MetadataValue<_> = bearer_token.parse()?; let certs = tokio::fs::read("examples/data/gcp/roots.pem").await?; diff --git a/examples/src/json-codec/client.rs b/examples/src/json-codec/client.rs new file mode 100644 index 000000000..dd6305ef6 --- /dev/null +++ b/examples/src/json-codec/client.rs @@ -0,0 +1,28 @@ +//! 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(()) +} diff --git a/examples/src/json-codec/common.rs b/examples/src/json-codec/common.rs new file mode 100644 index 000000000..9f0ffeb54 --- /dev/null +++ b/examples/src/json-codec/common.rs @@ -0,0 +1,80 @@ +//! 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::{ + codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}, + Status, +}; + +#[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) + } +} diff --git a/examples/src/json-codec/server.rs b/examples/src/json-codec/server.rs new file mode 100644 index 000000000..1029b0bf9 --- /dev/null +++ b/examples/src/json-codec/server.rs @@ -0,0 +1,48 @@ +//! 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::{transport::Server, Request, Response, Status}; + +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(()) +} diff --git a/examples/src/mock/mock.rs b/examples/src/mock/mock.rs index a776d0cb2..ac5d2a072 100644 --- a/examples/src/mock/mock.rs +++ b/examples/src/mock/mock.rs @@ -24,9 +24,7 @@ async fn main() -> Result<(), Box> { tokio::spawn(async move { Server::builder() .add_service(GreeterServer::new(greeter)) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - mock::MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await }); @@ -35,9 +33,18 @@ async fn main() -> Result<(), Box> { let mut client = Some(client); let channel = Endpoint::try_from("http://[::]:50051")? .connect_with_connector(service_fn(move |_: Uri| { - let client = client.take().unwrap(); - - async move { Ok::<_, std::io::Error>(mock::MockStream(client)) } + let client = client.take(); + + async move { + if let Some(client) = client { + Ok(client) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Client already taken", + )) + } + } })) .await?; @@ -71,54 +78,3 @@ impl Greeter for MyGreeter { Ok(Response::new(reply)) } } - -mod mock { - use std::{ - 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) - } - } -} diff --git a/examples/src/streaming/client.rs b/examples/src/streaming/client.rs index 67a876a63..afbf72f1e 100644 --- a/examples/src/streaming/client.rs +++ b/examples/src/streaming/client.rs @@ -2,28 +2,85 @@ pub mod pb { tonic::include_proto!("grpc.examples.echo"); } +use futures::stream::Stream; +use std::time::Duration; +use tokio_stream::StreamExt; +use tonic::transport::Channel; + use pb::{echo_client::EchoClient, EchoRequest}; -#[tokio::main] -async fn main() -> Result<(), Box> { - let mut client = EchoClient::connect("http://[::1]:50051").await.unwrap(); +fn echo_requests_iter() -> impl Stream { + tokio_stream::iter(1..usize::MAX).map(|i| EchoRequest { + message: format!("msg {:02}", i), + }) +} +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 droped here and the disconnect info is send 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(); - println!("Connected...now sleeping for 2 seconds..."); + 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(); - tokio::time::sleep(std::time::Duration::from_secs(2)).await; + println!("Streaming echo:"); + streaming_echo(&mut client, 5).await; + tokio::time::sleep(Duration::from_secs(1)).await; //do not mess server println functions - // Disconnect - drop(stream); - drop(client); + // Echo stream that sends 17 requests then graceful end that connection + println!("\r\nBidirectional stream echo:"); + bidirectional_streaming_echo(&mut client, 17).await; - println!("Disconnected..."); + // Echo stream that sends up to `usize::MAX` requets. 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(()) } diff --git a/examples/src/streaming/server.rs b/examples/src/streaming/server.rs index 9c0738704..864626072 100644 --- a/examples/src/streaming/server.rs +++ b/examples/src/streaming/server.rs @@ -3,10 +3,9 @@ pub mod pb { } use futures::Stream; -use std::net::ToSocketAddrs; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tokio::sync::oneshot; +use std::{error::Error, io::ErrorKind, net::ToSocketAddrs, pin::Pin, time::Duration}; +use tokio::sync::mpsc; +use tokio_stream::{wrappers::ReceiverStream, StreamExt}; use tonic::{transport::Server, Request, Response, Status, Streaming}; use pb::{EchoRequest, EchoResponse}; @@ -14,6 +13,29 @@ 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::() { + if let Some(io_err) = h2_err.get_io() { + return Some(io_err); + } + } + + err = match err.source() { + Some(err) => err, + None => return None, + }; + } +} + #[derive(Debug)] pub struct EchoServer {} @@ -29,28 +51,36 @@ impl pb::echo_server::Echo for EchoServer { &self, req: Request, ) -> EchoResult { - println!("Client connected from: {:?}", req.remote_addr()); + println!("EchoServer::server_streaming_echo"); + println!("\tclient connected from: {:?}", req.remote_addr()); - let (tx, rx) = oneshot::channel::<()>(); - - tokio::spawn(async move { - let _ = rx.await; - println!("The rx resolved therefore the client disconnected!"); + // 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))); - struct ClientDisconnect(oneshot::Sender<()>); - - impl Stream for ClientDisconnect { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { - // A stream that never resolves to anything.... - Poll::Pending + // 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(ClientDisconnect(tx)) as Self::ServerStreamingEchoStream + Box::pin(output_stream) as Self::ServerStreamingEchoStream )) } @@ -65,9 +95,50 @@ impl pb::echo_server::Echo for EchoServer { async fn bidirectional_streaming_echo( &self, - _: Request>, + req: Request>, ) -> EchoResult { - Err(Status::unimplemented("not implemented")) + 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 drooped 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) { + if 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 droped + } + } + } + } + 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 + )) } } diff --git a/examples/src/tls/client_rustls.rs b/examples/src/tls/client_rustls.rs new file mode 100644 index 000000000..f5a7e4d09 --- /dev/null +++ b/examples/src/tls/client_rustls.rs @@ -0,0 +1,85 @@ +//! 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.echo"); +} + +use hyper::{client::HttpConnector, Uri}; +use pb::{echo_client::EchoClient, EchoRequest}; +use tokio_rustls::rustls::{ClientConfig, RootCertStore}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let fd = std::fs::File::open("examples/data/tls/ca.pem")?; + + let mut roots = RootCertStore::empty(); + + let mut buf = std::io::BufReader::new(&fd); + let certs = rustls_pemfile::certs(&mut buf)?; + roots.add_parsable_certificates(&certs); + + let tls = ClientConfig::builder() + .with_safe_defaults() + .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::Client::builder().build(connector); + + // Hyper expects an absolute `Uri` to allow it to know which server to connect too. + // Currently, tonic's generated code only sets the `path_and_query` section so we + // are going to write a custom tower layer in front of the hyper client to add the + // scheme and authority. + // + // Again, this Uri is `example.com` because our tls certs is signed with this SNI but above + // we actually map this back to `[::1]:50051` before the `Uri` is passed to hyper's `HttpConnector` + // to allow it to correctly establish the tcp connection to the local `tls-server`. + let uri = Uri::from_static("https://example.com"); + let svc = tower::ServiceBuilder::new() + .map_request(move |mut req: http::Request| { + let uri = Uri::builder() + .scheme(uri.scheme().unwrap().clone()) + .authority(uri.authority().unwrap().clone()) + .path_and_query(req.uri().path_and_query().unwrap().clone()) + .build() + .unwrap(); + + *req.uri_mut() = uri; + req + }) + .service(client); + + let mut client = EchoClient::new(svc); + + let request = tonic::Request::new(EchoRequest { + message: "hello".into(), + }); + + let response = client.unary_echo(request).await?; + + println!("RESPONSE={:?}", response); + + Ok(()) +} diff --git a/examples/src/tls/server_rustls.rs b/examples/src/tls/server_rustls.rs new file mode 100644 index 000000000..d8cd2b903 --- /dev/null +++ b/examples/src/tls/server_rustls.rs @@ -0,0 +1,143 @@ +pub mod pb { + tonic::include_proto!("/grpc.examples.echo"); +} + +use futures::Stream; +use hyper::server::conn::Http; +use pb::{EchoRequest, EchoResponse}; +use std::{pin::Pin, sync::Arc}; +use tokio::net::TcpListener; +use tokio_rustls::{ + rustls::{Certificate, PrivateKey, ServerConfig}, + TlsAcceptor, +}; +use tonic::{transport::Server, Request, Response, Status, Streaming}; +use tower_http::ServiceBuilderExt; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let certs = { + let fd = std::fs::File::open("examples/data/tls/server.pem")?; + let mut buf = std::io::BufReader::new(&fd); + rustls_pemfile::certs(&mut buf)? + .into_iter() + .map(Certificate) + .collect() + }; + let key = { + let fd = std::fs::File::open("examples/data/tls/server.key")?; + let mut buf = std::io::BufReader::new(&fd); + rustls_pemfile::pkcs8_private_keys(&mut buf)? + .into_iter() + .map(PrivateKey) + .next() + .unwrap() + + // let key = std::fs::read("examples/data/tls/server.key")?; + // PrivateKey(key) + }; + + let mut tls = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(certs, key)?; + tls.alpn_protocols = vec![b"h2".to_vec()]; + + let server = EchoServer::default(); + + let svc = Server::builder() + .add_service(pb::echo_server::EchoServer::new(server)) + .into_service(); + + let mut http = Http::new(); + http.http2_only(true); + + 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(conn, svc).await.unwrap(); + }); + } +} + +#[derive(Debug)] +struct ConnInfo { + addr: std::net::SocketAddr, + certificates: Vec, +} + +type EchoResult = Result, Status>; +type ResponseStream = Pin> + Send>>; + +#[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 })) + } + + type ServerStreamingEchoStream = ResponseStream; + + async fn server_streaming_echo( + &self, + _: Request, + ) -> EchoResult { + Err(Status::unimplemented("not implemented")) + } + + async fn client_streaming_echo( + &self, + _: Request>, + ) -> EchoResult { + Err(Status::unimplemented("not implemented")) + } + + type BidirectionalStreamingEchoStream = ResponseStream; + + async fn bidirectional_streaming_echo( + &self, + _: Request>, + ) -> EchoResult { + Err(Status::unimplemented("not implemented")) + } +} diff --git a/examples/src/tower/server.rs b/examples/src/tower/server.rs index d1e27cf30..cb5f57aa5 100644 --- a/examples/src/tower/server.rs +++ b/examples/src/tower/server.rs @@ -88,9 +88,9 @@ struct MyMiddleware { } impl Service> for MyMiddleware -where - S: Service, Response = hyper::Response> + Clone + Send + 'static, - S::Future: Send + 'static, + where + S: Service, Response = hyper::Response> + Clone + Send + 'static, + S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; @@ -114,4 +114,4 @@ where Ok(response) }) } -} +} \ No newline at end of file diff --git a/examples/src/uds/server.rs b/examples/src/uds/server.rs index 8ffe4fa0a..b4ca31002 100644 --- a/examples/src/uds/server.rs +++ b/examples/src/uds/server.rs @@ -1,9 +1,12 @@ #![cfg_attr(not(unix), allow(unused_imports))] -use futures::TryFutureExt; 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::{transport::Server, Request, Response, Status}; pub mod hello_world { @@ -26,7 +29,7 @@ impl Greeter for MyGreeter { ) -> Result, Status> { #[cfg(unix)] { - let conn_info = request.extensions().get::().unwrap(); + let conn_info = request.extensions().get::().unwrap(); println!("Got a request {:?} with info {:?}", request, conn_info); } @@ -46,89 +49,17 @@ async fn main() -> Result<(), Box> { let greeter = MyGreeter::default(); - let incoming = { - let uds = UnixListener::bind(path)?; - - async_stream::stream! { - loop { - let item = uds.accept().map_ok(|(st, _)| unix::UnixStream(st)).await; - - yield item; - } - } - }; + let uds = UnixListener::bind(path)?; + let uds_stream = UnixListenerStream::new(uds); Server::builder() .add_service(GreeterServer::new(greeter)) - .serve_with_incoming(incoming) + .serve_with_incoming(uds_stream) .await?; Ok(()) } -#[cfg(unix)] -mod unix { - use std::{ - pin::Pin, - sync::Arc, - task::{Context, Poll}, - }; - - use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - use tonic::transport::server::Connected; - - #[derive(Debug)] - pub struct UnixStream(pub tokio::net::UnixStream); - - impl Connected for UnixStream { - type ConnectInfo = UdsConnectInfo; - - fn connect_info(&self) -> Self::ConnectInfo { - UdsConnectInfo { - peer_addr: self.0.peer_addr().ok().map(Arc::new), - peer_cred: self.0.peer_cred().ok(), - } - } - } - - #[derive(Clone, Debug)] - pub struct UdsConnectInfo { - pub peer_addr: Option>, - pub peer_cred: Option, - } - - impl AsyncRead for UnixStream { - 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 UnixStream { - 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) - } - } -} - #[cfg(not(unix))] fn main() { panic!("The `uds` example only works on unix systems!"); diff --git a/interop/Cargo.toml b/interop/Cargo.toml index 2956f9219..dc18d6f75 100644 --- a/interop/Cargo.toml +++ b/interop/Cargo.toml @@ -17,15 +17,15 @@ path = "src/bin/server.rs" [dependencies] async-stream = "0.3" bytes = "1.0" +clap = {version = "3", features = ["derive"]} console = "0.14" futures-core = "0.3" futures-util = "0.3" http = "0.2" http-body = "0.4.2" hyper = "0.14" -prost = "0.9" -prost-derive = "0.9" -structopt = "0.3" +prost = "0.10" +prost-derive = "0.10" tokio = {version = "1.0", features = ["rt-multi-thread", "time", "macros", "fs"]} tokio-stream = "0.1" tonic = {path = "../tonic", features = ["tls"]} diff --git a/interop/LICENSE b/interop/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/interop/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/interop/src/bin/client.rs b/interop/src/bin/client.rs index 1f676faeb..de9510c43 100644 --- a/interop/src/bin/client.rs +++ b/interop/src/bin/client.rs @@ -1,19 +1,19 @@ +use clap::Parser; use interop::client; use std::time::Duration; -use structopt::{clap::arg_enum, StructOpt}; use tonic::transport::Endpoint; use tonic::transport::{Certificate, ClientTlsConfig}; -#[derive(StructOpt)] +#[derive(Parser)] struct Opts { - #[structopt(name = "use_tls", long)] + #[clap(name = "use_tls", long)] use_tls: bool, - #[structopt( + #[clap( long = "test_case", - use_delimiter = true, + use_value_delimiter = true, min_values = 1, - possible_values = &Testcase::variants() + arg_enum )] test_case: Vec, } @@ -22,7 +22,7 @@ struct Opts { async fn main() -> Result<(), Box> { interop::trace_init(); - let matches = Opts::from_args(); + let matches = Opts::parse(); let test_cases = matches.test_case; @@ -53,29 +53,29 @@ async fn main() -> Result<(), Box> { let mut test_results = Vec::new(); match test_case { - Testcase::empty_unary => client::empty_unary(&mut client, &mut test_results).await, - Testcase::large_unary => client::large_unary(&mut client, &mut test_results).await, - Testcase::client_streaming => { + Testcase::EmptyUnary => client::empty_unary(&mut client, &mut test_results).await, + Testcase::LargeUnary => client::large_unary(&mut client, &mut test_results).await, + Testcase::ClientStreaming => { client::client_streaming(&mut client, &mut test_results).await } - Testcase::server_streaming => { + Testcase::ServerStreaming => { client::server_streaming(&mut client, &mut test_results).await } - Testcase::ping_pong => client::ping_pong(&mut client, &mut test_results).await, - Testcase::empty_stream => client::empty_stream(&mut client, &mut test_results).await, - Testcase::status_code_and_message => { + Testcase::PingPong => client::ping_pong(&mut client, &mut test_results).await, + Testcase::EmptyStream => client::empty_stream(&mut client, &mut test_results).await, + Testcase::StatusCodeAndMessage => { client::status_code_and_message(&mut client, &mut test_results).await } - Testcase::special_status_message => { + Testcase::SpecialStatusMessage => { client::special_status_message(&mut client, &mut test_results).await } - Testcase::unimplemented_method => { + Testcase::UnimplementedMethod => { client::unimplemented_method(&mut client, &mut test_results).await } - Testcase::unimplemented_service => { + Testcase::UnimplementedService => { client::unimplemented_service(&mut unimplemented_client, &mut test_results).await } - Testcase::custom_metadata => { + Testcase::CustomMetadata => { client::custom_metadata(&mut client, &mut test_results).await } _ => unimplemented!(), @@ -98,33 +98,31 @@ async fn main() -> Result<(), Box> { Ok(()) } -arg_enum! { - #[derive(Debug, Copy, Clone)] - #[allow(non_camel_case_types)] - enum Testcase { - empty_unary, - cacheable_unary, - large_unary, - client_compressed_unary, - server_compressed_unary, - client_streaming, - client_compressed_streaming, - server_streaming, - server_compressed_streaming, - ping_pong, - empty_stream, - compute_engine_creds, - jwt_token_creds, - oauth2_auth_token, - per_rpc_creds, - custom_metadata, - status_code_and_message, - special_status_message, - unimplemented_method, - unimplemented_service, - cancel_after_begin, - cancel_after_first_response, - timeout_on_sleeping_server, - concurrent_large_unary - } +#[derive(Debug, Copy, Clone, clap::ArgEnum)] +#[clap(rename_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, } diff --git a/interop/src/bin/server.rs b/interop/src/bin/server.rs index b48987dad..bed34def4 100644 --- a/interop/src/bin/server.rs +++ b/interop/src/bin/server.rs @@ -1,11 +1,11 @@ +use clap::Parser; use interop::server; -use structopt::StructOpt; use tonic::transport::Server; use tonic::transport::{Identity, ServerTlsConfig}; -#[derive(StructOpt)] +#[derive(Parser)] struct Opts { - #[structopt(name = "use_tls", long)] + #[clap(name = "use_tls", long)] use_tls: bool, } @@ -13,7 +13,7 @@ struct Opts { async fn main() -> std::result::Result<(), Box> { interop::trace_init(); - let matches = Opts::from_args(); + let matches = Opts::parse(); let addr = "127.0.0.1:10000".parse().unwrap(); diff --git a/interop/src/client.rs b/interop/src/client.rs index a47441d2f..578999505 100644 --- a/interop/src/client.rs +++ b/interop/src/client.rs @@ -342,7 +342,7 @@ pub async fn unimplemented_service( pub async fn custom_metadata(client: &mut TestClient, assertions: &mut Vec) { let key1 = "x-grpc-test-echo-initial"; - let value1 = MetadataValue::from_str("test_initial_metadata_value").unwrap(); + 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]); diff --git a/tests/ambiguous_methods/Cargo.toml b/tests/ambiguous_methods/Cargo.toml index b009546ee..e37e00cc6 100644 --- a/tests/ambiguous_methods/Cargo.toml +++ b/tests/ambiguous_methods/Cargo.toml @@ -9,7 +9,7 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -prost = "0.9" +prost = "0.10" tonic = {path = "../../tonic"} [build-dependencies] diff --git a/tests/ambiguous_methods/LICENSE b/tests/ambiguous_methods/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/ambiguous_methods/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/compression/Cargo.toml b/tests/compression/Cargo.toml index a9a0df1b4..f97fd6fa0 100644 --- a/tests/compression/Cargo.toml +++ b/tests/compression/Cargo.toml @@ -13,12 +13,12 @@ http = "0.2" http-body = "0.4" hyper = "0.14.3" pin-project = "1.0" -prost = "0.9" +prost = "0.10" tokio = {version = "1.0", features = ["macros", "rt-multi-thread", "net"]} tokio-stream = {version = "0.1.5", features = ["net"]} tonic = {path = "../../tonic", features = ["compression"]} tower = {version = "0.4", features = []} -tower-http = {version = "0.2", features = ["map-response-body", "map-request-body"]} +tower-http = {version = "0.3", features = ["map-response-body", "map-request-body"]} [build-dependencies] tonic-build = {path = "../../tonic-build", features = ["compression"]} diff --git a/tests/compression/LICENSE b/tests/compression/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/compression/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/compression/src/bidirectional_stream.rs b/tests/compression/src/bidirectional_stream.rs index 53dc83393..55461cde5 100644 --- a/tests/compression/src/bidirectional_stream.rs +++ b/tests/compression/src/bidirectional_stream.rs @@ -36,9 +36,7 @@ async fn client_enabled_server_enabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } diff --git a/tests/compression/src/client_stream.rs b/tests/compression/src/client_stream.rs index 620f917c9..e8ad6dab7 100644 --- a/tests/compression/src/client_stream.rs +++ b/tests/compression/src/client_stream.rs @@ -27,9 +27,7 @@ async fn client_enabled_server_enabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -73,9 +71,7 @@ async fn client_disabled_server_enabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -102,9 +98,7 @@ async fn client_enabled_server_disabled() { tokio::spawn(async move { Server::builder() .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); }); @@ -147,9 +141,7 @@ async fn compressing_response_from_client_stream() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } diff --git a/tests/compression/src/compressing_request.rs b/tests/compression/src/compressing_request.rs index eb81ccb91..a5352945a 100644 --- a/tests/compression/src/compressing_request.rs +++ b/tests/compression/src/compressing_request.rs @@ -29,9 +29,7 @@ async fn client_enabled_server_enabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -60,9 +58,7 @@ async fn client_enabled_server_disabled() { tokio::spawn(async move { Server::builder() .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); }); @@ -98,9 +94,7 @@ async fn client_mark_compressed_without_header_server_enabled() { async move { Server::builder() .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } diff --git a/tests/compression/src/compressing_response.rs b/tests/compression/src/compressing_response.rs index e60903dc6..28674a5d5 100644 --- a/tests/compression/src/compressing_response.rs +++ b/tests/compression/src/compressing_response.rs @@ -51,9 +51,7 @@ async fn client_enabled_server_enabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -93,9 +91,7 @@ async fn client_enabled_server_disabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -159,9 +155,7 @@ async fn client_disabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -198,9 +192,7 @@ async fn server_replying_with_unsupported_encoding() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); }); @@ -241,9 +233,7 @@ async fn disabling_compression_on_single_response() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -283,9 +273,7 @@ async fn disabling_compression_on_response_but_keeping_compression_on_stream() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -340,9 +328,7 @@ async fn disabling_compression_on_response_from_client_stream() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } diff --git a/tests/compression/src/lib.rs b/tests/compression/src/lib.rs index de806b5cb..87e054486 100644 --- a/tests/compression/src/lib.rs +++ b/tests/compression/src/lib.rs @@ -1,7 +1,7 @@ #![allow(unused_imports)] use self::util::*; -use crate::util::{mock_io_channel, MockStream}; +use crate::util::mock_io_channel; use futures::{Stream, StreamExt}; use std::convert::TryFrom; use std::{ diff --git a/tests/compression/src/server_stream.rs b/tests/compression/src/server_stream.rs index 2d302bf31..3d82cff55 100644 --- a/tests/compression/src/server_stream.rs +++ b/tests/compression/src/server_stream.rs @@ -24,9 +24,7 @@ async fn client_enabled_server_enabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -78,9 +76,7 @@ async fn client_disabled_server_enabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } @@ -125,9 +121,7 @@ async fn client_enabled_server_disabled() { .into_inner(), ) .add_service(svc) - .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>( - MockStream(server), - )])) + .serve_with_incoming(futures::stream::iter(vec![Ok::<_, std::io::Error>(server)])) .await .unwrap(); } diff --git a/tests/compression/src/util.rs b/tests/compression/src/util.rs index 75df07f9b..34fdc0f3a 100644 --- a/tests/compression/src/util.rs +++ b/tests/compression/src/util.rs @@ -87,43 +87,6 @@ pub fn measure_request_body_size_layer( }) } -#[derive(Debug)] -pub struct MockStream(pub tokio::io::DuplexStream); - -impl Connected for MockStream { - type ConnectInfo = (); - - 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) - } -} - #[allow(dead_code)] pub async fn mock_io_channel(client: tokio::io::DuplexStream) -> Channel { let mut client = Some(client); @@ -132,7 +95,7 @@ pub async fn mock_io_channel(client: tokio::io::DuplexStream) -> Channel { .unwrap() .connect_with_connector(service_fn(move |_: Uri| { let client = client.take().unwrap(); - async move { Ok::<_, std::io::Error>(MockStream(client)) } + async move { Ok::<_, std::io::Error>(client) } })) .await .unwrap() diff --git a/tests/extern_path/my_application/Cargo.toml b/tests/extern_path/my_application/Cargo.toml index e16c0f29b..c4e82491b 100644 --- a/tests/extern_path/my_application/Cargo.toml +++ b/tests/extern_path/my_application/Cargo.toml @@ -9,8 +9,8 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -prost = "0.9" -prost-types = "0.9" +prost = "0.10" +prost-types = "0.10" tonic = {path = "../../../tonic"} uuid = {package = "uuid1", path = "../uuid"} diff --git a/tests/extern_path/my_application/LICENSE b/tests/extern_path/my_application/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/extern_path/my_application/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/extern_path/uuid/Cargo.toml b/tests/extern_path/uuid/Cargo.toml index 450aa74f1..72c2fee64 100644 --- a/tests/extern_path/uuid/Cargo.toml +++ b/tests/extern_path/uuid/Cargo.toml @@ -10,6 +10,6 @@ version = "0.1.0" [dependencies] bytes = "1.0" -prost = "0.9" +prost = "0.10" [build-dependencies] -prost-build = "0.9" +prost-build = "0.10" diff --git a/tests/extern_path/uuid/LICENSE b/tests/extern_path/uuid/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/extern_path/uuid/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/included_service/Cargo.toml b/tests/included_service/Cargo.toml index 6b4ce199b..b774779e0 100644 --- a/tests/included_service/Cargo.toml +++ b/tests/included_service/Cargo.toml @@ -9,7 +9,7 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -prost = "0.9" +prost = "0.10" tonic = {path = "../../tonic"} [build-dependencies] diff --git a/tests/included_service/LICENSE b/tests/included_service/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/included_service/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/integration_tests/Cargo.toml b/tests/integration_tests/Cargo.toml index 13826491d..8afd94da3 100644 --- a/tests/integration_tests/Cargo.toml +++ b/tests/integration_tests/Cargo.toml @@ -11,7 +11,7 @@ version = "0.1.0" [dependencies] bytes = "1.0" futures-util = "0.3" -prost = "0.9" +prost = "0.10" tokio = {version = "1.0", features = ["macros", "rt-multi-thread", "net"]} tonic = {path = "../../tonic"} @@ -23,6 +23,7 @@ http-body = "0.4" hyper = "0.14" tokio-stream = {version = "0.1.5", features = ["net"]} tower = {version = "0.4", features = []} +tower-http = { version = "0.3", features = ["set-header", "trace"] } tower-service = "0.3" tracing-subscriber = {version = "0.3", features = ["env-filter"]} diff --git a/tests/integration_tests/LICENSE b/tests/integration_tests/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/integration_tests/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/integration_tests/tests/client_layer.rs b/tests/integration_tests/tests/client_layer.rs new file mode 100644 index 000000000..c33f388d7 --- /dev/null +++ b/tests/integration_tests/tests/client_layer.rs @@ -0,0 +1,58 @@ +use std::time::Duration; + +use futures::{channel::oneshot, FutureExt}; +use http::{header::HeaderName, HeaderValue}; +use integration_tests::pb::{test_client::TestClient, test_server, Input, Output}; +use tonic::{ + transport::{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); + + // Start the server now, second call should succeed + let jh = tokio::spawn(async move { + Server::builder() + .add_service(svc) + .serve_with_shutdown("127.0.0.1:1340".parse().unwrap(), rx.map(drop)) + .await + .unwrap(); + }); + + let channel = Endpoint::from_static("http://127.0.0.1:1340").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(); +} diff --git a/tests/integration_tests/tests/connect_info.rs b/tests/integration_tests/tests/connect_info.rs index 936eedac1..cc57f6b6b 100644 --- a/tests/integration_tests/tests/connect_info.rs +++ b/tests/integration_tests/tests/connect_info.rs @@ -48,3 +48,77 @@ async fn getting_connect_info() { jh.await.unwrap(); } + +#[cfg(unix)] +pub mod unix { + use std::convert::TryFrom as _; + + use futures_util::FutureExt; + 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.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, rx.map(drop)) + .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| UnixStream::connect(path.clone()))) + .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(); + } +} diff --git a/tests/integration_tests/tests/extensions.rs b/tests/integration_tests/tests/extensions.rs index e41f87d02..12191c946 100644 --- a/tests/integration_tests/tests/extensions.rs +++ b/tests/integration_tests/tests/extensions.rs @@ -205,13 +205,13 @@ struct InterceptedService { } impl Service> for InterceptedService -where - S: Service, Response = HyperResponse> + where + S: Service, Response = HyperResponse> + NamedService + Clone + Send + 'static, - S::Future: Send + 'static, + S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; @@ -236,4 +236,4 @@ where impl NamedService for InterceptedService { const NAME: &'static str = S::NAME; -} +} \ No newline at end of file diff --git a/tests/integration_tests/tests/status.rs b/tests/integration_tests/tests/status.rs index af32ba49b..0c3920d9c 100644 --- a/tests/integration_tests/tests/status.rs +++ b/tests/integration_tests/tests/status.rs @@ -185,8 +185,7 @@ async fn status_from_server_stream_with_source() { .unwrap() .connect_with_connector_lazy(tower::service_fn(move |_: Uri| async move { Err::(std::io::Error::new(std::io::ErrorKind::Other, "WTF")) - })) - .unwrap(); + })); let mut client = test_stream_client::TestStreamClient::new(channel); diff --git a/tests/root-crate-path/Cargo.toml b/tests/root-crate-path/Cargo.toml index 852169b23..1a20afbab 100644 --- a/tests/root-crate-path/Cargo.toml +++ b/tests/root-crate-path/Cargo.toml @@ -7,7 +7,7 @@ publish = false version = "0.1.0" [dependencies] -prost = "0.9" +prost = "0.10" tonic = {path = "../../tonic"} [build_dependencies] diff --git a/tests/root-crate-path/LICENSE b/tests/root-crate-path/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/root-crate-path/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/same_name/Cargo.toml b/tests/same_name/Cargo.toml index 56408420b..8b5579b4d 100644 --- a/tests/same_name/Cargo.toml +++ b/tests/same_name/Cargo.toml @@ -9,7 +9,7 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -prost = "0.9" +prost = "0.10" tonic = {path = "../../tonic"} [build-dependencies] diff --git a/tests/same_name/LICENSE b/tests/same_name/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/same_name/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/service_named_service/Cargo.toml b/tests/service_named_service/Cargo.toml index 3eb617844..263da26b3 100644 --- a/tests/service_named_service/Cargo.toml +++ b/tests/service_named_service/Cargo.toml @@ -9,7 +9,7 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -prost = "0.9" +prost = "0.10" tonic = {path = "../../tonic"} [build-dependencies] diff --git a/tests/service_named_service/LICENSE b/tests/service_named_service/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/service_named_service/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/stream_conflict/Cargo.toml b/tests/stream_conflict/Cargo.toml index 9826e2166..facf1c20a 100644 --- a/tests/stream_conflict/Cargo.toml +++ b/tests/stream_conflict/Cargo.toml @@ -4,12 +4,13 @@ edition = "2018" name = "stream_conflict" publish = false version = "0.1.0" +license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -prost = "0.9" -tonic = {path = "../../tonic"} +prost = "0.10" +tonic = { path = "../../tonic" } [build-dependencies] -tonic-build = {path = "../../tonic-build"} +tonic-build = { path = "../../tonic-build" } diff --git a/tests/stream_conflict/LICENSE b/tests/stream_conflict/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/stream_conflict/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/wellknown-compiled/Cargo.toml b/tests/wellknown-compiled/Cargo.toml index c2894b61e..0268bd858 100644 --- a/tests/wellknown-compiled/Cargo.toml +++ b/tests/wellknown-compiled/Cargo.toml @@ -12,9 +12,9 @@ version = "0.1.0" doctest = false [dependencies] -prost = "0.9" +prost = "0.10" tonic = {path = "../../tonic"} [build-dependencies] -prost-build = "0.9" +prost-build = "0.10" tonic-build = {path = "../../tonic-build"} diff --git a/tests/wellknown-compiled/LICENSE b/tests/wellknown-compiled/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/wellknown-compiled/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tests/wellknown/Cargo.toml b/tests/wellknown/Cargo.toml index 9fc39f5d4..73f9349fb 100644 --- a/tests/wellknown/Cargo.toml +++ b/tests/wellknown/Cargo.toml @@ -9,8 +9,8 @@ version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -prost = "0.9" -prost-types = "0.9" +prost = "0.10" +prost-types = "0.10" tonic = {path = "../../tonic"} [build-dependencies] diff --git a/tests/wellknown/LICENSE b/tests/wellknown/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tests/wellknown/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tonic-build/Cargo.toml b/tonic-build/Cargo.toml index 86602ff37..76198abb2 100644 --- a/tonic-build/Cargo.toml +++ b/tonic-build/Cargo.toml @@ -4,7 +4,7 @@ categories = ["network-programming", "asynchronous"] description = """ Codegen module of `tonic` gRPC implementation. """ -documentation = "https://docs.rs/tonic-build/0.6.2/tonic_build/" +documentation = "https://docs.rs/tonic-build/0.7.2/tonic_build/" edition = "2018" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "async", "codegen", "protobuf"] @@ -12,12 +12,12 @@ license = "MIT" name = "tonic-build" readme = "README.md" repository = "https://github.com/hyperium/tonic" -version = "0.6.2" +version = "0.7.2" [dependencies] prettyplease = {version = "0.1"} proc-macro2 = "1.0" -prost-build = {version = "0.9", optional = true} +prost-build = {version = "0.10", optional = true} quote = "1.0" syn = "1.0" diff --git a/tonic-build/LICENSE b/tonic-build/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tonic-build/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tonic-build/src/client.rs b/tonic-build/src/client.rs index cfe62d9ef..4c3198484 100644 --- a/tonic-build/src/client.rs +++ b/tonic-build/src/client.rs @@ -57,8 +57,8 @@ pub fn generate( impl #service_ident where T: tonic::client::GrpcService, - T::ResponseBody: Body + Send + 'static, T::Error: Into, + T::ResponseBody: Body + Send + 'static, ::Error: Into + Send, { pub fn new(inner: T) -> Self { @@ -69,6 +69,7 @@ pub fn generate( 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> @@ -166,7 +167,7 @@ fn generate_unary( compile_well_known_types: bool, path: String, ) -> TokenStream { - let codec_name = syn::parse_str::(T::CODEC_PATH).unwrap(); + 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); @@ -191,7 +192,7 @@ fn generate_server_streaming( compile_well_known_types: bool, path: String, ) -> TokenStream { - let codec_name = syn::parse_str::(T::CODEC_PATH).unwrap(); + 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); @@ -217,7 +218,7 @@ fn generate_client_streaming( compile_well_known_types: bool, path: String, ) -> TokenStream { - let codec_name = syn::parse_str::(T::CODEC_PATH).unwrap(); + 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); @@ -243,7 +244,7 @@ fn generate_streaming( compile_well_known_types: bool, path: String, ) -> TokenStream { - let codec_name = syn::parse_str::(T::CODEC_PATH).unwrap(); + 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); diff --git a/tonic-build/src/lib.rs b/tonic-build/src/lib.rs index 385051419..1992ca85a 100644 --- a/tonic-build/src/lib.rs +++ b/tonic-build/src/lib.rs @@ -62,7 +62,7 @@ html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" )] #![deny(rustdoc::broken_intra_doc_links)] -#![doc(html_root_url = "https://docs.rs/tonic-build/0.6.2")] +#![doc(html_root_url = "https://docs.rs/tonic-build/0.7.2")] #![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))] @@ -79,6 +79,8 @@ mod prost; #[cfg_attr(docsrs, doc(cfg(feature = "prost")))] pub use prost::{compile_protos, configure, Builder}; +pub mod manual; + /// Service code generation for client pub mod client; /// Service code generation for Server @@ -91,9 +93,6 @@ pub mod server; /// to allow any codegen module to generate service /// abstractions. pub trait Service { - /// Path to the codec. - const CODEC_PATH: &'static str; - /// Comment type. type Comment: AsRef; @@ -119,8 +118,6 @@ pub trait Service { /// to generate abstraction implementations for /// the provided methods. pub trait Method { - /// Path to the codec. - const CODEC_PATH: &'static str; /// Comment type. type Comment: AsRef; @@ -128,6 +125,8 @@ pub trait 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. diff --git a/tonic-build/src/manual.rs b/tonic-build/src/manual.rs new file mode 100644 index 000000000..90cb95a39 --- /dev/null +++ b/tonic-build/src/manual.rs @@ -0,0 +1,482 @@ +//! 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 super::{client, server, Attributes}; +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, + /// 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 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, + /// 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, + 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 = server::generate( + service, + true, // emit_package, + "", // proto_path, -- not used + false, // compile_well_known_types -- not used + &Attributes::default(), + ); + self.servers.extend(server); + } + + if self.builder.build_client { + let client = client::generate( + service, + true, // emit_package, + "", // proto_path, -- not used + false, // compile_well_known_types, -- not used + &Attributes::default(), + ); + 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, + + out_dir: Option, +} + +impl Default for Builder { + fn default() -> Self { + Self { + build_server: true, + build_client: 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 + } + + /// 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(); + } + } +} diff --git a/tonic-build/src/prost.rs b/tonic-build/src/prost.rs index d5a1b2789..779414dd9 100644 --- a/tonic-build/src/prost.rs +++ b/tonic-build/src/prost.rs @@ -2,9 +2,11 @@ use super::{client, server, Attributes}; use proc_macro2::TokenStream; use prost_build::{Config, Method, Service}; use quote::ToTokens; -use std::ffi::OsString; -use std::io; -use std::path::{Path, PathBuf}; +use std::{ + ffi::OsString, + io, + path::{Path, PathBuf}, +}; /// Configure `tonic-build` code generation. /// @@ -51,8 +53,6 @@ const PROST_CODEC_PATH: &str = "tonic::codec::ProstCodec"; const NON_PATH_TYPE_ALLOWLIST: &[&str] = &["()"]; impl crate::Service for Service { - const CODEC_PATH: &'static str = PROST_CODEC_PATH; - type Method = Method; type Comment = String; @@ -78,7 +78,6 @@ impl crate::Service for Service { } impl crate::Method for Method { - const CODEC_PATH: &'static str = PROST_CODEC_PATH; type Comment = String; fn name(&self) -> &str { @@ -89,6 +88,10 @@ impl crate::Method for Method { &self.proto_name } + fn codec_path(&self) -> &str { + PROST_CODEC_PATH + } + fn client_streaming(&self) -> bool { self.client_streaming } @@ -388,7 +391,7 @@ impl Builder { PathBuf::from(std::env::var("OUT_DIR").unwrap()) }; - config.out_dir(out_dir.clone()); + config.out_dir(out_dir); if let Some(path) = self.file_descriptor_set_path.as_ref() { config.file_descriptor_set_path(path); } @@ -412,10 +415,16 @@ impl Builder { config.protoc_arg(arg); } - config.service_generator(Box::new(ServiceGenerator::new(self))); + config.service_generator(self.service_generator()); config.compile_protos(protos, includes)?; 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)) + } } diff --git a/tonic-build/src/server.rs b/tonic-build/src/server.rs index 4d405b374..affd2db38 100644 --- a/tonic-build/src/server.rs +++ b/tonic-build/src/server.rs @@ -124,7 +124,7 @@ pub fn generate( B::Error: Into + Send + 'static, { type Response = http::Response; - type Error = Never; + type Error = std::convert::Infallible; type Future = BoxFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { @@ -366,7 +366,7 @@ fn generate_unary( method_ident: Ident, server_trait: Ident, ) -> TokenStream { - let codec_name = syn::parse_str::(T::CODEC_PATH).unwrap(); + let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let service_ident = quote::format_ident!("{}Svc", method.identifier()); @@ -415,7 +415,7 @@ fn generate_server_streaming( method_ident: Ident, server_trait: Ident, ) -> TokenStream { - let codec_name = syn::parse_str::(T::CODEC_PATH).unwrap(); + let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let service_ident = quote::format_ident!("{}Svc", method.identifier()); @@ -470,7 +470,7 @@ fn generate_client_streaming( 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::(T::CODEC_PATH).unwrap(); + let codec_name = syn::parse_str::(method.codec_path()).unwrap(); quote! { #[allow(non_camel_case_types)] @@ -517,7 +517,7 @@ fn generate_streaming( method_ident: Ident, server_trait: Ident, ) -> TokenStream { - let codec_name = syn::parse_str::(T::CODEC_PATH).unwrap(); + let codec_name = syn::parse_str::(method.codec_path()).unwrap(); let service_ident = quote::format_ident!("{}Svc", method.identifier()); diff --git a/tonic-health/Cargo.toml b/tonic-health/Cargo.toml index 3993f1bc2..05741c462 100644 --- a/tonic-health/Cargo.toml +++ b/tonic-health/Cargo.toml @@ -4,7 +4,7 @@ categories = ["network-programming", "asynchronous"] description = """ Health Checking module of `tonic` gRPC implementation. """ -documentation = "https://docs.rs/tonic-health/0.4.0/tonic-health/" +documentation = "https://docs.rs/tonic-health/0.6.0/tonic-health/" edition = "2018" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "async", "healthcheck"] @@ -12,7 +12,7 @@ license = "MIT" name = "tonic-health" readme = "README.md" repository = "https://github.com/hyperium/tonic" -version = "0.5.0" +version = "0.6.0" [features] default = ["transport"] @@ -21,13 +21,13 @@ transport = ["tonic/transport", "tonic-build/transport"] [dependencies] async-stream = "0.3" bytes = "1.0" -prost = "0.9" +prost = "0.10" tokio = {version = "1.0", features = ["sync"]} tokio-stream = "0.1" -tonic = {version = "0.6", path = "../tonic", features = ["codegen", "prost"]} +tonic = {version = "0.7", path = "../tonic", features = ["codegen", "prost"]} [dev-dependencies] tokio = {version = "1.0", features = ["rt-multi-thread", "macros"]} [build-dependencies] -tonic-build = {version = "0.6", path = "../tonic-build", features = ["prost"]} +tonic-build = {version = "0.7", path = "../tonic-build", features = ["prost"]} diff --git a/tonic-health/LICENSE b/tonic-health/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tonic-health/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tonic-health/src/lib.rs b/tonic-health/src/lib.rs index ed9078a20..59aab1649 100644 --- a/tonic-health/src/lib.rs +++ b/tonic-health/src/lib.rs @@ -16,7 +16,7 @@ html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" )] #![deny(rustdoc::broken_intra_doc_links)] -#![doc(html_root_url = "https://docs.rs/tonic-health/0.5.0")] +#![doc(html_root_url = "https://docs.rs/tonic-health/0.6.0")] #![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))] diff --git a/tonic-health/src/server.rs b/tonic-health/src/server.rs index a094bf619..53e4ef9e2 100644 --- a/tonic-health/src/server.rs +++ b/tonic-health/src/server.rs @@ -103,7 +103,9 @@ impl HealthReporter { } } -struct HealthService { +/// A service providing implementations of gRPC health checking protocol. +#[derive(Debug)] +pub struct HealthService { statuses: Arc>>, } diff --git a/tonic-reflection/Cargo.toml b/tonic-reflection/Cargo.toml index 719b4ce8d..adcb37b7e 100644 --- a/tonic-reflection/Cargo.toml +++ b/tonic-reflection/Cargo.toml @@ -9,23 +9,24 @@ Server Reflection module of `tonic` gRPC implementation. """ edition = "2018" homepage = "https://github.com/hyperium/tonic" +documentation = "https://docs.rs/tonic-reflection/0.4.0/tonic-reflection/" keywords = ["rpc", "grpc", "async", "reflection"] license = "MIT" name = "tonic-reflection" readme = "README.md" repository = "https://github.com/hyperium/tonic" -version = "0.3.0" +version = "0.4.0" [dependencies] bytes = "1.0" -prost = "0.9" -prost-types = "0.9" +prost = "0.10" +prost-types = "0.10" tokio = {version = "1.0", features = ["sync"]} tokio-stream = {version = "0.1", features = ["net"]} -tonic = {version = "0.6", path = "../tonic", features = ["codegen", "prost"]} +tonic = {version = "0.7", path = "../tonic", features = ["codegen", "prost"]} [build-dependencies] -tonic-build = {version = "0.6", path = "../tonic-build", features = ["transport", "prost"]} +tonic-build = {version = "0.7", path = "../tonic-build", features = ["transport", "prost"]} [dev-dependencies] futures = "0.3" diff --git a/tonic-reflection/LICENSE b/tonic-reflection/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tonic-reflection/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tonic-reflection/src/lib.rs b/tonic-reflection/src/lib.rs index e9cb48fde..c4774c751 100644 --- a/tonic-reflection/src/lib.rs +++ b/tonic-reflection/src/lib.rs @@ -10,7 +10,7 @@ html_logo_url = "https://github.com/hyperium/tonic/raw/master/.github/assets/tonic-docs.png" )] #![deny(rustdoc::broken_intra_doc_links)] -#![doc(html_root_url = "https://docs.rs/tonic-reflection/0.3.0")] +#![doc(html_root_url = "https://docs.rs/tonic-reflection/0.4.0")] #![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))] diff --git a/tonic-types/Cargo.toml b/tonic-types/Cargo.toml index d902f6944..25d0ab6a9 100644 --- a/tonic-types/Cargo.toml +++ b/tonic-types/Cargo.toml @@ -4,7 +4,7 @@ categories = ["web-programming", "network-programming", "asynchronous"] description = """ A collection of useful protobuf types that can be used with `tonic`. """ -documentation = "https://docs.rs/tonic-types/0.4.0/tonic-types/" +documentation = "https://docs.rs/tonic-types/0.5.0/tonic-types/" edition = "2018" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "protobuf"] @@ -12,11 +12,11 @@ license = "MIT" name = "tonic-types" readme = "../README.md" repository = "https://github.com/hyperium/tonic" -version = "0.4.0" +version = "0.5.0" [dependencies] -prost = "0.9" -prost-types = "0.9" +prost = "0.10" +prost-types = "0.10" [build-dependencies] -prost-build = "0.9" +prost-build = "0.10" diff --git a/tonic-types/LICENSE b/tonic-types/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tonic-types/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index 8e18bde1a..cbfaa2eed 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -10,7 +10,7 @@ html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg" )] #![deny(rustdoc::broken_intra_doc_links)] -#![doc(html_root_url = "https://docs.rs/tonic-types/0.3.0")] +#![doc(html_root_url = "https://docs.rs/tonic-types/0.5.0")] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] mod pb { diff --git a/tonic-web/Cargo.toml b/tonic-web/Cargo.toml index 35acf4e06..ba9d2b95c 100644 --- a/tonic-web/Cargo.toml +++ b/tonic-web/Cargo.toml @@ -4,7 +4,7 @@ categories = ["network-programming", "asynchronous"] description = """ grpc-web protocol translation for tonic services. """ -documentation = "https://docs.rs/tonic-web/0.2.0/tonic-web/" +documentation = "https://docs.rs/tonic-web/0.3.0/tonic-web/" edition = "2018" homepage = "https://github.com/hyperium/tonic" keywords = ["rpc", "grpc", "grpc-web"] @@ -12,7 +12,7 @@ license = "MIT" name = "tonic-web" readme = "README.md" repository = "https://github.com/hyperium/tonic" -version = "0.2.0" +version = "0.3.0" [dependencies] base64 = "0.13" @@ -22,7 +22,7 @@ http = "0.2" http-body = "0.4" hyper = "0.14" pin-project = "1" -tonic = {version = "0.6", path = "../tonic", default-features = false, features = ["transport"]} +tonic = {version = "0.7", path = "../tonic", default-features = false, features = ["transport"]} tower-service = "0.3" tracing = "0.1" diff --git a/tonic-web/LICENSE b/tonic-web/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tonic-web/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tonic-web/README.md b/tonic-web/README.md index 318061374..035468d72 100644 --- a/tonic-web/README.md +++ b/tonic-web/README.md @@ -1,13 +1,13 @@ # tonic-web Enables tonic servers to handle requests from `grpc-web` clients directly, -without the need of an external proxy. +without the need of an external proxy. ## Getting Started ```toml [dependencies] -tonic_web = "0.1" +tonic-web = "" ``` ## Enabling tonic services @@ -35,4 +35,4 @@ async fn main() -> Result<(), Box> { See [the examples folder][example] for a server and client example. -[example]: https://github.com/hyperium/tonic/tree/master/examples/src/tower +[example]: https://github.com/hyperium/tonic/tree/master/examples/src/grpc-web diff --git a/tonic-web/src/lib.rs b/tonic-web/src/lib.rs index e3a869ff4..f0e18edbf 100644 --- a/tonic-web/src/lib.rs +++ b/tonic-web/src/lib.rs @@ -84,7 +84,7 @@ rust_2018_idioms, unreachable_pub )] -#![doc(html_root_url = "https://docs.rs/tonic-reflection/0.3.0")] +#![doc(html_root_url = "https://docs.rs/tonic-web/0.3.0")] #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] pub use config::Config; diff --git a/tonic-web/tests/integration/Cargo.toml b/tonic-web/tests/integration/Cargo.toml index cdc28163a..f26ecdd02 100644 --- a/tonic-web/tests/integration/Cargo.toml +++ b/tonic-web/tests/integration/Cargo.toml @@ -4,16 +4,17 @@ edition = "2018" name = "integration" publish = false version = "0.1.0" +license = "MIT" [dependencies] base64 = "0.13" bytes = "1.0" hyper = "0.14" -prost = "0.9" -tokio = {version = "1", features = ["macros", "rt", "net"]} -tokio-stream = {version = "0.1", features = ["net"]} -tonic = {path = "../../../tonic"} -tonic-web = {path = "../../../tonic-web"} +prost = "0.10" +tokio = { version = "1", features = ["macros", "rt", "net"] } +tokio-stream = { version = "0.1", features = ["net"] } +tonic = { path = "../../../tonic" } +tonic-web = { path = "../../../tonic-web" } [build-dependencies] -tonic-build = {path = "../../../tonic-build"} +tonic-build = { path = "../../../tonic-build" } diff --git a/tonic-web/tests/integration/LICENSE b/tonic-web/tests/integration/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tonic-web/tests/integration/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tonic/Cargo.toml b/tonic/Cargo.toml index 4912df98b..f101c7105 100644 --- a/tonic/Cargo.toml +++ b/tonic/Cargo.toml @@ -7,20 +7,20 @@ name = "tonic" # - Cargo.toml # - README.md # - Update CHANGELOG.md. -# - Create "v0.6.x" git tag. +# - Create "v0.7.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. """ -documentation = "https://docs.rs/tonic/0.6.2/tonic/" +documentation = "https://docs.rs/tonic/0.7.2/tonic/" edition = "2018" 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.6.2" +version = "0.7.2" [features] codegen = ["async-trait"] @@ -32,6 +32,10 @@ tls-roots = ["tls-roots-common", "rustls-native-certs"] tls-roots-common = ["tls"] tls-webpki-roots = ["tls-roots-common", "webpki-roots"] transport = [ + "axum", + "channel" +] +channel = [ "h2", "hyper", "tokio", @@ -58,13 +62,13 @@ async-stream = "0.3" http-body = "0.4.4" percent-encoding = "2.1" pin-project = "1.0" -tokio-util = {version = "0.6", features = ["codec"]} +tokio-util = {version = "0.7", features = ["codec"]} tower-layer = "0.3" tower-service = "0.3" # prost -prost-derive = {version = "0.9", optional = true} -prost1 = {package = "prost", version = "0.9", optional = true} +prost-derive = {version = "0.10", optional = true} +prost1 = {package = "prost", version = "0.10", optional = true} # codegen async-trait = {version = "0.1.13", optional = true} @@ -75,11 +79,12 @@ hyper = {version = "0.14.14", features = ["full"], optional = true} hyper-timeout = {version = "0.4", optional = true} tokio = {version = "1.0.1", features = ["net"], optional = true} tokio-stream = "0.1" -tower = {version = "0.4.7", features = ["balance", "buffer", "discover", "limit", "load", "make", "timeout", "util"], optional = true} +tower = {version = "0.4.7", default-features = false, features = ["balance", "buffer", "discover", "limit", "load", "make", "timeout", "util"], optional = true} tracing-futures = {version = "0.2", optional = true} +axum = {version = "0.5", default_features = false, optional = true} # rustls -rustls-pemfile = { version = "0.2.1", optional = true } +rustls-pemfile = { version = "1.0", optional = true } rustls-native-certs = { version = "0.6.1", optional = true } tokio-rustls = { version = "0.23.1", optional = true } webpki-roots = { version = "0.22.1", optional = true } diff --git a/tonic/LICENSE b/tonic/LICENSE new file mode 100644 index 000000000..307709840 --- /dev/null +++ b/tonic/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 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. diff --git a/tonic/src/body.rs b/tonic/src/body.rs index d66a4fcb2..53a917ec6 100644 --- a/tonic/src/body.rs +++ b/tonic/src/body.rs @@ -5,6 +5,15 @@ use http_body::Body; /// A type erased HTTP body used for tonic services. pub type BoxBody = http_body::combinators::UnsyncBoxBody; +/// Convert a [`http_body::Body`] into a [`BoxBody`]. +pub(crate) fn boxed(body: B) -> BoxBody +where + B: http_body::Body + Send + 'static, + B::Error: Into, +{ + body.map_err(crate::Status::map_error).boxed_unsync() +} + // this also exists in `crate::codegen` but we need it here since `codegen` has // `#[cfg(feature = "codegen")]`. /// Create an empty `BoxBody` diff --git a/tonic/src/client/grpc.rs b/tonic/src/client/grpc.rs index 210d44432..19c994011 100644 --- a/tonic/src/client/grpc.rs +++ b/tonic/src/client/grpc.rs @@ -248,7 +248,12 @@ impl Grpc { }) .map(BoxBody::new); - let mut request = request.into_http(uri, SanitizeHeaders::Yes); + let mut request = request.into_http( + uri, + http::Method::POST, + http::Version::HTTP_2, + SanitizeHeaders::Yes, + ); // Add the gRPC related HTTP headers request diff --git a/tonic/src/codec/decode.rs b/tonic/src/codec/decode.rs index b4dda4729..66bc14e8a 100644 --- a/tonic/src/codec/decode.rs +++ b/tonic/src/codec/decode.rs @@ -39,6 +39,7 @@ impl Unpin for Streaming {} enum State { ReadHeader, ReadBody { compression: bool, len: usize }, + Error, } #[derive(Debug)] @@ -311,6 +312,10 @@ impl Stream for Streaming { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { loop { + if let State::Error = &self.state { + return Poll::Ready(None); + } + // FIXME: implement the ability to poll trailers when we _know_ that // the consumer of this stream will only poll for the first message. // This means we skip the poll_trailers step. @@ -321,6 +326,7 @@ impl Stream for Streaming { let chunk = match ready!(Pin::new(&mut self.body).poll_data(cx)) { Some(Ok(d)) => Some(d), Some(Err(e)) => { + let _ = std::mem::replace(&mut self.state, State::Error); let err: crate::Error = e.into(); debug!("decoder inner stream error: {:?}", err); let status = Status::from_error(err); diff --git a/tonic/src/codegen.rs b/tonic/src/codegen.rs index 42fe53f20..fae80235f 100644 --- a/tonic/src/codegen.rs +++ b/tonic/src/codegen.rs @@ -13,6 +13,7 @@ pub type StdError = Box; #[cfg(feature = "compression")] pub use crate::codec::{CompressionEncoding, EnabledCompressionEncodings}; pub use crate::service::interceptor::InterceptedService; +pub use bytes::Bytes; pub use http_body::Body; pub type BoxFuture = self::Pin> + Send + 'static>>; @@ -23,17 +24,6 @@ pub mod http { pub use http::*; } -#[derive(Debug)] -pub enum Never {} - -impl std::fmt::Display for Never { - fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self {} - } -} - -impl std::error::Error for Never {} - pub fn empty_body() -> crate::body::BoxBody { http_body::Empty::new() .map_err(|err| match err {}) diff --git a/tonic/src/lib.rs b/tonic/src/lib.rs index cb5c332b4..23ae149fa 100644 --- a/tonic/src/lib.rs +++ b/tonic/src/lib.rs @@ -17,7 +17,9 @@ //! # Feature Flags //! //! - `transport`: Enables the fully featured, batteries included client and server -//! implementation based on [`hyper`], [`tower`] and [`tokio`]. Enabled by default. +//! implementation based on [`hyper`], [`tower`] and [`tokio`]. Enabled by default. +//! - `channel`: Enables just the full featured channel/client portion of the `transport` +//! feature. //! - `codegen`: Enables all the required exports and optional dependencies required //! for [`tonic-build`]. Enabled by default. //! - `tls`: Enables the `rustls` based TLS options for the `transport` feature. Not @@ -79,7 +81,7 @@ #![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/0.6.2")] +#![doc(html_root_url = "https://docs.rs/tonic/0.7.2")] #![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))] diff --git a/tonic/src/metadata/encoding.rs b/tonic/src/metadata/encoding.rs index 106be4eab..657b2bf65 100644 --- a/tonic/src/metadata/encoding.rs +++ b/tonic/src/metadata/encoding.rs @@ -51,11 +51,27 @@ pub trait ValueEncoding: Clone + Eq + PartialEq + Hash + self::value_encoding::S 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)] -#[doc(hidden)] +#[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)] -#[doc(hidden)] +#[non_exhaustive] pub enum Binary {} // ===== impl ValueEncoding ===== diff --git a/tonic/src/metadata/value.rs b/tonic/src/metadata/value.rs index b976440c9..57e9719ac 100644 --- a/tonic/src/metadata/value.rs +++ b/tonic/src/metadata/value.rs @@ -7,6 +7,7 @@ use super::key::MetadataKey; use bytes::Bytes; use http::header::HeaderValue; +use std::convert::TryFrom; use std::error::Error; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; @@ -86,9 +87,6 @@ impl MetadataValue { /// For Binary metadata values this method cannot fail. See also the Binary /// only version of this method `from_bytes`. /// - /// This function is intended to be replaced in the future by a `TryFrom` - /// implementation once the trait is stabilized in std. - /// /// # Examples /// /// ``` @@ -105,11 +103,9 @@ impl MetadataValue { /// assert!(val.is_err()); /// ``` #[inline] + #[deprecated = "Use TryFrom instead"] pub fn try_from_bytes(src: &[u8]) -> Result { - VE::from_bytes(src).map(|value| MetadataValue { - inner: value, - phantom: PhantomData, - }) + Self::try_from(src) } /// Attempt to convert a `Bytes` buffer to a `MetadataValue`. @@ -122,15 +118,10 @@ impl MetadataValue { /// 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. - /// - /// This function is intended to be replaced in the future by a `TryFrom` - /// implementation once the trait is stabilized in std. #[inline] + #[deprecated = "Use TryFrom instead"] pub fn from_shared(src: Bytes) -> Result { - VE::from_shared(src).map(|value| MetadataValue { - inner: value, - phantom: PhantomData, - }) + Self::try_from(src) } /// Convert a `Bytes` directly into a `MetadataValue` without validating. @@ -282,6 +273,165 @@ impl MetadataValue { } } +/// 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::*; +/// # use std::convert::TryFrom; +/// let val = AsciiMetadataValue::try_from(b"hello\xfa").unwrap(); +/// assert_eq!(val, &b"hello\xfa"[..]); +/// ``` +/// +/// An invalid value +/// +/// ``` +/// # use tonic::metadata::*; +/// # use std::convert::TryFrom; +/// let val = AsciiMetadataValue::try_from(b"\n"); +/// assert!(val.is_err()); +/// ``` +impl<'a, VE: ValueEncoding> TryFrom<&'a [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::*; +/// # use std::convert::TryFrom; +/// let val = AsciiMetadataValue::try_from(b"hello\xfa").unwrap(); +/// assert_eq!(val, &b"hello\xfa"[..]); +/// ``` +/// +/// An invalid value +/// +/// ``` +/// # use tonic::metadata::*; +/// # use std::convert::TryFrom; +/// let val = AsciiMetadataValue::try_from(b"\n"); +/// assert!(val.is_err()); +/// ``` +impl<'a, VE: ValueEncoding, const N: usize> TryFrom<&'a [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() + } +} + // is_empty is defined in the generic impl block above #[allow(clippy::len_without_is_empty)] impl MetadataValue { @@ -292,9 +442,6 @@ impl MetadataValue { /// `from_bytes` to create a `MetadataValue` that includes opaque octets /// (128-255). /// - /// This function is intended to be replaced in the future by a `TryFrom` - /// implementation once the trait is stabilized in std. - /// /// # Examples /// /// ``` @@ -311,14 +458,10 @@ impl MetadataValue { /// assert!(val.is_err()); /// ``` #[allow(clippy::should_implement_trait)] + #[deprecated = "Use TryFrom or FromStr instead"] #[inline] pub fn from_str(src: &str) -> Result { - HeaderValue::from_str(src) - .map(|value| MetadataValue { - inner: value, - phantom: PhantomData, - }) - .map_err(|_| InvalidMetadataValue::new()) + src.parse() } /// Converts a MetadataKey into a MetadataValue. @@ -330,8 +473,9 @@ impl MetadataValue { /// /// ``` /// # use tonic::metadata::*; + /// # use std::convert::TryFrom; /// let val = AsciiMetadataValue::from_key::("accept".parse().unwrap()); - /// assert_eq!(val, AsciiMetadataValue::try_from_bytes(b"accept").unwrap()); + /// assert_eq!(val, AsciiMetadataValue::try_from(b"accept").unwrap()); /// ``` #[inline] pub fn from_key(key: MetadataKey) -> Self { @@ -402,8 +546,8 @@ impl MetadataValue { /// ``` #[inline] pub fn from_bytes(src: &[u8]) -> Self { - // Only the Ascii version of try_from_bytes can fail. - Self::try_from_bytes(src).unwrap() + // Only the Ascii version of try_from can fail. + Self::try_from(src).unwrap() } } @@ -501,7 +645,7 @@ mod from_metadata_value_tests { assert_eq!( map.get("accept").unwrap(), - AsciiMetadataValue::try_from_bytes(b"hello-world").unwrap() + AsciiMetadataValue::try_from(b"hello-world").unwrap() ); } } @@ -511,7 +655,12 @@ impl FromStr for MetadataValue { #[inline] fn from_str(s: &str) -> Result, Self::Err> { - MetadataValue::::from_str(s) + HeaderValue::from_str(s) + .map(|value| MetadataValue { + inner: value, + phantom: PhantomData, + }) + .map_err(|_| InvalidMetadataValue::new()) } } @@ -730,7 +879,7 @@ fn test_debug() { ]; for &(value, expected) in cases { - let val = AsciiMetadataValue::try_from_bytes(value.as_bytes()).unwrap(); + let val = AsciiMetadataValue::try_from(value.as_bytes()).unwrap(); let actual = format!("{:?}", val); assert_eq!(expected, actual); } @@ -760,7 +909,7 @@ fn test_is_empty() { #[test] fn test_from_shared_base64_encodes() { - let value = BinaryMetadataValue::from_shared(Bytes::from_static(b"Hello")).unwrap(); + let value = BinaryMetadataValue::try_from(Bytes::from_static(b"Hello")).unwrap(); assert_eq!(value.as_encoded_bytes(), b"SGVsbG8"); } diff --git a/tonic/src/request.rs b/tonic/src/request.rs index 46a2d486d..5827bb77b 100644 --- a/tonic/src/request.rs +++ b/tonic/src/request.rs @@ -169,12 +169,14 @@ impl Request { 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() = http::Version::HTTP_2; - *request.method_mut() = http::Method::POST; + *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(), @@ -202,8 +204,8 @@ impl Request { /// Get the remote address of this connection. /// /// This will return `None` if the `IO` type used - /// does not implement `Connected`. This currently, - /// only works on the server side. + /// does not implement `Connected` or when using a unix domain socket. + /// This currently only works on the server side. pub fn remote_addr(&self) -> Option { #[cfg(feature = "transport")] { @@ -283,7 +285,7 @@ impl Request { /// /// [the spec]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md pub fn set_timeout(&mut self, deadline: Duration) { - let value = MetadataValue::from_str(&duration_to_grpc_timeout(deadline)).unwrap(); + let value: MetadataValue<_> = duration_to_grpc_timeout(deadline).parse().unwrap(); self.metadata_mut() .insert(crate::metadata::GRPC_TIMEOUT_HEADER, value); } @@ -441,7 +443,12 @@ mod tests { .insert(*header, MetadataValue::from_static("invalid")); } - let http_request = r.into_http(Uri::default(), SanitizeHeaders::Yes); + let http_request = r.into_http( + Uri::default(), + http::Method::POST, + http::Version::HTTP_2, + SanitizeHeaders::Yes, + ); assert!(http_request.headers().is_empty()); } diff --git a/tonic/src/service/interceptor.rs b/tonic/src/service/interceptor.rs index b27d6be7a..76552e502 100644 --- a/tonic/src/service/interceptor.rs +++ b/tonic/src/service/interceptor.rs @@ -2,8 +2,13 @@ //! //! See [`Interceptor`] for more details. -use crate::{request::SanitizeHeaders, Status}; -use http::Uri; +use crate::{ + body::{boxed, BoxBody}, + request::SanitizeHeaders, + Request, Status, +}; +use bytes::Bytes; +use http::{Method, Uri, Version}; use pin_project::pin_project; use std::{ fmt, @@ -46,8 +51,8 @@ pub trait Interceptor { } impl Interceptor for F -where - F: FnMut(crate::Request<()>) -> Result, Status>, + where + F: FnMut(crate::Request<()>) -> Result, Status>, { fn call(&mut self, request: crate::Request<()>) -> Result, Status> { self(request) @@ -58,127 +63,56 @@ where pub trait AsyncInterceptor { /// The Future returned by the interceptor. type Future: Future, Status>>; - /// Call the underlying async function that transforms a body-less gRPC request. - fn call_underlying(&mut self, request: crate::Request<()>) -> Self::Future; /// Intercept a request before it is sent, optionally cancelling it. - fn call( - &mut self, - request: http::Request, - ) -> AsyncInterceptorFuture; + fn call(&mut self, request: crate::Request<()>) -> Self::Future; } impl AsyncInterceptor for F -where - F: FnMut(crate::Request<()>) -> U, - U: Future, Status>>, + where + F: FnMut(crate::Request<()>) -> U, + U: Future, Status>>, { type Future = U; - fn call_underlying(&mut self, request: crate::Request<()>) -> Self::Future { + fn call(&mut self, request: crate::Request<()>) -> Self::Future { self(request) } - - fn call( - &mut self, - request: http::Request, - ) -> AsyncInterceptorFuture { - AsyncInterceptorFuture::new(self, request) - } -} - -/// Wrapper that hides the gRPC body from the underlying [`AsyncInterceptor`] function. -#[pin_project] -#[derive(Debug)] -pub struct AsyncInterceptorFuture -where - I: Future, Status>>, -{ - #[pin] - interceptor_fut: I, - uri: Uri, - msg: ReqBody, -} - -impl AsyncInterceptorFuture -where - F: Future, Status>>, -{ - fn new>( - interceptor: &mut A, - req: http::Request, - ) -> Self { - let uri = req.uri().clone(); - let grpc_req = crate::Request::from_http(req); - let (metadata, extensions, msg) = grpc_req.into_parts(); - - let req_without_body = crate::Request::from_parts(metadata, extensions, ()); - AsyncInterceptorFuture { - interceptor_fut: interceptor.call_underlying(req_without_body), - uri, - msg, - } - } -} - -impl Future for AsyncInterceptorFuture -where - F: Future, Status>>, - ReqBody: Default, -{ - type Output = Result, crate::Error>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - match this.interceptor_fut.poll(cx) { - Poll::Ready(intercepted_req) => match intercepted_req { - Ok(r) => { - let (metadata, extensions, _) = r.into_parts(); - let msg = mem::replace(this.msg, ReqBody::default()); - let req = crate::Request::from_parts(metadata, extensions, msg); - let req = req.into_http(this.uri.clone(), SanitizeHeaders::No); - Poll::Ready(Ok(req)) - } - Err(status) => Poll::Ready(Err(status.into())), - }, - Poll::Pending => return Poll::Pending, - } - } } /// Create a new interceptor layer. /// /// See [`Interceptor`] for more details. pub fn interceptor(f: F) -> InterceptorLayer -where - F: Interceptor, + where + F: Interceptor, { InterceptorLayer { f } } -/// Create a new async interceptor layer. -/// -/// See [`AsyncInterceptor`] and [`Interceptor`] for more details. -pub fn async_interceptor(f: F) -> AsyncInterceptorLayer -where - F: AsyncInterceptor, -{ - AsyncInterceptorLayer { f } -} - #[deprecated( - since = "0.5.1", - note = "Please use the `interceptor` function instead" +since = "0.5.1", +note = "Please use the `interceptor` function instead" )] /// Create a new interceptor layer. /// /// See [`Interceptor`] for more details. pub fn interceptor_fn(f: F) -> InterceptorLayer -where - F: Interceptor, + where + F: Interceptor, { interceptor(f) } +/// Create a new async interceptor layer. +/// +/// See [`AsyncInterceptor`] and [`Interceptor`] for more details. +pub fn async_interceptor(f: F) -> AsyncInterceptorLayer + where + F: AsyncInterceptor, +{ + AsyncInterceptorLayer { f } +} + /// A gRPC interceptor that can be used as a [`Layer`], /// created by calling [`interceptor`]. /// @@ -189,8 +123,8 @@ pub struct InterceptorLayer { } impl Layer for InterceptorLayer -where - F: Interceptor + Clone, + where + F: Interceptor + Clone, { type Service = InterceptedService; @@ -209,9 +143,9 @@ pub struct AsyncInterceptorLayer { } impl Layer for AsyncInterceptorLayer -where - S: Clone, - F: AsyncInterceptor + Clone, + where + S: Clone, + F: AsyncInterceptor + Clone, { type Service = AsyncInterceptedService; @@ -221,8 +155,8 @@ where } #[deprecated( - since = "0.5.1", - note = "Please use the `InterceptorLayer` type instead" +since = "0.5.1", +note = "Please use the `InterceptorLayer` type instead" )] /// A gRPC interceptor that can be used as a [`Layer`], /// created by calling [`interceptor`]. @@ -243,16 +177,16 @@ impl InterceptedService { /// Create a new `InterceptedService` that wraps `S` and intercepts each request with the /// function `F`. pub fn new(service: S, f: F) -> Self - where - F: Interceptor, + where + F: Interceptor, { Self { inner: service, f } } } impl fmt::Debug for InterceptedService -where - S: fmt::Debug, + where + S: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("InterceptedService") @@ -262,41 +196,97 @@ where } } +// Components and attributes of a request, without metadata or extensions. +#[derive(Debug)] +struct DecomposedRequest { + uri: Uri, + method: Method, + http_version: Version, + msg: ReqBody, +} + +/// Decompose the request into its contents and properties, and create a new request without a body. +/// +/// 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 original message body with the `recompose` function. Also note that 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 `recompose`. +fn decompose(req: http::Request) -> (DecomposedRequest, Request<()>) { + let uri = req.uri().clone(); + let method = req.method().clone(); + let http_version = req.version(); + let req = crate::Request::from_http(req); + let (metadata, extensions, msg) = req.into_parts(); + + let dreq = DecomposedRequest { + uri, + method, + http_version, + msg, + }; + let req_without_body = crate::Request::from_parts(metadata, extensions, ()); + + (dreq, req_without_body) +} + +/// Combine the modified metadata and extensions with the original message body and attributes. +fn recompose( + dreq: DecomposedRequest, + modified_req: Request<()>, +) -> http::Request { + let (metadata, extensions, _) = modified_req.into_parts(); + let req = crate::Request::from_parts(metadata, extensions, dreq.msg); + + req.into_http( + dreq.uri, + dreq.method, + dreq.http_version, + SanitizeHeaders::No, + ) +} + impl Service> for InterceptedService -where - F: Interceptor, - S: Service, Response = http::Response>, - S::Error: Into, + where + F: Interceptor, + S: Service, Response = http::Response>, + S::Error: Into, + ResBody: Default + http_body::Body + Send + 'static, + ResBody::Error: Into, { - type Response = http::Response; - type Error = crate::Error; + 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).map_err(Into::into) + self.inner.poll_ready(cx) } fn call(&mut self, req: http::Request) -> Self::Future { - let uri = req.uri().clone(); - let req = crate::Request::from_http(req); - let (metadata, extensions, msg) = req.into_parts(); - - match self - .f - .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, SanitizeHeaders::No); - ResponseFuture::future(self.inner.call(req)) + let (dreq, req_without_body) = decompose(req); + + match self.f.call(req_without_body) { + Ok(modified_req) => { + let modified_req_with_body = recompose(dreq, modified_req); + + ResponseFuture::future(self.inner.call(modified_req_with_body)) } - Err(status) => ResponseFuture::error(status), + Err(status) => ResponseFuture::status(status), } } } +// required to use `InterceptedService` with `Router` +#[cfg(feature = "transport")] +impl crate::transport::NamedService for InterceptedService + where + S: crate::transport::NamedService, +{ + const NAME: &'static str = S::NAME; +} + /// A service wrapped in an async interceptor middleware. /// /// See [`AsyncInterceptor`] for more details. @@ -315,8 +305,8 @@ impl AsyncInterceptedService { } impl fmt::Debug for AsyncInterceptedService -where - S: fmt::Debug, + where + S: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AsyncInterceptedService") @@ -327,40 +317,33 @@ where } impl Service> for AsyncInterceptedService -where - F: AsyncInterceptor + Clone, - S: Service, Response = http::Response> + Clone, - S::Error: Into, - ReqBody: Default, + where + F: AsyncInterceptor + Clone, + S: Service, Response = http::Response> + Clone, + S::Error: Into, + ReqBody: Default, + ResBody: Default + http_body::Body + Send + 'static, + ResBody::Error: Into, { - type Response = S::Response; - type Error = crate::Error; - type Future = AsyncResponseFuture, ReqBody>; + type Response = http::Response; + type Error = S::Error; + type Future = AsyncResponseFuture; #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx).map_err(Into::into) + self.inner.poll_ready(cx) } fn call(&mut self, req: http::Request) -> Self::Future { - AsyncResponseFuture::new(self.f.call(req), self.inner.clone()) + AsyncResponseFuture::new(req, &mut self.f, self.inner.clone()) } } -// required to use `InterceptedService` with `Router` -#[cfg(feature = "transport")] -impl crate::transport::NamedService for InterceptedService -where - S: crate::transport::NamedService, -{ - const NAME: &'static str = S::NAME; -} - // required to use `AsyncInterceptedService` with `Router` #[cfg(feature = "transport")] impl crate::transport::NamedService for AsyncInterceptedService -where - S: crate::transport::NamedService, + where + S: crate::transport::NamedService, { const NAME: &'static str = S::NAME; } @@ -380,9 +363,9 @@ impl ResponseFuture { } } - fn error(status: Status) -> Self { + fn status(status: Status) -> Self { Self { - kind: Kind::Error(Some(status)), + kind: Kind::Status(Some(status)), } } } @@ -391,22 +374,31 @@ impl ResponseFuture { #[derive(Debug)] enum Kind { Future(#[pin] F), - Error(Option), + Status(Option), } impl Future for ResponseFuture -where - F: Future, E>>, - E: Into, + where + F: Future, E>>, + E: Into, + B: Default + http_body::Body + Send + 'static, + B::Error: Into, { - type Output = Result, crate::Error>; + 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_err(Into::into), - KindProj::Error(status) => { - let error = status.take().unwrap().into(); - Poll::Ready(Err(error)) + KindProj::Future(future) => future + .poll(cx) + .map(|result| result.map(|res| res.map(boxed))), + KindProj::Status(status) => { + let response = status + .take() + .unwrap() + .to_http() + .map(|_| B::default()) + .map(boxed); + Poll::Ready(Ok(response)) } } } @@ -420,66 +412,111 @@ enum PinnedOption { } /// Response future for [`AsyncInterceptedService`]. -#[pin_project] +/// +/// Handles the call to the async interceptor, then calls the inner service and wraps the result in +/// [`ResponseFuture`]. +#[pin_project(project = AsyncResponseFutureProj)] #[derive(Debug)] pub struct AsyncResponseFuture -where - S: Service>, - S::Error: Into, - I: Future, crate::Error>>, + where + S: Service>, + S::Error: Into, + I: Future, Status>>, { #[pin] interceptor_fut: PinnedOption, #[pin] inner_fut: PinnedOption>, inner: S, + dreq: DecomposedRequest, } impl AsyncResponseFuture -where - S: Service>, - S::Error: Into, - I: Future, crate::Error>>, + where + S: Service>, + S::Error: Into, + I: Future, Status>>, + ReqBody: Default, { - fn new(interceptor_fut: I, inner: S) -> Self { + fn new>( + req: http::Request, + interceptor: &mut A, + inner: S, + ) -> Self { + let (dreq, req_without_body) = decompose(req); + let interceptor_fut = interceptor.call(req_without_body); + AsyncResponseFuture { interceptor_fut: PinnedOption::Some(interceptor_fut), inner_fut: PinnedOption::None, inner, + dreq, + } + } + + /// Calls the inner service with the intercepted request (which has been modified by the + /// async interceptor func). + fn create_inner_fut( + this: &mut AsyncResponseFutureProj<'_, S, I, ReqBody>, + intercepted_req: Result, Status>, + ) -> ResponseFuture { + match intercepted_req { + Ok(req) => { + // We can't move the message body out of the pin projection. So, to + // avoid copying it, we swap its memory with an empty body and then can + // move it into the recomposed request. + let msg = mem::take(&mut this.dreq.msg); + let movable_dreq = DecomposedRequest { + uri: this.dreq.uri.clone(), + method: this.dreq.method.clone(), + http_version: this.dreq.http_version, + msg, + }; + let modified_req_with_body = recompose(movable_dreq, req); + + ResponseFuture::future(this.inner.call(modified_req_with_body)) + } + Err(status) => ResponseFuture::status(status), } } } impl Future for AsyncResponseFuture -where - S: Service, Response = http::Response>, - I: Future, crate::Error>>, - S::Error: Into, - ReqBody: Default, + where + S: Service, Response = http::Response>, + I: Future, Status>>, + S::Error: Into, + ReqBody: Default, + ResBody: Default + http_body::Body + Send + 'static, + ResBody::Error: Into, { - type Output = Result, crate::Error>; + type Output = Result, S::Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut this = self.project(); + + // The struct was initialized (via `new`) with interceptor func future, which we poll here. if let PinnedOptionProj::Some(f) = this.interceptor_fut.as_mut().project() { match f.poll(cx) { - Poll::Ready(intercepted_req) => match intercepted_req { - Ok(req) => { - this.inner_fut - .set(PinnedOption::Some(ResponseFuture::future( - this.inner.call(req), - ))); - this.interceptor_fut.set(PinnedOption::None); - } - Err(e) => return Poll::Ready(Err(e)), - }, + Poll::Ready(intercepted_req) => { + let inner_fut = AsyncResponseFuture::::create_inner_fut( + &mut this, + intercepted_req, + ); + // Set the inner service future and clear the interceptor future. + this.inner_fut.set(PinnedOption::Some(inner_fut)); + this.interceptor_fut.set(PinnedOption::None); + } Poll::Pending => return Poll::Pending, } } - if let PinnedOptionProj::Some(inner_fut) = this.inner_fut.project() { - return inner_fut.poll(cx); - } - panic!() + // At this point, inner_fut should always be Some. + let inner_fut = match this.inner_fut.project() { + PinnedOptionProj::None => panic!(), + PinnedOptionProj::Some(f) => f, + }; + + inner_fut.poll(cx) } } @@ -487,11 +524,38 @@ where mod tests { #[allow(unused_imports)] use super::*; + use http::header::HeaderMap; + use std::{ + pin::Pin, + task::{Context, Poll}, + }; use tower::ServiceExt; + #[derive(Debug, Default)] + struct TestBody; + + impl http_body::Body for TestBody { + type Data = Bytes; + type Error = Status; + + fn poll_data( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + Poll::Ready(None) + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Poll::Ready(Ok(None)) + } + } + #[tokio::test] - async fn doesnt_remove_headers() { - let svc = tower::service_fn(|request: http::Request| async move { + async fn doesnt_remove_headers_from_requests() { + let svc = tower::service_fn(|request: http::Request| async move { assert_eq!( request .headers() @@ -500,7 +564,7 @@ mod tests { "test-tonic" ); - Ok::<_, hyper::Error>(hyper::Response::new(hyper::Body::empty())) + Ok::<_, Status>(http::Response::new(TestBody)) }); let svc = InterceptedService::new(svc, |request: crate::Request<()>| { @@ -511,12 +575,13 @@ mod tests { .expect("missing in interceptor"), "test-tonic" ); + Ok(request) }); let request = http::Request::builder() .header("user-agent", "test-tonic") - .body(hyper::Body::empty()) + .body(TestBody) .unwrap(); svc.oneshot(request).await.unwrap(); @@ -554,4 +619,84 @@ mod tests { 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).to_http(); + + let svc = tower::service_fn(|_: http::Request| async { + Ok::<_, Status>(http::Response::new(TestBody)) + }); + + let svc = InterceptedService::new(svc, |_: crate::Request<()>| { + Err(Status::permission_denied(message)) + }); + + let request = http::Request::builder().body(TestBody).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 async_interceptor_handles_intercepted_status_as_response() { + let message = "Blocked by the interceptor"; + let expected = Status::permission_denied(message).to_http(); + + let svc = tower::service_fn(|_: http::Request| async { + Ok::<_, Status>(http::Response::new(TestBody)) + }); + + let svc = AsyncInterceptedService::new(svc, |_: crate::Request<()>| { + std::future::ready(Err(Status::permission_denied(message))) + }); + + let request = http::Request::builder().body(TestBody).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(hyper::Body::empty())) + }); + + let svc = InterceptedService::new(svc, |request: crate::Request<()>| Ok(request)); + + let request = http::Request::builder() + .method(http::Method::OPTIONS) + .body(hyper::Body::empty()) + .unwrap(); + + svc.oneshot(request).await.unwrap(); + } + + #[tokio::test] + async fn async_interceptor_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(hyper::Body::empty())) + }); + + let svc = AsyncInterceptedService::new(svc, |request: crate::Request<()>| { + std::future::ready(Ok(request)) + }); + + let request = http::Request::builder() + .method(http::Method::OPTIONS) + .body(hyper::Body::empty()) + .unwrap(); + + svc.oneshot(request).await.unwrap(); + } +} \ No newline at end of file diff --git a/tonic/src/status.rs b/tonic/src/status.rs index b50f6e55f..1d9d9d7c1 100644 --- a/tonic/src/status.rs +++ b/tonic/src/status.rs @@ -53,7 +53,7 @@ pub struct 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)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Code { /// The operation completed successfully. Ok = 0, diff --git a/tonic/src/transport/channel/endpoint.rs b/tonic/src/transport/channel/endpoint.rs index 3a0b5b896..5d08e22f3 100644 --- a/tonic/src/transport/channel/endpoint.rs +++ b/tonic/src/transport/channel/endpoint.rs @@ -4,19 +4,19 @@ use super::Channel; use super::ClientTlsConfig; #[cfg(feature = "tls")] use crate::transport::service::TlsConnector; -use crate::transport::Error; +use crate::transport::{service::SharedExec, Error, Executor}; use bytes::Bytes; -use http::{ - uri::{InvalidUri, Uri}, - HeaderValue, -}; +use http::{uri::Uri, HeaderValue}; use std::{ convert::{TryFrom, TryInto}, fmt, + future::Future, + pin::Pin, str::FromStr, time::Duration, }; use tower::make::MakeConnection; +// use crate::transport::E /// Channel builder. /// @@ -40,6 +40,7 @@ pub struct Endpoint { pub(crate) http2_keep_alive_while_idle: Option, pub(crate) connect_timeout: Option, pub(crate) http2_adaptive_window: Option, + pub(crate) executor: SharedExec, } impl Endpoint { @@ -76,8 +77,8 @@ impl Endpoint { /// # use tonic::transport::Endpoint; /// Endpoint::from_shared("https://example.com".to_string()); /// ``` - pub fn from_shared(s: impl Into) -> Result { - let uri = Uri::from_maybe_shared(s.into())?; + pub fn from_shared(s: impl Into) -> Result { + let uri = Uri::from_maybe_shared(s.into()).map_err(|e| Error::new_invalid_uri().with(e))?; Ok(Self::from(uri)) } @@ -266,6 +267,17 @@ impl Endpoint { } } + /// 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 + } + /// Create a channel from this config. pub async fn connect(&self) -> Result { let mut http = hyper::client::connect::HttpConnector::new(); @@ -349,7 +361,7 @@ impl Endpoint { /// /// 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) -> Result + pub fn connect_with_connector_lazy(&self, connector: C) -> Channel where C: MakeConnection + Send + 'static, C::Connection: Unpin + Send + 'static, @@ -362,7 +374,7 @@ impl Endpoint { #[cfg(not(feature = "tls"))] let connector = service::connector(connector); - Ok(Channel::new(connector, self.clone())) + Channel::new(connector, self.clone()) } /// Get the endpoint uri. @@ -399,12 +411,13 @@ impl From for Endpoint { http2_keep_alive_while_idle: None, connect_timeout: None, http2_adaptive_window: None, + executor: SharedExec::tokio(), } } } impl TryFrom for Endpoint { - type Error = InvalidUri; + type Error = Error; fn try_from(t: Bytes) -> Result { Self::from_shared(t) @@ -412,7 +425,7 @@ impl TryFrom for Endpoint { } impl TryFrom for Endpoint { - type Error = InvalidUri; + type Error = Error; fn try_from(t: String) -> Result { Self::from_shared(t.into_bytes()) @@ -420,7 +433,7 @@ impl TryFrom for Endpoint { } impl TryFrom<&'static str> for Endpoint { - type Error = InvalidUri; + type Error = Error; fn try_from(t: &'static str) -> Result { Self::from_shared(t.as_bytes()) @@ -434,7 +447,7 @@ impl fmt::Debug for Endpoint { } impl FromStr for Endpoint { - type Err = InvalidUri; + type Err = Error; fn from_str(s: &str) -> Result { Self::try_from(s.to_string()) diff --git a/tonic/src/transport/channel/mod.rs b/tonic/src/transport/channel/mod.rs index af8cfeebf..9254cf022 100644 --- a/tonic/src/transport/channel/mod.rs +++ b/tonic/src/transport/channel/mod.rs @@ -9,8 +9,9 @@ pub use endpoint::Endpoint; #[cfg(feature = "tls")] pub use tls::ClientTlsConfig; -use super::service::{Connection, DynamicServiceStream}; +use super::service::{Connection, DynamicServiceStream, SharedExec}; use crate::body::BoxBody; +use crate::transport::Executor; use bytes::Bytes; use http::{ uri::{InvalidUri, Uri}, @@ -106,7 +107,7 @@ impl Channel { /// Balance a list of [`Endpoint`]'s. /// - /// This creates a [`Channel`] that will load balance accross all the + /// 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); @@ -124,10 +125,26 @@ impl Channel { 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), tx) + (Self::balance(list, DEFAULT_BUFFER_SIZE, executor), tx) } pub(crate) fn new(connector: C, endpoint: Endpoint) -> Self @@ -138,9 +155,11 @@ impl Channel { C::Response: AsyncRead + AsyncWrite + HyperConnection + 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 = Buffer::new(Either::A(svc), buffer_size); + let (svc, worker) = Buffer::pair(Either::A(svc), buffer_size); + executor.execute(Box::pin(worker)); Channel { svc } } @@ -153,25 +172,29 @@ impl Channel { C::Response: AsyncRead + AsyncWrite + HyperConnection + 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 = Buffer::new(Either::A(svc), buffer_size); + let (svc, worker) = Buffer::pair(Either::A(svc), buffer_size); + executor.execute(Box::pin(worker)); Ok(Channel { svc }) } - pub(crate) fn balance(discover: D, buffer_size: usize) -> Self + 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 = Buffer::new(Either::B(svc), buffer_size); + let (svc, worker) = Buffer::pair(Either::B(svc), buffer_size); + executor.execute(Box::pin(worker)); Channel { svc } } diff --git a/tonic/src/transport/mod.rs b/tonic/src/transport/mod.rs index 789e531a6..e83b4c424 100644 --- a/tonic/src/transport/mod.rs +++ b/tonic/src/transport/mod.rs @@ -94,15 +94,19 @@ mod service; mod tls; #[doc(inline)] +#[cfg(feature = "channel")] +#[cfg_attr(docsrs, doc(cfg(feature = "channel")))] pub use self::channel::{Channel, Endpoint}; pub use self::error::Error; #[doc(inline)] pub use self::server::{NamedService, Server}; #[doc(inline)] -pub use self::service::TimeoutExpired; +pub use self::service::grpc_timeout::TimeoutExpired; pub use self::tls::Certificate; pub use hyper::{Body, Uri}; +pub(crate) use self::service::executor::Executor; + #[cfg(feature = "tls")] #[cfg_attr(docsrs, doc(cfg(feature = "tls")))] pub use self::channel::ClientTlsConfig; diff --git a/tonic/src/transport/server/conn.rs b/tonic/src/transport/server/conn.rs index 53bd47c31..40a303d93 100644 --- a/tonic/src/transport/server/conn.rs +++ b/tonic/src/transport/server/conn.rs @@ -116,10 +116,7 @@ where let inner = inner.connect_info(); let certs = if let Some(certs) = session.peer_certificates() { - let certs = certs - .into_iter() - .map(|c| Certificate::from_pem(c)) - .collect(); + let certs = certs.iter().map(Certificate::from_pem).collect(); Some(Arc::new(certs)) } else { None diff --git a/tonic/src/transport/server/mod.rs b/tonic/src/transport/server/mod.rs index adba6f896..aa2db0d42 100644 --- a/tonic/src/transport/server/mod.rs +++ b/tonic/src/transport/server/mod.rs @@ -6,7 +6,10 @@ mod recover_error; #[cfg(feature = "tls")] #[cfg_attr(docsrs, doc(cfg(feature = "tls")))] mod tls; +#[cfg(unix)] +mod unix; +pub use super::service::Routes; pub use conn::{Connected, TcpConnectInfo}; #[cfg(feature = "tls")] pub use tls::ServerTlsConfig; @@ -17,6 +20,9 @@ pub use conn::TlsConnectInfo; #[cfg(feature = "tls")] use super::service::TlsAcceptor; +#[cfg(unix)] +pub use unix::UdsConnectInfo; + use incoming::TcpIncoming; #[cfg(feature = "tls")] @@ -26,19 +32,17 @@ pub(crate) use tokio_rustls::server::TlsStream; use crate::transport::Error; use self::recover_error::RecoverError; -use super::service::{GrpcTimeout, Or, Routes, ServerIo}; +use super::service::{GrpcTimeout, ServerIo}; use crate::body::BoxBody; use bytes::Bytes; use futures_core::Stream; -use futures_util::{ - future::{self, MapErr}, - ready, TryFutureExt, -}; +use futures_util::{future, ready}; use http::{Request, Response}; use http_body::Body as _; use hyper::{server::accept, Body}; use pin_project::pin_project; use std::{ + convert::Infallible, fmt, future::Future, marker::PhantomData, @@ -50,7 +54,10 @@ use std::{ }; use tokio::io::{AsyncRead, AsyncWrite}; use tower::{ - layer::util::Identity, layer::Layer, limit::concurrency::ConcurrencyLimitLayer, util::Either, + layer::util::{Identity, Stack}, + layer::Layer, + limit::concurrency::ConcurrencyLimitLayer, + util::Either, Service, ServiceBuilder, }; @@ -68,7 +75,7 @@ const DEFAULT_HTTP2_KEEPALIVE_TIMEOUT_SECS: u64 = 20; /// 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(Default, Clone)] +#[derive(Clone)] pub struct Server { trace_interceptor: Option, concurrency_limit: Option, @@ -84,46 +91,36 @@ pub struct Server { http2_keepalive_timeout: Option, max_frame_size: Option, accept_http1: bool, - layer: L, + service_builder: ServiceBuilder, +} + +impl Default for Server { + fn default() -> Self { + Self { + trace_interceptor: None, + concurrency_limit: None, + timeout: None, + #[cfg(feature = "tls")] + tls: None, + init_stream_window_size: None, + init_connection_window_size: None, + max_concurrent_streams: None, + tcp_keepalive: None, + tcp_nodelay: false, + http2_keepalive_interval: None, + http2_keepalive_timeout: None, + max_frame_size: None, + accept_http1: false, + service_builder: Default::default(), + } + } } /// A stack based `Service` router. #[derive(Debug)] -pub struct Router { +pub struct Router { server: Server, - routes: Routes>, -} - -/// A service that is produced from a Tonic `Router`. -/// -/// This service implementation will route between multiple Tonic -/// gRPC endpoints and can be consumed with the rest of the `tower` -/// ecosystem. -#[derive(Debug, Clone)] -pub struct RouterService { - inner: S, -} - -impl Service> for RouterService -where - S: Service, Response = Response> + Clone + Send + 'static, - S::Future: Send + 'static, - S::Error: Into + Send, -{ - type Response = Response; - type Error = crate::Error; - - #[allow(clippy::type_complexity)] - type Future = MapErr crate::Error>; - - #[inline] - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Request) -> Self::Future { - self.inner.call(req).map_err(Into::into) - } + routes: Routes, } /// A trait to provide a static reference to the service's @@ -171,6 +168,7 @@ impl Server { /// # 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), @@ -186,12 +184,15 @@ impl Server { /// # use tonic::transport::Server; /// # use tower_service::Service; /// # use std::time::Duration; - /// # let mut builder = Server::builder(); + /// # let builder = Server::builder(); /// builder.timeout(Duration::from_secs(30)); /// ``` - pub fn timeout(&mut self, timeout: Duration) -> &mut Self { - self.timeout = Some(timeout); - self + #[must_use] + pub fn timeout(self, timeout: Duration) -> Self { + Server { + timeout: Some(timeout), + ..self + } } /// Sets the [`SETTINGS_INITIAL_WINDOW_SIZE`][spec] option for HTTP2 @@ -200,6 +201,7 @@ impl Server { /// Default is 65,535 /// /// [spec]: https://http2.github.io/http2-spec/#SETTINGS_INITIAL_WINDOW_SIZE + #[must_use] pub fn initial_stream_window_size(self, sz: impl Into>) -> Self { Server { init_stream_window_size: sz.into(), @@ -210,6 +212,7 @@ impl Server { /// 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(), @@ -223,6 +226,7 @@ impl Server { /// Default is no limit (`None`). /// /// [spec]: https://http2.github.io/http2-spec/#SETTINGS_MAX_CONCURRENT_STREAMS + #[must_use] pub fn max_concurrent_streams(self, max: impl Into>) -> Self { Server { max_concurrent_streams: max.into(), @@ -239,6 +243,7 @@ impl Server { /// /// Default is no HTTP2 keepalive (`None`) /// + #[must_use] pub fn http2_keepalive_interval(self, http2_keepalive_interval: Option) -> Self { Server { http2_keepalive_interval, @@ -253,6 +258,7 @@ impl Server { /// /// Default is 20 seconds. /// + #[must_use] pub fn http2_keepalive_timeout(self, http2_keepalive_timeout: Option) -> Self { Server { http2_keepalive_timeout, @@ -268,6 +274,7 @@ impl Server { /// /// Default is no keepalive (`None`) /// + #[must_use] pub fn tcp_keepalive(self, tcp_keepalive: Option) -> Self { Server { tcp_keepalive, @@ -276,6 +283,7 @@ impl Server { } /// Set the value of `TCP_NODELAY` option for accepted connections. Enabled by default. + #[must_use] pub fn tcp_nodelay(self, enabled: bool) -> Self { Server { tcp_nodelay: enabled, @@ -288,6 +296,7 @@ impl Server { /// 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(), @@ -303,6 +312,7 @@ impl Server { /// return confusing (but correct) protocol errors. /// /// Default is `false`. + #[must_use] pub fn accept_http1(self, accept_http1: bool) -> Self { Server { accept_http1, @@ -311,6 +321,7 @@ impl Server { } /// 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, @@ -325,18 +336,17 @@ impl Server { /// /// This will clone the `Server` builder and create a router that will /// route around different services. - pub fn add_service(&mut self, svc: S) -> Router + pub fn add_service(&mut self, svc: S) -> Router where - S: Service, Response = Response> + S: Service, Response = Response, Error = Infallible> + NamedService + Clone + Send + 'static, S::Future: Send + 'static, - S::Error: Into + Send, L: Clone, { - Router::new(self.clone(), svc) + Router::new(self.clone(), Routes::new(svc)) } /// Create a router with the optional `S` typed service as the first service. @@ -347,25 +357,18 @@ impl Server { /// # 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, - ) -> Router, Unimplemented, L> + pub fn add_optional_service(&mut self, svc: Option) -> Router where - S: Service, Response = Response> + S: Service, Response = Response, Error = Infallible> + NamedService + Clone + Send + 'static, S::Future: Send + 'static, - S::Error: Into + Send, L: Clone, { - let svc = match svc { - Some(some) => Either::A(some), - None => Either::B(Unimplemented::default()), - }; - Router::new(self.clone(), svc) + let routes = svc.map(Routes::new).unwrap_or_default(); + Router::new(self.clone(), routes) } /// Set the [Tower] [`Layer`] all services will be wrapped in. @@ -429,9 +432,9 @@ impl Server { /// [eco]: https://github.com/tower-rs /// [`ServiceBuilder`]: tower::ServiceBuilder /// [interceptors]: crate::service::Interceptor - pub fn layer(self, new_layer: NewLayer) -> Server { + pub fn layer(self, new_layer: NewLayer) -> Server> { Server { - layer: new_layer, + service_builder: self.service_builder.layer(new_layer), trace_interceptor: self.trace_interceptor, concurrency_limit: self.concurrency_limit, timeout: self.timeout, @@ -482,7 +485,7 @@ impl Server { .http2_keepalive_timeout .unwrap_or_else(|| Duration::new(DEFAULT_HTTP2_KEEPALIVE_TIMEOUT_SECS, 0)); - let svc = self.layer.layer(svc); + let svc = self.service_builder.service(svc); let tcp = incoming::tcp_incoming(incoming, self); let incoming = accept::from_stream::<_, _, crate::Error>(tcp); @@ -518,63 +521,25 @@ impl Server { } } -impl Router { - pub(crate) fn new(server: Server, svc: S) -> Self - where - S: Service, Response = Response> - + NamedService - + Clone - + Send - + 'static, - S::Future: Send + 'static, - S::Error: Into + Send, - { - let svc_name = ::NAME; - let svc_route = format!("/{}", svc_name); - let pred = move |req: &Request| { - let path = req.uri().path(); - - path.starts_with(&svc_route) - }; - Self { - server, - routes: Routes::new(pred, svc, Unimplemented::default()), - } +impl Router { + pub(crate) fn new(server: Server, routes: Routes) -> Self { + Self { server, routes } } } -impl Router -where - A: Service, Response = Response> + Clone + Send + 'static, - A::Future: Send + 'static, - A::Error: Into + Send, - B: Service, Response = Response> + Clone + Send + 'static, - B::Future: Send + 'static, - B::Error: Into + Send, -{ +impl Router { /// Add a new service to this router. - pub fn add_service(self, svc: S) -> Router>, L> + pub fn add_service(mut self, svc: S) -> Self where - S: Service, Response = Response> + S: Service, Response = Response, Error = Infallible> + NamedService + Clone + Send + 'static, S::Future: Send + 'static, - S::Error: Into + Send, { - let Self { routes, server } = self; - - let svc_name = ::NAME; - let svc_route = format!("/{}", svc_name); - let pred = move |req: &Request| { - let path = req.uri().path(); - - path.starts_with(&svc_route) - }; - let routes = routes.push(pred, svc); - - Router { server, routes } + self.routes = self.routes.add_service(svc); + self } /// Add a new optional service to this router. @@ -583,35 +548,19 @@ where /// 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. #[allow(clippy::type_complexity)] - pub fn add_optional_service( - self, - svc: Option, - ) -> Router, Or>, L> + pub fn add_optional_service(mut self, svc: Option) -> Self where - S: Service, Response = Response> + S: Service, Response = Response, Error = Infallible> + NamedService + Clone + Send + 'static, S::Future: Send + 'static, - S::Error: Into + Send, { - let Self { routes, server } = self; - - let svc_name = ::NAME; - let svc_route = format!("/{}", svc_name); - let pred = move |req: &Request| { - let path = req.uri().path(); - - path.starts_with(&svc_route) - }; - let svc = match svc { - Some(some) => Either::A(some), - None => Either::B(Unimplemented::default()), - }; - let routes = routes.push(pred, svc); - - Router { server, routes } + if let Some(svc) = svc { + self.routes = self.routes.add_service(svc); + } + self } /// Consume this [`Server`] creating a future that will execute the server @@ -621,12 +570,10 @@ where /// [tokio]: https://docs.rs/tokio pub async fn serve(self, addr: SocketAddr) -> Result<(), super::Error> where - L: Layer>>, + L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, - <>>>::Service as Service>>::Future: - Send + 'static, - <>>>::Service as Service>>::Error: - Into + Send, + <>::Service as Service>>::Future: Send + 'static, + <>::Service as Service>>::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { @@ -653,12 +600,10 @@ where signal: F, ) -> Result<(), super::Error> where - L: Layer>>, + L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, - <>>>::Service as Service>>::Future: - Send + 'static, - <>>>::Service as Service>>::Error: - Into + Send, + <>::Service as Service>>::Future: Send + 'static, + <>::Service as Service>>::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { @@ -682,12 +627,10 @@ where IO: AsyncRead + AsyncWrite + Connected + Unpin + Send + 'static, IO::ConnectInfo: Clone + Send + Sync + 'static, IE: Into, - L: Layer>>, + L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, - <>>>::Service as Service>>::Future: - Send + 'static, - <>>>::Service as Service>>::Error: - Into + Send, + <>::Service as Service>>::Future: Send + 'static, + <>::Service as Service>>::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { @@ -717,12 +660,10 @@ where IO::ConnectInfo: Clone + Send + Sync + 'static, IE: Into, F: Future, - L: Layer>>, + L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, - <>>>::Service as Service>>::Future: - Send + 'static, - <>>>::Service as Service>>::Error: - Into + Send, + <>::Service as Service>>::Future: Send + 'static, + <>::Service as Service>>::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { @@ -732,19 +673,16 @@ where } /// Create a tower service out of a router. - pub fn into_service(self) -> RouterService + pub fn into_service(self) -> L::Service where - L: Layer>>, + L: Layer, L::Service: Service, Response = Response> + Clone + Send + 'static, - <>>>::Service as Service>>::Future: - Send + 'static, - <>>>::Service as Service>>::Error: - Into + Send, + <>::Service as Service>>::Future: Send + 'static, + <>::Service as Service>>::Error: Into + Send, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into, { - let inner = self.server.layer.layer(self.routes); - RouterService { inner } + self.server.service_builder.service(self.routes) } } @@ -900,30 +838,3 @@ where future::ready(Ok(svc)) } } - -#[derive(Default, Clone, Debug)] -#[doc(hidden)] -pub struct Unimplemented { - _p: (), -} - -impl Service> for Unimplemented { - type Response = Response; - type Error = crate::Error; - type Future = future::Ready>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Ok(()).into() - } - - fn call(&mut self, _req: Request) -> Self::Future { - future::ok( - http::Response::builder() - .status(200) - .header("grpc-status", "12") - .header("content-type", "application/grpc") - .body(crate::body::empty_body()) - .unwrap(), - ) - } -} diff --git a/tonic/src/transport/server/unix.rs b/tonic/src/transport/server/unix.rs new file mode 100644 index 000000000..31454b7d1 --- /dev/null +++ b/tonic/src/transport/server/unix.rs @@ -0,0 +1,31 @@ +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 +/// [Connected]: crate::transport::server::Connected +#[cfg_attr(docsrs, doc(cfg(unix)))] +#[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(), + } + } +} diff --git a/tonic/src/transport/service/connection.rs b/tonic/src/transport/service/connection.rs index f321f3402..3aee2681c 100644 --- a/tonic/src/transport/service/connection.rs +++ b/tonic/src/transport/service/connection.rs @@ -39,6 +39,7 @@ impl Connection { .http2_initial_connection_window_size(endpoint.init_connection_window_size) .http2_only(true) .http2_keep_alive_interval(endpoint.http2_keep_alive_interval) + .executor(endpoint.executor.clone()) .clone(); if let Some(val) = endpoint.http2_keep_alive_timeout { diff --git a/tonic/src/transport/service/connector.rs b/tonic/src/transport/service/connector.rs index d0625ef0b..9d49d3ec7 100644 --- a/tonic/src/transport/service/connector.rs +++ b/tonic/src/transport/service/connector.rs @@ -5,6 +5,7 @@ use super::tls::TlsConnector; use http::Uri; #[cfg(feature = "tls-roots-common")] use std::convert::TryInto; +use std::fmt; use std::task::{Context, Poll}; use tower::make::MakeConnection; use tower_service::Service; @@ -78,6 +79,8 @@ where #[cfg(feature = "tls-roots-common")] let tls = self.tls_or_default(uri.scheme_str(), uri.host()); + #[cfg(feature = "tls")] + let is_https = uri.scheme_str() == Some("https"); let connect = self.inner.make_connection(uri); Box::pin(async move { @@ -88,6 +91,8 @@ where if let Some(tls) = tls { let conn = tls.connect(io).await?; return Ok(BoxedIo::new(conn)); + } else if is_https { + return Err(HttpsUriWithoutTlsSupport(()).into()); } } @@ -95,3 +100,16 @@ where }) } } + +/// Error returned when trying to connect to an HTTPS endpoint without TLS enabled. +#[derive(Debug)] +pub(crate) struct HttpsUriWithoutTlsSupport(()); + +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 +impl std::error::Error for HttpsUriWithoutTlsSupport {} diff --git a/tonic/src/transport/service/executor.rs b/tonic/src/transport/service/executor.rs new file mode 100644 index 000000000..d9706b55f --- /dev/null +++ b/tonic/src/transport/service/executor.rs @@ -0,0 +1,43 @@ +use futures_core::future::BoxFuture; +use std::{future::Future, sync::Arc}; + +pub(crate) use hyper::rt::Executor; + +#[derive(Copy, Clone)] +struct TokioExec; + +impl Executor for TokioExec +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + fn execute(&self, fut: F) { + tokio::spawn(fut); + } +} + +#[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(TokioExec) + } +} + +impl Executor> for SharedExec { + fn execute(&self, fut: BoxFuture<'static, ()>) { + self.inner.execute(fut) + } +} diff --git a/tonic/src/transport/service/mod.rs b/tonic/src/transport/service/mod.rs index 4e1d89c0c..355aadf09 100644 --- a/tonic/src/transport/service/mod.rs +++ b/tonic/src/transport/service/mod.rs @@ -2,7 +2,8 @@ mod add_origin; mod connection; mod connector; mod discover; -mod grpc_timeout; +pub(crate) mod executor; +pub(crate) mod grpc_timeout; mod io; mod reconnect; mod router; @@ -14,11 +15,11 @@ pub(crate) use self::add_origin::AddOrigin; pub(crate) use self::connection::Connection; pub(crate) use self::connector::connector; pub(crate) use self::discover::DynamicServiceStream; +pub(crate) use self::executor::SharedExec; pub(crate) use self::grpc_timeout::GrpcTimeout; pub(crate) use self::io::ServerIo; -pub(crate) use self::router::{Or, Routes}; #[cfg(feature = "tls")] pub(crate) use self::tls::{TlsAcceptor, TlsConnector}; pub(crate) use self::user_agent::UserAgent; -pub use self::grpc_timeout::TimeoutExpired; +pub use self::router::Routes; diff --git a/tonic/src/transport/service/router.rs b/tonic/src/transport/service/router.rs index 2eff8cafe..5f5e10d18 100644 --- a/tonic/src/transport/service/router.rs +++ b/tonic/src/transport/service/router.rs @@ -1,132 +1,95 @@ -use futures_util::{ - future::Either, - future::{MapErr, TryFutureExt}, +use crate::{ + body::{boxed, BoxBody}, + transport::NamedService, }; +use axum::handler::Handler; +use http::{Request, Response}; +use hyper::Body; +use pin_project::pin_project; use std::{ + convert::Infallible, fmt, - sync::Arc, + future::Future, + pin::Pin, task::{Context, Poll}, }; +use tower::ServiceExt; use tower_service::Service; -#[doc(hidden)] -#[derive(Debug)] -pub struct Routes { - routes: Or, +/// A [`Service`] router. +#[derive(Debug, Default, Clone)] +pub struct Routes { + router: axum::Router, } -impl Routes { - pub(crate) fn new( - predicate: impl Fn(&Request) -> bool + Send + Sync + 'static, - a: A, - b: B, - ) -> Self { - let routes = Or::new(predicate, a, b); - Self { routes } +impl Routes { + pub(crate) fn new(svc: S) -> Self + where + S: Service, Response = Response, Error = Infallible> + + NamedService + + Clone + + Send + + 'static, + S::Future: Send + 'static, + S::Error: Into + Send, + { + let router = axum::Router::new().fallback(unimplemented.into_service()); + Self { router }.add_service(svc) } -} -impl Routes { - pub(crate) fn push( - self, - predicate: impl Fn(&Request) -> bool + Send + Sync + 'static, - route: C, - ) -> Routes, Request> { - let routes = Or::new(predicate, route, self.routes); - Routes { routes } + pub(crate) fn add_service(mut self, svc: S) -> Self + where + S: Service, Response = Response, Error = Infallible> + + NamedService + + Clone + + Send + + 'static, + S::Future: Send + 'static, + S::Error: Into + Send, + { + let svc = svc.map_response(|res| res.map(axum::body::boxed)); + self.router = self.router.route(&format!("/{}/*rest", S::NAME), svc); + self } } -impl Service for Routes -where - A: Service, - A::Future: Send + 'static, - A::Error: Into, - B: Service, - B::Future: Send + 'static, - B::Error: Into, -{ - type Response = A::Response; - type Error = crate::Error; - type Future = as Service>::Future; +async fn unimplemented() -> impl axum::response::IntoResponse { + let status = http::StatusCode::OK; + let headers = [("grpc-status", "12"), ("content-type", "application/grpc")]; + (status, headers) +} - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Ok(()).into() - } +impl Service> for Routes { + type Response = Response; + type Error = crate::Error; + type Future = RoutesFuture; - fn call(&mut self, req: Request) -> Self::Future { - self.routes.call(req) + #[inline] + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) } -} -impl Clone for Routes { - fn clone(&self) -> Self { - Self { - routes: self.routes.clone(), - } + fn call(&mut self, req: Request) -> Self::Future { + RoutesFuture(self.router.call(req)) } } -#[doc(hidden)] -pub struct Or { - predicate: Arc bool + Send + Sync + 'static>, - a: A, - b: B, -} +#[pin_project] +pub struct RoutesFuture(#[pin] axum::routing::future::RouteFuture); -impl Or { - pub(crate) fn new(predicate: F, a: A, b: B) -> Self - where - F: Fn(&Request) -> bool + Send + Sync + 'static, - { - let predicate = Arc::new(predicate); - Self { predicate, a, b } +impl fmt::Debug for RoutesFuture { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("RoutesFuture").finish() } } -impl Service for Or -where - A: Service, - A::Future: Send + 'static, - A::Error: Into, - B: Service, - B::Future: Send + 'static, - B::Error: Into, -{ - type Response = A::Response; - type Error = crate::Error; - - #[allow(clippy::type_complexity)] - type Future = Either< - MapErr crate::Error>, - MapErr crate::Error>, - >; +impl Future for RoutesFuture { + type Output = Result, crate::Error>; - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Ok(()).into() - } - - fn call(&mut self, req: Request) -> Self::Future { - if (self.predicate)(&req) { - Either::Left(self.a.call(req).map_err(|e| e.into())) - } else { - Either::Right(self.b.call(req).map_err(|e| e.into())) + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match futures_util::ready!(self.project().0.poll(cx)) { + Ok(res) => Ok(res.map(boxed)).into(), + Err(err) => match err {}, } } } - -impl Clone for Or { - fn clone(&self) -> Self { - Self { - predicate: self.predicate.clone(), - a: self.a.clone(), - b: self.b.clone(), - } - } -} - -impl fmt::Debug for Or { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Or {{ .. }}") - } -} From 885e45708d9c5d615a01907f59739878d8ec4f90 Mon Sep 17 00:00:00 2001 From: Joe Dahlquist Date: Tue, 10 May 2022 14:47:03 -0700 Subject: [PATCH 3/3] fix fmt issues --- examples/src/tower/server.rs | 8 +- tests/integration_tests/tests/extensions.rs | 8 +- tonic/src/service/interceptor.rs | 130 ++++++++++---------- 3 files changed, 73 insertions(+), 73 deletions(-) diff --git a/examples/src/tower/server.rs b/examples/src/tower/server.rs index cb5f57aa5..d1e27cf30 100644 --- a/examples/src/tower/server.rs +++ b/examples/src/tower/server.rs @@ -88,9 +88,9 @@ struct MyMiddleware { } impl Service> for MyMiddleware - where - S: Service, Response = hyper::Response> + Clone + Send + 'static, - S::Future: Send + 'static, +where + S: Service, Response = hyper::Response> + Clone + Send + 'static, + S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; @@ -114,4 +114,4 @@ impl Service> for MyMiddleware Ok(response) }) } -} \ No newline at end of file +} diff --git a/tests/integration_tests/tests/extensions.rs b/tests/integration_tests/tests/extensions.rs index 12191c946..e41f87d02 100644 --- a/tests/integration_tests/tests/extensions.rs +++ b/tests/integration_tests/tests/extensions.rs @@ -205,13 +205,13 @@ struct InterceptedService { } impl Service> for InterceptedService - where - S: Service, Response = HyperResponse> +where + S: Service, Response = HyperResponse> + NamedService + Clone + Send + 'static, - S::Future: Send + 'static, + S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; @@ -236,4 +236,4 @@ impl Service> for InterceptedService impl NamedService for InterceptedService { const NAME: &'static str = S::NAME; -} \ No newline at end of file +} diff --git a/tonic/src/service/interceptor.rs b/tonic/src/service/interceptor.rs index 76552e502..96348a384 100644 --- a/tonic/src/service/interceptor.rs +++ b/tonic/src/service/interceptor.rs @@ -51,8 +51,8 @@ pub trait Interceptor { } impl Interceptor for F - where - F: FnMut(crate::Request<()>) -> Result, Status>, +where + F: FnMut(crate::Request<()>) -> Result, Status>, { fn call(&mut self, request: crate::Request<()>) -> Result, Status> { self(request) @@ -68,9 +68,9 @@ pub trait AsyncInterceptor { } impl AsyncInterceptor for F - where - F: FnMut(crate::Request<()>) -> U, - U: Future, Status>>, +where + F: FnMut(crate::Request<()>) -> U, + U: Future, Status>>, { type Future = U; @@ -83,22 +83,22 @@ impl AsyncInterceptor for F /// /// See [`Interceptor`] for more details. pub fn interceptor(f: F) -> InterceptorLayer - where - F: Interceptor, +where + F: Interceptor, { InterceptorLayer { f } } #[deprecated( -since = "0.5.1", -note = "Please use the `interceptor` function instead" + since = "0.5.1", + note = "Please use the `interceptor` function instead" )] /// Create a new interceptor layer. /// /// See [`Interceptor`] for more details. pub fn interceptor_fn(f: F) -> InterceptorLayer - where - F: Interceptor, +where + F: Interceptor, { interceptor(f) } @@ -107,8 +107,8 @@ pub fn interceptor_fn(f: F) -> InterceptorLayer /// /// See [`AsyncInterceptor`] and [`Interceptor`] for more details. pub fn async_interceptor(f: F) -> AsyncInterceptorLayer - where - F: AsyncInterceptor, +where + F: AsyncInterceptor, { AsyncInterceptorLayer { f } } @@ -123,8 +123,8 @@ pub struct InterceptorLayer { } impl Layer for InterceptorLayer - where - F: Interceptor + Clone, +where + F: Interceptor + Clone, { type Service = InterceptedService; @@ -143,9 +143,9 @@ pub struct AsyncInterceptorLayer { } impl Layer for AsyncInterceptorLayer - where - S: Clone, - F: AsyncInterceptor + Clone, +where + S: Clone, + F: AsyncInterceptor + Clone, { type Service = AsyncInterceptedService; @@ -155,8 +155,8 @@ impl Layer for AsyncInterceptorLayer } #[deprecated( -since = "0.5.1", -note = "Please use the `InterceptorLayer` type instead" + since = "0.5.1", + note = "Please use the `InterceptorLayer` type instead" )] /// A gRPC interceptor that can be used as a [`Layer`], /// created by calling [`interceptor`]. @@ -177,16 +177,16 @@ impl InterceptedService { /// Create a new `InterceptedService` that wraps `S` and intercepts each request with the /// function `F`. pub fn new(service: S, f: F) -> Self - where - F: Interceptor, + where + F: Interceptor, { Self { inner: service, f } } } impl fmt::Debug for InterceptedService - where - S: fmt::Debug, +where + S: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("InterceptedService") @@ -248,12 +248,12 @@ fn recompose( } impl Service> for InterceptedService - where - F: Interceptor, - S: Service, Response = http::Response>, - S::Error: Into, - ResBody: Default + http_body::Body + Send + 'static, - ResBody::Error: Into, +where + F: Interceptor, + S: Service, Response = http::Response>, + S::Error: Into, + ResBody: Default + http_body::Body + Send + 'static, + ResBody::Error: Into, { type Response = http::Response; type Error = S::Error; @@ -281,8 +281,8 @@ impl Service> for InterceptedServ // required to use `InterceptedService` with `Router` #[cfg(feature = "transport")] impl crate::transport::NamedService for InterceptedService - where - S: crate::transport::NamedService, +where + S: crate::transport::NamedService, { const NAME: &'static str = S::NAME; } @@ -305,8 +305,8 @@ impl AsyncInterceptedService { } impl fmt::Debug for AsyncInterceptedService - where - S: fmt::Debug, +where + S: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AsyncInterceptedService") @@ -317,13 +317,13 @@ impl fmt::Debug for AsyncInterceptedService } impl Service> for AsyncInterceptedService - where - F: AsyncInterceptor + Clone, - S: Service, Response = http::Response> + Clone, - S::Error: Into, - ReqBody: Default, - ResBody: Default + http_body::Body + Send + 'static, - ResBody::Error: Into, +where + F: AsyncInterceptor + Clone, + S: Service, Response = http::Response> + Clone, + S::Error: Into, + ReqBody: Default, + ResBody: Default + http_body::Body + Send + 'static, + ResBody::Error: Into, { type Response = http::Response; type Error = S::Error; @@ -342,8 +342,8 @@ impl Service> for AsyncIntercepte // required to use `AsyncInterceptedService` with `Router` #[cfg(feature = "transport")] impl crate::transport::NamedService for AsyncInterceptedService - where - S: crate::transport::NamedService, +where + S: crate::transport::NamedService, { const NAME: &'static str = S::NAME; } @@ -378,11 +378,11 @@ enum Kind { } impl Future for ResponseFuture - where - F: Future, E>>, - E: Into, - B: Default + http_body::Body + Send + 'static, - B::Error: Into, +where + F: Future, E>>, + E: Into, + B: Default + http_body::Body + Send + 'static, + B::Error: Into, { type Output = Result, E>; @@ -418,10 +418,10 @@ enum PinnedOption { #[pin_project(project = AsyncResponseFutureProj)] #[derive(Debug)] pub struct AsyncResponseFuture - where - S: Service>, - S::Error: Into, - I: Future, Status>>, +where + S: Service>, + S::Error: Into, + I: Future, Status>>, { #[pin] interceptor_fut: PinnedOption, @@ -432,11 +432,11 @@ pub struct AsyncResponseFuture } impl AsyncResponseFuture - where - S: Service>, - S::Error: Into, - I: Future, Status>>, - ReqBody: Default, +where + S: Service>, + S::Error: Into, + I: Future, Status>>, + ReqBody: Default, { fn new>( req: http::Request, @@ -482,13 +482,13 @@ impl AsyncResponseFuture } impl Future for AsyncResponseFuture - where - S: Service, Response = http::Response>, - I: Future, Status>>, - S::Error: Into, - ReqBody: Default, - ResBody: Default + http_body::Body + Send + 'static, - ResBody::Error: Into, +where + S: Service, Response = http::Response>, + I: Future, Status>>, + S::Error: Into, + ReqBody: Default, + ResBody: Default + http_body::Body + Send + 'static, + ResBody::Error: Into, { type Output = Result, S::Error>; @@ -699,4 +699,4 @@ mod tests { svc.oneshot(request).await.unwrap(); } -} \ No newline at end of file +}