authentication!

This commit is contained in:
Michael Zhang 2021-02-22 20:47:00 -06:00
parent 27818bd93a
commit f76e5d4753
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
6 changed files with 138 additions and 89 deletions

View file

@ -1,11 +1,12 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet, VecDeque};
use std::mem;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::task::{Context, Poll, Waker}; use std::task::{Context, Poll, Waker};
use anyhow::{Context as AnyhowContext, Result}; use anyhow::{Context as AnyhowContext, Result};
use futures::future::{self, Either, Future, FutureExt}; use futures::future::{self, Either, Future, FutureExt};
use parking_lot::RwLock; use parking_lot::{RwLock, RwLockWriteGuard};
use tokio::{ use tokio::{
io::{ io::{
self, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadHalf, WriteHalf, self, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadHalf, WriteHalf,
@ -18,14 +19,15 @@ use tokio_rustls::{
}; };
use crate::command::Command; use crate::command::Command;
use crate::parser::{parse_capability, parse_response};
use crate::response::{Capability, Response, ResponseCode, Status}; use crate::response::{Capability, Response, ResponseCode, Status};
// use crate::types::{Capability as Capability_, Status}; // use crate::types::{Capability as Capability_, Status};
use super::ClientConfig; use super::ClientConfig;
pub type CapsLock = Arc<RwLock<Option<HashSet<Capability>>>>; pub type CapsLock = Arc<RwLock<Option<HashSet<Capability>>>>;
pub type ResultMap = Arc<RwLock<HashMap<usize, (Option<Response>, Option<Waker>)>>>; pub type ResultMap = Arc<RwLock<VecDeque<(usize, Option<Response>, Vec<Response>, Option<Waker>)>>>;
pub type GreetingState = Arc<RwLock<(bool, Option<Waker>)>>; pub type GreetingState = Arc<RwLock<(Option<Response>, Option<Waker>)>>;
pub const TAG_PREFIX: &str = "panorama"; pub const TAG_PREFIX: &str = "panorama";
/// The lower-level Client struct, that is shared by all of the exported structs in the state machine. /// The lower-level Client struct, that is shared by all of the exported structs in the state machine.
@ -56,9 +58,9 @@ where
/// Creates a new client that wraps a connection /// Creates a new client that wraps a connection
pub fn new(conn: C, config: ClientConfig) -> Self { pub fn new(conn: C, config: ClientConfig) -> Self {
let (read_half, write_half) = io::split(conn); let (read_half, write_half) = io::split(conn);
let results = Arc::new(RwLock::new(HashMap::new())); let results = Arc::new(RwLock::new(VecDeque::new()));
let (exit_tx, exit_rx) = mpsc::channel(1); let (exit_tx, exit_rx) = mpsc::channel(1);
let greeting = Arc::new(RwLock::new((false, None))); let greeting = Arc::new(RwLock::new((None, None)));
let caps: CapsLock = Arc::new(RwLock::new(None)); let caps: CapsLock = Arc::new(RwLock::new(None));
let listener_handle = tokio::spawn(listen( let listener_handle = tokio::spawn(listen(
@ -88,13 +90,13 @@ where
} }
/// Sends a command to the server and returns a handle to retrieve the result /// Sends a command to the server and returns a handle to retrieve the result
pub async fn execute(&mut self, cmd: Command) -> Result<Response> { pub async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec<Response>)> {
debug!("executing command {:?}", cmd); debug!("executing command {:?}", cmd);
let id = self.id; let id = self.id;
self.id += 1; self.id += 1;
{ {
let mut handlers = self.results.write(); let mut handlers = self.results.write();
handlers.insert(id, (None, None)); handlers.push_back((id, None, vec![], None));
} }
let cmd_str = format!("{}{} {}\r\n", TAG_PREFIX, id, cmd); let cmd_str = format!("{}{} {}\r\n", TAG_PREFIX, id, cmd);
@ -103,23 +105,39 @@ where
self.conn.flush().await?; self.conn.flush().await?;
debug!("[{}] written.", id); debug!("[{}] written.", id);
ExecWaiter(self, id).await; let resp = ExecWaiter(self, id, false).await;
let resp = { // let resp = {
let mut handlers = self.results.write(); // let mut handlers = self.results.write();
handlers.remove(&id).unwrap().0.unwrap() // handlers.remove(&id).unwrap().0.unwrap()
}; // };
Ok(resp) Ok(resp)
} }
/// Executes the CAPABILITY command /// Executes the CAPABILITY command
pub async fn capabilities(&mut self) -> Result<()> { pub async fn capabilities(&mut self, force: bool) -> Result<()> {
{
let caps = &*self.caps.read();
if caps.is_some() && !force {
return Ok(());
}
}
let cmd = Command::Capability; let cmd = Command::Capability;
debug!("sending: {:?} {:?}", cmd, cmd.to_string()); debug!("sending: {:?} {:?}", cmd, cmd.to_string());
let result = self let (result, intermediate) = self
.execute(cmd) .execute(cmd)
.await .await
.context("error executing CAPABILITY command")?; .context("error executing CAPABILITY command")?;
debug!("cap resp: {:?}", result); debug!("cap resp: {:?}", result);
if let Some(Response::Capabilities(new_caps)) = intermediate
.iter()
.find(|resp| matches!(resp, Response::Capabilities(_)))
{
let caps = &mut *self.caps.write();
*caps = Some(new_caps.iter().cloned().collect());
}
// if let Response::Capabilities(caps) = resp { // if let Response::Capabilities(caps) = resp {
// debug!("capabilities: {:?}", caps); // debug!("capabilities: {:?}", caps);
// } // }
@ -159,24 +177,12 @@ where
} }
/// Check if this client has a particular capability /// Check if this client has a particular capability
pub async fn has_capability(&self, cap: impl AsRef<str>) -> Result<bool> { pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
let cap = cap.as_ref().to_owned(); let cap = cap.as_ref().to_owned();
debug!("checking for the capability: {:?}", cap); debug!("checking for the capability: {:?}", cap);
let cap = parse_capability(cap)?;
let cap_bytes = cap.as_bytes(); self.capabilities(false).await?;
debug!("cap_bytes {:?}", cap_bytes);
// let (_, cap) = match crate::oldparser::rfc3501::capability(cap_bytes) {
// Ok(v) => v,
// Err(err) => {
// error!("ERROR PARSING {:?} {} {:?}", cap, err, err);
// use std::error::Error;
// let bt = err.backtrace().unwrap();
// error!("{}", bt);
// std::process::exit(1);
// }
// };
let cap = Capability::from(Capability::Atom(cap));
let caps = &*self.caps.read(); let caps = &*self.caps.read();
// TODO: refresh caps // TODO: refresh caps
@ -190,7 +196,7 @@ where
pub struct GreetingWaiter(GreetingState); pub struct GreetingWaiter(GreetingState);
impl Future for GreetingWaiter { impl Future for GreetingWaiter {
type Output = (); type Output = Response;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let (state, waker) = &mut *self.0.write(); let (state, waker) = &mut *self.0.write();
debug!("g {:?}", state); debug!("g {:?}", state);
@ -198,32 +204,43 @@ impl Future for GreetingWaiter {
*waker = Some(cx.waker().clone()); *waker = Some(cx.waker().clone());
} }
match state { match state.take() {
true => Poll::Ready(()), Some(v) => Poll::Ready(v),
false => Poll::Pending, None => Poll::Pending,
} }
} }
} }
pub struct ExecWaiter<'a, C>(&'a Client<C>, usize); pub struct ExecWaiter<'a, C>(&'a Client<C>, usize, bool);
impl<'a, C> Future for ExecWaiter<'a, C> { impl<'a, C> Future for ExecWaiter<'a, C> {
type Output = (); type Output = (Response, Vec<Response>);
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let mut handlers = self.0.results.write(); // add the waker
let state = handlers.get_mut(&self.1); let mut results = self.0.results.write();
if !self.2 {
// TODO: handle the None case here if let Some((_, _, _, waker_ref)) =
debug!("f[{}] {:?}", self.1, state); results.iter_mut().find(|(id, _, _, _)| *id == self.1)
let (result, waker) = state.unwrap(); {
let waker = cx.waker().clone();
match result { *waker_ref = Some(waker);
Some(_) => Poll::Ready(()), self.2 = true;
None => {
*waker = Some(cx.waker().clone());
Poll::Pending
} }
} }
// if this struct exists then there's definitely at least one entry
let (id, last_response, _, _) = &results[0];
if *id != self.1 || last_response.is_none() {
return Poll::Pending;
}
let (_, last_response, intermediate_responses, _) = results.pop_front().unwrap();
mem::drop(results);
Poll::Ready((
last_response.expect("already checked"),
intermediate_responses,
))
} }
} }
@ -257,26 +274,17 @@ where
} }
debug!("got a new line {:?}", next_line); debug!("got a new line {:?}", next_line);
let resp = Response::Capabilities(vec![]); let resp = parse_response(next_line)?;
// let (_, resp) = match crate::oldparser::parse_response(next_line.as_bytes()) {
// Ok(v) => v,
// Err(err) => {
// debug!("shiet: {:?}", err);
// continue;
// }
// };
if let Some(greeting) = greeting.take() { if let Some(greeting) = greeting.take() {
let (greeting, waker) = &mut *greeting.write(); let (greeting, waker) = &mut *greeting.write();
debug!("received greeting!"); debug!("received greeting!");
*greeting = true; *greeting = Some(resp.clone());
if let Some(waker) = waker.take() { if let Some(waker) = waker.take() {
waker.wake(); waker.wake();
} }
} }
let resp = Response::from(resp);
debug!("resp: {:?}", resp);
match &resp { match &resp {
// capabilities list // capabilities list
Response::Capabilities(new_caps) Response::Capabilities(new_caps)
@ -302,8 +310,8 @@ where
if tag.starts_with(TAG_PREFIX) { if tag.starts_with(TAG_PREFIX) {
let id = tag.trim_start_matches(TAG_PREFIX).parse::<usize>()?; let id = tag.trim_start_matches(TAG_PREFIX).parse::<usize>()?;
let mut results = results.write(); let mut results = results.write();
if let Some((c, waker)) = results.get_mut(&id) { if let Some((_, opt, _, waker)) = results.iter_mut().next() {
*c = Some(resp); *opt = Some(resp);
if let Some(waker) = waker.take() { if let Some(waker) = waker.take() {
waker.wake(); waker.wake();
} }
@ -311,7 +319,7 @@ where
} }
} }
_ => todo!("unhandled response: {:?}", resp), _ => {}
} }
// debug!("parsed as: {:?}", resp); // debug!("parsed as: {:?}", resp);

