115 lines
3.5 KiB
Rust
115 lines
3.5 KiB
Rust
use std::net::SocketAddr;
|
|
use std::sync::Arc;
|
|
|
|
use color_eyre::Result;
|
|
use tokio::net::{TcpListener, TcpStream};
|
|
use tokio_util::sync::CancellationToken;
|
|
use tracing::{info, warn};
|
|
|
|
use crate::protocol::{StreamHeader, write_stream_header};
|
|
use crate::relay::{QuicBiStream, relay};
|
|
use crate::server::state::ServerState;
|
|
|
|
pub async fn run(
|
|
addr: SocketAddr,
|
|
state: Arc<ServerState>,
|
|
cancel: CancellationToken,
|
|
) -> Result<()> {
|
|
let listener = TcpListener::bind(addr).await?;
|
|
info!(%addr, "HTTP server listening");
|
|
|
|
loop {
|
|
tokio::select! {
|
|
_ = cancel.cancelled() => break,
|
|
accepted = listener.accept() => {
|
|
let (stream, peer) = match accepted {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
warn!("HTTP accept error: {e}");
|
|
continue;
|
|
}
|
|
};
|
|
let state = state.clone();
|
|
tokio::spawn(async move {
|
|
if let Err(e) = handle_connection(stream, peer, state).await {
|
|
warn!("HTTP connection error: {e:#}");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Extract the Host header from raw HTTP bytes without consuming them.
|
|
/// Returns (subdomain, peer_display_string).
|
|
fn extract_host_from_headers(buf: &[u8], base_domain: &str) -> Option<String> {
|
|
let header_str = std::str::from_utf8(buf).ok()?;
|
|
|
|
// Find Host header (case-insensitive)
|
|
for line in header_str.split("\r\n").skip(1) {
|
|
if line.is_empty() {
|
|
break;
|
|
}
|
|
if let Some(value) = line.strip_prefix("Host:").or_else(|| line.strip_prefix("host:")) {
|
|
let host = value.trim();
|
|
// Strip port if present
|
|
let host = host.split(':').next().unwrap_or(host);
|
|
let suffix = format!(".{base_domain}");
|
|
let subdomain = host.strip_suffix(&suffix).filter(|s| !s.is_empty())?;
|
|
return Some(subdomain.to_string());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
async fn handle_connection(
|
|
stream: TcpStream,
|
|
peer: SocketAddr,
|
|
state: Arc<ServerState>,
|
|
) -> Result<()> {
|
|
// Peek at the beginning of the HTTP request to extract the Host header.
|
|
// We read into a buffer but then send ALL of it through the tunnel.
|
|
let mut buf = vec![0u8; 8192];
|
|
let n = stream.peek(&mut buf).await?;
|
|
let buf = &buf[..n];
|
|
|
|
let subdomain = extract_host_from_headers(buf, &state.base_domain)
|
|
.ok_or_else(|| color_eyre::eyre::eyre!("no matching Host header in HTTP request"))?;
|
|
|
|
// Look up the tunnel
|
|
let tunnel_id = *state
|
|
.http_routes
|
|
.get(&subdomain)
|
|
.ok_or_else(|| color_eyre::eyre::eyre!("no tunnel for subdomain {subdomain}"))?;
|
|
|
|
let entry = state
|
|
.tunnels
|
|
.get(&tunnel_id)
|
|
.ok_or_else(|| color_eyre::eyre::eyre!("tunnel {tunnel_id} not found"))?;
|
|
|
|
let connection = entry.connection.clone();
|
|
drop(entry);
|
|
|
|
// Open QUIC stream and write header
|
|
let (mut quic_send, quic_recv) = connection.open_bi().await?;
|
|
write_stream_header(
|
|
&mut quic_send,
|
|
&StreamHeader {
|
|
tunnel_id,
|
|
peer_addr: peer.to_string(),
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
// Raw bidirectional relay — TCP stream carries the full HTTP conversation
|
|
// including upgrades (WebSocket), SSE, chunked responses, etc.
|
|
let quic_stream = QuicBiStream {
|
|
send: quic_send,
|
|
recv: quic_recv,
|
|
};
|
|
relay(stream, quic_stream).await?;
|
|
|
|
Ok(())
|
|
}
|