use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use foundation_core::extensions::result_ext::BoxedError;
use foundation_core::io::ioutils::SharedByteBufferStream;
use foundation_core::url::Uri;
use foundation_core::valtron::{block_on_future, from_future, send, Pipe, PipeReceiver, PipeSender};
use foundation_netio::netcap::RawStream;
use foundation_netio::shared::client::body_reader::collect_bytes_from_send_safe;
use foundation_netio::shared::http::{
ChunkedData, Proto, RequestDescriptor, SendSafeBody, SimpleHeader, SimpleHeaders,
SimpleIncomingRequest, SimpleMethod, SimpleOutgoingResponse, SimpleUrl, Status,
};
use foundation_connectrpc::shared::protocol::connect::constants as connect_consts;
use foundation_connectrpc::shared::protocol::connect::{
streaming_content_type as connect_streaming_content_type,
};
use foundation_connectrpc::shared::protocol::grpc::constants as grpc_consts;
use foundation_connectrpc::shared::protocol::grpc::content_type as grpc_content_type;
use foundation_connectrpc::shared::protocol::grpc_web::{
content_type as grpc_web_content_type, parse_content_type as parse_grpc_web_content_type,
};
use foundation_connectrpc::shared::protocol::UnaryOutcome;
use foundation_connectrpc::shared::transport::DEFAULT_PIPE_DEPTH;
use foundation_connectrpc::{
encode_grpc_timeout, parse_connect_content_type, CancelSignal, ClientExchange,
ClientExchangeConfig, CompressionRegistry, ConnectHandler, ExchangeConfig, GrpcClient,
GrpcWebHandler, H2Transport, HandlerExchange, ProtocolClient, ProtocolHandler, Spec, Transport,
};
use futures_util::FutureExt;
use crate::forward::{forward_request_headers, upstream_url, write_response};
use crate::runtime::BackendRuntime;
pub(crate) fn translate_connect_deadline_to_grpc(out_headers: &mut SimpleHeaders) {
let connect_timeout = SimpleHeader::from(connect_consts::HEADER_TIMEOUT.to_string());
let Some(ms) = out_headers
.get(&connect_timeout)
.and_then(|v| v.first())
.cloned()
else {
return;
};
out_headers.remove(&connect_timeout);
if let Ok(ms) = ms.trim().parse::<u64>() {
out_headers.insert(
SimpleHeader::from(grpc_consts::HEADER_TIMEOUT.to_string()),
vec![encode_grpc_timeout(Duration::from_millis(ms))],
);
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn relay_grpc_web_to_grpc(
conn: &mut SharedByteBufferStream<RawStream>,
backend: &Arc<BackendRuntime>,
mut req: SimpleIncomingRequest,
client_ip: &str,
scheme: &str,
sticky_header: Option<&SimpleHeaders>,
) -> Result<(), String> {
let inbound_ct = req
.headers
.get(&SimpleHeader::CONTENT_TYPE)
.and_then(|v| v.first())
.cloned()
.unwrap_or_else(|| "application/grpc-web+proto".to_string());
let (codec, text) = parse_grpc_web_content_type(&inbound_ct)
.unwrap_or_else(|| ("proto".to_string(), false));
let out_content_type = grpc_content_type(&codec);
let resp_content_type = grpc_web_content_type(&codec, text);
let req_body = Bytes::from(collect_bytes_from_send_safe(
req.body.take().unwrap_or(SendSafeBody::None),
));
let mut out_headers = forward_request_headers(&req.headers, client_ip, scheme);
out_headers.insert(SimpleHeader::CONTENT_TYPE, vec![out_content_type]);
out_headers.insert(
SimpleHeader::from("te".to_string()),
vec!["trailers".to_string()],
);
let url = upstream_url(backend.url(), &req.request_url.url);
let (driver, responder_rx) =
setup_streaming_relay_to_grpc(GrpcWebHandler, req, req_body, url, out_headers)
.map_err(|e| format!("grpc-web→grpc relay setup failed: {e}"))?;
send(from_future(driver))
.map_err(|e| format!("grpc-web→grpc relay spawn failed: {e}"))?;
let mut headers = SimpleHeaders::new();
headers.insert(SimpleHeader::CONTENT_TYPE, vec![resp_content_type]);
let body = SendSafeBody::ChunkedStream(Some(Box::new(BlockingChunkReader {
receiver: responder_rx,
ended: false,
})));
write_response(conn, Status::OK, &headers, body, sticky_header)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn relay_connect_to_grpc(
conn: &mut SharedByteBufferStream<RawStream>,
backend: &Arc<BackendRuntime>,
mut req: SimpleIncomingRequest,
client_ip: &str,
scheme: &str,
sticky_header: Option<&SimpleHeaders>,
) -> Result<(), String> {
let inbound_ct = req
.headers
.get(&SimpleHeader::CONTENT_TYPE)
.and_then(|v| v.first())
.cloned()
.unwrap_or_else(|| "application/json".to_string());
let (codec, is_streaming) = parse_connect_content_type(&inbound_ct)
.ok_or_else(|| format!("unrecognized connect content-type {inbound_ct:?}"))?;
let out_content_type = grpc_content_type(&codec);
let req_body = Bytes::from(collect_bytes_from_send_safe(
req.body.take().unwrap_or(SendSafeBody::None),
));
let mut out_headers = forward_request_headers(&req.headers, client_ip, scheme);
out_headers.insert(SimpleHeader::CONTENT_TYPE, vec![out_content_type]);
out_headers.insert(
SimpleHeader::from("te".to_string()),
vec!["trailers".to_string()],
);
translate_connect_deadline_to_grpc(&mut out_headers);
let url = upstream_url(backend.url(), &req.request_url.url);
if is_streaming {
let resp_content_type = connect_streaming_content_type(&codec);
let (driver, responder_rx) =
setup_streaming_relay_to_grpc(ConnectHandler, req, req_body, url, out_headers)
.map_err(|e| format!("connect→grpc relay setup failed: {e}"))?;
send(from_future(driver))
.map_err(|e| format!("connect→grpc relay spawn failed: {e}"))?;
let mut headers = SimpleHeaders::new();
headers.insert(SimpleHeader::CONTENT_TYPE, vec![resp_content_type]);
let body = SendSafeBody::ChunkedStream(Some(Box::new(BlockingChunkReader {
receiver: responder_rx,
ended: false,
})));
write_response(conn, Status::OK, &headers, body, sticky_header)
} else {
let response = block_on_future(connect_unary_roundtrip(
req, req_body, codec, url, out_headers,
))
.map_err(|e| format!("connect unary→grpc relay failed: {e}"))?;
write_response(
conn,
response.status,
&response.headers,
response.body.unwrap_or(SendSafeBody::None),
sticky_header,
)
}
}
async fn connect_unary_to_grpc(
message: Bytes,
url: String,
out_headers: SimpleHeaders,
) -> Result<(Bytes, SimpleHeaders), String> {
let uri = Uri::parse(&url).map_err(|e| format!("parse upstream uri {url}: {e}"))?;
let out_request = RequestDescriptor {
proto: Proto::HTTP20,
request_url: SimpleUrl::url_only(&url),
request_uri: uri,
headers: out_headers,
method: SimpleMethod::POST,
};
let stream = H2Transport::new()
.open(out_request)
.map_err(|e| format!("open h2 upstream {url}: {e}"))?;
let cancel = CancelSignal::new();
let outbound = GrpcClient
.new_conn(
&Spec::empty(true),
SimpleHeaders::new(),
stream,
ClientExchangeConfig {
cancel: cancel.clone(),
read_max_bytes: 0,
},
)
.map_err(|e| format!("build grpc outbound exchange: {e}"))?;
let ClientExchange {
conn,
reader_task,
writer_task,
} = outbound;
let (mut out_send, mut out_recv) = conn.split();
let result: Arc<std::sync::Mutex<Option<Result<(Bytes, SimpleHeaders), String>>>> =
Arc::new(std::sync::Mutex::new(None));
let sender = async move {
let _ = out_send.send(message).await;
let _ = out_send.close_send().await;
};
let result_ref = Arc::clone(&result);
let receiver = async move {
let outcome = async {
let mut msg: Option<Bytes> = None;
loop {
match out_recv.receive().await {
Ok(Some(bytes)) => msg = Some(bytes),
Ok(None) => break,
Err(e) => return Err(format!("gRPC unary receive failed: {e}")),
}
}
let msg = msg.ok_or_else(|| "gRPC unary returned no message".to_string())?;
let trailers = out_recv
.response_trailers()
.await
.map(|t| t.clone())
.unwrap_or_default();
Ok((msg, trailers))
}
.await;
*result_ref.lock().unwrap() = Some(outcome);
};
futures_util::future::join_all(vec![
sender.boxed(),
receiver.boxed(),
reader_task.map(|_| ()).boxed(),
writer_task.map(|_| ()).boxed(),
])
.await;
let outcome = result
.lock()
.unwrap()
.take()
.unwrap_or_else(|| Err("gRPC unary relay produced no result".to_string()));
outcome
}
pub(crate) async fn connect_unary_roundtrip(
request: SimpleIncomingRequest,
req_body: Bytes,
codec: String,
url: String,
out_headers: SimpleHeaders,
) -> Result<SimpleOutgoingResponse, String> {
let compression = CompressionRegistry::empty();
let message = ConnectHandler
.decode_unary_request(&request, req_body, &compression, 0)
.map_err(|e| format!("decode connect unary request: {e}"))?;
let (resp_bytes, trailers) = connect_unary_to_grpc(message, url, out_headers)
.await
.map_err(|e| format!("connect unary→grpc relay failed: {e}"))?;
let outcome = UnaryOutcome {
frame: resp_bytes,
headers: SimpleHeaders::new(),
trailers,
};
let mut response = SimpleOutgoingResponse::builder()
.with_status(Status::OK)
.with_headers(SimpleHeaders::new())
.with_body(SendSafeBody::None)
.build()
.map_err(|e| format!("build connect unary response: {e}"))?;
ConnectHandler
.encode_unary_response(&mut response, &request, &codec, outcome, &compression)
.map_err(|e| format!("encode connect unary response: {e}"))?;
Ok(response)
}
pub(crate) fn build_streaming_relay_to_grpc<H: ProtocolHandler>(
handler: H,
request: SimpleIncomingRequest,
body_rx: PipeReceiver<Bytes>,
responder_tx: PipeSender<Bytes>,
url: String,
out_headers: SimpleHeaders,
) -> Result<(futures_util::future::BoxFuture<'static, ()>, CancelSignal), String> {
let cancel = CancelSignal::new();
let inbound = {
let compression = CompressionRegistry::empty();
handler
.new_conn(
&request,
Spec::empty(false),
body_rx,
responder_tx,
ExchangeConfig {
compression: &compression,
cancel: cancel.clone(),
read_max_bytes: 0,
},
)
.map_err(|e| format!("build inbound exchange: {e}"))?
};
let uri = Uri::parse(&url).map_err(|e| format!("parse upstream uri {url}: {e}"))?;
let out_request = RequestDescriptor {
proto: Proto::HTTP20,
request_url: SimpleUrl::url_only(&url),
request_uri: uri,
headers: out_headers,
method: SimpleMethod::POST,
};
let stream = H2Transport::new()
.open(out_request)
.map_err(|e| format!("open h2 upstream {url}: {e}"))?;
let outbound = GrpcClient
.new_conn(
&Spec::empty(true),
SimpleHeaders::new(),
stream,
ClientExchangeConfig {
cancel: cancel.clone(),
read_max_bytes: 0,
},
)
.map_err(|e| format!("build grpc outbound exchange: {e}"))?;
let HandlerExchange {
conn: inbound_conn,
reader_task: in_reader,
writer_task: in_writer,
} = inbound;
let ClientExchange {
conn: outbound_conn,
reader_task: out_reader,
writer_task: out_writer,
} = outbound;
let (mut in_recv, mut in_send) = inbound_conn.split();
let (mut out_send, mut out_recv) = outbound_conn.split();
let request_pump = async move {
loop {
match in_recv.receive().await {
Ok(Some(bytes)) => {
if out_send.send(bytes).await.is_err() {
break;
}
}
Ok(None) | Err(_) => break,
}
}
let _ = out_send.close_send().await;
};
let response_pump = async move {
loop {
match out_recv.receive().await {
Ok(Some(bytes)) => {
if in_send.send(bytes).await.is_err() {
return;
}
}
Ok(None) => {
let trailers = out_recv
.response_trailers()
.await
.map(|t| t.clone())
.unwrap_or_default();
let _ = in_send.close(None, trailers).await;
return;
}
Err(e) => {
let trailers = out_recv
.response_trailers()
.await
.map(|t| t.clone())
.unwrap_or_default();
let _ = in_send.close(Some(e), trailers).await;
return;
}
}
}
};
let work = futures_util::future::join_all(vec![
in_reader.map(|_| ()).boxed(),
in_writer.map(|_| ()).boxed(),
out_reader.map(|_| ()).boxed(),
out_writer.map(|_| ()).boxed(),
request_pump.boxed(),
response_pump.boxed(),
]);
let cancel_for_driver = cancel.clone();
let driver: futures_util::future::BoxFuture<'static, ()> = async move {
let cancelled = Box::pin(cancel_for_driver.cancelled());
match futures_util::future::select(work, cancelled).await {
futures_util::future::Either::Left(_) => {}
futures_util::future::Either::Right(_) => {}
}
}
.boxed();
Ok((driver, cancel))
}
fn setup_streaming_relay_to_grpc<H: ProtocolHandler>(
handler: H,
request: SimpleIncomingRequest,
req_body: Bytes,
url: String,
out_headers: SimpleHeaders,
) -> Result<(futures_util::future::BoxFuture<'static, ()>, PipeReceiver<Bytes>), String> {
let (body_tx, body_rx) = Pipe::<Bytes>::with_depth(DEFAULT_PIPE_DEPTH);
let (responder_tx, responder_rx) = Pipe::<Bytes>::with_depth(DEFAULT_PIPE_DEPTH);
if !req_body.is_empty() {
let _ = body_tx.try_send(req_body);
}
body_tx.close();
let (driver, _cancel) =
build_streaming_relay_to_grpc(handler, request, body_rx, responder_tx, url, out_headers)?;
Ok((driver, responder_rx))
}
struct BlockingChunkReader {
receiver: PipeReceiver<Bytes>,
ended: bool,
}
impl Iterator for BlockingChunkReader {
type Item = Result<ChunkedData, BoxedError>;
fn next(&mut self) -> Option<Self::Item> {
if self.ended {
return None;
}
let rx = self.receiver.clone();
match block_on_future(async move { rx.receive().await }) {
Some(bytes) => Some(Ok(ChunkedData::Data(bytes.to_vec(), None))),
None => {
self.ended = true;
Some(Ok(ChunkedData::DataEnded))
}
}
}
}