View file

@ -43,6 +43,9 @@ use tokio_rustls::{
client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector, client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector,
}; };
use crate::command::Command;
use crate::response::Response;
pub use self::inner::Client; pub use self::inner::Client;
/// Struct used to start building the config for a client. /// Struct used to start building the config for a client.
@ -118,12 +121,20 @@ impl ClientUnauthenticated {
} }
} }
pub async fn capabilities(&mut self) -> Result<()> { /// TODO: Exposing low-level execute , shoudl remove later
pub async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec<Response>)> {
match self { match self {
ClientUnauthenticated::Encrypted(e) => e.inner.capabilities().await?, ClientUnauthenticated::Encrypted(e) => e.inner.execute(cmd).await,
ClientUnauthenticated::Unencrypted(e) => e.inner.capabilities().await?, ClientUnauthenticated::Unencrypted(e) => e.inner.execute(cmd).await,
}
}
/// Checks if the server that the client is talking to has support for the given capability.
pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
match self {
ClientUnauthenticated::Encrypted(e) => e.inner.has_capability(cap).await,
ClientUnauthenticated::Unencrypted(e) => e.inner.has_capability(cap).await,
} }
Ok(())
} }
} }

View file

@ -5,6 +5,7 @@ use std::fmt;
pub enum Command { pub enum Command {
Capability, Capability,
Starttls, Starttls,
Login { username: String, password: String },
} }
impl fmt::Display for Command { impl fmt::Display for Command {
@ -12,6 +13,7 @@ impl fmt::Display for Command {
match self { match self {
Command::Capability => write!(f, "CAPABILITY"), Command::Capability => write!(f, "CAPABILITY"),
Command::Starttls => write!(f, "STARTTLS"), Command::Starttls => write!(f, "STARTTLS"),
Command::Login { username, password } => write!(f, "LOGIN {} {}", username, password),
} }
} }
} }

