Did a bunch of refactoring.

This commit is contained in:
Werner Kroneman 2023-12-10 17:41:27 +01:00
parent cbb307e8a2
commit 5a33bf2280
8 changed files with 117 additions and 83 deletions

1
Cargo.lock generated
View File

@ -2212,6 +2212,7 @@ checksum = "b4a52cacd869b804660986b10aa2076c3a4b6da644c7198f9fd0b613f4a7b249"
dependencies = [ dependencies = [
"memchr", "memchr",
"minidom", "minidom",
"serde",
"stringprep", "stringprep",
] ]

View File

@ -31,7 +31,7 @@ dioxus-hot-reload = { version="0.4.3", features=["file_watcher"] }
futures-util = "0.3.29" futures-util = "0.3.29"
xmpp = { git = "https://gitlab.com/werner.kroneman/xmpp-rs.git", rev="ecd0be4aad985e9812626d6c4499c2586c158aba"} xmpp = { git = "https://gitlab.com/werner.kroneman/xmpp-rs.git", rev="ecd0be4aad985e9812626d6c4499c2586c158aba"}
keyring = "2.1.0" keyring = "2.1.0"
jid = "0.10.0" jid = { version = "0.10.0", features = ["serde"] }
confy = "0.5.1" confy = "0.5.1"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"

View File

@ -16,15 +16,17 @@
use crate::widgets::login_screen::LoginAttempt; use crate::widgets::login_screen::LoginAttempt;
use dioxus::prelude::*; use dioxus::prelude::*;
use jid::BareJid;
use keyring::Entry; use keyring::Entry;
use log::error; use log::error;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use crate::passwords::Password;
/// The configuration struct containing all the configuration options. /// The configuration struct containing all the configuration options.
#[derive(Serialize, Deserialize, Default)] #[derive(Default, Debug, Serialize, Deserialize)]
pub struct Configuration { pub struct Configuration {
pub stored_username: String, pub stored_username: Option<BareJid>,
pub stored_default_nick: String, pub stored_default_nick: Option<String>,
} }
/// Cache the login attempt into the config and keyring. /// Cache the login attempt into the config and keyring.
@ -36,30 +38,25 @@ pub struct Configuration {
/// # Arguments /// # Arguments
/// * `attempt` - The login attempt to store. /// * `attempt` - The login attempt to store.
/// * `config` - The current app configuration; UseState should prevent race conditions in writing. /// * `config` - The current app configuration; UseState should prevent race conditions in writing.
pub fn store_login_details(attempt: LoginAttempt, config: &mut UseRef<Configuration>) { pub fn store_login_details(jid: BareJid,
// Extract the fields from the login attempt. default_nick: String,
let LoginAttempt { password: &Password,
username, c: &mut Configuration) {
default_nick,
password,
} = attempt;
// Open the config state and store the username and default nick. // Open the config state and store the username and default nick.
config.with_mut(|c| {
// Open the keyring and store the password. // 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 entry
.set_password(&password) .set_password(&password.0)
.expect("Failed to set password in keyring."); .expect("Failed to set password in keyring.");
// Store the username and default nick in the config. // Store the username and default nick in the config.
c.stored_username = username; c.stored_username = Some(jid);
c.stored_default_nick = default_nick; c.stored_default_nick = Some(default_nick);
if let Err(e) = confy::store("dergchat", None, c) { if let Err(e) = confy::store("dergchat", None, c) {
error!("Failed to store the config file: {}", e); error!("Failed to store the config file: {}", e);
} }
});
} }
/// Load the configuration from the config file. /// Load the configuration from the config file.

View File

@ -20,6 +20,7 @@ mod configuration;
mod types; mod types;
mod widgets; mod widgets;
mod xmpp_interface; mod xmpp_interface;
mod passwords;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_desktop::Config; use dioxus_desktop::Config;

40
src/passwords.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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<Password> {
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.");
}

View File

@ -16,6 +16,7 @@
use jid::BareJid; use jid::BareJid;
use std::fmt::Debug; use std::fmt::Debug;
use crate::passwords::Password;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Room { pub struct Room {
@ -28,18 +29,9 @@ pub struct Message {
pub body: String, pub body: String,
} }
#[derive(Debug)]
pub struct LoginCredentials { pub struct LoginCredentials {
pub username: BareJid, pub username: BareJid,
pub default_nick: String, pub default_nick: String,
pub password: String, pub password: Password,
}
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()
}
} }

