1use 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#[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 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 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
101fn 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
121fn 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 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 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
178fn 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
201fn 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 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 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 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 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 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 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 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(¬_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 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}