use std::convert::Infallible;
use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use axum::body::Body;
use axum::extract::{ConnectInfo, Request};
use axum::response::Response;
use bytes::Bytes;
use futures_util::future::{BoxFuture, Either};
use http::{HeaderMap, Version};
use http_body::{Frame, SizeHint};
use tokio_util::sync::WaitForCancellationFutureOwned;
use tokio_util::task::task_tracker::TaskTrackerToken;
use tonic::transport::server::TcpConnectInfo;
use tower::{Layer, Service, ServiceExt as _};
use crate::lifecycle::Lifecycle;
use crate::server::Shared;
const GRACE_EXPIRED: &str = "gRPC shutdown grace period expired";
pub struct Target {
pub router: axum::Router,
pub timeout: Option<Duration>,
}
#[derive(Clone, Debug)]
pub struct SharedOwner(pub String);
#[derive(Clone)]
pub struct GrpcGate {
shared: Arc<Shared>,
}
impl GrpcGate {
pub const fn new(shared: Arc<Shared>) -> Self {
Self { shared }
}
}
impl<S> Layer<S> for GrpcGate {
type Service = Gate<S>;
fn layer(&self, inner: S) -> Self::Service {
Gate {
inner,
shared: self.shared.clone(),
}
}
}
#[derive(Clone)]
pub struct Gate<S> {
inner: S,
shared: Arc<Shared>,
}
impl<S> Service<Request> for Gate<S>
where
S: Service<Request, Response = Response, Error = Infallible>,
{
type Response = Response;
type Error = Infallible;
type Future = Either<S::Future, BoxFuture<'static, Result<Response, Infallible>>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: Request) -> Self::Future {
if is_grpc(&request) {
Either::Right(Box::pin(dispatch(self.shared.clone(), request)))
} else {
Either::Left(self.inner.call(request))
}
}
}
fn is_grpc(request: &Request) -> bool {
if request.version() != Version::HTTP_2 {
return false;
}
request
.headers()
.get(http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| {
let value = value.trim().to_ascii_lowercase();
value == "application/grpc"
|| value.starts_with("application/grpc+")
|| value.starts_with("application/grpc;")
})
}
async fn dispatch(shared: Arc<Shared>, mut request: Request) -> Result<Response, Infallible> {
let token = shared.calls.token();
let target = match shared.gate.get() {
Some(target) if shared.lifecycle.get() == Lifecycle::Serving => target,
_ => {
return Ok(status(tonic::Status::unavailable(
"the gRPC server is not serving",
)));
}
};
if let Some(info) = peer_info(&request) {
request.extensions_mut().insert(info);
}
let deadline = deadline(target.timeout, request.headers());
let call = target.router.clone().oneshot(request);
let call = async move {
match deadline {
Some(limit) => tokio::time::timeout(limit, call).await.ok(),
None => Some(call.await),
}
};
let kill = shared.kill.clone();
let response = tokio::select! {
biased;
() = kill.cancelled() => return Ok(status(tonic::Status::unavailable(GRACE_EXPIRED))),
response = call => match response {
Some(Ok(response)) => response,
Some(Err(never)) => match never {},
None => return Ok(status(tonic::Status::cancelled("Timeout expired"))),
},
};
Ok(response.map(|body| {
Body::new(Guarded {
inner: body,
kill: Box::pin(kill.cancelled_owned()),
done: false,
_token: token,
})
}))
}
fn peer_info(request: &Request) -> Option<TcpConnectInfo> {
let ConnectInfo(peer) = request.extensions().get::<ConnectInfo<SocketAddr>>()?;
if peer.port() == 0 {
return None;
}
Some(TcpConnectInfo {
local_addr: None,
remote_addr: Some(*peer),
})
}
fn status(status: tonic::Status) -> Response {
status.into_http()
}
fn deadline(server: Option<Duration>, headers: &HeaderMap) -> Option<Duration> {
let client = headers
.get("grpc-timeout")
.and_then(|value| value.to_str().ok())
.and_then(crate::timeout::parse);
match (server, client) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
pin_project_lite::pin_project! {
struct Guarded {
#[pin]
inner: Body,
kill: Pin<Box<WaitForCancellationFutureOwned>>,
done: bool,
_token: TaskTrackerToken,
}
}
impl http_body::Body for Guarded {
type Data = Bytes;
type Error = axum::Error;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Bytes>, axum::Error>>> {
let this = self.project();
if *this.done {
return Poll::Ready(None);
}
if this.kill.as_mut().poll(cx).is_ready() {
*this.done = true;
let status = tonic::Status::unavailable(GRACE_EXPIRED);
let mut trailers = HeaderMap::new();
let _ = status.add_header(&mut trailers);
return Poll::Ready(Some(Ok(Frame::trailers(trailers))));
}
let frame = this.inner.poll_frame(cx);
match &frame {
Poll::Ready(None) => *this.done = true,
Poll::Ready(Some(Ok(frame))) if frame.is_trailers() => *this.done = true,
_ => {}
}
frame
}
fn is_end_stream(&self) -> bool {
self.done || self.inner.is_end_stream()
}
fn size_hint(&self) -> SizeHint {
self.inner.size_hint()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
fn request(version: Version, content_type: Option<&str>) -> Request {
let mut builder = http::Request::builder()
.method("POST")
.uri("/pkg.Service/Method")
.version(version);
if let Some(value) = content_type {
builder = builder.header("content-type", value);
}
builder.body(Body::empty()).unwrap()
}
#[test]
fn only_http2_grpc_content_types_are_grpc() {
for value in [
"application/grpc",
"application/grpc+proto",
"application/grpc;charset=utf-8",
"Application/GRPC",
] {
assert!(is_grpc(&request(Version::HTTP_2, Some(value))), "{value}");
}
for value in [
"application/grpc-web",
"application/grpc-web+proto",
"application/grpc-web-text",
"application/grpcx",
"application/json",
] {
assert!(!is_grpc(&request(Version::HTTP_2, Some(value))), "{value}");
}
assert!(!is_grpc(&request(Version::HTTP_2, None)));
assert!(!is_grpc(&request(
Version::HTTP_11,
Some("application/grpc")
)));
}
#[test]
fn the_peer_is_the_tcp_peer_but_not_the_unix_socket_stamp() {
let mut with_peer = request(Version::HTTP_2, None);
let peer: SocketAddr = "10.1.2.3:4567".parse().unwrap();
with_peer.extensions_mut().insert(ConnectInfo(peer));
assert_eq!(peer_info(&with_peer).unwrap().remote_addr, Some(peer));
let mut unix = request(Version::HTTP_2, None);
unix.extensions_mut()
.insert(ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 0))));
assert!(peer_info(&unix).is_none());
assert!(peer_info(&request(Version::HTTP_2, None)).is_none());
}
#[test]
fn the_deadline_is_the_smaller_timeout() {
let mut headers = HeaderMap::new();
let second = Some(Duration::from_secs(1));
assert_eq!(deadline(None, &headers), None);
assert_eq!(deadline(second, &headers), second);
headers.insert("grpc-timeout", "100m".parse().unwrap());
assert_eq!(deadline(None, &headers), Some(Duration::from_millis(100)));
assert_eq!(deadline(second, &headers), Some(Duration::from_millis(100)));
headers.insert("grpc-timeout", "5S".parse().unwrap());
assert_eq!(deadline(second, &headers), second);
headers.insert("grpc-timeout", "bad".parse().unwrap());
assert_eq!(deadline(second, &headers), second);
}
#[tokio::test]
async fn a_call_before_start_is_unavailable() {
let shared = Arc::new(Shared::new());
let inner = tower::service_fn(|_: Request| async {
Ok::<_, Infallible>(Response::new(Body::from("http")))
});
let mut gate = GrpcGate::new(shared).layer(inner);
let response = gate
.ready()
.await
.unwrap()
.call(request(Version::HTTP_2, Some("application/grpc")))
.await
.unwrap();
let code = response.headers().get("grpc-status").unwrap();
assert_eq!(code, "14", "UNAVAILABLE");
let response = gate
.ready()
.await
.unwrap()
.call(request(Version::HTTP_11, Some("application/grpc")))
.await
.unwrap();
assert!(response.headers().get("grpc-status").is_none(), "HTTP");
}
}