//! Extractor for getting connection information from a client. //! //! See [`RoutingDsl::into_make_service_with_connect_info`] for more details. //! //! [`RoutingDsl::into_make_service_with_connect_info`]: crate::routing::RoutingDsl::into_make_service_with_connect_info use super::{Extension, FromRequest, RequestParts}; use async_trait::async_trait; use hyper::server::conn::AddrStream; use std::{ convert::Infallible, fmt, marker::PhantomData, net::SocketAddr, task::{Context, Poll}, }; use tower::Service; use tower_http::add_extension::AddExtension; /// A [`MakeService`] created from a router. /// /// See [`RoutingDsl::into_make_service_with_connect_info`] for more details. /// /// [`MakeService`]: tower::make::MakeService /// [`RoutingDsl::into_make_service_with_connect_info`]: crate::routing::RoutingDsl::into_make_service_with_connect_info pub struct IntoMakeServiceWithConnectInfo { svc: S, _connect_info: PhantomData C>, } impl IntoMakeServiceWithConnectInfo { pub(crate) fn new(svc: S) -> Self { Self { svc, _connect_info: PhantomData, } } } impl fmt::Debug for IntoMakeServiceWithConnectInfo where S: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("IntoMakeServiceWithConnectInfo") .field("svc", &self.svc) .finish() } } /// Trait that connected IO resources implement and use to produce information /// about the connection. /// /// The goal for this trait is to allow users to implement custom IO types that /// can still provide the same connection metadata. /// /// See [`RoutingDsl::into_make_service_with_connect_info`] for more details. /// /// [`RoutingDsl::into_make_service_with_connect_info`]: crate::routing::RoutingDsl::into_make_service_with_connect_info pub trait Connected { /// The connection information type the IO resources generates. type ConnectInfo: Clone + Send + Sync + 'static; /// Create type holding information about the connection. fn connect_info(target: T) -> Self::ConnectInfo; } impl Connected<&AddrStream> for SocketAddr { type ConnectInfo = SocketAddr; fn connect_info(target: &AddrStream) -> Self::ConnectInfo { target.remote_addr() } } impl Service for IntoMakeServiceWithConnectInfo where S: Clone, C: Connected, { type Response = AddExtension>; type Error = Infallible; type Future = ResponseFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn call(&mut self, target: T) -> Self::Future { let connect_info = ConnectInfo(C::connect_info(target)); let svc = AddExtension::new(self.svc.clone(), connect_info); ResponseFuture { future: futures_util::future::ok(svc), } } } opaque_future! { /// Response future for [`IntoMakeServiceWithConnectInfo`]. pub type ResponseFuture = futures_util::future::Ready>; } /// Extractor for getting connection information produced by a [`Connected`]. /// /// Note this extractor requires you to use /// [`RoutingDsl::into_make_service_with_connect_info`] to run your app /// otherwise it will fail at runtime. /// /// See [`RoutingDsl::into_make_service_with_connect_info`] for more details. /// /// [`RoutingDsl::into_make_service_with_connect_info`]: crate::routing::RoutingDsl::into_make_service_with_connect_info #[derive(Clone, Copy, Debug)] pub struct ConnectInfo(pub T); #[async_trait] impl FromRequest for ConnectInfo where B: Send, T: Clone + Send + Sync + 'static, { type Rejection = as FromRequest>::Rejection; async fn from_request(req: &mut RequestParts) -> Result { let Extension(connect_info) = Extension::::from_request(req).await?; Ok(connect_info) } } #[cfg(test)] mod tests { use super::*; use crate::prelude::*; use crate::Server; use std::net::{SocketAddr, TcpListener}; #[tokio::test] async fn socket_addr() { async fn handler(ConnectInfo(addr): ConnectInfo) -> String { format!("{}", addr) } let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); let (tx, rx) = tokio::sync::oneshot::channel(); tokio::spawn(async move { let app = route("/", get(handler)); let server = Server::from_tcp(listener) .unwrap() .serve(app.into_make_service_with_connect_info::()); tx.send(()).unwrap(); server.await.expect("server error"); }); rx.await.unwrap(); let client = reqwest::Client::new(); let res = client.get(format!("http://{}", addr)).send().await.unwrap(); let body = res.text().await.unwrap(); assert!(body.starts_with("127.0.0.1:")); } #[tokio::test] async fn custom() { #[derive(Clone, Debug)] struct MyConnectInfo { value: &'static str, } impl Connected<&AddrStream> for MyConnectInfo { type ConnectInfo = Self; fn connect_info(_target: &AddrStream) -> Self::ConnectInfo { Self { value: "it worked!", } } } async fn handler(ConnectInfo(addr): ConnectInfo) -> &'static str { addr.value } let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); let (tx, rx) = tokio::sync::oneshot::channel(); tokio::spawn(async move { let app = route("/", get(handler)); let server = Server::from_tcp(listener) .unwrap() .serve(app.into_make_service_with_connect_info::()); tx.send(()).unwrap(); server.await.expect("server error"); }); rx.await.unwrap(); let client = reqwest::Client::new(); let res = client.get(format!("http://{}", addr)).send().await.unwrap(); let body = res.text().await.unwrap(); assert_eq!(body, "it worked!"); } }