Skip to content

Commit 6ad1c32

Browse files
committed
Add public folder and path resolution logic
1 parent c3fc1a8 commit 6ad1c32

8 files changed

Lines changed: 179 additions & 34 deletions

File tree

public/hello.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Rust Web Server</title>
7+
</head>
8+
<body>
9+
<h1>Hello World</h1>
10+
</body>
11+
</html>

public/index.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Rust Web Server</title>
7+
<link rel="stylesheet" href="style.css" />
8+
</head>
9+
<body>
10+
<h1>Rust Web Server</h1>
11+
<p>This is a simple web server written in Rust.</p>
12+
<p>It is a work in progress and is not yet ready for production.</p>
13+
<p>
14+
<a href="/hello">Click here to see the "Hello World" page</a>
15+
</p>
16+
</body>
17+
</html>

public/style.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h1 {
2+
color: red;
3+
}

src/http/request.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ pub struct Request<'buf> {
8787
path: &'buf str,
8888
}
8989
impl<'buf> Request<'buf> {
90+
pub fn method(&self) -> &Method {
91+
&self.method
92+
}
93+
94+
pub fn path(&self) -> &str {
95+
&self.path
96+
}
97+
98+
/// Returns a reference to the query string if it exists, otherwise returns None
99+
/// NOTE: instead of returning &Option<QueryString>, we return Option<&QueryString>
100+
/// which is more flexible and easier to use in the caller.
101+
pub fn query_string(&self) -> Option<&QueryString> {
102+
self.query_string.as_ref() // as_ref() converts &Option<QueryString> to Option<&QueryString>
103+
}
90104
// don't need to implement convert method on own own, just use std::convert::TryFrom in idiomatic Rust (see below)
91105
// NOT NEEDED: fn from_byte_array(byte_array: &[u8]) -> Result<Self, String>
92106
}

