http_handle/
server.rs

1// src/server.rs
2
3//! Server module for handling HTTP requests and responses.
4//!
5//! This module defines the core functionality for a simple HTTP server. It includes
6//! the `Server` struct, which represents the configuration of the server and manages
7//! incoming client connections. The server can serve static files from a document root
8//! and handle basic HTTP requests.
9//!
10//! The primary components of this module are:
11//!
12//! - `Server`: Represents the HTTP server and its configuration (address and document root).
13//! - `handle_connection`: Manages a single client connection, processing HTTP requests and sending responses.
14//! - `generate_response`: Generates an appropriate HTTP response based on the requested file or directory.
15//! - `get_content_type`: Determines the content type of files based on their extension.
16//!
17//! This module also includes error handling using `ServerError`, which covers
18//! I/O errors, invalid requests, file not found errors, and forbidden access.
19//!
20//! # Features
21//!
22//! - Handles HTTP GET requests and serves static files.
23//! - Supports serving an `index.html` for directories.
24//! - Returns a `404 Not Found` response for missing files.
25//! - Provides security against directory traversal attacks by restricting access
26//!   to the document root.
27//! - Serves appropriate content types based on file extensions (e.g., `.html`, `.css`, `.js`).
28//!
29
30use crate::error::ServerError;
31use crate::request::Request;
32use crate::response::Response;
33use serde::{Deserialize, Serialize};
34use std::fs;
35use std::io;
36use std::net::{TcpListener, TcpStream};
37use std::path::{Path, PathBuf};
38use std::thread;
39
40/// Represents the Http Handle and its configuration.
41#[derive(
42    Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize,
43)]
44pub struct Server {
45    address: String,
46    document_root: PathBuf,
47}
48
49impl Server {
50    /// Creates a new `Server` instance.
51    ///
52    /// # Arguments
53    ///
54    /// * `address` - A string slice that holds the IP address and port (e.g., "127.0.0.1:8080").
55    /// * `document_root` - A string slice that holds the path to the document root directory.
56    ///
57    /// # Returns
58    ///
59    /// A new `Server` instance.
60    pub fn new(address: &str, document_root: &str) -> Self {
61        Server {
62            address: address.to_string(),
63            document_root: PathBuf::from(document_root),
64        }
65    }
66
67    /// Starts the server and begins listening for incoming connections.
68    ///
69    /// # Returns
70    ///
71    /// A `Result` indicating success or an I/O error.
72    pub fn start(&self) -> io::Result<()> {
73        let listener = TcpListener::bind(&self.address)?;
74        println!("❯ Server is now running at http://{}", self.address);
75        println!("  Document root: {}", self.document_root.display());
76        println!("  Press Ctrl+C to stop the server.");
77
78        for stream in listener.incoming() {
79            match stream {
80                Ok(stream) => {
81                    let document_root = self.document_root.clone();
82                    let _ = thread::spawn(move || {
83                        if let Err(e) =
84                            handle_connection(stream, &document_root)
85                        {
86                            eprintln!(
87                                "Error handling connection: {}",
88                                e
89                            );
90                        }
91                    });
92                }
93                Err(e) => eprintln!("Connection error: {}", e),
94            }
95        }
96
97        Ok(())
98    }
99}
100
101/// Handles a single client connection.
102///
103/// # Arguments
104///
105/// * `stream` - A `TcpStream` representing the client connection.
106/// * `document_root` - A `PathBuf` representing the server's document root.
107///
108/// # Returns
109///
110/// A `Result` indicating success or a `ServerError`.
111fn handle_connection(
112    mut stream: TcpStream,
113    document_root: &Path,
114) -> Result<(), ServerError> {
115    let request = Request::from_stream(&stream)?;
116    let response = generate_response(&request, document_root)?;
117    response.send(&mut stream)?;
118    Ok(())
119}
120
121/// Generates an HTTP response based on the requested file.
122///
123/// # Arguments
124///
125/// * `request` - A `Request` instance representing the client's request.
126/// * `document_root` - A `Path` representing the server's document root.
127///
128/// # Returns
129///
130/// A `Result` containing the `Response` or a `ServerError`.
131fn generate_response(
132    request: &Request,
133    document_root: &Path,
134) -> Result<Response, ServerError> {
135    let mut path = PathBuf::from(document_root);
136    let request_path = request.path().trim_start_matches('/');
137
138    if request_path.is_empty() {
139        // If the request is for the root, append "index.html"
140        path.push("index.html");
141    } else {
142        for component in request_path.split('/') {
143            if component == ".." {
144                let _ = path.pop();
145            } else {
146                path.push(component);
147            }
148        }
149    }
150
151    if !path.starts_with(document_root) {
152        return Err(ServerError::forbidden("Access denied"));
153    }
154
155    if path.is_file() {
156        let contents = fs::read(&path)?;
157        let content_type = get_content_type(&path);
158        let mut response = Response::new(200, "OK", contents);
159        response.add_header("Content-Type", content_type);
160        Ok(response)
161    } else if path.is_dir() {
162        // If it's a directory, try to serve index.html from that directory
163        path.push("index.html");
164        if path.is_file() {
165            let contents = fs::read(&path)?;
166            let content_type = get_content_type(&path);
167            let mut response = Response::new(200, "OK", contents);
168            response.add_header("Content-Type", content_type);
169            Ok(response)
170        } else {
171            generate_404_response(document_root)
172        }
173    } else {
174        generate_404_response(document_root)
175    }
176}
177
178/// Generates a 404 Not Found response.
179///
180/// # Arguments
181///
182/// * `document_root` - A `Path` representing the server's document root.
183///
184/// # Returns
185///
186/// A `Result` containing the `Response` or a `ServerError`.
187fn generate_404_response(
188    document_root: &Path,
189) -> Result<Response, ServerError> {
190    let not_found_path = document_root.join("404/index.html");
191    let contents = if not_found_path.is_file() {
192        fs::read(not_found_path)?
193    } else {
194        b"404 Not Found".to_vec()
195    };
196    let mut response = Response::new(404, "NOT FOUND", contents);
197    response.add_header("Content-Type", "text/html");
198    Ok(response)
199}
200
201/// Determines the content type based on the file extension.
202///
203/// # Arguments
204///
205/// * `path` - A `Path` representing the file path.
206///
207/// # Returns
208///
209/// A string slice representing the content type.
210fn get_content_type(path: &Path) -> &'static str {
211    match path.extension().and_then(std::ffi::OsStr::to_str) {
212        Some("html") => "text/html",
213        Some("css") => "text/css",
214        Some("js") => "application/javascript",
215        Some("json") => "application/json",
216        Some("png") => "image/png",
217        Some("jpg") | Some("jpeg") => "image/jpeg",
218        Some("gif") => "image/gif",
219        Some("svg") => "image/svg+xml",
220        _ => "application/octet-stream",
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use std::fs::File;
228    use std::io::Write;
229    use tempfile::TempDir;
230
231    fn setup_test_directory() -> TempDir {
232        let temp_dir = TempDir::new().unwrap();
233        let root_path = temp_dir.path();
234
235        // Create index.html
236        let mut index_file =
237            File::create(root_path.join("index.html")).unwrap();
238        index_file
239            .write_all(b"<html><body>Hello, World!</body></html>")
240            .unwrap();
241
242        // Create 404/index.html
243        fs::create_dir(root_path.join("404")).unwrap();
244        let mut not_found_file =
245            File::create(root_path.join("404/index.html")).unwrap();
246        not_found_file
247            .write_all(b"<html><body>404 Not Found</body></html>")
248            .unwrap();
249
250        // Create a subdirectory with its own index.html
251        fs::create_dir(root_path.join("subdir")).unwrap();
252        let mut subdir_index_file =
253            File::create(root_path.join("subdir/index.html")).unwrap();
254        subdir_index_file
255            .write_all(b"<html><body>Subdirectory Index</body></html>")
256            .unwrap();
257
258        temp_dir
259    }
260
261    #[test]
262    fn test_server_creation() {
263        let server = Server::new("127.0.0.1:8080", "/var/www");
264        assert_eq!(server.address, "127.0.0.1:8080");
265        assert_eq!(server.document_root, PathBuf::from("/var/www"));
266    }
267
268    #[test]
269    fn test_get_content_type() {
270        assert_eq!(
271            get_content_type(Path::new("test.html")),
272            "text/html"
273        );
274        assert_eq!(
275            get_content_type(Path::new("style.css")),
276            "text/css"
277        );
278        assert_eq!(
279            get_content_type(Path::new("script.js")),
280            "application/javascript"
281        );
282        assert_eq!(
283            get_content_type(Path::new("data.json")),
284            "application/json"
285        );
286        assert_eq!(
287            get_content_type(Path::new("image.png")),
288            "image/png"
289        );
290        assert_eq!(
291            get_content_type(Path::new("photo.jpg")),
292            "image/jpeg"
293        );
294        assert_eq!(
295            get_content_type(Path::new("animation.gif")),
296            "image/gif"
297        );
298        assert_eq!(
299            get_content_type(Path::new("icon.svg")),
300            "image/svg+xml"
301        );
302        assert_eq!(
303            get_content_type(Path::new("unknown.xyz")),
304            "application/octet-stream"
305        );
306    }
307
308    #[test]
309    fn test_generate_response() {
310        let temp_dir = setup_test_directory();
311        let document_root = temp_dir.path();
312
313        // Test root request (should serve index.html)
314        let root_request = Request {
315            method: "GET".to_string(),
316            path: "/".to_string(),
317            version: "HTTP/1.1".to_string(),
318        };
319
320        let root_response =
321            generate_response(&root_request, document_root).unwrap();
322        assert_eq!(root_response.status_code, 200);
323        assert_eq!(root_response.status_text, "OK");
324        assert!(root_response
325            .body
326            .starts_with(b"<html><body>Hello, World!</body></html>"));
327
328        // Test specific file request
329        let file_request = Request {
330            method: "GET".to_string(),
331            path: "/index.html".to_string(),
332            version: "HTTP/1.1".to_string(),
333        };
334
335        let file_response =
336            generate_response(&file_request, document_root).unwrap();
337        assert_eq!(file_response.status_code, 200);
338        assert_eq!(file_response.status_text, "OK");
339        assert!(file_response
340            .body
341            .starts_with(b"<html><body>Hello, World!</body></html>"));
342
343        // Test subdirectory index request
344        let subdir_request = Request {
345            method: "GET".to_string(),
346            path: "/subdir/".to_string(),
347            version: "HTTP/1.1".to_string(),
348        };
349
350        let subdir_response =
351            generate_response(&subdir_request, document_root).unwrap();
352        assert_eq!(subdir_response.status_code, 200);
353        assert_eq!(subdir_response.status_text, "OK");
354        assert!(subdir_response.body.starts_with(
355            b"<html><body>Subdirectory Index</body></html>"
356        ));
357
358        // Test non-existent file request
359        let not_found_request = Request {
360            method: "GET".to_string(),
361            path: "/nonexistent.html".to_string(),
362            version: "HTTP/1.1".to_string(),
363        };
364
365        let not_found_response =
366            generate_response(&not_found_request, document_root)
367                .unwrap();
368        assert_eq!(not_found_response.status_code, 404);
369        assert_eq!(not_found_response.status_text, "NOT FOUND");
370        assert!(not_found_response
371            .body
372            .starts_with(b"<html><body>404 Not Found</body></html>"));
373
374        // Test directory traversal attempt
375        let traversal_request = Request {
376            method: "GET".to_string(),
377            path: "/../outside.html".to_string(),
378            version: "HTTP/1.1".to_string(),
379        };
380
381        let traversal_response =
382            generate_response(&traversal_request, document_root);
383        assert!(matches!(
384            traversal_response,
385            Err(ServerError::Forbidden(_))
386        ));
387    }
388}