use std::collections::HashSet;
use std::io::{ErrorKind, Write};
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::time::{Duration, Instant};
use foundation_core::io::ioutils::SharedByteBufferStream;
use foundation_iogate::ServerIo;
use foundation_netio::netcap::{Connection, RawStream};
use foundation_netio::http::{NativeHttpClient, ClientRequestBuilder};
use foundation_netio::shared::client::SystemDnsResolver;
use foundation_netio::shared::http::{
Http11, RenderHttp, SendSafeBody, SimpleHeader, SimpleHeaders, SimpleIncomingRequest,
SimpleOutgoingResponse, Status,
};
use crate::passthrough::splice_bidirectional;
use crate::runtime::BackendRuntime;
pub type SharedHttpClient = NativeHttpClient<SystemDnsResolver>;
const HOP_BY_HOP: &[&str] = &[
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"trailers",
"transfer-encoding",
"upgrade",
];
#[must_use]
pub fn is_upgrade_request(req: &SimpleIncomingRequest) -> bool {
let connection_upgrade = req
.headers
.get(&SimpleHeader::CONNECTION)
.is_some_and(|vals| {
vals.iter()
.any(|v| v.split(',').any(|t| t.trim().eq_ignore_ascii_case("upgrade")))
});
connection_upgrade && req.headers.contains_key(&SimpleHeader::UPGRADE)
}
fn drop_set(headers: &SimpleHeaders) -> HashSet<String> {
let mut names: HashSet<String> = HOP_BY_HOP.iter().map(|s| (*s).to_string()).collect();
if let Some(values) = headers.get(&SimpleHeader::CONNECTION) {
for value in values {
for token in value.split(',') {
let token = token.trim();
if !token.is_empty() {
names.insert(token.to_ascii_lowercase());
}
}
}
}
names
}
#[must_use]
pub fn strip_hop_by_hop(headers: &SimpleHeaders) -> SimpleHeaders {
let drop = drop_set(headers);
headers
.iter()
.filter(|(name, _)| !drop.contains(&name.to_string().to_ascii_lowercase()))
.map(|(name, values)| (name.clone(), values.clone()))
.collect()
}
#[must_use]
pub fn strip_hop_by_hop_request(headers: &SimpleHeaders) -> SimpleHeaders {
let drop = drop_set(headers);
headers
.iter()
.filter(|(name, values)| {
let lower = name.to_string().to_ascii_lowercase();
if !drop.contains(&lower) {
return true;
}
if lower == "te" {
return values
.iter()
.any(|v| v.split(',').any(|t| t.trim().eq_ignore_ascii_case("trailers")));
}
lower == "trailer" || lower == "trailers"
})
.map(|(name, values)| (name.clone(), values.clone()))
.collect()
}
#[must_use]
pub fn is_streaming_request(headers: &SimpleHeaders) -> bool {
headers
.get(&SimpleHeader::CONTENT_TYPE)
.and_then(|values| values.first())
.is_some_and(|ct| {
ct == "application/grpc"
|| ct.starts_with("application/grpc+")
|| ct.starts_with("application/connect+")
|| ct.starts_with("application/grpc-web")
})
}
pub fn forward_request_headers(
original: &SimpleHeaders,
client_ip: &str,
scheme: &str,
) -> SimpleHeaders {
let mut headers = strip_hop_by_hop_request(original);
let xff = SimpleHeader::custom("x-forwarded-for");
let mut chain: Vec<String> = headers
.get(&xff)
.map(|vals| vals.iter().flat_map(|v| v.split(',')).map(|s| s.trim().to_string()).collect())
.unwrap_or_default();
chain.push(client_ip.to_string());
headers.insert(xff, vec![chain.join(", ")]);
headers.insert(
SimpleHeader::custom("x-forwarded-proto"),
vec![scheme.to_string()],
);
if let Some(host) = original.get(&SimpleHeader::HOST).and_then(|v| v.first()) {
headers.insert(
SimpleHeader::custom("x-forwarded-host"),
vec![host.clone()],
);
}
if !is_streaming_request(original) {
headers.insert(SimpleHeader::CONNECTION, vec!["close".to_string()]);
}
headers
}
pub(crate) fn upstream_url(backend_url: &str, path: &str) -> String {
let base = backend_url.strip_suffix('/').unwrap_or(backend_url);
if path.starts_with('/') {
format!("{base}{path}")
} else {
format!("{base}/{path}")
}
}
pub fn forward_http(
conn: &mut SharedByteBufferStream<RawStream>,
backend: &Arc<BackendRuntime>,
req: SimpleIncomingRequest,
client_ip: &str,
scheme: &str,
client: &SharedHttpClient,
) -> Result<(), String> {
forward_http_with_headers(conn, backend, req, client_ip, scheme, client, None)
}
pub fn forward_http_with_headers(
conn: &mut SharedByteBufferStream<RawStream>,
backend: &Arc<BackendRuntime>,
req: SimpleIncomingRequest,
client_ip: &str,
scheme: &str,
client: &SharedHttpClient,
extra_headers: Option<&SimpleHeaders>,
) -> Result<(), String> {
let reuse = is_streaming_request(&req.headers);
let url = upstream_url(backend.url(), &req.request_url.url);
let headers = forward_request_headers(&req.headers, client_ip, scheme);
let body = req.body.unwrap_or(SendSafeBody::None);
let builder = ClientRequestBuilder::<SystemDnsResolver>::new(req.method.clone(), &url)
.map_err(|e| format!("build upstream request for {url}: {e}"))?
.headers(headers)
.body(body);
let request = client
.request(builder)
.map_err(|e| format!("prepare upstream request: {e}"))?;
let response = request
.send()
.map_err(|e| format!("send upstream request to {url}: {e}"))?;
let (status, headers, body, pool, upstream_conn) = response.into_parts();
write_response(conn, status, &headers, body, extra_headers)?;
if reuse {
if let (Some(pool), Some(mut stream)) = (pool, upstream_conn) {
stream.drain_stream();
pool.return_to_pool(stream);
}
}
Ok(())
}
pub(crate) fn write_response(
conn: &mut SharedByteBufferStream<RawStream>,
status: Status,
headers: &SimpleHeaders,
body: SendSafeBody,
extra_headers: Option<&SimpleHeaders>,
) -> Result<(), String> {
let mut out = strip_hop_by_hop(headers);
if let Some(extras) = extra_headers {
for (name, values) in extras.iter() {
out.insert(name.clone(), values.clone());
}
}
out.insert(SimpleHeader::CONNECTION, vec!["close".to_string()]);
let response = SimpleOutgoingResponse::builder()
.with_status(status)
.with_headers(out)
.with_body(body)
.build()
.map_err(|e| format!("build response: {e}"))?;
Http11::response(response)
.http_render_to_writer(conn)
.map_err(|e| format!("write response: {e}"))?;
conn.flush().map_err(|e| format!("flush response: {e}"))?;
Ok(())
}
pub fn forward_upgrade(
conn: SharedByteBufferStream<RawStream>,
backend: &Arc<BackendRuntime>,
req: &SimpleIncomingRequest,
client_ip: &str,
scheme: &str,
io_mode: ServerIo,
) -> Result<(), String> {
let authority = backend.target().authority();
let addr = authority
.to_socket_addrs()
.map_err(|e| format!("resolve upstream {authority}: {e}"))?
.next()
.ok_or_else(|| format!("no address for upstream {authority}"))?;
let mut upstream = foundation_iogate::connect_completion(addr, io_mode)
.map_err(|e| format!("connect upstream {authority}: {e}"))?;
let mut headers = forward_request_headers(&req.headers, client_ip, scheme);
if let Some(v) = req.headers.get(&SimpleHeader::CONNECTION) {
headers.insert(SimpleHeader::CONNECTION, v.clone());
}
if let Some(v) = req.headers.get(&SimpleHeader::UPGRADE) {
headers.insert(SimpleHeader::UPGRADE, v.clone());
}
let request_bytes =
serialize_request_head(&req.method.to_string(), &req.request_url.url, &headers);
write_head_blocking(&mut upstream, request_bytes.as_bytes())?;
splice_bidirectional(conn, upstream);
Ok(())
}
fn write_head_blocking(upstream: &mut Connection, bytes: &[u8]) -> Result<(), String> {
let deadline = Instant::now() + Duration::from_secs(5);
let mut written = 0;
while written < bytes.len() {
match upstream.write(&bytes[written..]) {
Ok(0) => return Err("upstream closed during upgrade write".to_string()),
Ok(n) => written += n,
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
if Instant::now() > deadline {
return Err("upgrade request write timed out".to_string());
}
std::thread::sleep(Duration::from_millis(1));
}
Err(e) => return Err(format!("write upgrade request: {e}")),
}
}
loop {
match upstream.flush() {
Ok(()) => return Ok(()),
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
if Instant::now() > deadline {
return Err("upgrade request flush timed out".to_string());
}
std::thread::sleep(Duration::from_millis(1));
}
Err(e) => return Err(format!("flush upgrade request: {e}")),
}
}
}
fn serialize_request_head(method: &str, path: &str, headers: &SimpleHeaders) -> String {
let mut out = format!("{method} {path} HTTP/1.1\r\n");
for (name, values) in headers {
for value in values {
out.push_str(&format!("{name}: {value}\r\n"));
}
}
out.push_str("\r\n");
out
}