use anyhow::Result; use bytes::Bytes; use h3::server::RequestStream; use http::{Request, Response, HeaderMap}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use tracing::{debug, info}; use crate::config::Config; use crate::routing::{RoutingCore, RequestInfo}; use crate::file_sync::FileSyncHandler; /// HTTP/3 specific router that handles requests with native h3 types pub struct Http3Router { config: Arc, routing_core: Arc, file_sync_handlers: HashMap>, } impl Http3Router { pub fn new( config: Arc, routing_core: Arc, file_sync_handlers: HashMap>, ) -> Self { Self { config, routing_core, file_sync_handlers, } } /// Handle an HTTP/3 request with native h3 types pub async fn handle_request( &self, req: Request<()>, mut stream: RequestStream, Bytes>, remote_addr: SocketAddr, server_name: String, connection_id: String, ) -> Result<()> { debug!("HTTP/3 request: {} {} (connection: {})", req.method(), req.uri(), connection_id); // Extract request information for routing let request_info = self.extract_request_info(&req, remote_addr); // Read request body let body_bytes = self.read_request_body(&mut stream).await?; // Route the request match self.route_request(&request_info, &server_name).await? { Some(route_result) => { self.handle_routed_request( route_result, req, body_bytes, &mut stream, &request_info, &server_name, ).await?; } None => { // No route found - return 404 self.send_error_response(&mut stream, 404, "Not Found").await?; } } Ok(()) } /// Extract protocol-agnostic request information fn extract_request_info(&self, req: &Request<()>, remote_addr: SocketAddr) -> RequestInfo { let method = req.method().to_string(); let path = req.uri().path().to_string(); // Convert headers to generic format let headers: Vec<(String, String)> = req .headers() .iter() .map(|(name, value)| { ( name.to_string(), value.to_str().unwrap_or("").to_string(), ) }) .collect(); RequestInfo::new(method, path, headers, remote_addr) } /// Read HTTP/3 request body with size limits async fn read_request_body( &self, stream: &mut RequestStream, Bytes>, ) -> Result { let mut body_bytes = Vec::new(); let max_body_size = 10 * 1024 * 1024; // 10MB limit while let Some(chunk) = stream.recv_data().await? { use bytes::Buf; let chunk_bytes = chunk.chunk(); // Check body size limit if body_bytes.len() + chunk_bytes.len() > max_body_size { return Err(anyhow::anyhow!("Request body too large")); } body_bytes.extend_from_slice(chunk_bytes); } Ok(Bytes::from(body_bytes)) } /// Route the request to the appropriate handler async fn route_request( &self, request_info: &RequestInfo, _server_name: &str, ) -> Result> { // Handle ACME challenges first if RoutingCore::is_acme_challenge(&request_info.path) { return Ok(Some(RouteResult::AcmeChallenge)); } // TODO: Implement proper route matching based on configuration // For now, we'll implement basic routing logic // Check for file sync API requests if RoutingCore::is_file_sync_api(&request_info.path) { return Ok(Some(RouteResult::FileSync)); } // Default to reverse proxy for now // In a full implementation, this would match against the configuration Ok(Some(RouteResult::ReverseProxy)) } /// Handle a routed request async fn handle_routed_request( &self, route_result: RouteResult, req: Request<()>, body_bytes: Bytes, stream: &mut RequestStream, Bytes>, request_info: &RequestInfo, server_name: &str, ) -> Result<()> { match route_result { RouteResult::ReverseProxy => { self.handle_reverse_proxy(req, body_bytes, stream, request_info, server_name).await } RouteResult::FileSync => { self.handle_file_sync(req, body_bytes, stream, request_info, server_name).await } RouteResult::StaticFile => { self.handle_static_file(req, stream, request_info).await } RouteResult::AcmeChallenge => { self.handle_acme_challenge(req, stream, request_info).await } } } /// Handle reverse proxy requests using HTTP/3 to HTTP/1.1 translation async fn handle_reverse_proxy( &self, req: Request<()>, _body_bytes: Bytes, stream: &mut RequestStream, Bytes>, _request_info: &RequestInfo, _server_name: &str, ) -> Result<()> { // TODO: Implement HTTP/3 to upstream HTTP/1.1 proxy // This would: // 1. Select upstream using routing_core.select_upstream() // 2. Convert HTTP/3 request to HTTP/1.1 request // 3. Send to upstream using hyper client // 4. Convert HTTP/1.1 response back to HTTP/3 // 5. Record metrics using routing_core.record_upstream_result() info!("HTTP/3 reverse proxy: {} {} -> [upstream]", req.method(), req.uri()); // For now, return a placeholder response self.send_error_response(stream, 501, "HTTP/3 Reverse Proxy Not Yet Implemented").await } /// Handle file sync requests natively in HTTP/3 async fn handle_file_sync( &self, req: Request<()>, _body_bytes: Bytes, stream: &mut RequestStream, Bytes>, _request_info: &RequestInfo, _server_name: &str, ) -> Result<()> { // TODO: Implement native HTTP/3 file sync // This would convert the HTTP/3 request to work with FileSyncHandler info!("HTTP/3 file sync: {} {}", req.method(), req.uri()); // For now, return a placeholder response self.send_error_response(stream, 501, "HTTP/3 File Sync Not Yet Implemented").await } /// Handle static file serving in HTTP/3 async fn handle_static_file( &self, req: Request<()>, stream: &mut RequestStream, Bytes>, _request_info: &RequestInfo, ) -> Result<()> { // TODO: Implement native HTTP/3 static file serving info!("HTTP/3 static file: {} {}", req.method(), req.uri()); // For now, return a placeholder response self.send_error_response(stream, 501, "HTTP/3 Static Files Not Yet Implemented").await } /// Handle ACME challenges in HTTP/3 async fn handle_acme_challenge( &self, req: Request<()>, stream: &mut RequestStream, Bytes>, _request_info: &RequestInfo, ) -> Result<()> { // TODO: Implement HTTP/3 ACME challenge handling info!("HTTP/3 ACME challenge: {} {}", req.method(), req.uri()); // For now, return a placeholder response self.send_error_response(stream, 501, "HTTP/3 ACME Challenge Not Yet Implemented").await } /// Send an HTTP/3 error response async fn send_error_response( &self, stream: &mut RequestStream, Bytes>, status_code: u16, message: &str, ) -> Result<()> { let response = Response::builder() .status(status_code) .header("content-type", "text/plain") .body(())?; stream.send_response(response).await?; stream.send_data(Bytes::from(message.to_string())).await?; stream.finish().await?; Ok(()) } /// Send a successful HTTP/3 response with body async fn send_response( &self, stream: &mut RequestStream, Bytes>, status_code: u16, headers: Vec<(String, String)>, body: Bytes, ) -> Result<()> { let mut response_builder = Response::builder().status(status_code); // Add headers for (name, value) in headers { response_builder = response_builder.header(name, value); } let response = response_builder.body(())?; stream.send_response(response).await?; if !body.is_empty() { stream.send_data(body).await?; } stream.finish().await?; Ok(()) } } /// Normalize HTTP/3 headers for HTTP/1.1 compatibility pub fn normalize_h3_headers(headers: &mut http::HeaderMap) { // Remove HTTP/2+ pseudo-headers if present headers.remove(":method"); headers.remove(":path"); headers.remove(":scheme"); headers.remove(":authority"); // Ensure content-length is set for body requests if !headers.contains_key("content-length") && !headers.contains_key("transfer-encoding") { // Will be set later when we know the body size } // Remove HTTP/3 specific headers that might cause issues headers.remove("alt-svc"); } /// Normalize HTTP/1.1 response headers for HTTP/3 pub fn normalize_response_headers(headers: &mut http::HeaderMap) { // Remove connection-specific headers headers.remove("connection"); headers.remove("upgrade"); headers.remove("proxy-connection"); // HTTP/3 doesn't use transfer-encoding headers.remove("transfer-encoding"); // Ensure proper content-length if not already set if !headers.contains_key("content-length") { // The body handling will set this if needed } } /// Result of routing a request #[derive(Debug, Clone)] enum RouteResult { ReverseProxy, FileSync, StaticFile, AcmeChallenge, } #[cfg(test)] mod tests { use super::*; use std::net::{IpAddr, Ipv4Addr}; fn create_test_request_info() -> RequestInfo { RequestInfo::new( "GET".to_string(), "/test".to_string(), vec![("host".to_string(), "example.com".to_string())], SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), ) } #[tokio::test] async fn test_extract_request_info() { use http::Method; let req = Request::builder() .method(Method::GET) .uri("/test/path") .header("host", "example.com") .header("user-agent", "test-agent") .body(()) .unwrap(); let remote_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); // We can't easily test this without creating a full Http3Router // but we can test the logic conceptually assert_eq!(req.method(), "GET"); assert_eq!(req.uri().path(), "/test/path"); } #[test] fn test_route_patterns() { // Test ACME challenge detection let acme_info = RequestInfo::new( "GET".to_string(), "/.well-known/acme-challenge/test".to_string(), vec![], SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), ); assert!(RoutingCore::is_acme_challenge(&acme_info.path)); // Test file sync API detection let api_info = RequestInfo::new( "POST".to_string(), "/api/files/upload".to_string(), vec![], SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), ); assert!(RoutingCore::is_file_sync_api(&api_info.path)); } }