mod identity; mod crypto; mod session_manager; mod utils; mod ui_interface; mod constants; mod discovery; use std::{collections::HashMap, env, fmt::Display, fs, io, net::{TcpStream, TcpListener}, path::Path, str::FromStr, sync::{RwLock, Mutex, Arc}, thread, thread::sleep, time::Duration}; use actix_web::{http::{header, CookieBuilder}, App, HttpRequest, HttpResponse, HttpMessage, HttpServer, web, web::Data}; use rand_8::{RngCore, rngs::OsRng}; use session_manager::SessionError; use tungstenite::{ WebSocket, accept, }; use serde::{Deserialize, Serialize}; use utils::escape_double_quote; use uuid::Uuid; use crate::identity::Identity; use crate::session_manager::{SessionManager, protocol}; use crate::ui_interface::UiConnection; fn start_websocket_server(global_vars: &Arc>) -> u16 { let websocket_bind_addr = env::var("AIRA_WEBSOCKET_ADDR").unwrap_or("127.0.0.1".to_owned()); let websocket_port = env::var("AIRA_WEBSOCKET_PORT").unwrap_or("0".to_owned()); let server = TcpListener::bind(websocket_bind_addr+":"+&websocket_port).unwrap(); let websocket_port = server.local_addr().unwrap().port(); let global_vars_clone = Arc::clone(global_vars); thread::spawn(move || { for stream in server.incoming() { let stream = stream.unwrap(); stream.set_read_timeout(Some(Duration::from_millis(100))).unwrap(); match accept(stream) { Ok(websocket) => { websocket_worker(websocket, &global_vars_clone); } Err(e) => print_error(e) } } }); websocket_port } fn on_connected(index: usize, session_manager: &Arc, ui_connection: &Arc>){ let mut ui_connection = ui_connection.lock().unwrap(); ui_connection.on_connected(index); session_manager.handle_new_session(&index, ui_connection); } fn handle_connect(ip: &str, session_manager: &Arc, ui_connection: &Arc>){ match session_manager.connect_to(ip){ Ok(index) => { on_connected(index, session_manager, ui_connection); } Err(e) => { if e != SessionError::AlreadyConnected && e != SessionError::IsUs { print_error(e) } } } } fn websocket_worker(websocket: WebSocket, global_vars: &Arc>){ let ui_connection = Arc::new(Mutex::new(UiConnection::new(websocket))); let global_vars_read = global_vars.read().unwrap(); global_vars_read.session_manager.set_ui_connection(&ui_connection); if global_vars_read.is_backend_running { //ui reconnection global_vars_read.session_manager.list_sessions().into_iter().for_each(|index|{ on_connected(index, &global_vars_read.session_manager, &ui_connection); }); } else { match SessionManager::start_listener(&global_vars_read.session_manager) { Ok(_) => {} Err(e) => println!("{}. You won't be able to receive incomming connections from other peers.", e) } SessionManager::start_receiver_loop(&global_vars_read.session_manager); discovery::advertise_me(); drop(global_vars_read); global_vars.write().unwrap().is_backend_running = true; } let global_vars_read = global_vars.read().unwrap(); global_vars_read.session_manager.list_contacts().into_iter().for_each(|contact|{ let mut ui_connection_locked = ui_connection.lock().unwrap(); ui_connection_locked.set_as_contact(contact.0, &contact.1, contact.2); match global_vars_read.session_manager.load_msgs(&contact.0) { Some(msgs) => { ui_connection_locked.load_msgs(&contact.0, msgs); } None => {} } }); global_vars_read.session_manager.get_saved_msgs().into_iter().for_each(|entry| { ui_connection.lock().unwrap().on_received(&entry.0, &entry.1); }); let session_manager_clone = Arc::clone(&global_vars_read.session_manager); let ui_connection_clone = Arc::clone(&ui_connection); thread::spawn(move || { discovery::discover_peers(|discovery_manager, ip| { println!("New peer discovered: {}", ip); if ui_connection_clone.lock().unwrap().is_valid { handle_connect(&ip, &session_manager_clone, &ui_connection_clone); } else { discovery_manager.stop_service_discovery(); } }); }); loop { let mut ui_connection_locked = ui_connection.lock().unwrap(); let msg_result = ui_connection_locked.read_message(); drop(ui_connection_locked); //release mutex match msg_result { Ok(msg) => { let args: Vec<&str> = msg.split(" ").collect(); match args[0] { "connect" => { handle_connect(args[1], &global_vars_read.session_manager, &ui_connection); } "send" => { let buffer = protocol::new_message(args[2..].join(" ")); let index: usize = args[1].parse().unwrap(); match global_vars_read.session_manager.send_to(&index, &buffer){ Ok(_) => { if global_vars_read.session_manager.is_contact(&index) { match global_vars_read.session_manager.store_msg(&index, true, &buffer) { Ok(_) => {}, Err(e) => print_error(e) } } } Err(e) => print_error(e) } } "contact" => { let index: usize = args[1].parse().unwrap(); match global_vars_read.session_manager.add_contact(index, args[2..].join(" ")) { Ok(_) => {}, Err(e) => print_error(e) } } "uncontact" => { let index: usize = args[1].parse().unwrap(); match global_vars_read.session_manager.remove_contact(&index) { Ok(_) => {}, Err(e) => print_error(e) } } "fingerprints" => { let index: usize = args[1].parse().unwrap(); let (local, peer) = global_vars_read.session_manager.get_public_keys(&index); let local = crypto::generate_fingerprint(&local); let peer = crypto::generate_fingerprint(&peer); ui_connection.lock().unwrap().fingerprints(&local, &peer); } "verify" => { let index: usize = args[1].parse().unwrap(); match global_vars_read.session_manager.set_verified(&index) { Ok(_) => {}, Err(e) => print_error(e) } } "file" => { let index: usize = args[1].parse().unwrap(); let file_name = args[2..].join(" "); let mut ui_connection_locked = ui_connection.lock().unwrap(); let buffer = ui_connection_locked.read_binary(); drop(ui_connection_locked); match buffer { Ok(buffer) => { match global_vars_read.session_manager.send_to(&index, &protocol::file(&file_name, &buffer)) { Ok(_) => { if global_vars_read.session_manager.is_contact(&index) { match global_vars_read.session_manager.store_file(&index, &buffer) { Ok(file_uuid) => { ui_connection.lock().unwrap().file_sent(index, &file_name, Some(file_uuid.to_string())); match global_vars_read.session_manager.store_msg(&index, true, &[&[protocol::Headers::FILE][..], file_uuid.as_bytes(), &file_name.into_bytes()].concat()) { Ok(_) => {} Err(e) => print_error(e) } } Err(e) => print_error(e) } } else { ui_connection.lock().unwrap().file_sent(index, &file_name, None); } } Err(e) => print_error(e) } } Err(e) => print_error(e) } } _ => println!("Unknown: {}", msg) } } Err(e) => { match e { tungstenite::Error::Io(ref e) => { if e.kind() != io::ErrorKind::WouldBlock { print_error(e) } } tungstenite::Error::ConnectionClosed => { println!("Websocket connection closed"); ui_connection.lock().unwrap().is_valid = false; return } _ => print_error(e) } } } sleep(Duration::from_millis(constants::MUTEX_RELEASE_DELAY_MS)); //pause let other threads use ui_connection } } #[derive(Deserialize, Serialize, Debug)] struct FileInfo { uuid: String, file_name: String, } fn handle_load_file(req: HttpRequest, file_info: web::Query) -> HttpResponse { println!("Load file request from: {}", req.peer_addr().unwrap()); match req.cookie(constants::HTTP_COOKIE_NAME) { Some(cookie) => { let global_vars = req.app_data::>>>().unwrap(); let global_vars = global_vars.read().unwrap(); match global_vars.http_session_manager.get_name_from_cookie(cookie.value()) { Some(_) => { match Uuid::from_str(&file_info.uuid) { Ok(uuid) => { match global_vars.session_manager.load_file(uuid) { Some(buffer) => { return HttpResponse::Ok().header("Content-Disposition", format!("attachment; filename=\"{}\"", escape_double_quote(html_escape::decode_html_entities(&file_info.file_name).to_string()))).content_type("application/octet-stream").body(buffer); } None => {} } } Err(e) => print_error(e) } } None => {} } } None => {} } HttpResponse::NotFound().finish() } fn handle_logout(req: HttpRequest) -> HttpResponse { println!("Logout request from: {}", req.peer_addr().unwrap()); match req.cookie(constants::HTTP_COOKIE_NAME) { Some(cookie) => { let global_vars = req.app_data::>>>().unwrap(); let mut global_vars_write = global_vars.write().unwrap(); global_vars_write.http_session_manager.remove(cookie.value()); global_vars_write.session_manager.stop(); global_vars_write.is_backend_running = false; HttpResponse::Found().header(header::LOCATION, "/").finish() } None => HttpResponse::Unauthorized().finish() } } fn login(identity: Identity, global_vars: &Arc>) -> HttpResponse { let mut global_vars = global_vars.write().unwrap(); let cookie_value = global_vars.http_session_manager.register(identity.name.to_string()); if match global_vars.session_manager.get_identity_uuid() { Some(uuid) => uuid != identity.uuid, None => true } { global_vars.session_manager.set_identity(Some(identity)); } let cookie = CookieBuilder::new(constants::HTTP_COOKIE_NAME, cookie_value) .http_only(true) .max_age(time::Duration::hours(4) ).finish(); HttpResponse::Found().header(header::LOCATION, "/").set_header(header::SET_COOKIE, cookie.to_string()).finish() } fn handle_login(req: HttpRequest, params: web::Form) -> HttpResponse { println!("Login request from: {}", req.peer_addr().unwrap()); match Identity::get_identity(¶ms.uuid, ¶ms.password) { Ok(identity) => { let global_vars = req.app_data::>>>().unwrap(); match identity.clear_temporary_files() { Ok(n) => println!("Deleted {} files", n), Err(e) => print_error(e) } login(identity, global_vars.get_ref()) } Err(e) => { let body = get_login_body(Some(&e.to_string())); HttpResponse::NotFound().body(body) } } } fn get_login_body(error_msg: Option<&str>) -> String { let mut formated_identities = String::new(); match Identity::get_identities(){ Ok(identities) => { for i in 0..identities.len() { let uuid = identities[i].0.to_string(); formated_identities += &format!("[\"{}\", \"{}\"]", uuid, identities[i].1); if i != identities.len()-1 { formated_identities += ", "; } } } Err(e) => { print_error(e) } }; include_str!("frontend/login.html") .replace("IDENTITIES", &formated_identities).replace("ERROR_MSG", &match error_msg { Some(error_msg) => format!("Error: {}.", error_msg), None => String::new() }) } fn handle_create(req: HttpRequest, params: web::Form) -> HttpResponse { println!("Create request from: {}", req.peer_addr().unwrap()); if params.password == params.password_confirm { match Identity::create_new_identidy(¶ms.name, ¶ms.password) { Ok(identity) => { let global_vars = req.app_data::>>>().unwrap(); login(identity, global_vars.get_ref()) } Err(e) => { print_error(&e); let body = get_login_body(Some(&e.to_string())); HttpResponse::Ok().body(body) } } } else { let body = get_login_body(Some("Passwords don't match")); HttpResponse::Ok().body(body) } } fn index(req: HttpRequest) -> HttpResponse { println!("GET request from: {}", req.peer_addr().unwrap()); let body = match req.cookie(constants::HTTP_COOKIE_NAME) { Some(cookie) => { let global_vars = req.app_data::>>>().unwrap(); let global_vars = global_vars.write().unwrap(); match global_vars.http_session_manager.get_name_from_cookie(cookie.value()) { Some(name) => { include_str!("frontend/index.html") .replace("WEBSOCKET_PORT", &global_vars.websocket_port.to_string()) .replace("IDENTITY_NAME", &name) } None => get_login_body(None) } } None => get_login_body(None) }; HttpResponse::Ok().body(body) } const JS_CONTENT_TYPE: &str = "text/javascript"; fn handle_static(req: HttpRequest) -> HttpResponse { println!("Static: {}", req.path()); let splits: Vec<&str> = req.path()[1..].split("/").collect(); if splits[0] == "static" { let mut response_builder = HttpResponse::Ok(); match splits[1] { "imgs" => { if splits[2] == "icons" && splits.len() <= 5 { let color = if splits.len() == 5 { splits[4] } else { "none" }; match match splits[3] { "verified" => Some(include_str!("frontend/imgs/icons/verified.svg")), "add_contact" => Some(include_str!("frontend/imgs/icons/add_contact.svg")), "remove_contact" => Some(include_str!("frontend/imgs/icons/remove_contact.svg")), "logout" => Some(include_str!("frontend/imgs/icons/logout.svg")), "warning" => Some(include_str!("frontend/imgs/icons/warning.svg")), "attach" => Some(include_str!("frontend/imgs/icons/attach.svg")), "download" => Some(include_str!("frontend/imgs/icons/download.svg")), _ => None } { Some(body) => { response_builder.content_type("image/svg+xml"); return response_builder.body(body.replace("FILL_COLOR", color)) } None => {} } } else if splits.len() == 3 { response_builder.content_type("image/jpeg"); match splits[2] { "wallpaper" => return response_builder.body(&include_bytes!("frontend/imgs/wallpaper.jpeg")[..]), _ => {} } } } "commons" => { if splits.len() == 3 { match splits[2] { "script.js" => return response_builder.content_type(JS_CONTENT_TYPE).body(include_str!("frontend/commons/script.js")), "style.css" => return response_builder.body(include_str!("frontend/commons/style.css")), _ => {} } } } "libs" => { if splits.len() == 3 { match match splits[2] { "linkify.min.js" => Some(include_str!("frontend/libs/linkify.min.js")), "linkify-html.min.js" => Some(include_str!("frontend/libs/linkify-html.min.js")), "linkify-element.min.js" => Some(include_str!("frontend/libs/linkify-element.min.js")), _ => None } { Some(body) => return response_builder.content_type(JS_CONTENT_TYPE).body(body), None => {} } } } _ => {} } } HttpResponse::NotFound().finish() } #[actix_web::main] async fn start_http_server(global_vars: Arc>) -> io::Result<()> { let http_bind_addr = env::var("AIRA_HTTP_ADDR").unwrap_or("127.0.0.1".to_owned()); let http_port = env::var("AIRA_HTTP_PORT").unwrap_or("0".to_owned()); let server = HttpServer::new(move || { let global_vars_clone = global_vars.clone(); App::new() .data(global_vars_clone) .service(web::resource("/") .route(web::get().to(index)) .route(web::post().to(handle_create)) ) .route("/login", web::post().to(handle_login)) .route("/load_file", web::get().to(handle_load_file)) .route("/static/.*", web::get().to(handle_static)) .route("/logout", web::get().to(handle_logout)) } ).bind(http_bind_addr+":"+&http_port)?; let url = "http://127.0.0.1:".to_owned()+&server.addrs().get(0).unwrap().port().to_string(); println!("AIRA started on: {}", url); if webbrowser::open(&url).is_err() { println!("Failed to open browser. Please open the link manually."); } server.run().await } #[derive(Serialize, Deserialize)] struct LoginParams { uuid: String, password: String } #[derive(Serialize, Deserialize)] struct CreateParams { name: String, password: String, password_confirm: String } struct HttpSessionsManager { http_sessions: HashMap } impl HttpSessionsManager { pub fn new() -> HttpSessionsManager { HttpSessionsManager { http_sessions: HashMap::new() } } pub fn register(&mut self, identity_name: String) -> String { let mut raw_cookie = [0; 32]; OsRng.fill_bytes(&mut raw_cookie); let cookie = base64::encode(raw_cookie); self.http_sessions.insert(cookie.clone(), identity_name); cookie } pub fn remove(&mut self, cookie: &str) { self.http_sessions.remove(cookie); } pub fn get_name_from_cookie(&self, cookie_value: &str) -> Option { Some(self.http_sessions.get(cookie_value)?.clone()) } } /*impl Debug for HttpSessionsManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("{\n")?; for (k, v) in self.http_sessions.read().unwrap().iter() { f.write_str(&format!("\t{} : {}", k, v))?; } f.write_str("\n}") } }*/ struct GlobalVars { session_manager: Arc, is_backend_running: bool, websocket_port: u16, http_session_manager: HttpSessionsManager, } fn main() { match fs::create_dir(Path::new(&dirs::config_dir().unwrap()).join(constants::APPLICATION_FOLDER)){ Ok(_) => {} Err(e) => { if e.kind() != io::ErrorKind::AlreadyExists { print_error(e); } } } let session_manager = Arc::new(SessionManager::new()); let global_vars = Arc::new(RwLock::new(GlobalVars { session_manager: session_manager, is_backend_running: false, websocket_port: 0, http_session_manager: HttpSessionsManager::new() })); let websocket_port = start_websocket_server(&global_vars); global_vars.write().unwrap().websocket_port = websocket_port; start_http_server(global_vars).unwrap(); } fn print_error(error: T){ println!("{}", error); }