help@rskworld.in +91 93305 39277
RSK World
  • Home
  • Development
    • Web Development
    • Mobile Apps
    • Software
    • Games
    • Project
  • Technologies
    • Data Science
    • AI Development
    • Cloud Development
    • Blockchain
    • Cyber Security
    • Dev Tools
    • Testing Tools
  • About
  • Contact

Theme Settings

Color Scheme
Display Options
Font Size
100%
Back to Project
RSK World
rust-web-server
/
src
RSK World
rust-web-server
Rust Web Server - High-Performance Async Web Server + WebSocket Support + JWT Authentication + File Upload + Memory Safety + Educational Design
src
  • auth.rs15.7 KB
  • config.rs2.9 KB
  • error.rs5.2 KB
  • file_upload.rs19 KB
  • handlers.rs12.8 KB
  • lib.rs1.8 KB
  • main.rs6 KB
  • middleware.rs6.2 KB
  • static_files.rs9.9 KB
  • utils.rs9.6 KB
  • websocket.rs15.3 KB
static_files.rs
src/static_files.rs
Raw Download
Find: Go to:
/*
 * Static File Serving Module - Rust Web Server
 * 
 * Created by RSK World (https://rskworld.in)
 * Founder: Molla Samser
 * Designer & Tester: Rima Khatun
 * 
 * Contact:
 * - Email: hello@rskworld.in, support@rskworld.in
 * - Phone: +91 93305 39277
 * - Address: Nutanhat, Mongolkote, Purba Burdwan, West Bengal, India, 713147
 * 
 * © 2026 RSK World. All rights reserved.
 * Content used for educational purposes only.
 */

use hyper::{Body, Response, StatusCode};
use mime_guess::from_path;
use std::path::{Path, PathBuf};
use tokio::fs;
use tracing::{debug, error, warn};

use crate::error::{ServerError, ServerResult};

/// Static file handler
pub struct StaticFileHandler {
    base_dir: PathBuf,
}

impl StaticFileHandler {
    /// Create a new static file handler
    pub fn new(base_dir: PathBuf) -> Self {
        Self { base_dir }
    }

    /// Serve a static file
    pub async fn serve_file(&self, file_path: &str) -> ServerResult<Response<Body>> {
        let full_path = self.get_full_path(file_path)?;
        
        // Security check: ensure the path is within the base directory
        if !self.is_safe_path(&full_path) {
            warn!("Attempted to access file outside base directory: {:?}", full_path);
            return Err(ServerError::Forbidden("Access denied".to_string()));
        }

        debug!("Serving static file: {:?}", full_path);

        // Check if file exists
        if !full_path.exists() {
            return Err(ServerError::NotFound(format!("File not found: {}", file_path)));
        }

        // Check if it's a directory
        if full_path.is_dir() {
            // Try to serve index.html
            let index_path = full_path.join("index.html");
            if index_path.exists() {
                return self.serve_file_content(&index_path).await;
            } else {
                return Err(ServerError::NotFound("Directory listing not allowed".to_string()));
            }
        }

        // Serve the file
        self.serve_file_content(&full_path).await
    }

    /// Serve file content
    async fn serve_file_content(&self, file_path: &Path) -> ServerResult<Response<Body>> {
        // Read file content
        let content = fs::read(file_path).await
            .map_err(|e| ServerError::StaticFile(format!("Failed to read file: {}", e)))?;

        // Determine MIME type
        let mime_type = from_path(file_path)
            .first_or_octet_stream()
            .to_string();

        // Create response
        let mut response = Response::builder()
            .status(StatusCode::OK)
            .header("content-type", mime_type)
            .body(Body::from(content))
            .map_err(|e| ServerError::Internal(format!("Failed to create response: {}", e)))?;

        // Add cache headers for static files
        let headers = response.headers_mut();
        match headers.insert("cache-control", "public, max-age=3600".parse()) {
            Ok(_) => {},
            Err(_) => warn!("Failed to insert cache-control header"),
        }
        match headers.insert("etag", format!("\"{}\"", self.calculate_etag(&content)).parse()) {
            Ok(_) => {},
            Err(_) => warn!("Failed to insert etag header"),
        }

        Ok(response)
    }

    /// Get full path for a relative file path
    fn get_full_path(&self, file_path: &str) -> ServerResult<PathBuf> {
        let path = PathBuf::from(file_path);
        
        // Remove leading slash if present
        let relative_path = if path.starts_with("/") {
            path.strip_prefix("/").unwrap()
        } else {
            path.as_path()
        };

        let full_path = self.base_dir.join(relative_path);
        
        // Convert to absolute path
        match full_path.canonicalize() {
            Ok(path) => Ok(path),
            Err(_) => Ok(full_path), // Return original path if canonicalization fails
        }
    }

    /// Check if a path is safe (within base directory)
    fn is_safe_path(&self, full_path: &Path) -> bool {
        match self.base_dir.canonicalize() {
            Ok(base_canonical) => {
                match full_path.canonicalize() {
                    Ok(path_canonical) => {
                        path_canonical.starts_with(&base_canonical)
                    }
                    Err(_) => {
                        // If we can't canonicalize, do a simple prefix check
                        full_path.starts_with(&self.base_dir)
                    }
                }
            }
            Err(_) => true, // If base dir can't be canonicalized, assume safe
        }
    }

