From 8033966a7014bc76d4a24a04b9bbecc0a9c08c5b Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 10 May 2022 20:59:17 +0200 Subject: [PATCH] Validate and simplify LoginParam struct This makes sure that under normal circumstances the LoginParam struct is always fully validated, ensure future use does not have to be careful with this. The brittle handling of `server_flags` is also abstraced away from users of it and is now handled entirely internally, as the flags is really only a boolean a lot of the flag parsing complexity is removed. The OAuth2 flag is moved into the ServerLoginParam struct as it really belongs in there. --- src/configure.rs | 60 ++++++++---------------------------- src/context.rs | 2 +- src/imap.rs | 7 +---- src/login_param.rs | 77 +++++++++++++++++++++++++--------------------- src/smtp.rs | 5 +-- 5 files changed, 57 insertions(+), 94 deletions(-) diff --git a/src/configure.rs b/src/configure.rs index 35cfdce60..bf4028a61 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -11,7 +11,6 @@ use async_std::task; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use crate::config::Config; -use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2}; use crate::context::Context; use crate::dc_tools::{time, EmailAddress}; use crate::imap::Imap; @@ -145,28 +144,6 @@ impl Context { async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { progress!(ctx, 1); - // Check basic settings. - ensure!(!param.addr.is_empty(), "Please enter an email address."); - - // Only check for IMAP password, SMTP password is an "advanced" setting. - ensure!(!param.imap.password.is_empty(), "Please enter a password."); - if param.smtp.password.is_empty() { - param.smtp.password = param.imap.password.clone() - } - - // Normalize authentication flags. - let oauth2 = match param.server_flags & DC_LP_AUTH_FLAGS as i32 { - DC_LP_AUTH_OAUTH2 => true, - DC_LP_AUTH_NORMAL => false, - _ => false, - }; - param.server_flags &= !(DC_LP_AUTH_FLAGS as i32); - param.server_flags |= if oauth2 { - DC_LP_AUTH_OAUTH2 as i32 - } else { - DC_LP_AUTH_NORMAL as i32 - }; - let socks5_config = param.socks5_config.clone(); let socks5_enabled = socks5_config.is_some(); @@ -176,8 +153,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { // Step 1: Load the parameters and check email-address and password // Do oauth2 only if socks5 is disabled. As soon as we have a http library that can do - // socks5 requests, this can work with socks5 too - if oauth2 && !socks5_enabled { + // socks5 requests, this can work with socks5 too. OAuth is always set either for both + // IMAP and SMTP or not at all. + if param.imap.oauth2 && !socks5_enabled { // the used oauth2 addr may differ, check this. // if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one. progress!(ctx, 10); @@ -358,7 +336,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { &smtp_param, &socks5_config, &smtp_addr, - oauth2, provider_strict_tls, &mut smtp, ) @@ -406,7 +383,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { ¶m.imap, ¶m.socks5_config, ¶m.addr, - oauth2, provider_strict_tls, ) .await @@ -562,26 +538,22 @@ async fn try_imap_one_param( param: &ServerLoginParam, socks5_config: &Option, addr: &str, - oauth2: bool, provider_strict_tls: bool, ) -> Result { let inf = format!( "imap: {}@{}:{} security={} certificate_checks={} oauth2={}", - param.user, param.server, param.port, param.security, param.certificate_checks, oauth2 + param.user, + param.server, + param.port, + param.security, + param.certificate_checks, + param.oauth2 ); info!(context, "Trying: {}", inf); let (_s, r) = async_std::channel::bounded(1); - let mut imap = match Imap::new( - param, - socks5_config.clone(), - addr, - oauth2, - provider_strict_tls, - r, - ) - .await + let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r).await { Err(err) => { info!(context, "failure: {}", err); @@ -613,7 +585,6 @@ async fn try_smtp_one_param( param: &ServerLoginParam, socks5_config: &Option, addr: &str, - oauth2: bool, provider_strict_tls: bool, smtp: &mut Smtp, ) -> Result<(), ConfigurationError> { @@ -624,7 +595,7 @@ async fn try_smtp_one_param( param.port, param.security, param.certificate_checks, - oauth2, + param.oauth2, if let Some(socks5_config) = socks5_config { socks5_config.to_string() } else { @@ -634,14 +605,7 @@ async fn try_smtp_one_param( info!(context, "Trying: {}", inf); if let Err(err) = smtp - .connect( - context, - param, - socks5_config, - addr, - oauth2, - provider_strict_tls, - ) + .connect(context, param, socks5_config, addr, provider_strict_tls) .await { info!(context, "failure: {}", err); diff --git a/src/context.rs b/src/context.rs index 414dbbd37..14947a3b8 100644 --- a/src/context.rs +++ b/src/context.rs @@ -342,7 +342,7 @@ impl Context { pub async fn get_info(&self) -> Result> { let unset = "0"; - let l = LoginParam::load_candidate_params(self).await?; + let l = LoginParam::load_candidate_params_unchecked(self).await?; let l2 = LoginParam::load_configured_params(self).await?; let secondary_addrs = self.get_secondary_self_addrs().await?.join(", "); let displayname = self.get_config(Config::Displayname).await?; diff --git a/src/imap.rs b/src/imap.rs index 2f0d7593a..6d5ffee5b 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -22,7 +22,6 @@ use crate::chat::{self, ChatId, ChatIdBlocked}; use crate::config::Config; use crate::constants::{ Blocked, Chattype, ShowEmails, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION, - DC_LP_AUTH_OAUTH2, }; use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin}; use crate::context::Context; @@ -154,7 +153,6 @@ struct ImapConfig { pub lp: ServerLoginParam, pub socks5_config: Option, pub strict_tls: bool, - pub oauth2: bool, pub selected_folder: Option, pub selected_mailbox: Option, pub selected_folder_needs_expunge: bool, @@ -243,7 +241,6 @@ impl Imap { lp: &ServerLoginParam, socks5_config: Option, addr: &str, - oauth2: bool, provider_strict_tls: bool, idle_interrupt: Receiver, ) -> Result { @@ -262,7 +259,6 @@ impl Imap { lp: lp.clone(), socks5_config, strict_tls, - oauth2, selected_folder: None, selected_mailbox: None, selected_folder_needs_expunge: false, @@ -301,7 +297,6 @@ impl Imap { ¶m.imap, param.socks5_config.clone(), ¶m.addr, - param.server_flags & DC_LP_AUTH_OAUTH2 != 0, param .provider .map_or(param.socks5_config.is_some(), |provider| { @@ -334,7 +329,7 @@ impl Imap { self.connectivity.set_connecting(context).await; - let oauth2 = self.config.oauth2; + let oauth2 = self.config.lp.oauth2; let connection_res: Result = if self.config.lp.security == Socket::Starttls || self.config.lp.security == Socket::Plain diff --git a/src/login_param.rs b/src/login_param.rs index 46dd41eaa..2021a1898 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -4,9 +4,10 @@ use std::borrow::Cow; use std::fmt; use std::time::Duration; +use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2}; use crate::provider::{get_provider_by_id, Provider}; use crate::{context::Context, provider::Socket}; -use anyhow::Result; +use anyhow::{ensure, Result}; use async_std::io; use async_std::net::TcpStream; @@ -47,6 +48,7 @@ pub struct ServerLoginParam { pub password: String, pub port: u16, pub security: Socket, + pub oauth2: bool, /// TLS options: whether to allow invalid certificates and/or /// invalid hostnames @@ -133,7 +135,6 @@ pub struct LoginParam { pub addr: String, pub imap: ServerLoginParam, pub smtp: ServerLoginParam, - pub server_flags: i32, pub provider: Option<&'static Provider>, pub socks5_config: Option, } @@ -141,6 +142,23 @@ pub struct LoginParam { impl LoginParam { /// Load entered (candidate) account settings pub async fn load_candidate_params(context: &Context) -> Result { + let mut param = Self::load_candidate_params_unchecked(context).await?; + ensure!(!param.addr.is_empty(), "Missing email address."); + + // Only check for IMAP password, SMTP password is an "advanced" setting. + ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password."); + if param.smtp.password.is_empty() { + param.smtp.password = param.imap.password.clone() + } + Ok(param) + } + + /// Load entered (candidate) account settings without validation. + /// + /// This will result in a potentially invalid [`LoginParam`] struct as the values are + /// not validated. Only use this if you want to show this directly to the user e.g. in + /// [`Context::get_info`]. + pub async fn load_candidate_params_unchecked(context: &Context) -> Result { LoginParam::from_database(context, "").await } @@ -217,6 +235,7 @@ impl LoginParam { let key = format!("{}server_flags", prefix); let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default(); + let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); let key = format!("{}provider", prefix); let provider = sql @@ -234,6 +253,7 @@ impl LoginParam { password: mail_pw, port: mail_port as u16, security: mail_security, + oauth2, certificate_checks: imap_certificate_checks, }, smtp: ServerLoginParam { @@ -242,10 +262,10 @@ impl LoginParam { password: send_pw, port: send_port as u16, security: send_security, + oauth2, certificate_checks: smtp_certificate_checks, }, provider, - server_flags, socks5_config, }) } @@ -299,8 +319,13 @@ impl LoginParam { sql.set_raw_config_int(key, self.smtp.certificate_checks as i32) .await?; + // The OAuth2 flag is either set for both IMAP and SMTP or not at all. let key = format!("{}server_flags", prefix); - sql.set_raw_config_int(key, self.server_flags).await?; + let server_flags = match self.imap.oauth2 { + true => DC_LP_AUTH_OAUTH2, + false => DC_LP_AUTH_NORMAL, + }; + sql.set_raw_config_int(key, server_flags).await?; if let Some(provider) = self.provider { let key = format!("{}provider", prefix); @@ -316,11 +341,9 @@ impl fmt::Display for LoginParam { let unset = "0"; let pw = "***"; - let flags_readable = get_readable_flags(self.server_flags); - write!( f, - "{} imap:{}:{}:{}:{}:cert_{} smtp:{}:{}:{}:{}:cert_{} {}", + "{} imap:{}:{}:{}:{}:cert_{}:{} smtp:{}:{}:{}:{}:cert_{}:{}", unset_empty(&self.addr), unset_empty(&self.imap.user), if !self.imap.password.is_empty() { @@ -331,6 +354,11 @@ impl fmt::Display for LoginParam { unset_empty(&self.imap.server), self.imap.port, self.imap.certificate_checks, + if self.imap.oauth2 { + "OAUTH2" + } else { + "AUTH_NORMAL" + }, unset_empty(&self.smtp.user), if !self.smtp.password.is_empty() { pw @@ -340,7 +368,11 @@ impl fmt::Display for LoginParam { unset_empty(&self.smtp.server), self.smtp.port, self.smtp.certificate_checks, - flags_readable, + if self.smtp.oauth2 { + "OAUTH2" + } else { + "AUTH_NORMAL" + }, ) } } @@ -354,32 +386,6 @@ fn unset_empty(s: &String) -> Cow { } } -#[allow(clippy::useless_let_if_seq)] -fn get_readable_flags(flags: i32) -> String { - let mut res = String::new(); - for bit in 0..31 { - if 0 != flags & 1 << bit { - let mut flag_added = false; - if 1 << bit == 0x2 { - res += "OAUTH2 "; - flag_added = true; - } - if 1 << bit == 0x4 { - res += "AUTH_NORMAL "; - flag_added = true; - } - if flag_added { - res += &format!("{:#0x}", 1 << bit); - } - } - } - if res.is_empty() { - res += "0"; - } - - res -} - // this certificate is missing on older android devices (eg. lg with android6 from 2017) // certificate downloaded from https://letsencrypt.org/certificates/ static LETSENCRYPT_ROOT: Lazy = Lazy::new(|| { @@ -430,6 +436,7 @@ mod tests { password: "foo".to_string(), port: 123, security: Socket::Starttls, + oauth2: false, certificate_checks: CertificateChecks::Strict, }, smtp: ServerLoginParam { @@ -438,9 +445,9 @@ mod tests { password: "bar".to_string(), port: 456, security: Socket::Ssl, + oauth2: false, certificate_checks: CertificateChecks::AcceptInvalidCertificates, }, - server_flags: 0, provider: get_provider_by_id("example.com"), // socks5_config is not saved by `save_to_database`, using default value socks5_config: None, diff --git a/src/smtp.rs b/src/smtp.rs index 06cf0eb1b..9ab34e96c 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -11,7 +11,6 @@ use async_smtp::{smtp, EmailAddress, ServerAddress}; use async_std::task; use crate::config::Config; -use crate::constants::DC_LP_AUTH_OAUTH2; use crate::contact::{Contact, ContactId}; use crate::events::EventType; use crate::login_param::{ @@ -103,7 +102,6 @@ impl Smtp { &lp.smtp, &lp.socks5_config, &lp.addr, - lp.server_flags & DC_LP_AUTH_OAUTH2 != 0, lp.provider .map_or(lp.socks5_config.is_some(), |provider| provider.strict_tls), ) @@ -117,7 +115,6 @@ impl Smtp { lp: &ServerLoginParam, socks5_config: &Option, addr: &str, - oauth2: bool, provider_strict_tls: bool, ) -> Result<()> { if self.is_connected().await { @@ -146,7 +143,7 @@ impl Smtp { let tls_config = dc_build_tls(strict_tls); let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config); - let (creds, mechanism) = if oauth2 { + let (creds, mechanism) = if lp.oauth2 { // oauth2 let send_pw = &lp.password; let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await?;