diff --git a/Cargo.lock b/Cargo.lock index 860d8ab..f40d408 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2212,6 +2212,7 @@ checksum = "b4a52cacd869b804660986b10aa2076c3a4b6da644c7198f9fd0b613f4a7b249" dependencies = [ "memchr", "minidom", + "serde", "stringprep", ] diff --git a/Cargo.toml b/Cargo.toml index 5f2cd27..37d26dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ dioxus-hot-reload = { version="0.4.3", features=["file_watcher"] } futures-util = "0.3.29" xmpp = { git = "https://gitlab.com/werner.kroneman/xmpp-rs.git", rev="ecd0be4aad985e9812626d6c4499c2586c158aba"} keyring = "2.1.0" -jid = "0.10.0" +jid = { version = "0.10.0", features = ["serde"] } confy = "0.5.1" serde = "1.0" serde_derive = "1.0" \ No newline at end of file diff --git a/src/configuration.rs b/src/configuration.rs index 1dfa634..8e4368d 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -16,15 +16,17 @@ use crate::widgets::login_screen::LoginAttempt; use dioxus::prelude::*; +use jid::BareJid; use keyring::Entry; use log::error; use serde_derive::{Deserialize, Serialize}; +use crate::passwords::Password; /// The configuration struct containing all the configuration options. -#[derive(Serialize, Deserialize, Default)] +#[derive(Default, Debug, Serialize, Deserialize)] pub struct Configuration { - pub stored_username: String, - pub stored_default_nick: String, + pub stored_username: Option, + pub stored_default_nick: Option, } /// Cache the login attempt into the config and keyring. @@ -36,30 +38,25 @@ pub struct Configuration { /// # Arguments /// * `attempt` - The login attempt to store. /// * `config` - The current app configuration; UseState should prevent race conditions in writing. -pub fn store_login_details(attempt: LoginAttempt, config: &mut UseRef) { - // Extract the fields from the login attempt. - let LoginAttempt { - username, - default_nick, - password, - } = attempt; +pub fn store_login_details(jid: BareJid, + default_nick: String, + password: &Password, + c: &mut Configuration) { // Open the config state and store the username and default nick. - config.with_mut(|c| { // Open the keyring and store the password. - let entry = Entry::new("dergchat", &username).expect("Failed to create keyring entry."); + let entry = Entry::new("dergchat", &jid.to_string()).expect("Failed to create keyring entry."); entry - .set_password(&password) + .set_password(&password.0) .expect("Failed to set password in keyring."); // Store the username and default nick in the config. - c.stored_username = username; - c.stored_default_nick = default_nick; + c.stored_username = Some(jid); + c.stored_default_nick = Some(default_nick); if let Err(e) = confy::store("dergchat", None, c) { error!("Failed to store the config file: {}", e); } - }); } /// Load the configuration from the config file. diff --git a/src/main.rs b/src/main.rs index 1ac7a0d..bd0e17e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ mod configuration; mod types; mod widgets; mod xmpp_interface; +mod passwords; use dioxus::prelude::*; use dioxus_desktop::Config; diff --git a/src/passwords.rs b/src/passwords.rs new file mode 100644 index 0000000..356a2c8 --- /dev/null +++ b/src/passwords.rs @@ -0,0 +1,40 @@ +// Dergchat, a free XMPP client. +// Copyright (C) 2023 Werner Kroneman +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use std::fmt::Debug; +use jid::BareJid; +use keyring::Entry; + +pub struct Password(pub String); + +impl Debug for Password { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Password") + .field("password", &"********") + .finish() + } +} + +/// Retrieve the password for the given username from the keyring, if it exists. +pub fn try_retrieve_password_from_keyring(username: &BareJid) -> Option { + Entry::new("dergchat", &username.to_string()).expect("Failed to create keyring entry.").get_password().ok().map(|p| Password(p)) +} + +/// Store the password for the given username in the keyring. +pub fn store_keyring_password(username: &BareJid, password: &Password) { + let entry = Entry::new("dergchat", &username.to_string()).expect("Failed to create keyring entry."); + entry.set_password(&*password.0).expect("Failed to set password in keyring."); +} \ No newline at end of file diff --git a/src/types.rs b/src/types.rs index 60dad5e..83ab5f6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -16,6 +16,7 @@ use jid::BareJid; use std::fmt::Debug; +use crate::passwords::Password; #[derive(Debug, Clone, PartialEq)] pub struct Room { @@ -28,18 +29,9 @@ pub struct Message { pub body: String, } +#[derive(Debug)] pub struct LoginCredentials { pub username: BareJid, pub default_nick: String, - pub password: String, -} - -impl Debug for LoginCredentials { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("LoginCredentials") - .field("username", &self.username) - .field("default_nick", &self.default_nick) - .field("password", &"********") - .finish() - } -} + pub password: Password, +} \ No newline at end of file diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 8cfc95a..c11cc7a 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -22,6 +22,7 @@ use crate::widgets::room_view::RoomView; use crate::widgets::sidebar::SideBar; use crate::xmpp_interface::NetworkCommand; use dioxus::core::{Element, Scope}; +use crate::passwords::Password; use dioxus::prelude::*; use futures_util::StreamExt; @@ -29,21 +30,42 @@ use jid::BareJid; use log::{error, info}; use std::collections::HashMap; -use crate::configuration::load_config; +use crate::configuration::{Configuration, load_config}; use crate::xmpp_interface; use keyring::Entry; use std::str::FromStr; use std::string::String; +use crate::passwords::try_retrieve_password_from_keyring; +use crate::configuration::store_login_details; pub mod login_screen; -mod no_room_open; +pub mod no_room_open; pub mod room_join_widget; pub mod room_list; pub mod room_view; pub mod send_message; pub mod sidebar; +fn try_retrieve_credentials(config: &Configuration) -> Option { + if let (Some(user), Some(nick)) = (&config.stored_username, &config.stored_default_nick) { + if let Some(password) = try_retrieve_password_from_keyring(&user) { + Some(LoginCredentials { + username: user.clone(), + default_nick: nick.clone(), + password, + }) + } else { + info!("No stored password found; will not try to log in."); + None + } + } else { + info!("No stored username or default nick found; will not try to log in."); + None + } +} + pub fn App(cx: Scope) -> Element { + let messages = use_ref(cx, || HashMap::new()); let current_room = use_state(cx, || None::); let connection_status = use_state(cx, || LoginStatus::LoggedOut); @@ -54,41 +76,19 @@ pub fn App(cx: Scope) -> Element { let config = use_ref(cx, load_config); - { - let config = config.to_owned(); - let _connection_status = connection_status.to_owned(); - let coroutine = coroutine.to_owned(); - - use_on_create(cx, move || { + use_on_create(cx, || { + let config = config.to_owned(); + let _connection_status = connection_status.to_owned(); + let coroutine = coroutine.to_owned(); async move { - let entry = Entry::new("dergchat", &config.read().stored_username) - .expect("Failed to create keyring entry."); - - let _password = match entry.get_password() { - Ok(_x) => { - info!("Password retrieved from keyring; will try to log in."); - - let (username, default_nick) = config - .with(|c| (c.stored_username.clone(), c.stored_default_nick.clone())); - - // If we have a username, nickname and password, immediately try to log in. - if username.len() > 0 && default_nick.len() > 0 { - let credentials = LoginCredentials { - username: BareJid::from_str(&username).expect("Invalid JID"), - default_nick: default_nick, - password: entry - .get_password() - .expect("Failed to get password from keyring"), - }; - - coroutine.send(NetworkCommand::TryLogin { credentials }); - } - } - Err(e) => println!("Failed to get password from keyring: {}", e), - }; + if let Some(credentials) = try_retrieve_credentials(&*config.read()) { + info!("Will try to log in as {} with default nick {}", credentials.username, credentials.default_nick); + coroutine.send(NetworkCommand::TryLogin { credentials }); + } else { + info!("No stored credentials found; will not try to automatically log in."); + } } }); - } render! { @@ -97,33 +97,36 @@ pub fn App(cx: Scope) -> Element { rsx! { LoginScreen { - cached_username: config.read().stored_username.clone(), - cached_nick: config.read().stored_default_nick.clone(), + cached_username: config.read().stored_username.clone().map(|x| x.to_string()).unwrap_or("".to_string()), + cached_nick: config.read().stored_default_nick.clone().unwrap_or("".to_string()), login_state: connection_status.current().as_ref().clone(), on_login_attempt: move |x: LoginAttempt| { - { - let x = x.clone(); - // Store the JID in the PKV. - config.with_mut(move |c| { - c.stored_username = x.username.clone(); - c.stored_default_nick = x.default_nick.clone(); + let LoginAttempt { username, default_nick, password } = x; - // Store the password in the keyring. - let entry = Entry::new("dergchat", &x.username).expect("Failed to create keyring entry."); - entry.set_password(&x.password).expect("Failed to set password in keyring."); + // First, validate the username as a jid: + let jid = match BareJid::from_str(&username) { + Ok(x) => x, + Err(e) => { + error!("Invalid JID: {}", e); + return; + } + }; - if let Err(e) = confy::store("dergchat", None, &*config.read()) { - error!("Failed to store JID in config file: {}", e); - } - }); - } + // Wrap the password in a Password struct. + let password = Password(password); + // Store the login details in the config and keyring for auto-login next time. + config.with_mut(|c| { + store_login_details(jid.clone(), default_nick.clone(), &password, c); + }); + + // Finally, send the login command to the xmpp interface. coroutine.send(NetworkCommand::TryLogin { credentials: LoginCredentials { - username: BareJid::from_str(&x.username).expect("Invalid JID"), - default_nick: x.default_nick, - password: x.password, + username: jid, + default_nick, + password, }, }); }, diff --git a/src/xmpp_interface.rs b/src/xmpp_interface.rs index 266d1b7..5bf0613 100644 --- a/src/xmpp_interface.rs +++ b/src/xmpp_interface.rs @@ -203,7 +203,7 @@ pub async fn run_xmpp_toplevel( connection_status.set(LoginStatus::LoggingIn); - let mut agent = ClientBuilder::new(credentials.username, &*credentials.password) + let mut agent = ClientBuilder::new(credentials.username, &credentials.password.0) .set_client(ClientType::Pc, "dergchat") .build();