    /// Calculate ETag for file content
    fn calculate_etag(&self, content: &[u8]) -> String {
        use std::hash::{Hash, Hasher};
        use std::collections::hash_map::DefaultHasher;
        
        let mut hasher = DefaultHasher::new();
        content.hash(&mut hasher);
        format!("{:x}", hasher.finish())
    }

    /// Check if file has been modified based on If-None-Match header
    pub fn is_not_modified(&self, etag: &str, if_none_match: Option<&str>) -> bool {
        match if_none_match {
            Some(header_etag) => header_etag == etag,
            None => false,
        }
    }

    /// Handle conditional GET requests
    pub async fn serve_file_conditional(
        &self,
        file_path: &str,
        if_none_match: Option<&str>,
    ) -> ServerResult<Response<Body>> {
        let full_path = self.get_full_path(file_path)?;
        
        if !self.is_safe_path(&full_path) {
            return Err(ServerError::Forbidden("Access denied".to_string()));
        }

        if !full_path.exists() {
            return Err(ServerError::NotFound(format!("File not found: {}", file_path)));
        }

        // Read file content to calculate ETag
        let content = fs::read(&full_path).await
            .map_err(|e| ServerError::StaticFile(format!("Failed to read file: {}", e)))?;

        let etag = format!("\"{}\"", self.calculate_etag(&content));

        // Check if file has not been modified
        if self.is_not_modified(&etag, if_none_match) {
            return Ok(Response::builder()
                .status(StatusCode::NOT_MODIFIED)
                .header("etag", etag)
                .body(Body::empty())
                .map_err(|e| ServerError::Internal(format!("Failed to create response: {}", e)))?);
        }

        // Serve the file with ETag header
        let mime_type = from_path(&full_path)
            .first_or_octet_stream()
            .to_string();

        let mut response = Response::builder()
            .status(StatusCode::OK)
            .header("content-type", mime_type)
            .header("etag", etag)
            .header("cache-control", "public, max-age=3600")
            .body(Body::from(content))
            .map_err(|e| ServerError::Internal(format!("Failed to create response: {}", e)))?;

        Ok(response)
    }

    /// Create default static files directory structure
    pub async fn create_default_structure(&self) -> ServerResult<()> {
        let dirs = vec![
            self.base_dir.join("css"),
            self.base_dir.join("js"),
            self.base_dir.join("images"),
            self.base_dir.join("fonts"),
        ];

        for dir in dirs {
            if !dir.exists() {
                fs::create_dir_all(&dir).await
                    .map_err(|e| ServerError::StaticFile(format!("Failed to create directory: {}", e)))?;
                debug!("Created directory: {:?}", dir);
            }
        }

        // Create a default index.html if it doesn't exist
        let index_path = self.base_dir.join("index.html");
        if !index_path.exists() {
            let default_html = r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome to Rust Web Server</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .container { text-align: center; }
        .logo { font-size: 4em; margin: 20px 0; }
    </style>
</head>
<body>
    <div class="container">
        <div class="logo">🦀</div>
        <h1>Welcome to Rust Web Server</h1>
        <p>High-performance web server built with Rust</p>
        <p>Created by <strong>RSK World</strong></p>
    </div>
</body>
</html>"#;

            fs::write(&index_path, default_html).await
                .map_err(|e| ServerError::StaticFile(format!("Failed to create index.html: {}", e)))?;
            debug!("Created default index.html");
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    #[tokio::test]
    async fn test_static_file_handler() {
        let temp_dir = TempDir::new().unwrap();
        let handler = StaticFileHandler::new(temp_dir.path().to_path_buf());

        // Create a test file
        let test_file = temp_dir.path().join("test.txt");
        fs::write(&test_file, "Hello, World!").await.unwrap();

        // Test serving the file
        let response = handler.serve_file("test.txt").await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        // Test non-existent file
        let result = handler.serve_file("nonexistent.txt").await;
        assert!(result.is_err());
    }

    #[test]
    fn test_safe_path() {
        let temp_dir = TempDir::new().unwrap();
        let handler = StaticFileHandler::new(temp_dir.path().to_path_buf());

        // Test safe path
        let safe_path = temp_dir.path().join("safe.txt");
        assert!(handler.is_safe_path(&safe_path));

        // Test unsafe path (would need to be outside base dir in real scenario)
        // This is a simplified test - in practice, you'd need to create an actual unsafe path
    }
}
290 lines•9.9 KB
rust

About RSK World

Founded by Molla Samser, with Designer & Tester Rima Khatun, RSK World is your one-stop destination for free programming resources, source code, and development tools.

Founder: Molla Samser
Designer & Tester: Rima Khatun

Development

  • Game Development
  • Web Development
  • Mobile Development
  • AI Development
  • Development Tools

Legal

  • Terms & Conditions
  • Privacy Policy
  • Disclaimer

Contact Info

Nutanhat, Mongolkote
Purba Burdwan, West Bengal
India, 713147

+91 93305 39277

hello@rskworld.in
support@rskworld.in

© 2026 RSK World. All rights reserved.

Content used for educational purposes only. View Disclaimer