use std::convert::Infallible;
use std::future::Future;
use std::pin::Pin;
use std::task::{ready, Context, Poll};
use bytes::Bytes;
use pin_project_lite::pin_project;
use tonic::client::GrpcService;
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub trait Upstream:
GrpcService<
tonic::body::Body,
ResponseBody: http_body::Body<Data = Bytes, Error: Into<BoxError> + Send> + Send + 'static,
Error: Into<BoxError>,
Future: Send,
> + Clone
+ Send
+ Sync
+ 'static
{
}
impl<T> Upstream for T where
T: GrpcService<
tonic::body::Body,
ResponseBody: http_body::Body<Data = Bytes, Error: Into<BoxError> + Send>
+ Send
+ 'static,
Error: Into<BoxError>,
Future: Send,
> + Clone
+ Send
+ Sync
+ 'static
{
}
pin_project! {
#[project = PassThroughProj]
#[project_replace = PassThroughReplace]
pub(crate) enum PassThrough<U: Upstream> {
Ready {
upstream: U,
request: http::Request<tonic::body::Body>,
protocol: GrpcProtocol,
},
Call {
#[pin]
future: U::Future,
protocol: GrpcProtocol,
},
Done,
}
}
impl<U: Upstream> PassThrough<U> {
pub(crate) fn new(
upstream: U,
request: http::Request<tonic::body::Body>,
protocol: GrpcProtocol,
) -> Self {
Self::Ready {
upstream,
request,
protocol,
}
}
}
impl<U: Upstream> Future for PassThrough<U> {
type Output = Result<http::Response<axum::body::Body>, Infallible>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
loop {
match self.as_mut().project() {
PassThroughProj::Ready {
upstream, protocol, ..
} => {
let protocol = *protocol;
if let Err(e) = ready!(upstream.poll_ready(cx)) {
self.set(Self::Done);
return Poll::Ready(Ok(failure(e.into(), protocol)));
}
let PassThroughReplace::Ready {
mut upstream,
request,
..
} = self.as_mut().project_replace(Self::Done)
else {
unreachable!("the state was just matched as Ready");
};
let future = upstream.call(request);
self.set(Self::Call { future, protocol });
}
PassThroughProj::Call { future, protocol } => {
let protocol = *protocol;
let result = ready!(future.poll(cx));
self.set(Self::Done);
return Poll::Ready(Ok(match result {
Ok(response) => response.map(axum::body::Body::new),
Err(e) => failure(e.into(), protocol),
}));
}
PassThroughProj::Done => panic!("PassThrough polled after completion"),
}
}
}
}
fn failure(error: BoxError, protocol: GrpcProtocol) -> http::Response<axum::body::Body> {
trailers_only(tonic::Status::from_error(error), protocol)
}
pub(crate) fn trailers_only(
status: tonic::Status,
protocol: GrpcProtocol,
) -> http::Response<axum::body::Body> {
let mut response: http::Response<axum::body::Body> = status.into_http();
if protocol != GrpcProtocol::Grpc {
response.headers_mut().insert(
http::header::CONTENT_TYPE,
http::HeaderValue::from_static(protocol.content_type()),
);
}
response
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum GrpcProtocol {
Grpc,
Web,
WebText,
}
impl GrpcProtocol {
fn content_type(self) -> &'static str {
match self {
Self::Grpc => "application/grpc",
Self::Web => "application/grpc-web+proto",
Self::WebText => "application/grpc-web-text+proto",
}
}
}
pub(crate) fn grpc_protocol(headers: &http::HeaderMap) -> Option<GrpcProtocol> {
let value = headers.get(http::header::CONTENT_TYPE)?.as_bytes();
let media = match value.iter().position(|&b| b == b';') {
Some(end) => &value[..end],
None => value,
}
.trim_ascii();
let rest = strip_prefix_ignore_case(media, b"application/grpc")?;
match rest {
[] | [b'+', ..] => Some(GrpcProtocol::Grpc),
_ => web_protocol(strip_prefix_ignore_case(rest, b"-web")?),
}
}
pub(crate) fn is_grpc_web_preflight(method: &http::Method, headers: &http::HeaderMap) -> bool {
method == http::Method::OPTIONS
&& headers.contains_key(http::header::ORIGIN)
&& headers
.get_all(http::header::ACCESS_CONTROL_REQUEST_HEADERS)
.iter()
.flat_map(|value| value.as_bytes().split(|&b| b == b','))
.any(|name| name.trim_ascii().eq_ignore_ascii_case(b"x-grpc-web"))
}
fn web_protocol(rest: &[u8]) -> Option<GrpcProtocol> {
let (protocol, rest) = match strip_prefix_ignore_case(rest, b"-text") {
Some(rest) => (GrpcProtocol::WebText, rest),
None => (GrpcProtocol::Web, rest),
};
matches!(rest, [] | [b'+', ..]).then_some(protocol)
}
fn strip_prefix_ignore_case<'a>(bytes: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
let (head, rest) = bytes.split_at_checked(prefix.len())?;
head.eq_ignore_ascii_case(prefix).then_some(rest)
}
#[cfg(test)]
mod tests;