diff --git a/Cargo.lock b/Cargo.lock index 52a42ee..b948f50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,8 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" name = "chatty" version = "0.1.0" dependencies = [ + "serde", + "serde_json", "sqlx", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 29cea98..f43db71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +serde = "1.0.225" +serde_json = { version = "1.0.145", features = ["default"] } sqlx = {version = "0.8.6", features = ["postgres", "runtime-tokio"]} tokio = {version = "1.47.1", features = ["full"]} diff --git a/requests/auth.http b/requests/auth.http new file mode 100644 index 0000000..b450483 --- /dev/null +++ b/requests/auth.http @@ -0,0 +1,18 @@ +### +POST /api/auth/login/ HTTP/1.1 +Host: localhost:7878 +User-Agent: curl/7.68.0 +Accept: */* + +### +POST /api/auth/register HTTP/1.1 +Host: localhost:7878 +User-Agent: curl/7.68.0 +Accept: */* +Content-Type: application/json + +{ + "username": "astria", + "email": "astria@example.com", + "password": "securepassword" +} \ No newline at end of file diff --git a/src/http.rs b/src/http.rs index c0fa75e..9986a26 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,9 +1,9 @@ use std::net::{TcpListener, TcpStream}; use std::thread; -use std::io::{BufReader, prelude::*}; -use std::boxed::Box; -use crate::routes::proxy; -use crate::models::HttpRequest::HttpRequest; +use std::io::{BufReader, BufRead, Write, prelude::*}; +use crate::models::{http_header::HttpHeader, HttpRequest::HttpRequest, user::User}; +use crate::parsers::{parse_headers, parse_json_body}; +use crate::routes::proxy::proxy; pub fn http_server() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); @@ -24,43 +24,47 @@ pub fn http_server() { } fn handle_connection(mut stream: TcpStream) { - // Handle the connection (currently does nothing) - let reader = BufReader::new(&mut stream); - let http_request = reader.lines().next().unwrap().unwrap(); + let mut reader = BufReader::new(&mut stream); + let mut header_lines: Vec = Vec::new(); - let parsed_request = parse_http_request(&http_request); - match parsed_request { - Ok(request) => { - proxy::proxy(request, &stream); + // Read the HTTP request line by line + loop { + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) => break, // EOF + Ok(_) => { + println!("Received line: {}", line.trim()); + if line.trim().is_empty() { + break; // End of headers + } + header_lines.push(line); + } + Err(e) => { + eprintln!("Error reading request: {}", e); + break; + } } - Err(e) => { - eprintln!("Failed to parse HTTP request: {}", e); + } + println!("Finished!"); + let headers = parse_headers(header_lines); + + let mut body = String::new(); + + if let Some(length) = headers.content_length { + let mut body_buffer = vec![0; length]; + if let Err(e) = reader.read_exact(&mut body_buffer) { + eprintln!("Error reading body: {}", e); + } else { + body = String::from_utf8_lossy(&body_buffer).to_string(); } } - + let request = HttpRequest { + headers, + body: if body.is_empty() { None } else { Some(body) }, + }; + + proxy(request, &stream); } // FUNCTION TO PARSE HTTP REQUESTS -fn parse_http_request(request: &str) -> Result> { - let parts: Vec<&str> = request.split_whitespace().collect(); - - if parts[1].ends_with('/') == false && parts[1].contains('.') == false { - // ADD TRAILING / IF MISSING - println!("Adding trailing / to path"); - let new_path = format!("{}/", parts[1]); - let new_request = format!("{} {} {}", parts[0], new_path, parts[2]); - return parse_http_request(&new_request); - } - - if parts.len() >= 3 { - let request = HttpRequest { - method: parts[0].to_string(), - path: parts[1].to_string(), - version: parts[2].to_string(), - }; - Ok(request) - } else { - Err("Invalid HTTP request".into()) - } -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 5a4966e..31c8d96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ mod http; -mod routes; mod models; +mod parsers; + +mod routes; #[tokio::main] async fn main() { diff --git a/src/models/HttpRequest.rs b/src/models/HttpRequest.rs index 1b3b50f..16f9c9b 100644 --- a/src/models/HttpRequest.rs +++ b/src/models/HttpRequest.rs @@ -1,6 +1,6 @@ +use crate::models::http_header::HttpHeader; #[derive(Debug, Clone,)] pub struct HttpRequest { - pub method: String, - pub path: String, - pub version: String, + pub headers: HttpHeader, + pub body: Option } \ No newline at end of file diff --git a/src/models/http_header.rs b/src/models/http_header.rs new file mode 100644 index 0000000..3f007a1 --- /dev/null +++ b/src/models/http_header.rs @@ -0,0 +1,9 @@ + +#[derive(Debug, Clone)] +pub struct HttpHeader { + pub method: Option, + pub path: Option>, + pub version: Option, + pub content_length: Option, + pub content_type: Option, +} \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index 985a822..e76e92e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1 +1,4 @@ -pub mod HttpRequest; \ No newline at end of file +pub mod HttpRequest; +pub mod http_header; + +pub mod user; \ No newline at end of file diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..c0ad14b --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,9 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct User { + pub id: Option, + pub username: String, + pub email: String, + pub password: String +} \ No newline at end of file diff --git a/src/parsers.rs b/src/parsers.rs new file mode 100644 index 0000000..8d5a053 --- /dev/null +++ b/src/parsers.rs @@ -0,0 +1,64 @@ +use crate::models::http_header::HttpHeader; + +pub fn parse_headers(header_lines: Vec) -> HttpHeader { + + let mut headers = HttpHeader { + method: None, + content_length: None, + content_type: None, + path: None, + version: None, + }; + + let mut iter = 0; + + for line in header_lines { + println!("Header line: {}", line); + + if iter == 0 { + // Parse request line + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() == 3 { + headers.method = Some(parts[0].to_string()); + headers.path = Some(parts[1].split("/").map(|s| s.to_string()).collect()); + headers.version = Some(parts[2].to_string()); + } else { + println!("Invalid request line: {}", line); + } + iter = iter + 1; + continue; + } else { + let splitted: Vec<&str> = line.split(":").collect(); + + if splitted.len() != 2 { + println!("Invalid header line: {}", line); + continue; + } + + match splitted[0].to_lowercase().as_str() { + "content-length" => { + if let Ok(length) = splitted[1].trim().parse::() { + headers.content_length = Some(length); + } + } + "content-type" => { + headers.content_type = Some(splitted[1].trim().to_string()); + } + _ => { + println!("Unknown header: {}", splitted[0]); + } + } + println!("Parsed header: {:?}", headers); + } + + + iter = iter + 1; + + + } + headers +} + +pub fn parse_json_body(body: &str) -> Result { + serde_json::from_str(body) +} \ No newline at end of file diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 55d75a1..2a1540a 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,19 +1,17 @@ -use crate::models::HttpRequest::HttpRequest; +use crate::models::{HttpRequest::HttpRequest, user::User}; +use crate::parsers::parse_json_body; use std::net::TcpStream; use std::io::prelude::*; pub fn handle_auth_request(http_request: HttpRequest, mut stream: &TcpStream) { - - match http_request.method.as_str() { + match http_request.headers.method.as_ref().expect("").as_str() { "POST" => { // Handle login or registration - println!("Handling auth POST request for path: {}", http_request.path); - let response = "HTTP/1.1 200 OK\r\n\r\nAuth POST Response"; - stream.write_all(response.as_bytes()).unwrap(); + post(http_request, &stream); } "GET" => { // Handle fetching user info or logout - println!("Handling auth GET request for path: {}", http_request.path); + println!("Handling auth GET request for path: {}", http_request.headers.path.as_ref().expect("").join("/")); let response = "HTTP/1.1 200 OK\r\n\r\nAuth GET Response"; stream.write_all(response.as_bytes()).unwrap(); } @@ -24,4 +22,38 @@ pub fn handle_auth_request(http_request: HttpRequest, mut stream: &TcpStream) { return; } } +} + +pub fn post(http_request: HttpRequest, mut stream: &TcpStream) { + println!("Handling auth POST request: {}", http_request.headers.path.as_ref().expect("").join("/")); + match http_request.headers.path.clone().expect("").join("/").as_str() { + "/api/auth/login" => { + println!("Handling login"); + let response = "HTTP/1.1 200 OK\r\n\r\nLogin Successful"; + stream.write_all(response.as_bytes()).unwrap(); + } + "/api/auth/register" => { + register(http_request, &stream); + } + _ => { + let response = "HTTP/1.1 404 NOT FOUND\r\n\r\nAuth Endpoint Not Found"; + stream.write_all(response.as_bytes()).unwrap(); + } + } +} + +pub fn register(http_request: HttpRequest, mut stream: &TcpStream) { + + let response = "HTTP/1.1 200 OK\r\n\r\nRegistration Successful"; + + if http_request.headers.content_type.as_ref().expect("") != "application/json" { + let response = "HTTP/1.1 400 BAD REQUEST\r\n\r\nContent-Type must be application/json"; + stream.write_all(response.as_bytes()).unwrap(); + return; + } + + let user = parse_json_body::(&http_request.body.expect("")).unwrap(); + println!("Registered user: {}", user.username); + + stream.write_all(response.as_bytes()).unwrap(); } \ No newline at end of file diff --git a/src/routes/proxy.rs b/src/routes/proxy.rs index 9fb7e44..7ba0034 100644 --- a/src/routes/proxy.rs +++ b/src/routes/proxy.rs @@ -10,10 +10,7 @@ fn is_file(file: &str) -> bool { pub fn proxy(http_request: HttpRequest, stream: &TcpStream) { println!("Proxying request..."); - let path = &http_request.path; - let parsed_path = path.trim_start_matches('/').split('/').collect::>(); - let target = parsed_path[parsed_path.len() -1]; - if is_file(target) { + if is_file(http_request.clone().headers.path.expect("").join("/").as_str()) { statics::serve_static_file(http_request.clone(), stream); } else { handle_api_request(http_request.clone(), stream); @@ -22,11 +19,10 @@ pub fn proxy(http_request: HttpRequest, stream: &TcpStream) { pub fn handle_api_request(http_request: HttpRequest, stream: &TcpStream) { - let parsed_path = http_request.path.trim_start_matches('/').split('/').collect::>(); - - match parsed_path[0] { + println!("{:?}", http_request.headers.path.clone().expect("")[0].as_str()); + match http_request.headers.path.clone().expect("")[1].as_str() { "api" => { - match parsed_path[1] { + match http_request.headers.path.clone().expect("")[2].as_str() { "auth" => auth::handle_auth_request(http_request, &stream), _ => (), } diff --git a/src/routes/statics.rs b/src/routes/statics.rs index 71551da..dba4b36 100644 --- a/src/routes/statics.rs +++ b/src/routes/statics.rs @@ -5,7 +5,7 @@ use crate::models::HttpRequest::HttpRequest; pub fn serve_static_file(file_path: HttpRequest, mut stream: &TcpStream) { - let file = std::fs::read_to_string(format!("./static/{}", file_path.path.trim_start_matches('/'))); + let file = std::fs::read_to_string(format!("./static/{}", file_path.headers.path.expect("").join("/").trim_start_matches('/'))); let response = match file { Ok(contents) => { diff --git a/src/utils/db.rs b/src/utils/db.rs new file mode 100644 index 0000000..9dbb01a --- /dev/null +++ b/src/utils/db.rs @@ -0,0 +1,7 @@ +use sqlx; + +pub async fn establish_connection() -> Result { + let database_url = "non"; + let pool = PgPool::connect(database_url).await?; + Ok(pool) +} \ No newline at end of file