From 128543eda8b2d84c55f2c2093845c194b4da75d3 Mon Sep 17 00:00:00 2001 From: astria Date: Thu, 18 Sep 2025 20:20:50 +0200 Subject: [PATCH] Added jwt service --- Cargo.lock | 36 +++++++++++++++ Cargo.toml | 2 + requests/auth.http | 8 +++- src/controllers/auth.rs | 94 +++++++++++++++++++++++++++++++++++++++ src/controllers/mod.rs | 1 + src/http.rs | 36 +++++++-------- src/main.rs | 9 +++- src/middlewares/mod.rs | 1 + src/middlewares/users.rs | 9 ++++ src/models/credentials.rs | 7 +++ src/models/mod.rs | 2 +- src/routes/auth.rs | 48 ++++++++------------ src/routes/proxy.rs | 28 ++++++------ src/routes/statics.rs | 11 ++--- src/services/mod.rs | 1 + src/services/users.rs | 64 ++++++++++++++++++++++++++ src/utils/crypt.rs | 37 +++++++++++++++ src/utils/db.rs | 3 +- src/utils/mod.rs | 2 + 19 files changed, 326 insertions(+), 73 deletions(-) create mode 100644 src/controllers/auth.rs create mode 100644 src/controllers/mod.rs create mode 100644 src/middlewares/mod.rs create mode 100644 src/middlewares/users.rs create mode 100644 src/models/credentials.rs create mode 100644 src/services/mod.rs create mode 100644 src/services/users.rs create mode 100644 src/utils/crypt.rs create mode 100644 src/utils/mod.rs diff --git a/Cargo.lock b/Cargo.lock index b948f50..006f0ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,8 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" name = "chatty" version = "0.1.0" dependencies = [ + "dotenv", + "futures", "serde", "serde_json", "sqlx", @@ -209,6 +211,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dotenvy" version = "0.15.7" @@ -278,6 +286,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -322,6 +345,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -340,8 +374,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", diff --git a/Cargo.toml b/Cargo.toml index f43db71..c59b5f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,5 @@ 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"]} +futures = "0.3" +dotenv = "0.15.0" diff --git a/requests/auth.http b/requests/auth.http index b450483..799c611 100644 --- a/requests/auth.http +++ b/requests/auth.http @@ -1,8 +1,14 @@ ### -POST /api/auth/login/ HTTP/1.1 +POST /api/auth/login HTTP/1.1 Host: localhost:7878 User-Agent: curl/7.68.0 Accept: */* +Content-Type: application/json + +{ + "email": "astria@example.com", + "password": "securepassword" +} ### POST /api/auth/register HTTP/1.1 diff --git a/src/controllers/auth.rs b/src/controllers/auth.rs new file mode 100644 index 0000000..75c374c --- /dev/null +++ b/src/controllers/auth.rs @@ -0,0 +1,94 @@ +use tokio::net::TcpStream as TokioTcpStream; +use tokio::io::AsyncWriteExt; +use crate::models::user; +use crate::parsers::parse_json_body; +use crate::models::{HttpRequest::HttpRequest, user::User, credentials::Credentials}; +use crate::middlewares::users::do_user_already_exist; +use crate::utils::crypt; + +pub async fn register(http_request: HttpRequest, stream: &mut TokioTcpStream) { + let response = "HTTP/1.1 200 OK\r\n\r\nRegistration Successful"; + + // MIDDLEWARE: Check if user already exists + 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"; + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } + return; + } + + let mut user = parse_json_body::(&http_request.body.expect("")).unwrap(); + user.password = crate::utils::crypt::encrypt(&user.password); + + if do_user_already_exist(user.clone()).await { + let response = "HTTP/1.1 409 CONFLICT\r\n\r\nUser already exists"; + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } + return; + } + + // SERVICE: Register the user + match crate::services::users::register(user).await { + Ok(_) => { + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } + } + Err(e) => { + let response = format!("HTTP/1.1 500 INTERNAL SERVER ERROR\r\n\r\nError: {}", e); + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } + } + } +} + +pub async fn login(http_request: HttpRequest, stream: &mut TokioTcpStream) { + + 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"; + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } + return; + } + + let body = http_request.body.as_ref().expect(""); + let creds = parse_json_body::(&http_request.body.expect("")).unwrap(); + + let user = match crate::services::users::get_user_by_email(&creds.email).await { + Ok(Some(user)) => user, + Ok(None) => { + let response = "HTTP/1.1 401 UNAUTHORIZED\r\n\r\nInvalid email or password"; + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } + return; + } + Err(e) => { + let response = format!("HTTP/1.1 500 INTERNAL SERVER ERROR\r\n\r\nError: {}", e); + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } + return; + } + }; + + if user.password != crate::utils::crypt::encrypt(&creds.password) { + let response = "HTTP/1.1 401 UNAUTHORIZED\r\n\r\nInvalid email or password"; + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } + return; + } + + let token = crypt::encrypt(&format!("jklhsdfhjkdfsjkhlfzerhjsdf_{}:{}", user.username, user.email)); + + let response = format!("HTTP/1.1 200 OK\r\n\r\nLogin Successful\r\nAuthorization: Bearer {}", token); + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } + +} \ No newline at end of file diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs new file mode 100644 index 0000000..5696e21 --- /dev/null +++ b/src/controllers/mod.rs @@ -0,0 +1 @@ +pub mod auth; \ No newline at end of file diff --git a/src/http.rs b/src/http.rs index 9986a26..872b735 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,19 +1,18 @@ -use std::net::{TcpListener, TcpStream}; -use std::thread; -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 tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader}; +use crate::models::{HttpRequest::HttpRequest}; +use crate::parsers::{parse_headers}; use crate::routes::proxy::proxy; +use tokio::net::{TcpListener as TokioTcpListener, TcpStream as TokioTcpStream}; -pub fn http_server() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); +pub async fn http_server() { + let listener = TokioTcpListener::bind("127.0.0.1:7878").await.unwrap(); println!("Listening on port 7878"); - for stream in listener.incoming() { - match stream { - Ok(stream) => { - thread::spawn(move || { - handle_connection(stream); + loop { + match listener.accept().await { + Ok((stream, _)) => { + tokio::spawn(async move { + handle_connection(stream).await; }); } Err(e) => { @@ -23,14 +22,15 @@ pub fn http_server() { } } -fn handle_connection(mut stream: TcpStream) { +async fn handle_connection(mut stream: TokioTcpStream) { + // Convert TokioTcpStream to std::io types for reading let mut reader = BufReader::new(&mut stream); let mut header_lines: Vec = Vec::new(); // Read the HTTP request line by line loop { let mut line = String::new(); - match reader.read_line(&mut line) { + match reader.read_line(&mut line).await { Ok(0) => break, // EOF Ok(_) => { println!("Received line: {}", line.trim()); @@ -45,14 +45,14 @@ fn handle_connection(mut stream: TcpStream) { } } } - 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) { + if let Err(e) = reader.read_exact(&mut body_buffer).await { eprintln!("Error reading body: {}", e); } else { body = String::from_utf8_lossy(&body_buffer).to_string(); @@ -64,7 +64,5 @@ fn handle_connection(mut stream: TcpStream) { body: if body.is_empty() { None } else { Some(body) }, }; - proxy(request, &stream); + proxy(request, &mut stream).await; } - -// FUNCTION TO PARSE HTTP REQUESTS diff --git a/src/main.rs b/src/main.rs index 31c8d96..0a11cd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,19 @@ mod http; mod models; mod parsers; - +mod controllers; +mod utils; +mod services; mod routes; +mod middlewares; +use dotenv::dotenv; #[tokio::main] async fn main() { + dotenv().ok(); println!("Starting application..."); - http::http_server(); + http::http_server().await; } diff --git a/src/middlewares/mod.rs b/src/middlewares/mod.rs new file mode 100644 index 0000000..e3146d3 --- /dev/null +++ b/src/middlewares/mod.rs @@ -0,0 +1 @@ +pub mod users; \ No newline at end of file diff --git a/src/middlewares/users.rs b/src/middlewares/users.rs new file mode 100644 index 0000000..365f412 --- /dev/null +++ b/src/middlewares/users.rs @@ -0,0 +1,9 @@ +use crate::models::user::User; + + + +pub async fn do_user_already_exist(user: User) -> bool { + // Placeholder for middleware logic + let do_exist = crate::services::users::get_user_by_username(&user.username).await; + do_exist.is_ok() +} \ No newline at end of file diff --git a/src/models/credentials.rs b/src/models/credentials.rs new file mode 100644 index 0000000..7eaa06d --- /dev/null +++ b/src/models/credentials.rs @@ -0,0 +1,7 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Credentials { + pub email: String, + pub password: String, +} \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index e76e92e..60e573f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,4 @@ pub mod HttpRequest; pub mod http_header; - +pub mod credentials; pub mod user; \ No newline at end of file diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 2a1540a..5145b58 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,59 +1,47 @@ -use crate::models::{HttpRequest::HttpRequest, user::User}; -use crate::parsers::parse_json_body; -use std::net::TcpStream; -use std::io::prelude::*; +use crate::models::HttpRequest::HttpRequest; +use crate::controllers::auth; +use tokio::net::TcpStream as TokioTcpStream; +use tokio::io::AsyncWriteExt; -pub fn handle_auth_request(http_request: HttpRequest, mut stream: &TcpStream) { +pub async fn handle_auth_request(http_request: HttpRequest, stream: &mut TokioTcpStream) { match http_request.headers.method.as_ref().expect("").as_str() { "POST" => { // Handle login or registration - post(http_request, &stream); + post(http_request, stream).await; } "GET" => { // Handle fetching user info or logout 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(); + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } } _ => { // Method not allowed let response = "HTTP/1.1 405 METHOD NOT ALLOWED\r\n\r\nMethod Not Allowed"; - stream.write_all(response.as_bytes()).unwrap(); + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } return; } } } -pub fn post(http_request: HttpRequest, mut stream: &TcpStream) { +pub async fn post(http_request: HttpRequest, stream: &mut TokioTcpStream) { 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(); + auth::login(http_request, stream).await; } "/api/auth/register" => { - register(http_request, &stream); + auth::register(http_request, stream).await; } _ => { let response = "HTTP/1.1 404 NOT FOUND\r\n\r\nAuth Endpoint Not Found"; - stream.write_all(response.as_bytes()).unwrap(); + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } } } -} - -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 7ba0034..c6a194c 100644 --- a/src/routes/proxy.rs +++ b/src/routes/proxy.rs @@ -1,33 +1,33 @@ use crate::models::HttpRequest::HttpRequest; use crate::routes::statics; -use std::net::TcpStream; +use tokio::net::TcpStream as TokioTcpStream; use crate::routes::auth; fn is_file(file: &str) -> bool { file.contains('.') } -pub fn proxy(http_request: HttpRequest, stream: &TcpStream) { +pub async fn proxy(http_request: HttpRequest, stream: &mut TokioTcpStream) { println!("Proxying request..."); if is_file(http_request.clone().headers.path.expect("").join("/").as_str()) { - statics::serve_static_file(http_request.clone(), stream); + statics::serve_static_file(http_request.clone(), stream).await; } else { - handle_api_request(http_request.clone(), stream); + handle_api_request(http_request.clone(), stream).await; } } -pub fn handle_api_request(http_request: HttpRequest, stream: &TcpStream) { +pub async fn handle_api_request(http_request: HttpRequest, stream: &mut TokioTcpStream) { println!("{:?}", http_request.headers.path.clone().expect("")[0].as_str()); - match http_request.headers.path.clone().expect("")[1].as_str() { - "api" => { - match http_request.headers.path.clone().expect("")[2].as_str() { - "auth" => auth::handle_auth_request(http_request, &stream), - _ => (), - } - }, - _ => (), - }; + match http_request.headers.path.clone().expect("")[1].as_str() { + "api" => { + match http_request.headers.path.clone().expect("")[2].as_str() { + "auth" => auth::handle_auth_request(http_request, stream).await, + _ => futures::future::ready(()).await, + } + }, + _ => futures::future::ready(()).await, + }; } \ No newline at end of file diff --git a/src/routes/statics.rs b/src/routes/statics.rs index dba4b36..b8a4763 100644 --- a/src/routes/statics.rs +++ b/src/routes/statics.rs @@ -1,9 +1,8 @@ -use std::net::TcpStream; -use std::io::prelude::*; - +use tokio::net::TcpStream as TokioTcpStream; +use tokio::io::AsyncWriteExt; use crate::models::HttpRequest::HttpRequest; -pub fn serve_static_file(file_path: HttpRequest, mut stream: &TcpStream) { +pub async fn serve_static_file(file_path: HttpRequest, stream: &mut TokioTcpStream) { let file = std::fs::read_to_string(format!("./static/{}", file_path.headers.path.expect("").join("/").trim_start_matches('/'))); @@ -22,5 +21,7 @@ pub fn serve_static_file(file_path: HttpRequest, mut stream: &TcpStream) { } }; - stream.write_all(response.as_bytes()).unwrap(); + if let Err(e) = stream.write_all(response.as_bytes()).await { + eprintln!("Error writing response: {}", e); + } } \ No newline at end of file diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..e3146d3 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1 @@ +pub mod users; \ No newline at end of file diff --git a/src/services/users.rs b/src/services/users.rs new file mode 100644 index 0000000..c80181d --- /dev/null +++ b/src/services/users.rs @@ -0,0 +1,64 @@ +use crate::models::user::User; +use sqlx::Row; + +pub async fn register(user: User) -> Result<(), sqlx::Error> { + // Here you would normally insert the user into the database + // For demonstration, we'll just print the user info + println!("Registering user: {}", user.username); + let db = crate::utils::db::establish_connection().await?; + sqlx::query( + "INSERT INTO users (username, email, password) VALUES ($1, $2, $3)" + ) + .bind(user.username) + .bind(user.email) + .bind(user.password) + .execute(&db) + .await?; + Ok(()) +} + +pub async fn get_user_by_username(username: &str) -> Result, sqlx::Error> { + let db = crate::utils::db::establish_connection().await?; + let user_query = sqlx::query( + "SELECT id, username, email, password FROM users WHERE username = $1" + ) + .bind(username) + .fetch_optional(&db) + .await?; + + let user = if let Some(row) = user_query { + Some(User { + id: row.get("id"), + username: row.get("username"), + email: row.get("email"), + password: row.get("password"), + }) + } else { + None + }; + + Ok(user) +} + +pub async fn get_user_by_email(email: &str) -> Result, sqlx::Error> { + let db = crate::utils::db::establish_connection().await?; + let user_query = sqlx::query( + "SELECT id, username, email, password FROM users WHERE email = $1" + ) + .bind(email) + .fetch_optional(&db) + .await?; + + let user = if let Some(row) = user_query { + Some(User { + id: row.get("id"), + username: row.get("username"), + email: row.get("email"), + password: row.get("password"), + }) + } else { + None + }; + + Ok(user) +} \ No newline at end of file diff --git a/src/utils/crypt.rs b/src/utils/crypt.rs new file mode 100644 index 0000000..7d18ac5 --- /dev/null +++ b/src/utils/crypt.rs @@ -0,0 +1,37 @@ + + +pub fn encrypt(plaintext: &str) -> String { + let data = plaintext.as_bytes(); + let key = std::env::var("ENCRYPTION_KEY").unwrap(); + let key = key.as_bytes(); + let mut encrypted = Vec::new(); + + for (i, byte) in data.iter().enumerate() { + let key_byte = key[i % key.len()]; + let encrypted_byte = byte ^ key_byte; + print!("{:02x}", encrypted_byte); + encrypted.push(encrypted_byte); + } + println!(); + encrypted.iter().map(|b| format!("{:02x}", b)).collect() + +} + +pub fn decrypt(ciphertext: &str) -> String { + let bytes = (0..ciphertext.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&ciphertext[i..i + 2], 16).unwrap()) + .collect::>(); + + let key = std::env::var("ENCRYPTION_KEY").unwrap(); + let key = key.as_bytes(); + let mut decrypted = Vec::new(); + + for (i, byte) in bytes.iter().enumerate() { + let key_byte = key[i % key.len()]; + let decrypted_byte = byte ^ key_byte; + decrypted.push(decrypted_byte); + } + + String::from_utf8(decrypted).unwrap() +} \ No newline at end of file diff --git a/src/utils/db.rs b/src/utils/db.rs index 9dbb01a..496f946 100644 --- a/src/utils/db.rs +++ b/src/utils/db.rs @@ -1,7 +1,8 @@ use sqlx; +use sqlx::PgPool; pub async fn establish_connection() -> Result { - let database_url = "non"; + let database_url = "postgres://astria:Rwqfsfasxc_974@192.168.1.109/chatty"; let pool = PgPool::connect(database_url).await?; Ok(pool) } \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..349ffb3 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod db; +pub mod crypt; \ No newline at end of file