src/http/response.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ impl Response {
2222
///
2323
/// This method is more efficient than using `Display` because it avoids the overhead of creating a new string.
2424
/// Especially when the response body is large (e.g. HTML pages with megabytes size of HTML content).
25+
/// It uses `write!` macro directly, instead of returning a copy of the response string.
26+
/// It also uses static dispatch, instead of dynamic dispatch,
27+
/// which resolves to the correct method of types implementing `Write` trait at compile time.
2528
///
2629
/// # Example
2730
///
@@ -32,7 +35,7 @@ impl Response {
3235
///
3336
/// # Arguments
3437
///
35-
/// * `stream` - The stream to write the response to.
38+
/// * `stream` - The stream to write the response to. It can be any type that implements `Write` trait.
3639
///
3740
/// # Returns
3841
/// * `IoResult<()>` - The result of the write operation.

src/main.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
#![allow(dead_code)]
2-
#![allow(unused_imports)]
3-
#![allow(unused_variables)]
42
mod http;
53
mod server;
6-
7-
use http::Method;
4+
mod website_handler;
85
use server::Server;
6+
use std::env;
7+
use std::path::{Path, absolute};
8+
use website_handler::WebsiteHandler;
99

1010
fn main() {
11-
let string = String::from("127.0.0.1:8080");
12-
let server = Server::new(string);
13-
println!("========== Server is running on {} ==========", server.addr);
14-
server.run();
11+
let default_path = format!("{}/public", env!("CARGO_MANIFEST_DIR"));
12+
// you can also ues String::from("./public") as default value
13+
let public_path = env::var("PUBLIC_PATH").unwrap_or(default_path);
14+
let host = env::var("HOST").unwrap_or(String::from("127.0.0.1"));
15+
let port = env::var("PORT").unwrap_or(String::from("8080"));
16+
let server = Server::new(format!("{}:{}", host, port));
17+
println!("================================================");
18+
println!("Server is running on http://{}", server.addr);
19+
println!(
20+
"Serving files from public path: {}",
21+
absolute(Path::new(&public_path)).unwrap().display()
22+
);
23+
println!("================================================");
24+
server.run(WebsiteHandler::new(public_path));
1525
}

src/server.rs

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,22 @@
1010
/// let server = Server::new("127.0.0.1:8080".to_string());
1111
/// server.run();
1212
/// ```
13-
use crate::http::{Request, Response, StatusCode};
13+
use crate::http::{ParseError, Request, Response, StatusCode};
1414
use std::io::{Read, Write}; // For reading from the TCP stream
1515
use std::net::TcpListener; // For listening to TCP connections
1616

17-
const DEFAULT_BODY: &str = "<html><body><h1>Hello</h1></body></html>";
17+
/// A trait for handling HTTP requests. Instead of implementing handling logic over
18+
/// and over in the `Server` struct, we can implement it in a separate struct
19+
/// and pass it to the `Server` as a parameter, in order to reduce repetition.
20+
/// This is useful for testing and for implementing different handlers for different routes.
21+
pub trait Handler {
22+
fn handle_request(&mut self, request: &Request) -> Response;
23+
fn handle_bad_request(&mut self, e: &ParseError) -> Response {
24+
println!("Error: parsing request\n{}", e);
25+
Response::new(StatusCode::BadRequest, None)
26+
}
27+
}
28+
1829
#[derive(Debug)]
1930
pub struct Server {
2031
pub addr: String, // Address to bind the server to (e.g., "127.0.0.1:8080")
@@ -30,7 +41,7 @@ impl Server {
3041
}
3142

3243
/// Runs the server, listening for incoming TCP connections and handling requests.
33-
pub fn run(&self) {
44+
pub fn run(&self, mut handler: impl Handler) {
3445
println!("Listening on {}", self.addr);
3546
// Bind the TCP listener to the specified address
3647
let listener = TcpListener::bind(&self.addr).unwrap();
@@ -53,29 +64,17 @@ impl Server {
5364
String::from_utf8_lossy(&buffer[..bytes_read])
5465
);
5566
// Attempt to parse the HTTP request from the buffer
56-
match Request::try_from(&buffer[..bytes_read]) {
57-
Ok(request) => {
58-
// Debug print the parsed request
59-
dbg!(request);
60-
// Create a response with a status code of 200 OK and a body of "Hello"
61-
let response = Response::new(
62-
StatusCode::Ok,
63-
Some(DEFAULT_BODY.to_string()),
64-
);
65-
// Write the response to the client
66-
if let Err(e) = response.send(&mut sock_stream) {
67-
// If there's an error writing to the client, print the error
68-
println!(
69-
"========== Error: Failed to write response to client ==========\n{}",
70-
e
71-
);
72-
}
73-
}
74-
Err(e) => {
75-
// Print any parsing errors
76-
println!("========== Error ==========\n{}", e);
77-
}
67+
let response = match Request::try_from(&buffer[..bytes_read]) {
68+
Ok(request) => handler.handle_request(&request),
69+
Err(e) => handler.handle_bad_request(&e),
70+
};
71+
72+
// Write the response to the client
73+
if let Err(e) = response.send(&mut sock_stream) {
74+
// If there's an error writing to the client, print the error
75+
println!("Error: Failed to write response\n{}", e);
7876
}
77+
7978
// 2 ways to convert between Request and &[u8] using TryFrom and TryInto:
8079
// Request::try_from(&buffer[..bytes_read]);
8180
// let res: &Result<Request, _> = &buffer[..].try_into();

src/website_handler.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use super::server::Handler;
2+
use crate::http::{Method, Request, Response, StatusCode};
3+
use std::fs;
4+
5+
pub struct WebsiteHandler {
6+
public_path: String, // path to the public directory
7+
}
8+
9+
impl WebsiteHandler {
10+
/// Creates a new `WebsiteHandler` with the given public path.
11+
///
12+
/// # Arguments
13+
///
14+
/// * `public_path` - The path to the public directory. Usually comes from the command line arguments.
15+
///
16+
/// # Returns
17+
///
18+
/// A `WebsiteHandler` object.
19+
pub fn new(public_path: String) -> Self {
20+
Self { public_path }
21+
}
22+
23+
/// Reads a file from the public directory and returns its contents as a string.
24+
/// This method provides a basic level of security by checking for directory traversal attacks.
25+
///
26+
/// # Arguments
27+
///
28+
/// * `file_path` - The path to the file to read.
29+
///
30+
/// # Returns
31+
///
32+
/// A `String` containing the contents of the file if it exists, otherwise `None`.
33+
fn read_file(&self, file_path: &str) -> Option<String> {
34+
let raw_path = format!("{}/{}", self.public_path, file_path);
35+
// For security reasons, we need to check if the requested path is under the public path
36+
match fs::canonicalize(&raw_path) {
37+
Ok(path) => {
38+
if path.starts_with(&self.public_path) {
39+
// ok() returns an Option<String>
40+
// if the file is not found, ok() returns None
41+
// if the file is found, ok() returns Some(String)
42+
fs::read_to_string(path).ok()
43+
} else {
44+
println!("Directory Traversal Attack Attempted: {}", file_path);
45+
None
46+
}
47+
}
48+
Err(e) => {
49+
println!("Error resolving path: {}, error: {}", &raw_path, e);
50+
None
51+
}
52+
}
53+
}
54+
}
55+
56+
impl Handler for WebsiteHandler {
57+
/// Handles HTTP requests for the website.
58+
///
59+
/// # Note
60+
///
61+
/// This method is called by the `Server` when a request is received.
62+
/// It handles GET requests for the root path ("/") and returns a default body.
63+
/// For other paths, it returns a 404 Not Found response.
64+
///
65+
/// # Arguments
66+
///
67+
/// * `request` - A reference to the HTTP request.
68+
///
69+
/// # Returns
70+
///
71+
/// A `Response` object containing the HTTP response.
72+
fn handle_request(&mut self, request: &Request) -> Response {
73+
// Use double nested match to handle both the method and the path
74+
match request.method() {
75+
Method::GET => match request.path() {
76+
"/" => Response::new(StatusCode::Ok, self.read_file("index.html")),
77+
"/hello" => Response::new(StatusCode::Ok, self.read_file("hello.html")),
78+
path => match self.read_file(path) {
79+
// if the file exists, read the contents and return a 200 OK response
80+
Some(contents) => Response::new(StatusCode::Ok, Some(contents)),
81+
// if the file does not exist, return a 404 Not Found response
82+
None => Response::new(StatusCode::NotFound, None),
83+
},
84+
},
85+
_ => Response::new(StatusCode::NotFound, None),
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)