View file

@ -16,22 +16,7 @@ struct Rfc3501;
pub fn parse_capability(s: impl AsRef<str>) -> Result<Capability, Error<Rule>> { pub fn parse_capability(s: impl AsRef<str>) -> Result<Capability, Error<Rule>> {
let mut pairs = Rfc3501::parse(Rule::capability, s.as_ref())?; let mut pairs = Rfc3501::parse(Rule::capability, s.as_ref())?;
let pair = pairs.next().unwrap(); let pair = pairs.next().unwrap();
let cap = match pair.as_rule() { Ok(build_capability(pair))
Rule::capability => {
let mut pairs = pair.into_inner();
let pair = pairs.next().unwrap();
match pair.as_rule() {
Rule::auth_type => Capability::Auth(pair.as_str().to_uppercase().to_owned()),
Rule::atom => match pair.as_str() {
"IMAP4rev1" => Capability::Imap4rev1,
s => Capability::Atom(s.to_uppercase().to_owned()),
},
_ => unreachable!("{:?}", pair),
}
}
_ => unreachable!("{:?}", pair),
};
Ok(cap)
} }
pub fn parse_response(s: impl AsRef<str>) -> Result<Response, Error<Rule>> { pub fn parse_response(s: impl AsRef<str>) -> Result<Response, Error<Rule>> {
@ -82,6 +67,7 @@ fn build_response(pair: Pair<Rule>) -> Response {
} }
} }
Rule::mailbox_data => Response::MailboxData(build_mailbox_data(pair)), Rule::mailbox_data => Response::MailboxData(build_mailbox_data(pair)),
Rule::capability_data => Response::Capabilities(build_capabilities(pair)),
_ => unreachable!("{:#?}", pair), _ => unreachable!("{:#?}", pair),
} }
} }
@ -100,7 +86,6 @@ fn build_resp_cond_state(pair: Pair<Rule>) -> (Status, Option<ResponseCode>, Opt
let mut code = None; let mut code = None;
let mut information = None; let mut information = None;
println!("pairs: {:#?}", pairs);
let pair = pairs.next().unwrap(); let pair = pairs.next().unwrap();
let pairs = pair.into_inner(); let pairs = pair.into_inner();
for pair in pairs { for pair in pairs {
@ -119,9 +104,13 @@ fn build_resp_code(pair: Pair<Rule>) -> Option<ResponseCode> {
unreachable!("{:#?}", pair); unreachable!("{:#?}", pair);
} }
// panic!("pair: {:#?}", pair);
debug!("pair: {:#?}", pair);
let mut pairs = pair.into_inner(); let mut pairs = pair.into_inner();
let pair = pairs.next()?; let pair = pairs.next()?;
Some(match pair.as_rule() { Some(match pair.as_rule() {
Rule::capability_data => ResponseCode::Capabilities(build_capabilities(pair)),
Rule::resp_text_code_readwrite => ResponseCode::ReadWrite, Rule::resp_text_code_readwrite => ResponseCode::ReadWrite,
Rule::resp_text_code_uidvalidity => ResponseCode::UidValidity(build_number(pair)), Rule::resp_text_code_uidvalidity => ResponseCode::UidValidity(build_number(pair)),
Rule::resp_text_code_unseen => ResponseCode::Unseen(build_number(pair)), Rule::resp_text_code_unseen => ResponseCode::Unseen(build_number(pair)),
@ -129,6 +118,31 @@ fn build_resp_code(pair: Pair<Rule>) -> Option<ResponseCode> {
}) })
} }
fn build_capability(pair: Pair<Rule>) -> Capability {
if !matches!(pair.as_rule(), Rule::capability) {
unreachable!("{:#?}", pair);
}
let mut pairs = pair.into_inner();
let pair = pairs.next().unwrap();
match pair.as_rule() {
Rule::auth_type => Capability::Auth(pair.as_str().to_uppercase().to_owned()),
Rule::atom => match pair.as_str() {
"IMAP4rev1" => Capability::Imap4rev1,
s => Capability::Atom(s.to_uppercase().to_owned()),
},
_ => unreachable!("{:?}", pair),
}
}
fn build_capabilities(pair: Pair<Rule>) -> Vec<Capability> {
if !matches!(pair.as_rule(), Rule::capability_data) {
unreachable!("{:#?}", pair);
}
pair.into_inner().map(build_capability).collect()
}
fn build_status(pair: Pair<Rule>) -> Status { fn build_status(pair: Pair<Rule>) -> Status {
match pair.as_rule() { match pair.as_rule() {
Rule::resp_status => match pair.as_str().to_uppercase().as_str() { Rule::resp_status => match pair.as_str().to_uppercase().as_str() {
@ -292,5 +306,13 @@ mod tests {
information: Some("SELECT completed".to_owned()), information: Some("SELECT completed".to_owned()),
}) })
); );
// assert_eq!(
// parse_response(concat!(
// r#"* 12 FETCH (FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US") ("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "<B27397-0100000@cac.washington.edu>") BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92))"#,
// "\r\n",
// )),
// Ok(Response::Fetch(12, vec![]))
// );
} }
} }