View File

@ -22,6 +22,7 @@ use crate::widgets::room_view::RoomView;
use crate::widgets::sidebar::SideBar; use crate::widgets::sidebar::SideBar;
use crate::xmpp_interface::NetworkCommand; use crate::xmpp_interface::NetworkCommand;
use dioxus::core::{Element, Scope}; use dioxus::core::{Element, Scope};
use crate::passwords::Password;
use dioxus::prelude::*; use dioxus::prelude::*;
use futures_util::StreamExt; use futures_util::StreamExt;
@ -29,21 +30,42 @@ use jid::BareJid;
use log::{error, info}; use log::{error, info};
use std::collections::HashMap; use std::collections::HashMap;
use crate::configuration::load_config; use crate::configuration::{Configuration, load_config};
use crate::xmpp_interface; use crate::xmpp_interface;
use keyring::Entry; use keyring::Entry;
use std::str::FromStr; use std::str::FromStr;
use std::string::String; use std::string::String;
use crate::passwords::try_retrieve_password_from_keyring;
use crate::configuration::store_login_details;
pub mod login_screen; pub mod login_screen;
mod no_room_open; pub mod no_room_open;
pub mod room_join_widget; pub mod room_join_widget;
pub mod room_list; pub mod room_list;
pub mod room_view; pub mod room_view;
pub mod send_message; pub mod send_message;
pub mod sidebar; pub mod sidebar;
fn try_retrieve_credentials(config: &Configuration) -> Option<LoginCredentials> {
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 { pub fn App(cx: Scope) -> Element {
let messages = use_ref(cx, || HashMap::new()); let messages = use_ref(cx, || HashMap::new());
let current_room = use_state(cx, || None::<BareJid>); let current_room = use_state(cx, || None::<BareJid>);
let connection_status = use_state(cx, || LoginStatus::LoggedOut); 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 = use_ref(cx, load_config);
{ use_on_create(cx, || {
let config = config.to_owned(); let config = config.to_owned();
let _connection_status = connection_status.to_owned(); let _connection_status = connection_status.to_owned();
let coroutine = coroutine.to_owned(); let coroutine = coroutine.to_owned();
use_on_create(cx, move || {
async move { async move {
let entry = Entry::new("dergchat", &config.read().stored_username) if let Some(credentials) = try_retrieve_credentials(&*config.read()) {
.expect("Failed to create keyring entry."); info!("Will try to log in as {} with default nick {}", credentials.username, credentials.default_nick);
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 }); coroutine.send(NetworkCommand::TryLogin { credentials });
} else {
info!("No stored credentials found; will not try to automatically log in.");
} }
} }
Err(e) => println!("Failed to get password from keyring: {}", e),
};
}
}); });
}
render! { render! {
@ -97,33 +97,36 @@ pub fn App(cx: Scope) -> Element {
rsx! { rsx! {
LoginScreen { LoginScreen {
cached_username: config.read().stored_username.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(), cached_nick: config.read().stored_default_nick.clone().unwrap_or("".to_string()),
login_state: connection_status.current().as_ref().clone(), login_state: connection_status.current().as_ref().clone(),
on_login_attempt: move |x: LoginAttempt| { on_login_attempt: move |x: LoginAttempt| {
{ let LoginAttempt { username, default_nick, password } = x;
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();
// Store the password in the keyring. // First, validate the username as a jid:
let entry = Entry::new("dergchat", &x.username).expect("Failed to create keyring entry."); let jid = match BareJid::from_str(&username) {
entry.set_password(&x.password).expect("Failed to set password in keyring."); Ok(x) => x,
Err(e) => {
if let Err(e) = confy::store("dergchat", None, &*config.read()) { error!("Invalid JID: {}", e);
error!("Failed to store JID in config file: {}", e); return;
} }
};
// 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 { coroutine.send(NetworkCommand::TryLogin {
credentials: LoginCredentials { credentials: LoginCredentials {
username: BareJid::from_str(&x.username).expect("Invalid JID"), username: jid,
default_nick: x.default_nick, default_nick,
password: x.password, password,
}, },
}); });
}, },

View File

@ -203,7 +203,7 @@ pub async fn run_xmpp_toplevel(
connection_status.set(LoginStatus::LoggingIn); 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") .set_client(ClientType::Pc, "dergchat")
.build(); .build();