
- Add comprehensive HTTP/1.1 and HTTP/2 server support - Implement reverse proxy with load balancing - Add static file serving with proper MIME types - Create multi-port server management - Add TLS/HTTPS support with SNI and ACME - Implement authentication middleware framework - Add advanced routing and matchers system - Create file sync service foundation - Add metrics collection and health monitoring - Implement simple configuration format - Successfully tested with production-equivalent config Core features working: - Reverse proxy to localhost:3000 ✓ - Static file serving ✓ - Multi-port configuration ✓ - CORS headers and security ✓ - Simple config format detection ✓ Ready for production testing as Caddy replacement.
150 lines
4.8 KiB
Rust
150 lines
4.8 KiB
Rust
use anyhow::Result;
|
|
use http::{Request, Response, StatusCode, HeaderValue};
|
|
use http_body_util::Full;
|
|
use hyper::body::Bytes;
|
|
use std::collections::HashMap;
|
|
use tracing::debug;
|
|
|
|
/// Handler for static responses (redirects, custom responses, etc.)
|
|
#[derive(Debug, Clone)]
|
|
pub struct StaticResponseHandler {
|
|
pub status_code: u16,
|
|
pub body: Option<String>,
|
|
pub headers: HashMap<String, String>,
|
|
}
|
|
|
|
impl StaticResponseHandler {
|
|
/// Create a redirect response
|
|
pub fn redirect(location: String, status_code: Option<u16>) -> Self {
|
|
let mut headers = HashMap::new();
|
|
headers.insert("location".to_string(), location);
|
|
|
|
Self {
|
|
status_code: status_code.unwrap_or(301),
|
|
body: None,
|
|
headers,
|
|
}
|
|
}
|
|
|
|
/// Create a custom response with body
|
|
pub fn with_body(status_code: u16, body: String) -> Self {
|
|
Self {
|
|
status_code,
|
|
body: Some(body),
|
|
headers: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Add a header to the response
|
|
pub fn with_header(mut self, name: String, value: String) -> Self {
|
|
self.headers.insert(name, value);
|
|
self
|
|
}
|
|
|
|
/// Add multiple headers
|
|
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
|
|
self.headers.extend(headers);
|
|
self
|
|
}
|
|
|
|
/// Generate the HTTP response
|
|
pub fn generate_response(&self) -> Result<Response<Full<Bytes>>> {
|
|
let status = StatusCode::from_u16(self.status_code)
|
|
.map_err(|e| anyhow::anyhow!("Invalid status code {}: {}", self.status_code, e))?;
|
|
|
|
let mut response_builder = Response::builder().status(status);
|
|
|
|
// Add headers
|
|
for (name, value) in &self.headers {
|
|
let header_name = name.to_lowercase();
|
|
let header_value = HeaderValue::from_str(value)
|
|
.map_err(|e| anyhow::anyhow!("Invalid header value for '{}': {}", name, e))?;
|
|
response_builder = response_builder.header(header_name, header_value);
|
|
}
|
|
|
|
// Set body
|
|
let body = self.body.as_deref().unwrap_or("");
|
|
response_builder
|
|
.body(Full::new(Bytes::from(body.to_string())))
|
|
.map_err(|e| anyhow::anyhow!("Failed to build response: {}", e))
|
|
}
|
|
|
|
/// Handle a request with this static response
|
|
pub async fn handle_request<T>(&self, _req: Request<T>) -> Result<Response<Full<Bytes>>> {
|
|
debug!("Serving static response: {} {}", self.status_code,
|
|
self.body.as_deref().unwrap_or("<no body>"));
|
|
self.generate_response()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use http::Method;
|
|
|
|
fn create_test_request<T>(method: Method, path: &str, body: T) -> Request<T> {
|
|
Request::builder()
|
|
.method(method)
|
|
.uri(path)
|
|
.body(body)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_redirect_handler() {
|
|
let handler = StaticResponseHandler::redirect(
|
|
"https://example.com".to_string(),
|
|
Some(302)
|
|
);
|
|
|
|
assert_eq!(handler.status_code, 302);
|
|
assert_eq!(handler.headers.get("location").unwrap(), "https://example.com");
|
|
assert!(handler.body.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_custom_response_handler() {
|
|
let handler = StaticResponseHandler::with_body(
|
|
410,
|
|
"Service has been migrated".to_string()
|
|
);
|
|
|
|
assert_eq!(handler.status_code, 410);
|
|
assert_eq!(handler.body.as_ref().unwrap(), "Service has been migrated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_response_with_headers() {
|
|
let handler = StaticResponseHandler::with_body(200, "OK".to_string())
|
|
.with_header("content-type".to_string(), "application/json".to_string())
|
|
.with_header("cache-control".to_string(), "no-cache".to_string());
|
|
|
|
assert_eq!(handler.headers.len(), 2);
|
|
assert_eq!(handler.headers.get("content-type").unwrap(), "application/json");
|
|
assert_eq!(handler.headers.get("cache-control").unwrap(), "no-cache");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_generate_response() {
|
|
let handler = StaticResponseHandler::redirect(
|
|
"https://example.com".to_string(),
|
|
Some(301)
|
|
);
|
|
|
|
let response = handler.generate_response().unwrap();
|
|
assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY);
|
|
assert_eq!(
|
|
response.headers().get("location").unwrap(),
|
|
"https://example.com"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_handle_request() {
|
|
let handler = StaticResponseHandler::with_body(200, "Hello World".to_string());
|
|
let req = create_test_request(Method::GET, "/test", "body");
|
|
|
|
let response = handler.handle_request(req).await.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
}
|
|
} |