From 9a4556e8819e14b2ed4f67a7e704e779899a6ad6 Mon Sep 17 00:00:00 2001 From: uzair Date: Sun, 14 Apr 2024 21:29:55 -0700 Subject: [PATCH] Finialized final implenentation --- .idea/.gitignore | 10 ---- .idea/checkstyle-idea.xml | 16 ------ .idea/modules.xml | 8 --- .idea/udemy-course-server.iml | 13 ----- .idea/vcs.xml | 6 -- public/index.html | 93 +++++++++++++++++++++++++++++++ public/login.html | 24 ++++++++ public/script.js | 6 ++ public/style.css | 61 ++++++++++++++++++++ src/http/method.rs | 31 ++++++++++- src/http/mod.rs | 8 ++- src/http/query_string.rs | 41 ++++++++++++++ src/http/request.rs | 102 ++++++++++++++++++++++++---------- src/http/response.rs | 31 +++++++++++ src/http/status_code.rs | 24 ++++++++ src/main.rs | 13 ++++- src/server-test.http | 1 + src/server.rs | 34 ++++++++---- src/website_handler.rs | 73 ++++++++++++++++++++++++ 19 files changed, 495 insertions(+), 100 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/checkstyle-idea.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/udemy-course-server.iml delete mode 100644 .idea/vcs.xml create mode 100644 public/index.html create mode 100644 public/login.html create mode 100644 public/script.js create mode 100644 public/style.css create mode 100644 src/http/query_string.rs create mode 100644 src/http/response.rs create mode 100644 src/http/status_code.rs create mode 100644 src/website_handler.rs diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index a9d7db9..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# GitHub Copilot persisted chat sessions -/copilot/chatSessions diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml deleted file mode 100644 index 4c1cc2d..0000000 --- a/.idea/checkstyle-idea.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - 10.14.0 - JavaOnly - true - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index c415781..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/udemy-course-server.iml b/.idea/udemy-course-server.iml deleted file mode 100644 index f95d272..0000000 --- a/.idea/udemy-course-server.iml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..9e17ceb --- /dev/null +++ b/public/index.html @@ -0,0 +1,93 @@ + + + + + + Your Portfolio + + + +
+

Your Name

+

Web Developer & Designer

+
+ +
+
+

Project 1

+

Description of your project goes here.

+
+
+

Project 2

+

Description of another project.

+
+ +
+ + + + diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000..ed48069 --- /dev/null +++ b/public/login.html @@ -0,0 +1,24 @@ + + + + Login + + + +
+

Login

+
+ + +
+ + +
+ +
+

Don't have an account? Sign up

+

Forgot password? Reset password

