diff --git a/README.md b/README.md index 58b70b0..37e33f5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # Amnezichat - + ## Anti-forensic and secure messenger @@ -49,7 +49,7 @@ Amnezichat offers a highly secure and privacy-focused messaging experience by en - EdDSA and Dilithium5 for authentication, ECDH and Kyber1024 for key exchange, encryption using ChaCha20-Poly1305 -## Basic server setup: +## Server setup: sudo apt update sudo apt install curl build-essential git @@ -59,16 +59,16 @@ Amnezichat offers a highly secure and privacy-focused messaging experience by en cargo build --release cargo run --release -## Onionsite setup with Docker: +## Server setup with Docker: sudo apt update sudo apt install docker.io git git clone https://github.com/umutcamliyurt/Amnezichat.git - cd Amnezichat/ - sudo docker build -t amnezichat:latest . - sudo docker run -p 8080:8080 amnezichat:latest + cd Amnezichat/server/ + sudo docker build -t amnezichatserver:latest . + sudo docker run -p 8080:8080 amnezichatserver:latest -## Client usage: +## Client setup: **For Web UI connect to http://localhost:8000** @@ -81,6 +81,15 @@ Amnezichat offers a highly secure and privacy-focused messaging experience by en cargo build --release cargo run --release +## Client setup with Docker: + + sudo apt update + sudo apt install docker.io git + git clone https://github.com/umutcamliyurt/Amnezichat.git + cd Amnezichat/client/ + sudo docker build -t amnezichat:latest . + sudo docker run -p 8000:8000 amnezichat:latest + ## Requirements: - [Rust](https://www.rust-lang.org), [Tor](https://gitlab.torproject.org/tpo/core/tor), [I2P](https://i2pd.website/) diff --git a/README_TR.md b/README_TR.md index 36b473b..69978ba 100644 --- a/README_TR.md +++ b/README_TR.md @@ -5,7 +5,7 @@ # Amnezichat - + ## İz bırakmayan güvenli mesajlaşma @@ -59,16 +59,16 @@ Amnezichat, hiçbir kayıt tutulmamasını ve tüm mesaj verilerinin yalnızca s cargo build --release cargo run --release -## Docker ile Onion sitesi kurulumu: +## Docker ile sunucu kurulumu: sudo apt update sudo apt install docker.io git git clone https://github.com/umutcamliyurt/Amnezichat.git - cd Amnezichat/ - sudo docker build -t amnezichat:latest . - sudo docker run -p 8080:8080 amnezichat:latest + cd Amnezichat/server/ + sudo docker build -t amnezichatserver:latest . + sudo docker run -p 8080:8080 amnezichatserver:latest -## İstemci kullanımı: +## İstemci kurulumu: **Web UI için http://localhost:8000 adresine bağlanın** @@ -81,6 +81,15 @@ Amnezichat, hiçbir kayıt tutulmamasını ve tüm mesaj verilerinin yalnızca s cargo build --release cargo run --release +## Docker ile istemci kurulumu: + + sudo apt update + sudo apt install docker.io git + git clone https://github.com/umutcamliyurt/Amnezichat.git + cd Amnezichat/client/ + sudo docker build -t amnezichat:latest . + sudo docker run -p 8000:8000 amnezichat:latest + ## Gereksinimler: - [Rust](https://www.rust-lang.org), [Tor](https://gitlab.torproject.org/tpo/core/tor), [I2P](https://i2pd.website/) diff --git a/client/Cargo.toml b/client/Cargo.toml index 71b3747..c16d9ff 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -21,4 +21,7 @@ rpassword = "7.3.1" sha3 = "0.10.8" x25519-dalek = "2.0.1" ed25519-dalek = "2.1.1" -rocket = { version = "0.5", features = ["json"] } \ No newline at end of file +rocket = { version = "0.5", features = ["json"] } +eframe = "0.26" +egui = "0.26" +rfd = "0.12" \ No newline at end of file diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..5d4f472 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,26 @@ +FROM debian:12 + +ENV DEBIAN_FRONTEND=noninteractive +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH + +RUN apt-get update && \ + apt-get install -y \ + curl \ + build-essential \ + git \ + tor && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y + +RUN git clone https://github.com/umutcamliyurt/Amnezichat.git /opt/Amnezichat + +WORKDIR /opt/Amnezichat/client +RUN cargo build --release + +EXPOSE 8000 + +CMD tor & cargo run --release diff --git a/client/cloudflare-ip-blacklist.txt b/client/cloudflare-ip-blacklist.txt deleted file mode 100644 index ca50c34..0000000 --- a/client/cloudflare-ip-blacklist.txt +++ /dev/null @@ -1,22 +0,0 @@ -173.245.48.0/20 -103.21.244.0/22 -103.22.200.0/22 -103.31.4.0/22 -141.101.64.0/18 -108.162.192.0/18 -190.93.240.0/20 -188.114.96.0/20 -197.234.240.0/22 -198.41.128.0/17 -162.158.0.0/15 -104.16.0.0/13 -104.24.0.0/14 -172.64.0.0/13 -131.0.72.0/22 -2400:cb00::/32 -2606:4700::/32 -2803:f800::/32 -2405:b500::/32 -2405:8100::/32 -2a06:98c0::/29 -2c0f:f248::/32 diff --git a/client/src/main.rs b/client/src/main.rs index 7bfa3c2..b0a6e83 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -34,29 +34,33 @@ use encryption::decrypt_data; use oqs::*; use oqs::sig::{Sig, PublicKey, SecretKey, Algorithm as SigAlgorithm}; use rand::Rng; -use reqwest::blocking::get; -use std::fs; use std::fs::File; use std::fs::OpenOptions; use std::io::BufRead; use std::io::BufReader; use std::path::Path; use std::process::Command; -use std::str::FromStr; +use std::sync::Arc; +use std::sync::Mutex; +use std::thread; +use std::time::Duration; use hex; use std::io::{self, Write}; -use rpassword::read_password; use std::result::Result; use std::{ collections::HashSet, error::Error, }; -use ipnetwork::IpNetwork; use serde::{Deserialize, Serialize}; use chacha20poly1305::aead::OsRng; use rand::RngCore; use sha3::{Sha3_512, Digest}; use ed25519_dalek::VerifyingKey as Ed25519PublicKey; +use eframe::egui; +use rfd::MessageDialog; +use rfd::MessageButtons; +use rfd::MessageLevel; +use rfd::MessageDialogResult; fn get_raw_bytes_public_key(pk: &PublicKey) -> &[u8] { pk.as_ref() @@ -95,7 +99,6 @@ fn request_user_confirmation( } let path = "contact_fingerprints.enc"; - let trusted_fingerprints = load_trusted_fingerprints(path, password)?; if trusted_fingerprints.contains(fingerprint) { @@ -103,34 +106,39 @@ fn request_user_confirmation( return Ok(true); } - println!("The fingerprint of the received public key is: {}", fingerprint); - print!("Do you confirm this fingerprint? (yes/no): "); - io::stdout().flush()?; + let message = format!( + "🔒 Fingerprint Verification\n\n\ + Your fingerprint:\n{}\n\n\ + Received fingerprint:\n{}\n\n\ + Do you want to trust the received fingerprint?", + own_fingerprint, fingerprint + ); - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - let response = input.trim().to_lowercase(); + let confirm = MessageDialog::new() + .set_title("Trust New Fingerprint") + .set_level(MessageLevel::Info) + .set_description(&message) + .set_buttons(MessageButtons::YesNo) + .show(); - match response.as_str() { - "yes" => { - print!("Would you like to remember this fingerprint for future sessions? (yes/no): "); - io::stdout().flush()?; + if confirm == MessageDialogResult::Yes { + let remember = MessageDialog::new() + .set_title("Remember Fingerprint?") + .set_level(MessageLevel::Info) + .set_description( + "💾 Would you like to remember this fingerprint for future sessions?\n\ + This prevents asking again for the same contact." + ) + .set_buttons(MessageButtons::YesNo) + .show(); - input.clear(); - io::stdin().read_line(&mut input)?; - let remember_response = input.trim().to_lowercase(); - - if remember_response == "yes" { - save_fingerprint(path, fingerprint, password)?; - } - - Ok(true) - } - "no" => Ok(false), - _ => { - println!("Invalid input. Please enter 'yes' or 'no'."); - request_user_confirmation(fingerprint, own_fingerprint, password) + if remember == MessageDialogResult::Yes { + save_fingerprint(path, fingerprint, password)?; } + + Ok(true) + } else { + Ok(false) } } @@ -194,57 +202,6 @@ fn generate_random_room_id() -> String { room_id } -fn load_blacklist(file_path: &str) -> HashSet { - match fs::read_to_string(file_path) { - Ok(contents) => contents - .lines() - .filter_map(|line| { - let line = line.trim(); - IpNetwork::from_str(line).ok() - }) - .collect(), - Err(_) => HashSet::new(), - } -} - -fn is_onion_site(url: &str) -> bool { - url.contains(".onion") -} - -fn is_eepsite(url: &str) -> bool { - url.contains(".i2p") -} - -fn resolve_dns(host: &str) -> Result> { - let output = Command::new("dig") - .args(["+short", host]) - .output()?; - - if output.status.success() { - let response = String::from_utf8_lossy(&output.stdout); - - if let Some(ip) = response - .lines() - .filter(|line| line.parse::().is_ok()) - .next() - { - return Ok(ip.to_string()); - } - } - - Err("Failed to resolve DNS to an IP address.".into()) -} - -fn is_ip_blacklisted(ip: &str, blacklist: &HashSet) -> bool { - - let ip: std::net::IpAddr = match ip.parse() { - Ok(ip) => ip, - Err(_) => return false, - }; - - blacklist.iter().any(|range| range.contains(ip)) -} - fn pad_message(message: &str, max_length: usize) -> String { let current_length = message.len(); @@ -262,176 +219,233 @@ fn pad_message(message: &str, max_length: usize) -> String { message.to_string() } +#[derive(Clone)] +struct AppState { + choice: String, + server_url: String, + username: String, + private_password: String, + is_group_chat: bool, + show_url_label: bool, + room_id_input: String, + room_password: String, + error_message: Option, +} + +impl Default for AppState { + fn default() -> Self { + Self { + choice: "".into(), + server_url: "".into(), + username: "".into(), + private_password: "".into(), + is_group_chat: false, + show_url_label: false, + room_id_input: "".into(), + room_password: "".into(), + error_message: None, + } + } +} + fn main() -> Result<(), Box> { - use std::sync::{Arc, Mutex}; - use std::{io::{self, Write}, thread, time::Duration}; + let mut options = eframe::NativeOptions::default(); + + options.viewport.resizable = Some(false); + + options.viewport.inner_size = Some(egui::vec2(600.0, 900.0)); + + eframe::run_native("Messaging Setup", options, Box::new(|_cc| Box::new(SetupApp::default())))?; + Ok(()) +} + +struct SetupApp { + state: AppState, +} + +impl Default for SetupApp { + fn default() -> Self { + Self { + state: AppState::default(), + } + } +} + +impl eframe::App for SetupApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.add_space(20.0); + ui.heading(egui::RichText::new("Amnezichat").size(40.0)); + ui.add_space(30.0); + + egui::Frame::group(ui.style()).inner_margin(egui::style::Margin::symmetric(20.0, 20.0)).show(ui, |ui| { + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new("Choose an action:").size(24.0)); + ui.add_space(10.0); + ui.horizontal_wrapped(|ui| { + + ui.add_space(20.0); + if ui.add( + egui::Button::new(egui::RichText::new("➕ Create Room").size(24.0)) + .min_size(egui::vec2(200.0, 60.0)) + .fill(egui::Color32::from_rgb(50, 50, 50)) + ).clicked() { + self.state.choice = "create".into(); + self.state.room_id_input = generate_random_room_id(); + } + ui.add_space(100.0); + if ui.add( + egui::Button::new(egui::RichText::new("🔗 Join Room").size(24.0)) + .min_size(egui::vec2(200.0, 60.0)) + .fill(egui::Color32::from_rgb(50, 50, 50)) + ).clicked() { + self.state.choice = "join".into(); + } + }); + + ui.add_space(20.0); + + match self.state.choice.as_str() { + "join" => { + ui.separator(); + ui.label(egui::RichText::new("🔑 Enter Room ID:").size(22.0)); + ui.add( + egui::TextEdit::singleline(&mut self.state.room_id_input) + .font(egui::TextStyle::Heading) + .desired_width(300.0) + ); + } + "create" => { + if !self.state.room_id_input.is_empty() { + ui.separator(); + ui.label(egui::RichText::new("🆔 Generated Room ID:").size(22.0)); + ui.code(egui::RichText::new(&self.state.room_id_input).size(20.0)); + } + } + _ => {} + } + }); + }); + + ui.add_space(30.0); + + egui::Frame::group(ui.style()).inner_margin(egui::style::Margin::symmetric(20.0, 20.0)).show(ui, |ui| { + ui.vertical_centered(|ui| { + ui.heading(egui::RichText::new("🔧 Connection Details").size(36.0)); + ui.add_space(20.0); + + egui::Grid::new("connection_details") + .num_columns(2) + .spacing([50.0, 16.0]) + .show(ui, |ui| { + ui.label(egui::RichText::new("Server URL:").size(22.0)); + ui.add( + egui::TextEdit::singleline(&mut self.state.server_url) + .font(egui::TextStyle::Heading) + .desired_width(300.0) + ); + ui.end_row(); + + ui.label(egui::RichText::new("Username:").size(22.0)); + ui.add( + egui::TextEdit::singleline(&mut self.state.username) + .font(egui::TextStyle::Heading) + .desired_width(300.0) + ); + ui.end_row(); + + ui.label(egui::RichText::new("Private Password:").size(22.0)); + ui.add( + egui::TextEdit::singleline(&mut self.state.private_password) + .password(true) + .font(egui::TextStyle::Heading) + .desired_width(300.0) + ); + ui.end_row(); + }); + + ui.add_space(20.0); + ui.checkbox(&mut self.state.is_group_chat, egui::RichText::new("👥 Is Group Chat?").size(22.0)); + + if self.state.is_group_chat { + ui.add_space(20.0); + ui.label(egui::RichText::new("🔒 Room Password (min 8 chars):").size(22.0)); + ui.add( + egui::TextEdit::singleline(&mut self.state.room_password) + .font(egui::TextStyle::Heading) + .desired_width(300.0) + ); + } + }); + }); + + ui.add_space(30.0); + + if ui.add( + egui::Button::new(egui::RichText::new("🚀 Start Messaging").size(28.0)) + .fill(egui::Color32::from_rgb(0, 100, 0)) + .min_size(egui::vec2(250.0, 60.0)) + ).clicked() { + if let Err(err) = validate_and_start(self.state.clone()) { + self.state.error_message = Some(err.to_string()); + } else { + + self.state.show_url_label = true; + } + } + + if let Some(err) = &self.state.error_message { + ui.add_space(20.0); + ui.colored_label(egui::Color32::RED, egui::RichText::new(format!("❗ {}", err)).size(22.0)); + } + + if self.state.show_url_label { + ui.add_space(20.0); + ui.label(egui::RichText::new("Open http://127.0.0.1:8000 in your web browser").size(22.0)); + } + }); + }); + } +} + +fn validate_and_start(state: AppState) -> Result<(), Box> { + if state.server_url.is_empty() || state.username.is_empty() || state.private_password.is_empty() { + return Err("Please fill in all fields.".into()); + } + if state.is_group_chat && state.room_password.len() <= 8 { + return Err("Room password must be longer than 8 characters.".into()); + } + + std::thread::spawn(move || { + if let Err(e) = run_app_logic(state) { + eprintln!("App error: {}", e); + } + }); + + Ok(()) +} + +fn run_app_logic(state: AppState) -> Result<(), Box> { let sigalg = sig::Sig::new(sig::Algorithm::Dilithium5)?; - println!("Would you like to create a new room or join an existing one?"); - println!("Type 'create' to create a new room or 'join' to join an existing one."); - let mut choice = String::new(); - io::stdin().read_line(&mut choice)?; - let choice = choice.trim(); + let room_id = state.room_id_input.clone(); + let url = state.server_url.clone(); + let username = state.username.clone(); + let private_password = state.private_password.clone(); - let room_id = match choice { - "create" => { - let new_room_id = generate_random_room_id(); - println!("Generated new room ID: {}", new_room_id); - new_room_id - } - "join" => { - println!("Enter the room ID to join:"); - let mut room_input = String::new(); - io::stdin().read_line(&mut room_input)?; - room_input.trim().to_string() - } - _ => { - println!("Invalid choice. Please restart the program and choose 'create' or 'join'."); - return Ok(()); - } - }; - - let blacklist_file = "cloudflare-ip-blacklist.txt"; - if !Path::new(blacklist_file).exists() { - println!("File '{}' not found. Fetching from Codeberg...", blacklist_file); - - let url = "https://codeberg.org/umutcamliyurt/Amnezichat/raw/branch/main/client/cloudflare-ip-blacklist.txt"; - let response = get(url)?; - - if response.status().is_success() { - let content = response.text()?; - - let mut file = File::create(blacklist_file)?; - file.write_all(content.as_bytes())?; - println!("File fetched and saved as '{}'.", blacklist_file); - } else { - println!("Failed to fetch the file from URL."); - return Err("Failed to fetch blacklist.".into()); - } - } - - let blacklist = load_blacklist("cloudflare-ip-blacklist.txt"); - - let mut input = String::new(); - print!("Enter the server URL: "); - io::stdout().flush()?; - io::stdin().read_line(&mut input)?; - let url = input.trim().to_string(); - input.clear(); - - if is_onion_site(&url) { - println!("This is an .onion site. Skipping IP check."); - } - else if is_eepsite(&url) - { - println!("This is an .i2p site. Skipping IP check."); - } - else { - - let host = url - .split('/') - .nth(2) - .unwrap_or(&url) - .split(':') - .next() - .unwrap_or(&url); - - match resolve_dns(host) { - Ok(ip) => { - - if is_ip_blacklisted(&ip, &blacklist) { - println!("WARNING! The IP {} is in the blacklist.", ip); - println!("The server you're trying to access is behind a Cloudflare reverse proxy."); - println!("Proceed with caution as this setup may expose you to several potential risks:"); - println!(); - println!("Deanonymization attacks (including 0-click exploits)"); - println!("Metadata leaks"); - println!("Encryption vulnerabilities"); - println!("AI-based traffic analysis"); - println!("Connectivity issues"); - println!("Other undetected malicious behavior"); - println!(); - println!("What you can do:"); - println!("1. Choose a different server"); - println!("2. Self-host your own server"); - println!("3. Proceed anyway (Dangerous!)"); - println!(); - println!("For more info: https://git.calitabby.net/mirrors/deCloudflare"); - println!(); - println!("Do you want to proceed? (yes/no)"); - - let mut input = String::new(); - io::stdin() - .read_line(&mut input) - .expect("Failed to read input"); - let input = input.trim().to_lowercase(); - - match input.as_str() { - "yes" | "y" => { - println!("Proceeding..."); - } - "no" | "n" => { - println!("Operation aborted!"); - return Ok(()); - } - _ => { - println!("Invalid input. Please enter 'yes' or 'no'."); - } - } - } - } - Err(e) => { - println!("Failed to resolve IP for the server: {}", e); - } - } - } - - print!("Enter your username: "); - io::stdout().flush()?; - io::stdin().read_line(&mut input)?; - let username = input.trim().to_string(); - input.clear(); - - print!("Enter private key encryption password: "); - io::stdout().flush()?; - let private_password = read_password()?.to_string(); - - println!("Is this a group chat? (yes/no): "); - let mut is_group_chat = String::new(); - io::stdin().read_line(&mut is_group_chat)?; - let is_group_chat = is_group_chat.trim().to_lowercase() == "yes"; - - let room_password = if is_group_chat { - - loop { - print!("Enter room password (must be longer than 8 characters): "); - io::stdout().flush()?; - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - let password_input = input.trim(); - if password_input.len() > 8 { - break password_input.to_string(); - } else { - println!("Error: Password must be longer than 8 characters. Please try again."); - } - } + let room_password = if state.is_group_chat { + let salt = derive_salt_from_password(&state.room_password); + let key = derive_key(&state.room_password, &salt); + hex::encode(key) } else { - String::new() }; - let room_password = if is_group_chat { - let salt = derive_salt_from_password(&room_password); - let key = derive_key(&room_password, &salt); - hex::encode(key) - } else { - String::new() - }; - - if is_group_chat { + if state.is_group_chat { println!("Skipping key exchange. Using room password as shared secret."); - let hybrid_shared_secret = room_password.clone(); + let hybrid_shared_secret = room_password.clone(); println!("Shared secret established."); println!("You can now start messaging!"); @@ -482,7 +496,7 @@ fn main() -> Result<(), Box> { &room_id_locked, &shared_hybrid_secret, &url_locked, - true, + true, ) { Ok(_) => {} Err(e) => eprintln!("Error fetching messages: {}", e), @@ -506,14 +520,8 @@ fn main() -> Result<(), Box> { } }); - if let Err(e) = random_data_thread.join() { - eprintln!("Random data thread terminated with error: {:?}", e); - } - - if let Err(e) = fetch_thread.join() { - eprintln!("Fetch thread terminated with error: {:?}", e); - } - + random_data_thread.join().ok(); + fetch_thread.join().ok(); return Ok(()); } diff --git a/client/src/network_operations.rs b/client/src/network_operations.rs index cb1fd36..747ddc3 100644 --- a/client/src/network_operations.rs +++ b/client/src/network_operations.rs @@ -412,7 +412,6 @@ pub fn send_encrypted_message( .send()?; if res.status().is_success() { - println!("Message sent successfully."); } else { eprintln!("Failed to send message: {}", res.status()); } diff --git a/client/static/index.html b/client/static/index.html index de2380b..147d829 100644 --- a/client/static/index.html +++ b/client/static/index.html @@ -13,37 +13,53 @@
+ +
+ +