View file

@ -34,7 +34,7 @@ body_type_mpart = { body{1,} ~ sp ~ media_subtype ~ (sp ~ body_ext_mpart)? }
body_type_msg = { media_message ~ sp ~ body_fields ~ sp ~ envelope ~ sp ~ body ~ sp ~ body_fld_lines } body_type_msg = { media_message ~ sp ~ body_fields ~ sp ~ envelope ~ sp ~ body ~ sp ~ body_fld_lines }
body_type_text = { media_text ~ sp ~ body_fields ~ sp ~ body_fld_lines } body_type_text = { media_text ~ sp ~ body_fields ~ sp ~ body_fld_lines }
capability = ${ ^"AUTH=" ~ auth_type | atom } capability = ${ ^"AUTH=" ~ auth_type | atom }
capability_data = { ^"CAPABILITY" ~ (sp ~ capability)* ~ sp ~ "IMAP4rev1" ~ (sp ~ capability)* } capability_data = { ^"CAPABILITY" ~ (sp ~ ("IMAP4rev1" ~ capability))* ~ sp ~ "IMAP4rev1" ~ (sp ~ capability)* }
char8 = @{ '\x01'..'\xff' } char8 = @{ '\x01'..'\xff' }
continue_req = { "+" ~ sp ~ (resp_text | base64) ~ crlf } continue_req = { "+" ~ sp ~ (resp_text | base64) ~ crlf }
date_day_fixed = { (sp ~ digit) | digit{2} } date_day_fixed = { (sp ~ digit) | digit{2} }

