use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::tcp::OwnedWriteHalf;
use tokio::net::{TcpListener, TcpStream};
use tokio::task::JoinHandle;
use super::{Relay, pump, serve};
const BAD_GATEWAY: &[u8] = b"HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n";
const MAX_HEAD: usize = 64 * 1024;
const MAX_LINE: u64 = 8 * 1024;
const WINRM_PORT: u16 = 5985;
#[derive(Clone, Debug)]
pub(crate) struct Targets {
esxi_host: String,
port: u16,
}
impl Targets {
pub(crate) fn new(esxi_host: &str) -> Targets {
Targets {
esxi_host: esxi_host.trim_start_matches('[').trim_end_matches(']').to_ascii_lowercase(),
port: WINRM_PORT,
}
}
#[cfg(all(test, target_os = "macos"))]
fn only(host: &str, port: u16) -> Targets {
Targets { port, ..Targets::new(host) }
}
pub(crate) fn allows(&self, target: &str) -> bool {
let Some((host, port)) = target.rsplit_once(':') else { return false };
if port.parse::<u16>() != Ok(self.port) {
return false;
}
let host = host.trim_start_matches('[').trim_end_matches(']').to_ascii_lowercase();
host == self.esxi_host || host.parse::<std::net::IpAddr>().is_ok_and(on_local_network)
}
}
fn on_local_network(ip: std::net::IpAddr) -> bool {
match ip {
std::net::IpAddr::V4(v4) => v4.is_private() || v4.is_link_local(),
std::net::IpAddr::V6(v6) => {
let first = v6.segments()[0];
(first & 0xfe00) == 0xfc00 || (first & 0xffc0) == 0xfe80
}
}
}
pub(crate) async fn start_http_proxy(targets: Targets) -> std::io::Result<Relay> {
let listener = TcpListener::bind(("127.0.0.1", 0)).await?;
let local = listener.local_addr()?.port();
let task = serve(listener, move |inbound| {
let targets = targets.clone();
async move {
if let Err(e) = proxy_connection(inbound, &targets).await {
tracing::debug!(error = %e, "a proxied connection ended early");
}
}
});
Ok(Relay { port: local, task })
}
pub(super) fn split_target(uri: &str) -> Option<(String, String)> {
let rest = uri.strip_prefix("http://")?;
let (authority, path) = match rest.find(['/', '?', '#']).and_then(|i| rest.split_at_checked(i)) {
Some((authority, path)) if path.starts_with('/') => (authority, path.to_owned()),
Some((authority, query)) => (authority, format!("/{query}")),
None => (rest, "/".to_owned()),
};
if authority.is_empty() {
return None;
}
let authority = if authority.rsplit_once(':').is_some_and(|(_, p)| p.parse::<u16>().is_ok()) && !authority.ends_with(']') {
authority.to_owned()
} else {
format!("{authority}:80")
};
if authority.rsplit_once(':').is_some_and(|(host, _)| host.is_empty()) {
return None;
}
Some((authority, path))
}
pub(super) fn rewrite_head(head: &str) -> Option<(String, String, u64)> {
let mut lines = head.split('\n').map(|l| l.strip_suffix('\r').unwrap_or(l));
let mut first = lines.next()?.splitn(3, ' ');
let (method, uri, version) = (first.next()?, first.next()?, first.next()?);
let (target, path) = split_target(uri)?;
let mut out = format!("{method} {path} {version}\r\n");
let mut len = 0;
for line in lines.filter(|l| !l.is_empty()) {
let lower = line.to_ascii_lowercase();
if lower.starts_with("proxy-connection:") || lower.starts_with("proxy-authorization:") {
continue;
}
if lower.starts_with("transfer-encoding:") {
return None;
}
if let Some(v) = lower.strip_prefix("content-length:") {
len = v.trim_matches([' ', '\t']).parse().ok()?;
}
out.push_str(line);
out.push_str("\r\n");
}
out.push_str("\r\n");
Some((target, out, len))
}
#[derive(Debug, PartialEq)]
enum Head {
Request(String),
TooLong,
Closed,
}
async fn read_head<R: tokio::io::AsyncBufRead + Unpin>(reader: &mut R) -> std::io::Result<Head> {
let mut head = String::new();
loop {
let mut line = String::new();
let n = (&mut *reader).take(MAX_LINE + 1).read_line(&mut line).await?;
if n == 0 {
return Ok(if head.is_empty() { Head::Closed } else { Head::TooLong });
}
if u64::try_from(line.len()).map_or(true, |len| len > MAX_LINE) || !line.ends_with('\n') {
return Ok(Head::TooLong);
}
let blank = line == "\r\n" || line == "\n";
if blank && head.is_empty() {
continue; }
head.push_str(&line);
if head.len() > MAX_HEAD {
return Ok(Head::TooLong);
}
if blank {
return Ok(Head::Request(head));
}
}
}
enum Peer {
Waiting(OwnedWriteHalf),
Connected(Upstream),
}
struct Upstream {
target: String,
to_origin: super::ToLan,
pump: JoinHandle<()>,
}
impl Upstream {
fn start(target: String, (mut from_origin, to_origin, nc): (super::FromLan, super::ToLan, tokio::process::Child), mut client: OwnedWriteHalf) -> Upstream {
let pump = tokio::spawn(async move {
let _nc = nc;
pump(&mut from_origin, &mut client).await;
});
Upstream { target, to_origin, pump }
}
}
impl Peer {
fn may_send_to(&self, target: &str) -> bool {
match self {
Peer::Waiting(_) => true,
Peer::Connected(upstream) => upstream.target == target,
}
}
async fn refuse(self) -> std::io::Result<()> {
if let Peer::Waiting(mut client) = self {
client.write_all(BAD_GATEWAY).await?;
}
Ok(())
}
async fn finish(self) {
if let Peer::Connected(mut upstream) = self {
if let Err(e) = upstream.to_origin.shutdown().await {
tracing::debug!(error = %e, "the origin's connection was already closed");
}
if let Err(e) = upstream.pump.await {
tracing::debug!(error = %e, "the responses' pump ended early");
}
}
}
async fn connected(self, target: String) -> std::result::Result<Upstream, std::io::Result<()>> {
match self {
Peer::Connected(upstream) => Ok(upstream),
Peer::Waiting(client) => match origin(&target) {
Ok(origin) => Ok(Upstream::start(target, origin, client)),
Err(_) => Err(Peer::Waiting(client).refuse().await),
},
}
}
}
fn origin(target: &str) -> std::io::Result<(super::FromLan, super::ToLan, tokio::process::Child)> {
let (host, port) = target.rsplit_once(':').ok_or_else(|| std::io::Error::other("no port"))?;
let port = port.parse().map_err(|_| std::io::Error::other("not a port"))?;
super::connect(host.trim_start_matches('[').trim_end_matches(']'), port)
}
async fn proxy_connection(inbound: TcpStream, targets: &Targets) -> std::io::Result<()> {
let (read_half, write_half) = inbound.into_split();
let mut reader = BufReader::new(read_half);
let mut peer = Peer::Waiting(write_half);
loop {
let head = match read_head(&mut reader).await? {
Head::Request(head) => head,
Head::TooLong => return peer.refuse().await,
Head::Closed => {
peer.finish().await;
return Ok(());
}
};
let parsed = rewrite_head(head.trim_end());
let Some((target, rewritten, body_len)) = parsed.filter(|(t, ..)| targets.allows(t) && peer.may_send_to(t)) else {
return peer.refuse().await;
};
let mut upstream = match peer.connected(target).await {
Ok(upstream) => upstream,
Err(refused) => return refused,
};
upstream.to_origin.write_all(rewritten.as_bytes()).await?;
let mut body = (&mut reader).take(body_len);
tokio::io::copy(&mut body, &mut upstream.to_origin).await?;
peer = Peer::Connected(upstream);
}
}
#[cfg(test)]
mod proptests;
#[cfg(test)]
mod tests;