Settings

- -
-
+ -
×
+ + diff --git a/client/static/styles.css b/client/static/styles.css index cbedcff..afc774f 100644 --- a/client/static/styles.css +++ b/client/static/styles.css @@ -2,113 +2,129 @@ html, body { height: 100%; margin: 0; font-family: 'Helvetica Neue', Arial, sans-serif; - background-color: #1e1f22; + background-color: #121212; color: #e1e1e1; + scroll-behavior: smooth; } * { box-sizing: border-box; - transition: background-color 0.3s, color 0.3s, border-color 0.3s, box-shadow 0.3s; + transition: all 0.3s ease; } .container { display: flex; flex-direction: column; height: 100%; - padding: 20px; + width: 100%; + padding: 24px; } /* Messages area */ #messages { flex-grow: 1; - padding: 15px; + width: 100%; + padding: 20px; overflow-y: auto; - border: 1px solid #333b41; - background-color: #2c2f36; - border-radius: 10px; + background: rgba(44, 47, 54, 0.7); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 16px; display: flex; flex-direction: column; - gap: 10px; - margin-bottom: 20px; + gap: 12px; + margin-bottom: 24px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2); } .message-row { display: flex; align-items: flex-start; - gap: 10px; + gap: 12px; max-width: 100%; } .profile-pic { - width: 50px; - height: 50px; + width: 48px; + height: 48px; border-radius: 50%; object-fit: cover; flex-shrink: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .message-bubble { - background-color: #444c56; - padding: 10px 15px; - border-radius: 20px; + background: linear-gradient(to bottom right, #3a3f4b, #2e333d); + padding: 12px 18px; + border-radius: 16px; display: flex; flex-direction: column; - gap: 8px; + gap: 6px; word-break: break-word; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); } .message-bubble p { margin: 0; - line-height: 1.4; + line-height: 1.6; + font-size: 15px; } /* Media content */ .media-img, .media-video { - width: 500px; - height: 500px; + width: 480px; + height: 480px; object-fit: cover; - border-radius: 10px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0,0,0,0.4); } /* Input section */ .input-container { display: flex; - gap: 10px; + gap: 12px; align-items: center; + width: 100%; } input[type="text"] { flex-grow: 1; - padding: 10px; - border-radius: 20px; - border: 1px solid #444c56; - background-color: #2c2f36; + padding: 12px 16px; + border-radius: 30px; + border: 1px solid #3a3f4b; + background-color: #1f2128; color: #e1e1e1; + font-size: 15px; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.4); } input[type="text"]:focus { outline: none; border-color: #4c8bf5; + box-shadow: 0 0 0 3px rgba(76, 139, 245, 0.25); } /* Button styles */ button { - padding: 10px 20px; + padding: 10px 22px; border: none; - border-radius: 20px; - background-color: #4c8bf5; + border-radius: 30px; + background: linear-gradient(135deg, #4c8bf5, #3b74d4); color: white; cursor: pointer; - font-size: 16px; + font-size: 15px; + font-weight: 600; + box-shadow: 0 2px 10px rgba(0,0,0,0.3); } button:hover { - background-color: #3978d1; + background: linear-gradient(135deg, #3b74d4, #2f5eb8); } button:active { - background-color: #2962a1; + background: #2f5eb8; + transform: scale(0.98); } /* Settings overlay */ @@ -119,25 +135,28 @@ button:active { left: 0; width: 100%; height: 100%; - background-color: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(6px); justify-content: center; align-items: center; } /* Settings content */ .settings-content { - background-color: #2c2f36; - padding: 20px; - border-radius: 10px; - width: 300px; + background-color: #1f2229; + padding: 24px; + border-radius: 16px; + width: 320px; display: flex; flex-direction: column; - gap: 10px; + gap: 14px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); } .settings-content h2 { margin-top: 0; - color: #e1e1e1; + color: #ffffff; + font-size: 20px; } /* Modal styles */ @@ -150,7 +169,7 @@ button:active { width: 100%; height: 100%; overflow: auto; - background-color: rgba(0,0,0,0.8); + background-color: rgba(0,0,0,0.85); justify-content: center; align-items: center; padding: 20px; @@ -160,7 +179,7 @@ button:active { #mediaModal video { max-width: 90%; max-height: 90%; - border-radius: 10px; + border-radius: 16px; object-fit: contain; } @@ -168,28 +187,28 @@ button:active { position: absolute; top: 20px; right: 30px; - font-size: 30px; + font-size: 32px; color: white; cursor: pointer; z-index: 1001; } -/* ============================= */ -/* Light Theme */ -/* ============================= */ +/* Light Theme Modernization */ body.light-theme { - background-color: #f9f9f9; + background-color: #f1f3f5; color: #1a1a1a; } body.light-theme #messages { - background-color: #ffffff; - border-color: #dcdcdc; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(12px); + border: 1px solid #ccc; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05); } body.light-theme .message-bubble { - background-color: #f1f3f5; + background: #ffffff; + box-shadow: 0 2px 4px rgba(0,0,0,0.06); } body.light-theme input[type="text"] { @@ -200,26 +219,18 @@ body.light-theme input[type="text"] { body.light-theme input[type="text"]:focus { border-color: #4c8bf5; - box-shadow: 0 0 0 2px rgba(76, 139, 245, 0.2); + box-shadow: 0 0 0 3px rgba(76, 139, 245, 0.2); } body.light-theme button { - background-color: #4c8bf5; + background: linear-gradient(135deg, #4c8bf5, #3b74d4); color: white; } -body.light-theme button:hover { - background-color: #3978d1; -} - -body.light-theme button:active { - background-color: #2962a1; -} - body.light-theme .settings-content { background-color: #ffffff; color: #1a1a1a; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); } body.light-theme .settings-content h2 { diff --git a/screenshot.png b/screenshot.png index a702ab0..c424f50 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/Dockerfile b/server/Dockerfile similarity index 70% rename from Dockerfile rename to server/Dockerfile index 85c9aa0..5228fab 100644 --- a/Dockerfile +++ b/server/Dockerfile @@ -1,7 +1,5 @@ -# Use Debian 12 as the base image FROM debian:12 -# Install system dependencies, Tor, and Rust RUN apt-get update && apt-get install -y \ curl \ build-essential \ @@ -10,30 +8,22 @@ RUN apt-get update && apt-get install -y \ && curl https://sh.rustup.rs -sSf | sh -s -- -y \ && rm -rf /var/lib/apt/lists/* -# Add Rust to PATH ENV PATH="/root/.cargo/bin:${PATH}" -# Set the working directory WORKDIR /app -# Clone the repository RUN git clone https://github.com/umutcamliyurt/Amnezichat.git . -# Navigate to the server directory WORKDIR /app/server -# Build the Rust project in release mode RUN cargo build --release -# Expose port 8080 for the application EXPOSE 8080 -# Configure Tor hidden service RUN mkdir -p /var/lib/tor/hidden_service && \ echo "HiddenServiceDir /var/lib/tor/hidden_service" >> /etc/tor/torrc && \ echo "HiddenServicePort 80 127.0.0.1:8080" >> /etc/tor/torrc && \ chown -R debian-tor:debian-tor /var/lib/tor/hidden_service && \ chmod 700 /var/lib/tor/hidden_service -# Start Tor and the application CMD service tor start && sleep 5 && cat /var/lib/tor/hidden_service/hostname && cargo run --release diff --git a/server/static/index.html b/server/static/index.html index 2ef940f..fbc9e6f 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -63,6 +63,11 @@ cd Amnezichat/client/ cargo build --release cargo run --release + + +

Or Download Prebuilt AppImage (Linux)

+

If you prefer a ready-to-use version, download the AppImage below:

+ Download AppImage