Files
bore/src/server/endpoints/http.rs
T
2026-03-26 23:00:10 +04:00

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(())
}