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 axum::extract::connect_info::ConnectInfo;
use axum::response::IntoResponse;
use axum::routing::future::RouteFuture;
use bytes::Bytes;
use pin_project_lite::pin_project;
use rustls::pki_types::CertificateDer;
use tonic::transport::server::{Connected, TcpConnectInfo};
use tower::{Layer, Service, ServiceExt};
use tower_http::cors::{Cors, CorsLayer};
use crate::guard::{BoxedService, Class, GrpcRejections, Guards};
pub type TlsConnectInfo =
<tokio_rustls::server::TlsStream<tokio::net::TcpStream> as Connected>::ConnectInfo;
use crate::upstream::{
grpc_protocol, is_grpc_web_preflight, BoxError, GrpcProtocol, PassThrough, Upstream,
};
#[derive(Clone)]
pub struct ProxyService<U> {
upstream: U,
routes: axum::Router,
grpc_web: Option<GrpcWebCors<U>>,
boxed: BoxedGrpc,
guards: Arc<Guards>,
connection: Option<ConnectionInfo>,
}
impl<U> std::fmt::Debug for ProxyService<U> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProxyService")
.field("boxed_grpc", &self.boxed.grpc.is_some())
.field("boxed_grpc_web", &self.boxed.web.is_some())
.field("connection", &self.connection)
.finish_non_exhaustive()
}
}
#[derive(Clone, Default)]
struct BoxedGrpc {
grpc: Option<BoxedService>,
web: Option<BoxedService>,
web_text: Option<BoxedService>,
}
#[derive(Clone, Debug)]
struct Translated<U> {
upstream: U,
}
impl<U: Upstream> Service<http::Request<tonic::body::Body>> for Translated<U> {
type Response = http::Response<axum::body::Body>;
type Error = Infallible;
type Future = PassThrough<U>;
#[inline]
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, request: http::Request<tonic::body::Body>) -> Self::Future {
let (mut parts, body) = request.into_parts();
parts.version = http::Version::HTTP_2;
let body = tonic::body::Body::new(Unsized { body });
PassThrough::new(
self.upstream.clone(),
http::Request::from_parts(parts, body),
GrpcProtocol::Grpc,
)
}
}
pin_project! {
struct Unsized {
#[pin]
body: tonic::body::Body,
}
}
impl http_body::Body for Unsized {
type Data = Bytes;
type Error = tonic::Status;
#[inline]
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Bytes>, tonic::Status>>> {
self.project().body.poll_frame(cx)
}
#[inline]
fn is_end_stream(&self) -> bool {
self.body.is_end_stream()
}
}
#[derive(Clone, Debug)]
struct GrpcWebCors<U> {
web: Cors<Forward<U>>,
web_text: Cors<Forward<U>>,
}
#[derive(Clone, Debug)]
struct Forward<U> {
upstream: U,
protocol: GrpcProtocol,
}
impl<U: Upstream> Service<http::Request<tonic::body::Body>> for Forward<U> {
type Response = http::Response<axum::body::Body>;
type Error = Infallible;
type Future = PassThrough<U>;
#[inline]
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, request: http::Request<tonic::body::Body>) -> Self::Future {
PassThrough::new(self.upstream.clone(), request, self.protocol)
}
}
impl<U: Upstream> Service<axum::extract::Request> for Forward<U> {
type Response = http::Response<axum::body::Body>;
type Error = Infallible;
type Future = PassThrough<U>;
#[inline]
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, request: axum::extract::Request) -> Self::Future {
let request = request.map(tonic::body::Body::new);
PassThrough::new(self.upstream.clone(), request, self.protocol)
}
}
#[derive(Clone, Debug)]
pub struct ConnectionInfo {
tcp: TcpConnectInfo,
tls: Option<TlsConnectInfo>,
}
impl ConnectionInfo {
pub fn remote_addr(&self) -> Option<SocketAddr> {
self.tcp.remote_addr
}
pub fn peer_certs(&self) -> Option<Arc<Vec<CertificateDer<'static>>>> {
self.tls.as_ref().and_then(|tls| tls.peer_certs())
}
pub(crate) fn into_tonic_extensions(self, extensions: &mut http::Extensions) {
extensions.insert(self.tcp);
if let Some(tls) = self.tls {
extensions.insert(tls);
}
}
}
impl From<TcpConnectInfo> for ConnectionInfo {
fn from(tcp: TcpConnectInfo) -> Self {
Self { tcp, tls: None }
}
}
impl ConnectionInfo {
pub fn tls(tls: TlsConnectInfo) -> Self {
Self {
tcp: tls.get_ref().clone(),
tls: Some(tls),
}
}
}
impl<U: Upstream> ProxyService<U> {
pub(crate) fn new(
upstream: U,
routes: axum::Router,
grpc_web_cors: Option<CorsLayer>,
guards: Arc<Guards>,
translate_grpc_web: bool,
) -> Self {
let forward = |protocol| Forward {
upstream: upstream.clone(),
protocol,
};
let guard_grpc = guards.cover(Class::Grpc);
let stack = |protocol: GrpcProtocol| {
let web = protocol != GrpcProtocol::Grpc;
let mut service = if web && translate_grpc_web {
let translator = tonic_web::GrpcWebLayer::new().layer(Translated {
upstream: upstream.clone(),
});
BoxedService::new(ServiceExt::<axum::extract::Request>::map_response(
translator,
|response| response.map(axum::body::Body::new),
))
} else {
BoxedService::new(forward(protocol))
};
if guard_grpc {
service = BoxedService::new(GrpcRejections {
inner: guards.service(service, Class::Grpc),
protocol,
});
}
match &grpc_web_cors {
Some(cors) if web => BoxedService::new(cors.layer(service)),
_ => service,
}
};
let boxed_web = guard_grpc || translate_grpc_web;
let boxed = BoxedGrpc {
grpc: guard_grpc.then(|| stack(GrpcProtocol::Grpc)),
web: boxed_web.then(|| stack(GrpcProtocol::Web)),
web_text: boxed_web.then(|| stack(GrpcProtocol::WebText)),
};
let grpc_web = grpc_web_cors.map(|cors| GrpcWebCors {
web: cors.layer(forward(GrpcProtocol::Web)),
web_text: cors.layer(forward(GrpcProtocol::WebText)),
});
Self {
upstream,
routes,
grpc_web,
boxed,
guards,
connection: None,
}
}
#[must_use]
pub fn with_fallback<F>(mut self, fallback: F) -> Self
where
F: Service<axum::extract::Request, Error = Infallible> + Clone + Send + Sync + 'static,
F::Response: IntoResponse,
F::Future: Send + 'static,
{
self.routes = if self.guards.cover(Class::Fallback) {
let fallback = BoxedService::new(fallback.map_response(IntoResponse::into_response));
self.routes
.fallback_service(self.guards.service(fallback, Class::Fallback))
} else {
self.routes.fallback_service(fallback)
};
self
}
#[must_use]
pub fn for_connection(&self, connection: impl Into<ConnectionInfo>) -> Self {
Self {
upstream: self.upstream.clone(),
routes: self.routes.clone(),
grpc_web: self.grpc_web.clone(),
boxed: self.boxed.clone(),
guards: self.guards.clone(),
connection: Some(connection.into()),
}
}
}
fn connection_of<B>(
connection: Option<&ConnectionInfo>,
request: &http::Request<B>,
) -> Option<ConnectionInfo> {
if let Some(connection) = connection {
return Some(connection.clone());
}
let ConnectInfo(remote) = request.extensions().get::<ConnectInfo<SocketAddr>>()?;
Some(ConnectionInfo::from(TcpConnectInfo {
local_addr: None,
remote_addr: Some(*remote),
}))
}
impl<U, B> Service<http::Request<B>> for ProxyService<U>
where
U: Upstream,
B: http_body::Body<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,
{
type Response = http::Response<axum::body::Body>;
type Error = Infallible;
type Future = ResponseFuture<U>;
#[inline]
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, mut request: http::Request<B>) -> Self::Future {
let call = grpc_protocol(request.headers());
let protocol = call.or_else(|| {
is_grpc_web_preflight(request.method(), request.headers()).then_some(GrpcProtocol::Web)
});
let boxed = match call {
Some(GrpcProtocol::Grpc) => self.boxed.grpc.as_mut(),
Some(GrpcProtocol::Web) => self.boxed.web.as_mut(),
Some(GrpcProtocol::WebText) => self.boxed.web_text.as_mut(),
None => None,
};
let inner = if let Some(service) = boxed {
if let Some(connection) = connection_of(self.connection.as_ref(), &request) {
let extensions = request.extensions_mut();
if let Some(remote) = connection.remote_addr() {
extensions.insert(ConnectInfo(remote));
}
connection.into_tonic_extensions(extensions);
}
Inner::Boxed {
future: service.call(request.map(axum::body::Body::new)),
}
} else if let Some(protocol) = protocol {
if let Some(connection) = connection_of(self.connection.as_ref(), &request) {
connection.into_tonic_extensions(request.extensions_mut());
}
let request = request.map(tonic::body::Body::new);
let cors = match (&mut self.grpc_web, protocol) {
(Some(cors), GrpcProtocol::Web) => Some(&mut cors.web),
(Some(cors), GrpcProtocol::WebText) => Some(&mut cors.web_text),
_ => None,
};
match cors {
Some(cors) => Inner::GrpcWeb {
future: cors.call(request),
},
None => Inner::Grpc {
call: PassThrough::new(self.upstream.clone(), request, protocol),
},
}
} else {
if let Some(connection) = &self.connection {
let extensions = request.extensions_mut();
if let Some(remote) = connection.remote_addr() {
extensions.insert(ConnectInfo(remote));
}
extensions.insert(connection.clone());
} else if let Some(connection) = connection_of(None, &request) {
request.extensions_mut().insert(connection);
}
Inner::Routes {
future: self.routes.call(request),
}
};
ResponseFuture { inner }
}
}
pin_project! {
pub struct ResponseFuture<U: Upstream> {
#[pin]
inner: Inner<U>,
}
}
pin_project! {
#[project = InnerProj]
enum Inner<U: Upstream> {
Routes {
#[pin]
future: RouteFuture<Infallible>,
},
Grpc {
#[pin]
call: PassThrough<U>,
},
GrpcWeb {
#[pin]
future: tower_http::cors::ResponseFuture<PassThrough<U>>,
},
Boxed {
#[pin]
future: <BoxedService as Service<axum::extract::Request>>::Future,
},
}
}
impl<U: Upstream> Future for ResponseFuture<U> {
type Output = Result<http::Response<axum::body::Body>, Infallible>;
#[inline]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project().inner.project() {
InnerProj::Routes { future } => future.poll(cx),
InnerProj::Grpc { call } => call.poll(cx),
InnerProj::GrpcWeb { future } => future.poll(cx),
InnerProj::Boxed { future } => future.poll(cx),
}
}
}
#[cfg(test)]
mod tests;