Skip to main content

structured_proxy/
service.rs

1//! The proxy as one tower service: native gRPC to the upstream, everything
2//! else to the proxy's routes, and what no route answers to a fallback.
3
4use std::convert::Infallible;
5use std::future::Future;
6use std::net::SocketAddr;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::task::{Context, Poll};
10
11use crate::cors::{Cors, CorsFuture, CorsLayer};
12use axum::extract::connect_info::ConnectInfo;
13use axum::response::IntoResponse;
14use axum::routing::future::RouteFuture;
15use bytes::Bytes;
16use pin_project_lite::pin_project;
17use rustls::pki_types::CertificateDer;
18use tonic::transport::server::{Connected, TcpConnectInfo};
19use tower::{Layer, Service, ServiceExt};
20
21use crate::client_address::ClientAddressLayer;
22use crate::guard::{BoxedService, Class, GrpcRejections, Guards};
23use crate::transcode::{Chooser, Direct};
24
25/// tonic's `TlsConnectInfo<TcpConnectInfo>`, the record its TLS server puts on
26/// a request and `Request::peer_certs` reads. tonic exports the name only with
27/// a TLS backend feature, so it is reached through the stream it describes.
28pub type TlsConnectInfo =
29    <tokio_rustls::server::TlsStream<tokio::net::TcpStream> as Connected>::ConnectInfo;
30
31use crate::upstream::{
32    grpc_protocol, is_grpc_web_preflight, BoxError, GrpcProtocol, PassThrough, Upstream,
33};
34
35/// The proxy as a tower service, built by
36/// [`ProxyServer::service`](crate::ProxyServer::service).
37///
38/// A request with a gRPC or gRPC-Web content type goes to the upstream as it
39/// arrived, so one listener carries REST and native gRPC. gRPC-Web is the
40/// upstream's to translate (tonic-web's `GrpcWebLayer` around its services),
41/// or the proxy's with `grpc_web.translate` for an upstream that speaks only
42/// gRPC. A gRPC-Web
43/// answer gets the proxy's CORS policy (`cors.grpc_web`), since the proxy
44/// answers the browser's preflight for it. Every other request goes to the
45/// proxy's routes (transcoded RPCs, health, metrics, OpenAPI, OIDC,
46/// forward-auth, extra routes) behind the proxy's middleware. A request no
47/// route matches is answered `404`, or handed to the service set with
48/// [`with_fallback`](Self::with_fallback). Native gRPC and the fallback pass
49/// only the guards whose scope names them (`grpc`, `fallback`); a guard's
50/// rejection of a gRPC call is a gRPC status.
51///
52/// Serve it with [`serve`](crate::serve()) or
53/// [`serve_with`](crate::serve_with) (TLS, a connection limit), or hand it to
54/// any server that takes a tower service of `http` types: a Unix socket, an
55/// existing hyper or axum server. Native gRPC needs HTTP/2 on that server
56/// (ALPN `h2` next to `http/1.1` behind TLS). Such a server tells the proxy
57/// which connection a request came on with
58/// [`for_connection`](Self::for_connection).
59///
60/// # Examples
61///
62/// ```
63/// use structured_proxy::ProxyServer;
64///
65/// # fn build() -> anyhow::Result<()> {
66/// // The embedder's own tonic services, called in process.
67/// let grpc = tonic::service::Routes::default();
68/// let service = ProxyServer::from_yaml_str("service:\n  name: demo\n")?.service(grpc)?;
69/// # let _ = service;
70/// # Ok(())
71/// # }
72/// # build().unwrap();
73/// ```
74#[derive(Clone)]
75pub struct ProxyService<U> {
76    /// Shared by every clone: a server clones the service for each request
77    /// and only one path serves it.
78    shared: Arc<Shared<U>>,
79    /// The connection the requests arrive on, set per connection by the
80    /// server.
81    connection: Option<ConnectionInfo>,
82}
83
84/// What every request of a [`ProxyService`] may take a path through.
85#[derive(Clone)]
86struct Shared<U> {
87    upstream: U,
88    /// Where the requests that are not gRPC go.
89    routing: Routing,
90    /// Binary and text gRPC-Web to the upstream under the CORS policy, each
91    /// built once; `None` when the upstream owns CORS for gRPC-Web.
92    grpc_web: Option<GrpcWebCors<U>>,
93    /// The gRPC paths behind guards or through gRPC-Web translation; a
94    /// protocol that needs neither passes through with nothing in between.
95    boxed: BoxedGrpc,
96    /// The guards, for the fallback an embedder sets later.
97    guards: Arc<Guards>,
98}
99
100impl<U> std::fmt::Debug for ProxyService<U> {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct("ProxyService")
103            .field("boxed_grpc", &self.shared.boxed.grpc.is_some())
104            .field("boxed_grpc_web", &self.shared.boxed.web.is_some())
105            .field("connection", &self.connection)
106            .finish_non_exhaustive()
107    }
108}
109
110/// How the requests that are not gRPC reach the proxy's routes.
111#[derive(Clone)]
112pub(crate) enum Routing {
113    /// The transcoded routes are matched by the proxy and served past any
114    /// router; the other routes by theirs.
115    Direct {
116        direct: Direct,
117        others: axum::Router,
118    },
119    /// Every route by one router, which `chooser` (with transcoded routes)
120    /// chooses a binding after; `served` is the router with it.
121    Routed {
122        router: axum::Router,
123        chooser: Option<Chooser>,
124        served: axum::Router,
125    },
126}
127
128impl Routing {
129    /// Every route by `router`, choosing among its transcoded bindings with
130    /// `chooser`.
131    pub(crate) fn routed(router: axum::Router, chooser: Option<Chooser>) -> Self {
132        let served = match &chooser {
133            Some(chooser) => chooser.clone().layer(router.clone()),
134            None => router.clone(),
135        };
136        Self::Routed {
137            router,
138            chooser,
139            served,
140        }
141    }
142
143    /// These routes, with `fallback` answering what none does.
144    fn with_fallback(self, fallback: BoxedService) -> Self {
145        match self {
146            Self::Direct { direct, others } => Self::Direct {
147                direct,
148                others: others.fallback_service(fallback),
149            },
150            Self::Routed {
151                router, chooser, ..
152            } => Self::routed(
153                router.fallback_service(fallback.clone()),
154                chooser.map(|chooser| chooser.with_fallback(fallback)),
155            ),
156        }
157    }
158}
159
160/// The gRPC paths that need more than a pass-through (guards, gRPC-Web
161/// translation), each protocol's stack built once; `None` for a protocol that
162/// passes through as it is.
163#[derive(Clone, Default)]
164struct BoxedGrpc {
165    grpc: Option<BoxedService>,
166    web: Option<BoxedService>,
167    web_text: Option<BoxedService>,
168}
169
170/// Hands a gRPC-Web call, translated to gRPC by tonic-web, to an upstream
171/// that speaks only gRPC.
172#[derive(Clone, Debug)]
173struct Translated<U> {
174    upstream: U,
175}
176
177impl<U: Upstream> Service<http::Request<tonic::body::Body>> for Translated<U> {
178    type Response = http::Response<axum::body::Body>;
179    type Error = Infallible;
180    type Future = PassThrough<U>;
181
182    #[inline]
183    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
184        Poll::Ready(Ok(()))
185    }
186
187    fn call(&mut self, request: http::Request<tonic::body::Body>) -> Self::Future {
188        let (mut parts, body) = request.into_parts();
189        // A browser's call arrives over HTTP/1.1; as gRPC it is an HTTP/2
190        // request (gRPC PROTOCOL-HTTP2).
191        parts.version = http::Version::HTTP_2;
192        // tonic-web reports the size of the base64 text body for the decoded
193        // one, and an HTTP/2 client would announce that as its length; the
194        // gRPC body goes without one.
195        let body = tonic::body::Body::new(Unsized { body });
196        PassThrough::new(
197            self.upstream.clone(),
198            http::Request::from_parts(parts, body),
199            GrpcProtocol::Grpc,
200        )
201    }
202}
203
204pin_project! {
205    /// `body` without its size hint.
206    struct Unsized {
207        #[pin]
208        body: tonic::body::Body,
209    }
210}
211
212impl http_body::Body for Unsized {
213    type Data = Bytes;
214    type Error = tonic::Status;
215
216    #[inline]
217    fn poll_frame(
218        self: Pin<&mut Self>,
219        cx: &mut Context<'_>,
220    ) -> Poll<Option<Result<http_body::Frame<Bytes>, tonic::Status>>> {
221        self.project().body.poll_frame(cx)
222    }
223
224    #[inline]
225    fn is_end_stream(&self) -> bool {
226        self.body.is_end_stream()
227    }
228}
229
230/// gRPC-Web pass-through, one CORS-wrapped path per encoding.
231#[derive(Clone, Debug)]
232struct GrpcWebCors<U> {
233    web: Cors<Forward<U>>,
234    web_text: Cors<Forward<U>>,
235}
236
237/// Hands a request to the upstream in a known protocol, as a tower service so
238/// a layer can wrap it.
239#[derive(Clone, Debug)]
240struct Forward<U> {
241    upstream: U,
242    protocol: GrpcProtocol,
243}
244
245impl<U: Upstream> Service<http::Request<tonic::body::Body>> for Forward<U> {
246    type Response = http::Response<axum::body::Body>;
247    type Error = Infallible;
248    type Future = PassThrough<U>;
249
250    #[inline]
251    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
252        // The upstream's readiness is waited for per request, on its clone.
253        Poll::Ready(Ok(()))
254    }
255
256    fn call(&mut self, request: http::Request<tonic::body::Body>) -> Self::Future {
257        PassThrough::new(self.upstream.clone(), request, self.protocol)
258    }
259}
260
261/// The guarded path's end: the guards run on axum's request type.
262impl<U: Upstream> Service<axum::extract::Request> for Forward<U> {
263    type Response = http::Response<axum::body::Body>;
264    type Error = Infallible;
265    type Future = PassThrough<U>;
266
267    #[inline]
268    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
269        Poll::Ready(Ok(()))
270    }
271
272    fn call(&mut self, request: axum::extract::Request) -> Self::Future {
273        let request = request.map(tonic::body::Body::new);
274        PassThrough::new(self.upstream.clone(), request, self.protocol)
275    }
276}
277
278/// The connection requests arrive on, in the form a tonic server records it:
279/// the TCP ends, and behind TLS the client's certificate chain.
280///
281/// Built from what [`Connected::connect_info`] returns for a
282/// `tokio::net::TcpStream` (`ConnectionInfo::from`) or a
283/// `tokio_rustls::server::TlsStream<TcpStream>` ([`ConnectionInfo::tls`]), so a
284/// tonic handler behind the proxy reads it as it would behind tonic's own
285/// server: `Request::remote_addr`, and `Request::peer_certs` for mTLS.
286///
287/// [`Connected::connect_info`]: tonic::transport::server::Connected::connect_info
288#[derive(Clone, Debug)]
289pub struct ConnectionInfo {
290    tcp: TcpConnectInfo,
291    tls: Option<TlsConnectInfo>,
292}
293
294impl ConnectionInfo {
295    /// The client's address.
296    pub fn remote_addr(&self) -> Option<SocketAddr> {
297        self.tcp.remote_addr
298    }
299
300    /// The certificate chain the client presented over TLS, if it did.
301    pub fn peer_certs(&self) -> Option<Arc<Vec<CertificateDer<'static>>>> {
302        self.tls.as_ref().and_then(|tls| tls.peer_certs())
303    }
304
305    /// Put the connection where tonic reads it: [`TcpConnectInfo`] for
306    /// `Request::remote_addr` and, behind TLS, [`TlsConnectInfo`] for
307    /// `Request::peer_certs`.
308    pub(crate) fn into_tonic_extensions(self, extensions: &mut http::Extensions) {
309        extensions.insert(self.tcp);
310        if let Some(tls) = self.tls {
311            extensions.insert(tls);
312        }
313    }
314}
315
316impl From<TcpConnectInfo> for ConnectionInfo {
317    fn from(tcp: TcpConnectInfo) -> Self {
318        Self { tcp, tls: None }
319    }
320}
321
322impl ConnectionInfo {
323    /// A TLS connection, from what `connect_info` reports for a
324    /// `tokio_rustls::server::TlsStream<TcpStream>`: the TCP ends and the
325    /// client's certificate chain.
326    pub fn tls(tls: TlsConnectInfo) -> Self {
327        Self {
328            tcp: tls.get_ref().clone(),
329            tls: Some(tls),
330        }
331    }
332}
333
334impl<U: Upstream> ProxyService<U> {
335    /// The service over `upstream` and the routes of `routing`; gRPC-Web
336    /// answers carry `grpc_web_cors` when set, and gRPC-Web calls are
337    /// translated to gRPC when `translate_grpc_web`.
338    pub(crate) fn new(
339        upstream: U,
340        routing: Routing,
341        grpc_web_cors: Option<CorsLayer>,
342        guards: Arc<Guards>,
343        translate_grpc_web: bool,
344    ) -> Self {
345        let forward = |protocol| Forward {
346            upstream: upstream.clone(),
347            protocol,
348        };
349        let guard_grpc = guards.cover(Class::Grpc);
350        let stack = |protocol: GrpcProtocol| {
351            let web = protocol != GrpcProtocol::Grpc;
352            let mut service = if web && translate_grpc_web {
353                let translator = tonic_web::GrpcWebLayer::new().layer(Translated {
354                    upstream: upstream.clone(),
355                });
356                BoxedService::new(ServiceExt::<axum::extract::Request>::map_response(
357                    translator,
358                    |response| response.map(axum::body::Body::new),
359                ))
360            } else {
361                BoxedService::new(forward(protocol))
362            };
363            if guard_grpc {
364                service = BoxedService::new(GrpcRejections {
365                    inner: guards.service(service, Class::Grpc),
366                    protocol,
367                });
368            }
369            // CORS outermost, so a rejected gRPC-Web call still carries it.
370            match &grpc_web_cors {
371                Some(cors) if web => BoxedService::new(cors.layer(service)),
372                _ => service,
373            }
374        };
375        let boxed_web = guard_grpc || translate_grpc_web;
376        let boxed = BoxedGrpc {
377            grpc: guard_grpc.then(|| stack(GrpcProtocol::Grpc)),
378            web: boxed_web.then(|| stack(GrpcProtocol::Web)),
379            web_text: boxed_web.then(|| stack(GrpcProtocol::WebText)),
380        };
381        // The pass-through of gRPC-Web, and the answer to its preflights.
382        let grpc_web = grpc_web_cors.map(|cors| GrpcWebCors {
383            web: cors.layer(forward(GrpcProtocol::Web)),
384            web_text: cors.layer(forward(GrpcProtocol::WebText)),
385        });
386        Self {
387            shared: Arc::new(Shared {
388                upstream,
389                routing,
390                grpc_web,
391                boxed,
392                guards,
393            }),
394            connection: None,
395        }
396    }
397
398    /// Hand the requests no route matches to `fallback` instead of answering
399    /// `404`: an embedder's own REST routes, a static site, anything that is a
400    /// tower service. Only the guards whose scope names `fallback` traffic see
401    /// them; CORS and tracing are the fallback's own.
402    ///
403    /// A URL whose path a transcoded template's route matches, but which no
404    /// binding answers (its field template or custom verb does not fit), is
405    /// the fallback's too. A request whose path a route answers but not with
406    /// its method stays with the proxy (`405`), as do every gRPC request and
407    /// every browser preflight for a gRPC-Web call, which follows the call it
408    /// announces.
409    #[must_use]
410    pub fn with_fallback<F>(mut self, fallback: F) -> Self
411    where
412        F: Service<axum::extract::Request, Error = Infallible> + Clone + Send + Sync + 'static,
413        F::Response: IntoResponse,
414        F::Future: Send + 'static,
415    {
416        let shared = Arc::make_mut(&mut self.shared);
417        let fallback = BoxedService::new(fallback.map_response(IntoResponse::into_response));
418        let guarded = if shared.guards.cover(Class::Fallback) {
419            shared.guards.service(fallback, Class::Fallback)
420        } else {
421            fallback
422        };
423        // The routes' own layers do not reach a fallback set after them, so
424        // it gets the client-address resolution of its own, around its guards.
425        // A URL no transcoded binding answers goes to the other routes, which
426        // hand what they do not answer to the same fallback.
427        let resolve = ClientAddressLayer::with(shared.guards.client_address.clone());
428        let fallback = BoxedService::new(resolve.layer(guarded));
429        shared.routing = shared.routing.clone().with_fallback(fallback);
430        self
431    }
432
433    /// This service for the requests of one connection: the proxy's
434    /// middleware sees its client's address (rate limits by IP, the auth
435    /// decider), and a tonic upstream in process reads it with
436    /// `Request::remote_addr`, and its TLS client certificates with
437    /// `Request::peer_certs`, for native and transcoded calls alike.
438    ///
439    /// A server of your own calls it once per accepted connection, with what
440    /// tonic's [`Connected`] trait
441    /// reports for the stream. [`serve_with`](crate::serve_with) does this
442    /// itself.
443    ///
444    /// # Examples
445    ///
446    /// ```no_run
447    /// use structured_proxy::ProxyServer;
448    /// use tonic::transport::server::Connected;
449    ///
450    /// # async fn run() -> anyhow::Result<()> {
451    /// let proxy = ProxyServer::from_yaml_str("service:\n  name: demo\n")?
452    ///     .service(tonic::service::Routes::default())?;
453    /// let listener = tokio::net::TcpListener::bind("0.0.0.0:8443").await?;
454    /// let (tcp, _) = listener.accept().await?;
455    /// // After a TLS handshake, the `TlsStream` reports the client's certificates too.
456    /// let service = proxy.for_connection(tcp.connect_info());
457    /// # let _ = service;
458    /// # Ok(())
459    /// # }
460    /// ```
461    ///
462    /// Without it, a request's peer comes from the `ConnectInfo` an outer
463    /// axum server recorded, when there is one.
464    #[must_use]
465    pub fn for_connection(&self, connection: impl Into<ConnectionInfo>) -> Self {
466        Self {
467            shared: self.shared.clone(),
468            connection: Some(connection.into()),
469        }
470    }
471}
472
473/// The connection `request` came on: `connection`, the one given to
474/// [`ProxyService::for_connection`], else the peer an outer axum server
475/// recorded as `ConnectInfo`, so the upstream sees the same client the proxy's
476/// middleware does. A free function, so the caller keeps its other fields.
477fn connection_of<B>(
478    connection: Option<&ConnectionInfo>,
479    request: &http::Request<B>,
480) -> Option<ConnectionInfo> {
481    if let Some(connection) = connection {
482        return Some(connection.clone());
483    }
484    let ConnectInfo(remote) = request.extensions().get::<ConnectInfo<SocketAddr>>()?;
485    Some(ConnectionInfo::from(TcpConnectInfo {
486        local_addr: None,
487        remote_addr: Some(*remote),
488    }))
489}
490
491impl<U, B> Service<http::Request<B>> for ProxyService<U>
492where
493    U: Upstream,
494    B: http_body::Body<Data = Bytes> + Send + 'static,
495    B::Error: Into<BoxError>,
496{
497    type Response = http::Response<axum::body::Body>;
498    type Error = Infallible;
499    type Future = ResponseFuture<U>;
500
501    #[inline]
502    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
503        // Readiness of the upstream is waited for per request, on its clone.
504        Poll::Ready(Ok(()))
505    }
506
507    fn call(&mut self, mut request: http::Request<B>) -> Self::Future {
508        // A gRPC-Web preflight goes where the call it announces goes, so both
509        // get one CORS policy: the proxy's, or the upstream's when it owns
510        // CORS. A fallback in between would answer it with neither.
511        let call = grpc_protocol(request.headers());
512        let protocol = call.or_else(|| {
513            is_grpc_web_preflight(request.method(), request.headers()).then_some(GrpcProtocol::Web)
514        });
515        // Only the path the request takes is cloned for it.
516        let shared = &*self.shared;
517        // A preflight is not a call: it skips the guards and the translation.
518        let boxed = match call {
519            Some(GrpcProtocol::Grpc) => shared.boxed.grpc.as_ref(),
520            Some(GrpcProtocol::Web) => shared.boxed.web.as_ref(),
521            Some(GrpcProtocol::WebText) => shared.boxed.web_text.as_ref(),
522            None => None,
523        };
524        let inner = if let Some(protocol) = protocol {
525            // Whatever goes the upstream's way, guarded or not: its client
526            // address is resolved and its forwarding headers rewritten here,
527            // once, for the guards and the upstream alike. The call carries
528            // its connection the way tonic's server hands it to a handler.
529            let connection = connection_of(self.connection.as_ref(), &request);
530            let peer = connection.as_ref().and_then(ConnectionInfo::remote_addr);
531            shared.guards.client_address.apply(&mut request, peer);
532            if let Some(connection) = connection {
533                connection.into_tonic_extensions(request.extensions_mut());
534            }
535            match boxed {
536                // Every service on these paths is ready at once: guards and
537                // the translator are middleware, and `Forward` waits for the
538                // upstream per request.
539                Some(service) => Inner::Boxed {
540                    future: service.clone().call(request.map(axum::body::Body::new)),
541                },
542                None => {
543                    let request = request.map(tonic::body::Body::new);
544                    // A browser only speaks gRPC-Web, and the routes answered
545                    // its preflight: its call gets the same CORS policy.
546                    let cors = match (&shared.grpc_web, protocol) {
547                        (Some(cors), GrpcProtocol::Web) => Some(&cors.web),
548                        (Some(cors), GrpcProtocol::WebText) => Some(&cors.web_text),
549                        _ => None,
550                    };
551                    match cors {
552                        // `Forward` is always ready, and so is CORS around it.
553                        Some(cors) => Inner::GrpcWeb {
554                            future: cors.clone().call(request),
555                        },
556                        None => Inner::Grpc {
557                            call: PassThrough::new(shared.upstream.clone(), request, protocol),
558                        },
559                    }
560                }
561            }
562        } else {
563            // The middleware reads the peer as axum's `ConnectInfo`; the
564            // transcoder passes the whole connection on to the upstream.
565            if let Some(connection) = &self.connection {
566                let extensions = request.extensions_mut();
567                if let Some(remote) = connection.remote_addr() {
568                    extensions.insert(ConnectInfo(remote));
569                }
570                extensions.insert(connection.clone());
571            } else if let Some(connection) = connection_of(None, &request) {
572                request.extensions_mut().insert(connection);
573            }
574            // A router and the transcoded routes' layers are always ready.
575            // A router is a reference count.
576            match &shared.routing {
577                Routing::Direct { direct, others } => {
578                    if direct.takes(&mut request) {
579                        Inner::Boxed {
580                            future: direct
581                                .service()
582                                .clone()
583                                .call(request.map(axum::body::Body::new)),
584                        }
585                    } else {
586                        Inner::Routes {
587                            future: others.clone().call(request),
588                        }
589                    }
590                }
591                Routing::Routed { served, .. } => Inner::Routes {
592                    future: served.clone().call(request),
593                },
594            }
595        };
596        ResponseFuture { inner }
597    }
598}
599
600pin_project! {
601    /// The response future of [`ProxyService`].
602    pub struct ResponseFuture<U: Upstream> {
603        #[pin]
604        inner: Inner<U>,
605    }
606}
607
608pin_project! {
609    #[project = InnerProj]
610    enum Inner<U: Upstream> {
611        Routes {
612            #[pin]
613            future: RouteFuture<Infallible>,
614        },
615        Grpc {
616            #[pin]
617            call: PassThrough<U>,
618        },
619        GrpcWeb {
620            #[pin]
621            future: CorsFuture<PassThrough<U>, axum::body::Body>,
622        },
623        Boxed {
624            #[pin]
625            future: <BoxedService as Service<axum::extract::Request>>::Future,
626        },
627    }
628}
629
630impl<U: Upstream> Future for ResponseFuture<U> {
631    type Output = Result<http::Response<axum::body::Body>, Infallible>;
632
633    #[inline]
634    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
635        match self.project().inner.project() {
636            InnerProj::Routes { future } => future.poll(cx),
637            InnerProj::Grpc { call } => call.poll(cx),
638            InnerProj::GrpcWeb { future } => future.poll(cx),
639            InnerProj::Boxed { future } => future.poll(cx),
640        }
641    }
642}
643
644#[cfg(test)]
645mod tests;