View file

@ -9,7 +9,7 @@ use panorama_imap::{
use tokio::{sync::mpsc::UnboundedReceiver, task::JoinHandle}; use tokio::{sync::mpsc::UnboundedReceiver, task::JoinHandle};
use tokio_stream::wrappers::WatchStream; use tokio_stream::wrappers::WatchStream;
use crate::config::{Config, ConfigWatcher, MailAccountConfig, TlsMethod}; use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod};
/// Command sent to the mail thread by something else (i.e. UI) /// Command sent to the mail thread by something else (i.e. UI)
pub enum MailCommand { pub enum MailCommand {
@ -89,7 +89,13 @@ async fn imap_main(acct: MailAccountConfig) -> Result<()> {
debug!("preparing to auth"); debug!("preparing to auth");
// check if the authentication method is supported // check if the authentication method is supported
unauth.capabilities().await?; let authed = match acct.imap.auth {
ImapAuth::Plain { username, password } => {
let ok = unauth.has_capability("AUTH=PLAIN").await?;
let res = unauth.execute(ImapCommand::Login { username, password }).await?;
debug!("res: {:?}", res);
}
};
// debug!("sending CAPABILITY"); // debug!("sending CAPABILITY");
// let result = unauth.capabilities().await?; // let result = unauth.capabilities().await?;