From 2be7cee99d9f67dd63fd9542f0ce044cf8ead4e5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:10:20 +0000 Subject: [PATCH] fix: resolve all compilation errors, clippy warnings, and formatting issues - Add `ring` feature to `rustls` for TLS support. - Fix 'temporary value dropped while borrowed' in `src/main.rs`. - Remove unused imports and variables across multiple files. - Fix clippy warnings regarding doc comments, range checks, and string comparisons. - Apply standard formatting. Co-authored-by: thetuxuser <234900867+thetuxuser@users.noreply.github.com> --- Cargo.toml | 2 +- src/config.rs | 60 +++++++++++++++++++++++++++++------------ src/error.rs | 14 +++++----- src/health_check.rs | 36 ++++++++++++++----------- src/load_balancer.rs | 36 ++++++++++++++----------- src/main.rs | 15 +++++++---- src/proxy.rs | 64 +++++++++++++++++++++++++------------------- src/rate_limit.rs | 12 ++++----- src/security.rs | 20 +++++++------- src/ssl.rs | 39 +++++++++++++++------------ src/static_files.rs | 38 +++++++++++++------------- 11 files changed, 195 insertions(+), 141 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4ac6f6d..72b9a1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ hyper-util = { version = "0.1", features = ["full"] } http-body-util = "0.1" # TLS support -rustls = "0.23" +rustls = { version = "0.23", features = ["ring"] } tokio-rustls = "0.26" rustls-pemfile = "2" diff --git a/src/config.rs b/src/config.rs index a252b1d..f82a6d8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ -/// curf configuration types and loader. -/// -/// curf is configured with a single YAML file (default: curf.yml). -/// Run `curf --config /path/to/curf.yml` to specify a different path. +//! curf configuration types and loader. +//! +//! curf is configured with a single YAML file (default: curf.yml). +//! Run `curf --config /path/to/curf.yml` to specify a different path. use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -246,19 +246,45 @@ pub struct ExtraHeader { // ─── Default value helpers ──────────────────────────────────────────────────── -fn default_http_port() -> u16 { 80 } -fn default_https_port() -> u16 { 443 } -fn default_timeout() -> u64 { 30 } -fn default_max_connections() -> usize { 10_000 } -fn default_max_connections_per_ip() -> usize { 100 } -fn default_rps() -> u32 { 100 } -fn default_burst() -> u32 { 200 } -fn default_true() -> bool { true } -fn default_false() -> bool { false } -fn default_tls_failures() -> u32 { 10 } -fn default_hc_interval() -> u64 { 15 } -fn default_hc_timeout() -> u64 { 5 } -fn default_hc_path() -> String { "/".to_string() } +fn default_http_port() -> u16 { + 80 +} +fn default_https_port() -> u16 { + 443 +} +fn default_timeout() -> u64 { + 30 +} +fn default_max_connections() -> usize { + 10_000 +} +fn default_max_connections_per_ip() -> usize { + 100 +} +fn default_rps() -> u32 { + 100 +} +fn default_burst() -> u32 { + 200 +} +fn default_true() -> bool { + true +} +fn default_false() -> bool { + false +} +fn default_tls_failures() -> u32 { + 10 +} +fn default_hc_interval() -> u64 { + 15 +} +fn default_hc_timeout() -> u64 { + 5 +} +fn default_hc_path() -> String { + "/".to_string() +} fn default_index_files() -> Vec { vec!["index.html".to_string()] } diff --git a/src/error.rs b/src/error.rs index ca297c0..e946ed7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -/// Simple error response helpers. +//! Simple error response helpers. use http_body_util::{combinators::BoxBody, BodyExt, Full}; use hyper::body::Bytes; @@ -19,7 +19,11 @@ pub fn error_response(status: StatusCode, message: &str) -> Response Response> { +pub fn html_error( + status: StatusCode, + title: &str, + body: &str, +) -> Response> { let html = format!( r#" @@ -41,10 +45,6 @@ pub fn html_error(status: StatusCode, title: &str, body: &str) -> Response bool { - let result = timeout( - Duration::from_secs(timeout_secs), - do_get(url), - ) - .await; + let result = timeout(Duration::from_secs(timeout_secs), do_get(url)).await; match result { - Ok(Ok(status)) => status >= 200 && status < 300, + Ok(Ok(status)) => (200..300).contains(&status), Ok(Err(e)) => { debug!("Health check error for {}: {}", url, e); false @@ -107,7 +110,10 @@ async fn do_get(url: &str) -> Result().unwrap_or(80)) + ( + &host_port[..idx], + host_port[idx + 1..].parse::().unwrap_or(80), + ) } else { (host_port, 80u16) }; diff --git a/src/load_balancer.rs b/src/load_balancer.rs index 509bf57..36238be 100644 --- a/src/load_balancer.rs +++ b/src/load_balancer.rs @@ -1,12 +1,12 @@ -/// Load balancer for curf. -/// -/// Supports three strategies: -/// round_robin — cycle through backends evenly (default) -/// least_connections — pick the backend with fewest active requests -/// ip_hash — always send the same client IP to the same backend -/// -/// A simple circuit-breaker per backend opens after 5 consecutive failures -/// and resets after 30 seconds, allowing the backend a chance to recover. +//! Load balancer for curf. +//! +//! Supports three strategies: +//! round_robin — cycle through backends evenly (default) +//! least_connections — pick the backend with fewest active requests +//! ip_hash — always send the same client IP to the same backend +//! +//! A simple circuit-breaker per backend opens after 5 consecutive failures +//! and resets after 30 seconds, allowing the backend a chance to recover. use crate::config::{DomainConfig, LoadBalance}; use dashmap::DashMap; @@ -16,10 +16,9 @@ use std::sync::{ Arc, }; use std::time::{SystemTime, UNIX_EPOCH}; -use tokio::sync::RwLock; -const CB_THRESHOLD: usize = 5; // failures before circuit opens -const CB_RESET_SECS: u64 = 30; // seconds before circuit half-opens +const CB_THRESHOLD: usize = 5; // failures before circuit opens +const CB_RESET_SECS: u64 = 30; // seconds before circuit half-opens // ─── Backend ────────────────────────────────────────────────────────────────── @@ -71,8 +70,7 @@ impl Backend { pub fn record_failure(&self) { let f = self.failures.fetch_add(1, Ordering::Relaxed) + 1; - self.last_failure - .store(unix_now(), Ordering::Relaxed); + self.last_failure.store(unix_now(), Ordering::Relaxed); if f >= CB_THRESHOLD { self.circuit_open.store(true, Ordering::Relaxed); } @@ -117,7 +115,11 @@ pub struct LoadBalancer { impl LoadBalancer { pub fn new(cfg: &DomainConfig) -> Self { - let backends = cfg.backends.iter().map(|u| Arc::new(Backend::new(u))).collect(); + let backends = cfg + .backends + .iter() + .map(|u| Arc::new(Backend::new(u))) + .collect(); Self { backends, strategy: cfg.load_balance.clone(), @@ -207,7 +209,9 @@ pub struct LoadBalancerManager { impl LoadBalancerManager { pub fn new() -> Self { - Self { map: DashMap::new() } + Self { + map: DashMap::new(), + } } pub fn add_domain(&self, domain: String, lb: Arc) { diff --git a/src/main.rs b/src/main.rs index 41f9026..1147dc5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -/// curf — a simple, fast reverse proxy and web server -/// https://github.com/thetuxuser/curf +//! curf — a simple, fast reverse proxy and web server +//! https://github.com/thetuxuser/curf mod config; mod error; @@ -220,7 +220,10 @@ async fn serve_http( // Per-IP connection cap if security.check_and_acquire_connection(peer.ip()).is_err() { - warn!("Per-IP connection limit reached for {} — dropping", peer.ip()); + warn!( + "Per-IP connection limit reached for {} — dropping", + peer.ip() + ); continue; } @@ -242,7 +245,8 @@ async fn serve_http( async move { proxy.handle(peer, req, false).await } }); - let conn = auto::Builder::new(TokioExecutor::new()).serve_connection(io, svc); + let builder = auto::Builder::new(TokioExecutor::new()); + let conn = builder.serve_connection(io, svc); if let Err(e) = timeout(Duration::from_secs(60), conn).await { warn!("HTTP connection timeout from {}: {:?}", peer, e); } @@ -326,7 +330,8 @@ async fn serve_https( async move { proxy.handle(peer, req, true).await } }); - let conn = auto::Builder::new(TokioExecutor::new()).serve_connection(io, svc); + let builder = auto::Builder::new(TokioExecutor::new()); + let conn = builder.serve_connection(io, svc); if let Err(e) = timeout(Duration::from_secs(60), conn).await { warn!("HTTPS connection timeout from {}: {:?}", peer, e); } diff --git a/src/proxy.rs b/src/proxy.rs index 232cab4..76fbdbf 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,12 +1,12 @@ -/// Core request handler for curf. -/// -/// For each incoming request it: -/// 1. Runs security checks (WAF, rate limit, blocked IPs) -/// 2. Checks if the domain should redirect HTTP → HTTPS -/// 3. Tries to serve from static files (if configured) -/// 4. Forwards to a backend via the load balancer -/// 5. Handles WebSocket upgrades transparently -/// 6. Injects any configured extra response headers +//! Core request handler for curf. +//! +//! For each incoming request it: +//! 1. Runs security checks (WAF, rate limit, blocked IPs) +//! 2. Checks if the domain should redirect HTTP → HTTPS +//! 3. Tries to serve from static files (if configured) +//! 4. Forwards to a backend via the load balancer +//! 5. Handles WebSocket upgrades transparently +//! 6. Injects any configured extra response headers use crate::config::DomainConfig; use crate::error::{error_response, html_error}; @@ -16,12 +16,12 @@ use crate::security::SecurityChecker; use crate::static_files::StaticFileServer; use anyhow::Result; -use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; +use http_body_util::{combinators::BoxBody, BodyExt, Empty}; use hyper::body::{Bytes, Incoming}; use hyper::client::conn::http1; use hyper::header::{self, HeaderValue}; use hyper::upgrade; -use hyper::{Method, Request, Response, StatusCode, Uri}; +use hyper::{Request, Response, StatusCode, Uri}; use hyper_util::rt::TokioIo; use std::collections::HashMap; use std::net::SocketAddr; @@ -59,11 +59,7 @@ impl ProxyHandler { if let Some(sf) = &cfg.static_files { static_servers.insert( domain.clone(), - StaticFileServer::new( - &sf.root, - sf.index.clone(), - sf.autoindex, - ), + StaticFileServer::new(&sf.root, sf.index.clone(), sf.autoindex), ); } } @@ -104,7 +100,10 @@ impl ProxyHandler { // ── 1. Rate limiting ───────────────────────────────────────────────── if !self.rate_limiter.is_allowed(client_ip) { warn!("Rate limit exceeded for {}", client_ip); - return Ok(error_response(StatusCode::TOO_MANY_REQUESTS, "Too Many Requests")); + return Ok(error_response( + StatusCode::TOO_MANY_REQUESTS, + "Too Many Requests", + )); } // ── 2. WAF / security checks ───────────────────────────────────────── @@ -113,7 +112,10 @@ impl ProxyHandler { .get(header::USER_AGENT) .and_then(|v| v.to_str().ok()); if let Err(reason) = self.security.check_request(&path, query.as_deref(), ua) { - warn!("Security block for {} on {}{}: {}", client_ip, host, path, reason); + warn!( + "Security block for {} on {}{}: {}", + client_ip, host, path, reason + ); return Ok(html_error(StatusCode::FORBIDDEN, "Forbidden", reason)); } @@ -141,7 +143,10 @@ impl ProxyHandler { Ok(r) => Ok(r), Err(e) => { error!("WebSocket proxy error: {}", e); - Ok(error_response(StatusCode::BAD_GATEWAY, "WebSocket proxy error")) + Ok(error_response( + StatusCode::BAD_GATEWAY, + "WebSocket proxy error", + )) } }; } @@ -162,7 +167,11 @@ impl ProxyHandler { } Err(e) => { error!("Proxy error for {} {}: {}", host, path, e); - Ok(html_error(StatusCode::BAD_GATEWAY, "Bad Gateway", "The upstream server is unreachable.")) + Ok(html_error( + StatusCode::BAD_GATEWAY, + "Bad Gateway", + "The upstream server is unreachable.", + )) } } } @@ -181,7 +190,7 @@ impl ProxyHandler { .get(host) .ok_or_else(|| anyhow::anyhow!("No backend for '{}'", host))?; - let (backend_url, guard) = lb + let (backend_url, _guard) = lb .select(Some(peer.ip())) .ok_or_else(|| anyhow::anyhow!("All backends for '{}' are down", host))?; @@ -289,7 +298,10 @@ impl ProxyHandler { .map_err(|e| anyhow::anyhow!("WS handshake failed to {}: {}", backend_addr, e))?; if backend_resp.status() != StatusCode::SWITCHING_PROTOCOLS { - anyhow::bail!("Backend rejected WebSocket upgrade: {}", backend_resp.status()); + anyhow::bail!( + "Backend rejected WebSocket upgrade: {}", + backend_resp.status() + ); } // Capture backend upgrade future @@ -344,10 +356,7 @@ fn add_forwarding_headers(req: &mut Request, peer: SocketAddr, host: & let headers = req.headers_mut(); // X-Forwarded-For — append this hop's IP - let xff = if let Some(existing) = headers - .get("x-forwarded-for") - .and_then(|v| v.to_str().ok()) - { + let xff = if let Some(existing) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) { format!("{}, {}", existing, peer.ip()) } else { peer.ip().to_string() @@ -360,8 +369,7 @@ fn add_forwarding_headers(req: &mut Request, peer: SocketAddr, host: & // X-Real-IP — always the direct connecting client IP headers.insert( "x-real-ip", - HeaderValue::from_str(&peer.ip().to_string()) - .unwrap_or(HeaderValue::from_static("")), + HeaderValue::from_str(&peer.ip().to_string()).unwrap_or(HeaderValue::from_static("")), ); // X-Forwarded-Host — original Host header diff --git a/src/rate_limit.rs b/src/rate_limit.rs index e800535..63878a7 100644 --- a/src/rate_limit.rs +++ b/src/rate_limit.rs @@ -1,9 +1,9 @@ -/// Token-bucket rate limiter for curf. -/// -/// Each IP gets a separate bucket. Tokens refill at `rps` tokens/second. -/// The bucket can hold up to `burst` tokens, allowing short bursts. -/// -/// A value of 0 for `rps` disables rate limiting entirely. +//! Token-bucket rate limiter for curf. +//! +//! Each IP gets a separate bucket. Tokens refill at `rps` tokens/second. +//! The bucket can hold up to `burst` tokens, allowing short bursts. +//! +//! A value of 0 for `rps` disables rate limiting entirely. use dashmap::DashMap; use std::net::IpAddr; diff --git a/src/security.rs b/src/security.rs index c874c94..94366ec 100644 --- a/src/security.rs +++ b/src/security.rs @@ -1,16 +1,15 @@ -/// Security module for curf. -/// -/// Provides: -/// - Per-IP connection tracking (prevents connection exhaustion from one IP) -/// - Basic WAF: blocks SQLi, XSS and path-traversal patterns in URLs -/// - TLS abuse detection: blocks IPs with too many failed handshakes -/// - Optional: block requests with no User-Agent +//! Security module for curf. +//! +//! Provides: +//! - Per-IP connection tracking (prevents connection exhaustion from one IP) +//! - Basic WAF: blocks SQLi, XSS and path-traversal patterns in URLs +//! - TLS abuse detection: blocks IPs with too many failed handshakes +//! - Optional: block requests with no User-Agent use crate::config::SecurityFlags; use dashmap::DashMap; use std::net::IpAddr; use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::Arc; use tracing::warn; // ─── SecurityChecker ───────────────────────────────────────────────────────── @@ -65,7 +64,8 @@ impl SecurityChecker { if !self.flags.block_tls_abusers { return; } - let entry = self.tls_failures + let entry = self + .tls_failures .entry(ip) .or_insert_with(|| AtomicU32::new(0)); let count = entry.fetch_add(1, Ordering::Relaxed) + 1; @@ -85,7 +85,7 @@ impl SecurityChecker { query: Option<&str>, user_agent: Option<&str>, ) -> Result<(), &'static str> { - let ip_blocked_sentinel = "blocked"; + let _ip_blocked_sentinel = "blocked"; // Block empty User-Agent if the flag is on if self.flags.block_empty_user_agent { diff --git a/src/ssl.rs b/src/ssl.rs index 2442338..49a2623 100644 --- a/src/ssl.rs +++ b/src/ssl.rs @@ -1,8 +1,8 @@ -/// TLS certificate manager for curf. -/// -/// Supports multiple domains via SNI (Server Name Indication). -/// Each domain gets its own certificate; the right one is automatically -/// picked based on the TLS ClientHello. +//! TLS certificate manager for curf. +//! +//! Supports multiple domains via SNI (Server Name Indication). +//! Each domain gets its own certificate; the right one is automatically +//! picked based on the TLS ClientHello. use anyhow::{Context, Result}; use rustls::crypto::ring; @@ -38,7 +38,11 @@ impl SslManager { pub fn add_domain(&mut self, domain: String, cert_path: &str, key_path: &str) -> Result<()> { let (certs, key) = load_cert_and_key(cert_path, key_path) .with_context(|| format!("Failed to load TLS materials for '{}'", domain))?; - self.domains.push(DomainCert { name: domain, certs, key }); + self.domains.push(DomainCert { + name: domain, + certs, + key, + }); Ok(()) } @@ -55,12 +59,11 @@ impl SslManager { .with_context(|| format!("Failed to register cert for '{}'", d.name))?; } - let mut cfg = - ServerConfig::builder_with_provider(Arc::new(ring::default_provider())) - .with_safe_default_protocol_versions() - .expect("Failed to configure TLS protocol versions") - .with_no_client_auth() - .with_cert_resolver(Arc::new(resolver)); + let mut cfg = ServerConfig::builder_with_provider(Arc::new(ring::default_provider())) + .with_safe_default_protocol_versions() + .expect("Failed to configure TLS protocol versions") + .with_no_client_auth() + .with_cert_resolver(Arc::new(resolver)); // Advertise HTTP/2 and HTTP/1.1 via ALPN cfg.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; @@ -75,7 +78,9 @@ impl SslManager { /// Returns the TLS acceptor. Panics if `build()` was not called first. pub fn acceptor(&self) -> &TlsAcceptor { - self.acceptor.as_ref().expect("SslManager: build() not called") + self.acceptor + .as_ref() + .expect("SslManager: build() not called") } } @@ -86,8 +91,8 @@ fn load_cert_and_key( key_path: &str, ) -> Result<(Vec>, PrivateKeyDer<'static>)> { // Load certificate chain - let cert_file = File::open(cert_path) - .with_context(|| format!("Cannot open cert file '{}'", cert_path))?; + let cert_file = + File::open(cert_path).with_context(|| format!("Cannot open cert file '{}'", cert_path))?; let mut cert_reader = BufReader::new(cert_file); let certs: Vec = rustls_pemfile::certs(&mut cert_reader) .collect::, _>>() @@ -97,8 +102,8 @@ fn load_cert_and_key( } // Load private key - let key_file = File::open(key_path) - .with_context(|| format!("Cannot open key file '{}'", key_path))?; + let key_file = + File::open(key_path).with_context(|| format!("Cannot open key file '{}'", key_path))?; let mut key_reader = BufReader::new(key_file); let key = load_key(&mut key_reader) .with_context(|| format!("Failed to load private key from '{}'", key_path))?; diff --git a/src/static_files.rs b/src/static_files.rs index 555595f..85f161f 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -1,14 +1,14 @@ -/// Static file server for curf. -/// -/// Serves files from a local directory. -/// Features: -/// - Path sanitisation — prevents directory traversal (../) -/// - Index files — looks for index.html (configurable) in directories -/// - ETag / If-None-Match caching -/// - Last-Modified / If-Modified-Since caching -/// - Accurate MIME types -/// - Optional directory listing (autoindex) -/// - HEAD request support +//! Static file server for curf. +//! +//! Serves files from a local directory. +//! Features: +//! - Path sanitisation — prevents directory traversal (../) +//! - Index files — looks for index.html (configurable) in directories +//! - ETag / If-None-Match caching +//! - Last-Modified / If-Modified-Since caching +//! - Accurate MIME types +//! - Optional directory listing (autoindex) +//! - HEAD request support use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; use hyper::body::Bytes; @@ -16,7 +16,7 @@ use hyper::{header, HeaderMap, Method, Response, StatusCode}; use std::path::{Component, Path, PathBuf}; use std::time::SystemTime; use tokio::fs; -use tracing::{debug, warn}; +use tracing::warn; pub struct StaticFileServer { root: PathBuf, @@ -194,16 +194,16 @@ impl StaticFileServer { Some(result) } - async fn directory_listing(&self, dir: &Path, url_path: &str) -> Response> { + async fn directory_listing( + &self, + dir: &Path, + url_path: &str, + ) -> Response> { let mut entries = Vec::new(); if let Ok(mut read_dir) = fs::read_dir(dir).await { while let Ok(Some(entry)) = read_dir.next_entry().await { let name = entry.file_name().to_string_lossy().into_owned(); - let is_dir = entry - .file_type() - .await - .map(|t| t.is_dir()) - .unwrap_or(false); + let is_dir = entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false); entries.push((name, is_dir)); } } @@ -217,7 +217,7 @@ impl StaticFileServer { base, base ); - if base != "" { + if !base.is_empty() { html.push_str("../"); } for (name, is_dir) in &entries {