init
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{ConnectInfo, Request, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use color_eyre::Result;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::protocol::{StreamHeader, write_stream_header};
|
||||
use crate::relay::QuicBiStream;
|
||||
use crate::server::state::ServerState;
|
||||
|
||||
pub async fn run(
|
||||
addr: SocketAddr,
|
||||
state: Arc<ServerState>,
|
||||
cancel: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let app = Router::new().fallback(proxy_handler).with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
info!(%addr, "HTTP server listening");
|
||||
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(async move { cancel.cancelled().await })
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn proxy_handler(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
ConnectInfo(peer): ConnectInfo<SocketAddr>,
|
||||
req: Request<Body>,
|
||||
) -> Response {
|
||||
match do_proxy(state, peer, req).await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
warn!("HTTP proxy error: {e:#}");
|
||||
(StatusCode::NOT_FOUND, format!("{e}")).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_proxy(
|
||||
state: Arc<ServerState>,
|
||||
peer: SocketAddr,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response> {
|
||||
// Use X-Forwarded-For if present (from Traefik), otherwise direct peer
|
||||
let peer_str = req
|
||||
.headers()
|
||||
.get("x-forwarded-for")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v.split(',').next().unwrap_or(v).trim().to_string())
|
||||
.unwrap_or_else(|| peer.to_string());
|
||||
|
||||
// Extract subdomain from Host header
|
||||
let host_header = req
|
||||
.headers()
|
||||
.get("host")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or_else(|| color_eyre::eyre::eyre!("missing Host header"))?;
|
||||
|
||||
// Strip port if present (e.g. "myapp.bore.localhost:8080" -> "myapp.bore.localhost")
|
||||
let host = host_header.split(':').next().unwrap_or(host_header);
|
||||
|
||||
let suffix = format!(".{}", state.base_domain);
|
||||
let subdomain = host
|
||||
.strip_suffix(&suffix)
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| color_eyre::eyre::eyre!("no tunnel found for host {host}"))?
|
||||
.to_string();
|
||||
|
||||
// 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 in state"))?;
|
||||
|
||||
let connection = entry.connection.clone();
|
||||
drop(entry);
|
||||
|
||||
// Open QUIC stream to client
|
||||
let (mut quic_send, quic_recv) = connection.open_bi().await?;
|
||||
write_stream_header(
|
||||
&mut quic_send,
|
||||
&StreamHeader {
|
||||
tunnel_id,
|
||||
peer_addr: peer_str,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Use hyper to proxy the HTTP request over the QUIC stream
|
||||
let io = TokioIo::new(QuicBiStream {
|
||||
send: quic_send,
|
||||
recv: quic_recv,
|
||||
});
|
||||
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = conn.await {
|
||||
warn!("HTTP proxy connection error: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
let resp = sender.send_request(req).await?;
|
||||
Ok(resp.map(Body::new))
|
||||
}
|
||||
Reference in New Issue
Block a user