+ +
+ + \ No newline at end of file diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..a87f3bb --- /dev/null +++ b/public/script.js @@ -0,0 +1,6 @@ +const button = document.querySelector('button'); + +button.addEventListener('click', () => { + // Add functionality to the button click here (e.g., alert, change color) + alert('Button Clicked!'); +}); diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..fba6a9f --- /dev/null +++ b/public/style.css @@ -0,0 +1,61 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + } + + .background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: linear-gradient(to right, #f2f2f2, #eee); + animation: gradient 10s ease-in-out infinite alternate; + filter: blur(5px); + } + + @keyframes gradient { + 0% { + background: linear-gradient(to right, #f2f2f2, #eee); + } + 50% { + background: linear-gradient(to right, #eee, #f7f7f7); + } + 100% { + background: linear-gradient(to right, #f7f7f7, #f2f2f2); + } + } + + .content { + position: relative; + max-width: 600px; + margin: 0 auto; + padding: 40px; + text-align: center; + color: #333; + } + + h1 { + font-size: 3em; + margin-bottom: 20px; + } + + p { + font-size: 1.2em; + line-height: 1.5; + } + + button { + background-color: #4CAF50; + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; + border-radius: 5px; + } + \ No newline at end of file diff --git a/src/http/method.rs b/src/http/method.rs index 936ceef..7142730 100644 --- a/src/http/method.rs +++ b/src/http/method.rs @@ -1,6 +1,10 @@ +use crate::http::ParseError; +use std::str::FromStr; + +#[derive(Debug)] pub enum Method { - GET(String), - DELETE(u64), + GET, + DELETE, POST, PUT, HEAD, @@ -8,4 +12,25 @@ pub enum Method { OPTIONS, TRACE, PATCH, -} \ No newline at end of file +} + +impl FromStr for Method { + type Err = MethodError; + + fn from_str(s: &str) -> Result { + match s { + "GET" => Ok(Self::GET), + "DELETE" => Ok(Self::DELETE), + "POST" => Ok(Self::POST), + "PUT" => Ok(Self::PUT), + "HEAD" => Ok(Self::HEAD), + "CONNECT" => Ok(Self::CONNECT), + "OPTIONS" => Ok(Self::OPTIONS), + "TRACE" => Ok(Self::TRACE), + "PATCH" => Ok(Self::PATCH), + _ => Err(MethodError) + } + } +} + +pub struct MethodError; \ No newline at end of file diff --git a/src/http/mod.rs b/src/http/mod.rs index 9744b08..5a9d0bd 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,6 +1,12 @@ pub use request::Request; pub use method::Method; pub use request::ParseError; +pub use query_string::{QueryString, Value as QueryStringValue}; +pub use response::Response; +pub use status_code::StatusCode; pub mod request; -pub mod method; \ No newline at end of file +pub mod method; +pub mod query_string; +pub mod response; +pub mod status_code; \ No newline at end of file diff --git a/src/http/query_string.rs b/src/http/query_string.rs new file mode 100644 index 0000000..185fd3e --- /dev/null +++ b/src/http/query_string.rs @@ -0,0 +1,41 @@ +use std::{collections::HashMap, thread::current}; + +#[derive(Debug)] +pub struct QueryString<'buffer_lifetime> { + data: HashMap<&'buffer_lifetime str, Value<'buffer_lifetime>>, +} + +#[derive(Debug)] +pub enum Value<'buffer_lifetime> { + Single(&'buffer_lifetime str), + Multiple(Vec<&'buffer_lifetime str>), +} + +impl<'buffer_lifetime> QueryString<'buffer_lifetime> { + pub fn get(&self, key: &str) -> Option<&Value> { + self.data.get(key) + } +} + +impl<'buffer_lifetime> From<&'buffer_lifetime str> for QueryString<'buffer_lifetime> { + fn from(value: &'buffer_lifetime str) -> Self { + let mut data = HashMap::new(); + + for sub_str in value.split('&') { + let mut key = sub_str; + let mut value = ""; + if let Some(i) = sub_str.find('=') { + key = &sub_str[..i]; + value = &sub_str[i + 1 ..] + } + data.entry(key).and_modify(|existing: &mut Value| match existing { + Value::Single(previous_value) => { + *existing = Value::Multiple(vec![previous_value, value]); + }, + Value::Multiple(vec) => vec.push(value), + }).or_insert(Value::Single(key)); + } + + QueryString { data } + } +} diff --git a/src/http/request.rs b/src/http/request.rs index 94c82a7..113bf05 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,35 +1,81 @@ -use std::str; -use super::method::Method; +use super::method::{Method, MethodError}; use std::convert::TryFrom; -use std::io::Error; -use std::fmt::{Display, Debug, Formatter}; +use std::fmt::{Debug, Display, Formatter}; +use std::str; use std::str::Utf8Error; +use crate::http::query_string::QueryString; -pub struct Request { - path: String, - query_string: Option, +#[derive(Debug)] +pub struct Request<'buffer_lifetime> { + path: &'buffer_lifetime str, + query_string: Option>, method: Method, } -impl Request { +impl<'buffer_lifetime> Request<'buffer_lifetime> { + pub fn path(&self) -> &str { + self.path + } + pub fn query_string(&self) -> Option<&QueryString<'buffer_lifetime>> { + self.query_string.as_ref() + } + + pub fn method(&self) -> &Method { + &self.method + } } -impl TryFrom<&[u8]> for Request { +impl<'buffer_lifetime> TryFrom<&'buffer_lifetime [u8]> for Request<'buffer_lifetime> { type Error = ParseError; - //GET /search?name=abc&sort=1 HTTP/1.1 - fn try_from(buffer: &[u8]) -> Result { - match str::from_utf8(buffer){ - Ok(request) => {} - Err(_) => return Err(ParseError::InvalidEncoding), - } - match str::from_utf8(buffer).or(Err(ParseError::InvalidEncoding)) { - Ok(request) => {} - Err(e) => return Err(e) + //GET /search?name=abc&sort=1 HTTP/1.1\r\n...HEADERS... + fn try_from(buffer: &'buffer_lifetime [u8]) -> Result, Self::Error> { + let request = str::from_utf8(buffer)?; + + let (method, request) = get_next_word(request).ok_or(ParseError::InvalidRequest)?; + let (mut path, request) = get_next_word(request).ok_or(ParseError::InvalidRequest)?; + let (protocol, _) = get_next_word(request).ok_or(ParseError::InvalidRequest)?; + + if protocol != "HTTP/1.1" { + return Err(ParseError::InvalidProtocol); } - let request = str::from_utf8(buffer).or(Err(ParseError::InvalidEncoding))?; + let method: Method = method.parse()?; + + let mut query_string = None; + + if let Some(i) = path.find('?') { + query_string = Some(QueryString::from(&path[i + 1..])); + path = &path[..i]; + } + + return Ok(Self { + path, + query_string, + method, + }); + } +} + +fn get_next_word(request: &str) -> Option<(&str, &str)> { + for (index, request_character) in request.chars().enumerate() { + if request_character == ' ' || request_character == '\r' { + return Some((&request[..index], &request[index + 1..])); + } + } + None +} + +impl From for ParseError { + fn from(value: MethodError) -> Self { + self::ParseError::InvalidMethod + } +} + +impl From for ParseError { + fn from(_: Utf8Error) -> Self { + Self::InvalidEncoding } } @@ -49,20 +95,16 @@ pub enum ParseError { InvalidRequest, InvalidEncoding, InvalidProtocol, - InvalidMethod + InvalidMethod, } -impl ParseError{ - fn message(&self) -> &str{ +impl ParseError { + fn message(&self) -> &str { match self { - ParseError::InvalidRequest => {"Invalid Request"} - ParseError::InvalidEncoding => {"Invalid Encoding"} - ParseError::InvalidProtocol => {"Invalid Protocol"} - ParseError::InvalidMethod => {"Invalid Method"} + ParseError::InvalidRequest => "Invalid Request", + ParseError::InvalidEncoding => "Invalid Encoding", + ParseError::InvalidProtocol => "Invalid Protocol", + ParseError::InvalidMethod => "Invalid Method", } } } - -impl Error for ParseError { - -} diff --git a/src/http/response.rs b/src/http/response.rs new file mode 100644 index 0000000..fe7ddab --- /dev/null +++ b/src/http/response.rs @@ -0,0 +1,31 @@ +use std::{fmt::Display, net::TcpStream}; +use std::io::{Write, Result as IOResult}; + +use super::status_code::StatusCode; + +#[derive(Debug)] +pub struct Response { + status_code: StatusCode, + body: Option, +} + +impl Response { + pub fn new(status_code: StatusCode, body: Option) -> Self { + Response { status_code, body } + } + + pub fn send(&self, stream: &mut impl Write) -> IOResult<()>{ + let body = match &self.body { + Some(body) => body, + None => "", + }; + + write!( + stream, + "HTTP/1.1 {} {}\r\n\r\n{}", + self.status_code, + self.status_code.reason_phrase(), + body + ) + } +} diff --git a/src/http/status_code.rs b/src/http/status_code.rs new file mode 100644 index 0000000..df436bc --- /dev/null +++ b/src/http/status_code.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result as FmtResult}; + +#[derive(Clone, Copy, Debug)] +pub enum StatusCode { + Ok = 200, + BadRequest = 400, + NotFound = 404 +} + +impl StatusCode { + pub fn reason_phrase(&self) -> &str { + match self { + Self::Ok => "Ok", + Self::BadRequest => "Bad Request", + Self::NotFound => "Not Found", + } + } +} + +impl Display for StatusCode { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{}", *self as u16) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index dc4de2d..37a2a70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,20 @@ +#![allow(dead_code)] +#![allow(unused_imports)] + mod server; mod http; +mod website_handler; use server::Server; use http::Request; -use http::Method; +use website_handler::WebsiteHandler; +use std::env; +//use http::Method; fn main() { + let default_path = format!("{}\\public", env!("CARGO_MANIFEST_DIR").replace("/", "\\")); + let public_path = env::var("PUBLIC_PATH").unwrap_or(default_path); + println!("public path is {}", public_path); let server = Server::new(String::from("127.0.0.1:3000")); - server.run(); + server.run(WebsiteHandler::new(public_path)); } \ No newline at end of file diff --git a/src/server-test.http b/src/server-test.http index e69de29..8021ed4 100644 --- a/src/server-test.http +++ b/src/server-test.http @@ -0,0 +1 @@ +GET localhost:3000 \ No newline at end of file diff --git a/src/server.rs b/src/server.rs index dd378b2..73eb07e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,11 +1,21 @@ -use std::io::Read; +use crate::http::{request, ParseError, Request, Response, StatusCode}; +use std::io::{Read, Write}; use std::net::TcpListener; use std::process; use std::ptr::read; -use super::Request; + +// use super::Request; use std::convert::TryFrom; use std::convert::TryInto; +pub trait Handler { + fn handle_request(&mut self, request: &Request) -> Response; + fn handle_bad_request(&mut self, error: &ParseError) -> Response { + println!("Failed to parse request: {}", error); + Response::new(StatusCode::BadRequest, None) + } +} + /// A sample HTTP request looks like this: /// ``` /// GET / HTTP/1.1\r\n @@ -18,12 +28,10 @@ pub struct Server { impl Server { pub fn new(address: String) -> Self { - Self { - address, - } + Self { address } } - pub fn run(self) { + pub fn run(self, mut handler: impl Handler) { let (ip, port) = self.address.split_at(self.address.find(":").unwrap()); let port = &port[1..]; @@ -44,11 +52,15 @@ impl Server { Ok(read_bytes) => { println!("Read {} bytes", read_bytes); println!("Received a request: {}", String::from_utf8_lossy(&buffer)); - match Request::try_from(&buffer[..]){ - Ok(request) => {}, - Err(e) => println!("Failed to parse a request: {e}") + + let response = match Request::try_from(&buffer[..]) { + Ok(request) => handler.handle_request(&request), + Err(e) => handler.handle_bad_request(&e), }; - let result: &Result = &buffer[..].try_into(); + + if let Err(e) = response.send(&mut stream) { + println!("Failed to send response: {}", e); + } } Err(error) => { println!("Failed to read: {}", error); @@ -62,4 +74,4 @@ impl Server { } } } -} \ No newline at end of file +} diff --git a/src/website_handler.rs b/src/website_handler.rs new file mode 100644 index 0000000..99f98c6 --- /dev/null +++ b/src/website_handler.rs @@ -0,0 +1,73 @@ +use std::{fs, path}; + +use crate::http::{Method, Request, Response, StatusCode}; + +use super::server::Handler; + +pub struct WebsiteHandler { + public_path: String, +} + +impl WebsiteHandler { + pub fn new(public_path: String) -> Self { + Self { public_path } + } + + fn read_file(&self, file_path: &str) -> Option { + let path = format!("{}\\{}", self.public_path, file_path); + + match fs::canonicalize(path) { + Ok(path) => { + if path.starts_with(&self.public_path) { + fs::read_to_string(path).ok() + } else { + println!("Directory Traversal Attack Attempted: {}", file_path); + Some("".to_string()) + } + } + Err(_) => None, + } + } +} + +impl Handler for WebsiteHandler { + fn handle_request(&mut self, request: &Request) -> Response { + match request.method() { + Method::GET => match request.path() { + "/" => Response::new(StatusCode::Ok, self.read_file("index.html")), + "/login" => Response::new(StatusCode::Ok, self.read_file("login.html")), + "/signup" => Response::new( + StatusCode::Ok, + Some( + r#" +

Sign Up

+
+ +

+ +

+ +

+ +
+ "# + .to_string(), + ), + ), + path => match self.read_file(path) { + Some(contents) => Response::new(StatusCode::Ok, Some(contents)), + None => Response::new(StatusCode::NotFound, None), + }, + }, + Method::DELETE => todo!(), + Method::POST => todo!(), + Method::PUT => todo!(), + Method::HEAD => todo!(), + Method::CONNECT => todo!(), + Method::OPTIONS => todo!(), + Method::TRACE => todo!(), + Method::PATCH => todo!(), + _ => Response::new(StatusCode::NotFound, None), + } + } +}