From 602ea1565d834f05fa7bc98a7d594e5c0add2c0a Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Sat, 22 Jun 2019 21:53:55 +0200 Subject: [PATCH] refactor(errors): Move almost everything to RequestError. This is nicer than stupid Box everywhere. --- src/authenticator.rs | 28 +++++---- src/authenticator_delegate.rs | 24 +------- src/device.rs | 43 +++++--------- src/installed.rs | 86 ++++++++++++++-------------- src/lib.rs | 6 +- src/refresh.rs | 18 +----- src/service_account.rs | 103 +++++++++++++++------------------- src/types.rs | 68 ++++++++++++++++++---- 8 files changed, 178 insertions(+), 198 deletions(-) diff --git a/src/authenticator.rs b/src/authenticator.rs index d3e1008..fa68076 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -1,7 +1,7 @@ use crate::authenticator_delegate::{AuthenticatorDelegate, Retry}; -use crate::refresh::{RefreshFlow, RefreshResult}; +use crate::refresh::RefreshFlow; use crate::storage::{hash_scopes, DiskTokenStorage, MemoryStorage, TokenStorage}; -use crate::types::{ApplicationSecret, GetToken, StringError, Token}; +use crate::types::{ApplicationSecret, GetToken, RefreshResult, RequestError, Token}; use futures::{future, prelude::*}; use tokio_timer; @@ -88,7 +88,7 @@ impl< fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator, @@ -100,7 +100,7 @@ impl< let appsecret = self.inner.lock().unwrap().application_secret(); let gettoken = self.inner.clone(); let loopfn = move |()| -> Box< - dyn Future, Error = Box> + Send, + dyn Future, Error = RequestError> + Send, > { // How well does this work with tokio? match store.lock().unwrap().get( @@ -121,27 +121,27 @@ impl< appsecret.clone(), refresh_token, ) - .and_then(move |rr| -> Box, Error=Box> + Send> { + .and_then(move |rr| -> Box, Error=RequestError> + Send> { match rr { - RefreshResult::Error(e) => { + RefreshResult::Error(ref e) => { delegate.token_refresh_failed( format!("{}", e.description().to_string()), &Some("the request has likely timed out".to_string()), ); - Box::new(Err(Box::new(e) as Box).into_future()) + Box::new(Err(RequestError::Refresh(rr)).into_future()) } RefreshResult::RefreshError(ref s, ref ss) => { delegate.token_refresh_failed( format!("{} {}", s, ss.clone().map(|s| format!("({})", s)).unwrap_or("".to_string())), &Some("the refresh token is likely invalid and your authorization has been revoked".to_string()), ); - Box::new(Err(Box::new(StringError::new(s, ss.as_ref())) as Box).into_future()) + Box::new(Err(RequestError::Refresh(rr)).into_future()) } RefreshResult::Success(t) => { if let Err(e) = store.lock().unwrap().set(scope_key, &scopes.iter().map(|s| s.as_str()).collect(), Some(t.clone())) { match delegate.token_storage_failure(true, &e) { Retry::Skip => Box::new(Ok(future::Loop::Break(t)).into_future()), - Retry::Abort => Box::new(Err(Box::new(e) as Box).into_future()), + Retry::Abort => Box::new(Err(RequestError::Cache(Box::new(e))).into_future()), Retry::After(d) => Box::new( tokio_timer::sleep(d) .then(|_| Ok(future::Loop::Continue(()))), @@ -149,9 +149,7 @@ impl< as Box< dyn Future< Item = future::Loop, - Error = Box, - > + Send, - >, + Error = RequestError> + Send>, } } else { Box::new(Ok(future::Loop::Break(t)).into_future()) @@ -181,7 +179,7 @@ impl< Box::new(Ok(future::Loop::Break(t)).into_future()) } Retry::Abort => Box::new( - Err(Box::new(e) as Box).into_future(), + Err(RequestError::Cache(Box::new(e))).into_future(), ), Retry::After(d) => Box::new( tokio_timer::sleep(d) @@ -190,7 +188,7 @@ impl< as Box< dyn Future< Item = future::Loop, - Error = Box, + Error = RequestError, > + Send, >, } @@ -202,7 +200,7 @@ impl< } Err(err) => match delegate.token_storage_failure(false, &err) { Retry::Abort | Retry::Skip => { - return Box::new(future::err(Box::new(err) as Box)) + return Box::new(Err(RequestError::Cache(Box::new(err))).into_future()) } Retry::After(d) => { return Box::new( diff --git a/src/authenticator_delegate.rs b/src/authenticator_delegate.rs index fb19d38..132b511 100644 --- a/src/authenticator_delegate.rs +++ b/src/authenticator_delegate.rs @@ -4,7 +4,7 @@ use std::error::Error; use std::fmt; use std::io; -use crate::types::RequestError; +use crate::types::{PollError, RequestError}; use chrono::{DateTime, Local, Utc}; use std::time::Duration; @@ -45,21 +45,6 @@ impl fmt::Display for PollInformation { } } -/// Encapsulates all possible results of a `poll_token(...)` operation -#[derive(Debug)] -pub enum PollError { - /// Connection failure - retry if you think it's worth it - HttpError(hyper::Error), - /// Indicates we are expired, including the expiration date - Expired(DateTime), - /// Indicates that the user declined access. String is server response - AccessDenied, - /// Indicates that too many attempts failed. - TimedOut, - /// Other type of error. - Other(String), -} - impl fmt::Display for PollError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { @@ -93,13 +78,6 @@ pub trait AuthenticatorDelegate: Clone { Retry::Abort } - /// Called whenever there is an HttpError, usually if there are network problems. - /// - /// Return retry information. - fn connection_error(&mut self, _: &hyper::http::Error) -> Retry { - Retry::Abort - } - /// Called whenever we failed to retrieve a token or set a token due to a storage error. /// You may use it to either ignore the incident or retry. /// This can be useful if the underlying `TokenStorage` may fail occasionally. diff --git a/src/device.rs b/src/device.rs index 3778ebb..c4b60df 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,4 +1,3 @@ -use std::error::Error; use std::iter::{FromIterator, IntoIterator}; use std::time::Duration; @@ -13,9 +12,9 @@ use serde_json as json; use tokio_timer; use url::form_urlencoded; -use crate::authenticator_delegate::{FlowDelegate, PollError, PollInformation, Retry}; +use crate::authenticator_delegate::{FlowDelegate, PollInformation, Retry}; use crate::types::{ - ApplicationSecret, Flow, FlowType, GetToken, JsonError, RequestError, StringError, Token, + ApplicationSecret, Flow, FlowType, GetToken, JsonError, PollError, RequestError, Token, }; pub const GOOGLE_DEVICE_CODE_URL: &'static str = "https://accounts.google.com/o/oauth2/device/code"; @@ -47,7 +46,7 @@ impl< fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator, @@ -97,7 +96,7 @@ where pub fn retrieve_device_token<'a>( &mut self, scopes: Vec, - ) -> Box> + Send> { + ) -> Box + Send> { let application_secret = self.application_secret.clone(); let client = self.client.clone(); let wait = self.wait; @@ -131,13 +130,9 @@ where .then(|_| pt) .then(move |r| match r { Ok(None) if i < maxn => match fd.pending(&pollinf) { - Retry::Abort | Retry::Skip => Box::new( - Err(Box::new(StringError::new( - "Pending authentication aborted".to_string(), - None, - )) as Box) - .into_future(), - ), + Retry::Abort | Retry::Skip => { + Box::new(Err(RequestError::Poll(PollError::TimedOut)).into_future()) + } Retry::After(d) => Box::new( tokio_timer::sleep(d) .then(move |_| Ok(future::Loop::Continue(i + 1))), @@ -145,7 +140,7 @@ where as Box< dyn Future< Item = future::Loop, - Error = Box, + Error = RequestError, > + Send, >, }, @@ -153,16 +148,15 @@ where Err(e @ PollError::AccessDenied) | Err(e @ PollError::TimedOut) | Err(e @ PollError::Expired(_)) => { - Box::new(Err(Box::new(e) as Box).into_future()) + Box::new(Err(RequestError::Poll(e)).into_future()) } Err(_) if i < maxn => { Box::new(Ok(future::Loop::Continue(i + 1)).into_future()) } // Too many attempts. - Ok(None) | Err(_) => Box::new( - Err(Box::new(PollError::TimedOut) as Box) - .into_future(), - ), + Ok(None) | Err(_) => { + Box::new(Err(RequestError::Poll(PollError::TimedOut)).into_future()) + } }) }) })) @@ -188,8 +182,7 @@ where client: hyper::Client, device_code_url: String, scopes: Vec, - ) -> impl Future> - { + ) -> impl Future { // note: cloned() shouldn't be needed, see issue // https://github.com/servo/rust-url/issues/81 let req = form_urlencoded::Serializer::new(String::new()) @@ -222,9 +215,7 @@ where |r: Result, hyper::error::Error>| { match r { Err(err) => { - return Err( - Box::new(RequestError::ClientError(err)) as Box - ); + return Err(RequestError::ClientError(err)); } Ok(res) => { #[derive(Deserialize)] @@ -246,11 +237,7 @@ where // check for error match json::from_str::(&json_str) { Err(_) => {} // ignore, move on - Ok(res) => { - return Err( - Box::new(RequestError::from(res)) as Box - ) - } + Ok(res) => return Err(RequestError::from(res)), } let decoded: JsonData = json::from_str(&json_str).unwrap(); diff --git a/src/installed.rs b/src/installed.rs index 52170e7..d734887 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -3,8 +3,6 @@ // Refer to the project root for licensing information. // use std::convert::AsRef; -use std::error::Error; -use std::io; use std::sync::{Arc, Mutex}; use futures::prelude::*; @@ -12,12 +10,11 @@ use futures::stream::Stream; use futures::sync::oneshot; use hyper; use hyper::{header, StatusCode, Uri}; -use serde_json::error; use url::form_urlencoded; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; use crate::authenticator_delegate::FlowDelegate; -use crate::types::{ApplicationSecret, GetToken, Token}; +use crate::types::{ApplicationSecret, GetToken, RequestError, Token}; const OOB_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob"; @@ -67,7 +64,7 @@ impl( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator, @@ -134,12 +131,12 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne pub fn obtain_token<'a>( &mut self, scopes: Vec, // Note: I haven't found a better way to give a list of strings here, due to ownership issues with futures. - ) -> impl 'a + Future> + Send { + ) -> impl 'a + Future + Send { let rduri = self.fd.redirect_uri(); // Start server on localhost to accept auth code. let server = if let InstalledFlowReturnMethod::HTTPRedirect(port) = self.method { match InstalledFlowServer::new(port) { - Result::Err(e) => Err(Box::new(e) as Box), + Result::Err(e) => Err(RequestError::ClientError(e)), Result::Ok(server) => Ok(Some(server)), } } else { @@ -166,27 +163,34 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne let result = client.request(request); // Handle result here, it makes ownership tracking easier. result - .map_err(|e| Box::new(e) as Box) .and_then(move |r| { - let result = r - .into_body() + r.into_body() .concat2() - .wait() - .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()); // TODO: error handling - - let resp = match result { - Err(e) => return Err(Box::new(e) as Box), + .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) // TODO: error handling + }) + .then(|body_or| { + let resp = match body_or { + Err(e) => return Err(RequestError::ClientError(e)), Ok(s) => s, }; - let token_resp: Result = + let token_resp: Result = serde_json::from_str(&resp); match token_resp { Err(e) => { - return Err(Box::new(e) as Box); + return Err(RequestError::JSONError(e)); + } + Ok(tok) => { + if tok.error.is_some() { + Err(RequestError::NegativeServerResponse( + tok.error.unwrap(), + tok.error_description, + )) + } else { + Ok(tok) + } } - Ok(tok) => Ok(tok) as Result>, } }) }) @@ -203,18 +207,12 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne }; token.set_expiry_absolute(); - Result::Ok(token) + Ok(token) } else { - let err = Box::new(io::Error::new( - io::ErrorKind::Other, - format!( - "Token API error: {} {}", - tokens.error.unwrap_or("".to_string()), - tokens.error_description.unwrap_or("".to_string()) - ) - .as_str(), - )) as Box; - Result::Err(err) + Err(RequestError::NegativeServerResponse( + tokens.error.unwrap(), + tokens.error_description, + )) } }) } @@ -224,7 +222,7 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne mut auth_delegate: FD, appsecret: &ApplicationSecret, scopes: S, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + 'a, S: Iterator, @@ -251,10 +249,7 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne } Ok(code) } - _ => Err(Box::new(io::Error::new( - io::ErrorKind::UnexpectedEof, - "couldn't read code", - )) as Box), + _ => Err(RequestError::UserError("couldn't read code".to_string())), } }), ) @@ -274,7 +269,12 @@ impl<'c, FD: 'static + FlowDelegate + Clone + Send, C: 'c + hyper::client::conne auth_delegate .present_user_url(&url, false /* need_code */) .then(move |_| server.block_till_auth()) - .map_err(|e| Box::new(e) as Box), + .map_err(|e| { + RequestError::UserError(format!( + "could not obtain token via redirect: {}", + e + )) + }), ) } } @@ -525,6 +525,7 @@ impl InstalledFlowService { #[cfg(test)] mod tests { + use std::error::Error; use std::fmt; use std::str::FromStr; @@ -673,14 +674,13 @@ mod tests { .expect(1) .create(); - let fut = - inf.token(vec!["https://googleapis.com/some/scope"].iter()) - .then(|tokr| { - assert!(tokr.is_err()); - assert!(format!("{}", tokr.unwrap_err()) - .contains("Token API error: invalid_code")); - Ok(()) as Result<(), ()> - }); + let fut = inf + .token(vec!["https://googleapis.com/some/scope"].iter()) + .then(|tokr| { + assert!(tokr.is_err()); + assert!(format!("{}", tokr.unwrap_err()).contains("invalid_code")); + Ok(()) as Result<(), ()> + }); rt.block_on(fut).expect("block on"); _m.assert(); } diff --git a/src/lib.rs b/src/lib.rs index 752df38..26b59cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,14 +115,14 @@ mod types; pub use crate::authenticator::Authenticator; pub use crate::authenticator_delegate::{ AuthenticatorDelegate, DefaultAuthenticatorDelegate, DefaultFlowDelegate, FlowDelegate, - PollError, PollInformation, + PollInformation, }; pub use crate::device::{DeviceFlow, GOOGLE_DEVICE_CODE_URL}; pub use crate::helper::*; pub use crate::installed::{InstalledFlow, InstalledFlowReturnMethod}; -pub use crate::refresh::{RefreshFlow, RefreshResult}; pub use crate::service_account::*; pub use crate::storage::{DiskTokenStorage, MemoryStorage, NullStorage, TokenStorage}; pub use crate::types::{ - ApplicationSecret, ConsoleApplicationSecret, FlowType, GetToken, Scheme, Token, TokenType, + ApplicationSecret, ConsoleApplicationSecret, FlowType, GetToken, PollError, RefreshResult, + RequestError, Scheme, Token, TokenType, }; diff --git a/src/refresh.rs b/src/refresh.rs index e6944c9..ac1fc72 100644 --- a/src/refresh.rs +++ b/src/refresh.rs @@ -1,6 +1,4 @@ -use crate::types::{ApplicationSecret, JsonError}; - -use std::error::Error; +use crate::types::{ApplicationSecret, JsonError, RefreshResult, RequestError}; use super::Token; use chrono::Utc; @@ -18,17 +16,6 @@ use url::form_urlencoded; /// and valid access token. pub struct RefreshFlow; -/// All possible outcomes of the refresh flow -#[derive(Debug)] -pub enum RefreshResult { - /// Indicates connection failure - Error(hyper::Error), - /// The server did not answer with a new token, providing the server message - RefreshError(String, Option), - /// The refresh operation finished successfully, providing a new `Token` - Success(Token), -} - impl RefreshFlow { /// Attempt to refresh the given token, and obtain a new, valid one. /// If the `RefreshResult` is `RefreshResult::Error`, you may retry within an interval @@ -48,7 +35,7 @@ impl RefreshFlow { client: hyper::Client, client_secret: ApplicationSecret, refresh_token: String, - ) -> impl 'a + Future> { + ) -> impl 'a + Future { let req = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[ ("client_id", client_secret.client_id.clone()), @@ -109,6 +96,7 @@ impl RefreshFlow { expires_in_timestamp: Some(Utc::now().timestamp() + t.expires_in), })) }) + .map_err(RequestError::Refresh) } } diff --git a/src/service_account.rs b/src/service_account.rs index e0b1b56..6eb4411 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -12,11 +12,10 @@ //! use std::default::Default; -use std::error; use std::sync::{Arc, Mutex}; use crate::storage::{hash_scopes, MemoryStorage, TokenStorage}; -use crate::types::{ApplicationSecret, GetToken, JsonError, StringError, Token}; +use crate::types::{ApplicationSecret, GetToken, JsonError, RequestError, StringError, Token}; use futures::stream::Stream; use futures::{future, prelude::*}; @@ -45,7 +44,7 @@ fn encode_base64>(s: T) -> String { } /// Decode a PKCS8 formatted RSA key. -fn decode_rsa_key(pem_pkcs8: &str) -> Result> { +fn decode_rsa_key(pem_pkcs8: &str) -> Result { let private = pem_pkcs8.to_string().replace("\\n", "\n").into_bytes(); let mut private_reader: &[u8] = private.as_ref(); let private_keys = pemfile::pkcs8_private_keys(&mut private_reader); @@ -54,16 +53,16 @@ fn decode_rsa_key(pem_pkcs8: &str) -> Result 0 { Ok(pk[0].clone()) } else { - Err(Box::new(io::Error::new( + Err(io::Error::new( io::ErrorKind::InvalidInput, "Not enough private keys in PEM", - ))) + )) } } else { - Err(Box::new(io::Error::new( + Err(io::Error::new( io::ErrorKind::InvalidInput, "Error reading key from PEM", - ))) + )) } } @@ -134,24 +133,20 @@ impl JWT { } /// Sign a JWT base string with `private_key`, which is a PKCS8 string. - fn sign(&self, private_key: &str) -> Result> { + fn sign(&self, private_key: &str) -> Result { let mut jwt_head = self.encode_claims(); let key = decode_rsa_key(private_key)?; - let signing_key = sign::RSASigningKey::new(&key).map_err(|_| { - Box::new(io::Error::new( - io::ErrorKind::Other, - "Couldn't initialize signer", - )) as Box - })?; + let signing_key = sign::RSASigningKey::new(&key) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "Couldn't initialize signer"))?; let signer = signing_key .choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256]) - .ok_or(Box::new(io::Error::new( + .ok_or(io::Error::new( io::ErrorKind::Other, "Couldn't choose signing scheme", - )) as Box)?; + ))?; let signature = signer .sign(jwt_head.as_bytes()) - .map_err(|e| Box::new(e) as Box)?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?; let signature_b64 = encode_base64(signature); jwt_head.push_str("."); @@ -256,13 +251,14 @@ impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { sub: Option, key: ServiceAccountKey, scopes: Vec, - ) -> impl Future> { + ) -> impl Future { let mut claims = init_claims_from_key(&key, &scopes); claims.sub = sub.clone(); let signed = JWT::new(claims) .sign(key.private_key.as_ref().unwrap()) .into_future(); signed + .map_err(RequestError::LowLevelError) .map(|signed| { form_urlencoded::Serializer::new(String::new()) .extend_pairs(vec![ @@ -277,48 +273,40 @@ impl<'a, C: 'static + hyper::client::connect::Connect> ServiceAccountAccess { .body(hyper::Body::from(rqbody)) .unwrap() }) - .and_then(move |request| { - client - .request(request) - .map_err(|e| Box::new(e) as Box) - }) + .and_then(move |request| client.request(request).map_err(RequestError::ClientError)) .and_then(|response| { response .into_body() .concat2() - .map_err(|e| Box::new(e) as Box) + .map_err(RequestError::ClientError) }) .map(|c| String::from_utf8(c.into_bytes().to_vec()).unwrap()) .and_then(|s| { if let Ok(jse) = serde_json::from_str::(&s) { - Err( - Box::new(StringError::new(&jse.error, jse.error_description.as_ref())) - as Box, - ) + Err(RequestError::NegativeServerResponse( + jse.error, + jse.error_description, + )) } else { - serde_json::from_str(&s) - .map_err(|e| Box::new(e) as Box) + serde_json::from_str(&s).map_err(RequestError::JSONError) } }) - .then( - |token: Result>| match token { - Err(e) => return Err(e), - Ok(token) => { - if token.access_token.is_none() - || token.token_type.is_none() - || token.expires_in.is_none() - { - Err(Box::new(StringError::new( - "Token response lacks fields".to_string(), - Some(format!("{:?}", token)), - )) - as Box) - } else { - Ok(token.to_oauth_token()) - } + .then(|token: Result| match token { + Err(e) => return Err(e), + Ok(token) => { + if token.access_token.is_none() + || token.token_type.is_none() + || token.expires_in.is_none() + { + Err(RequestError::BadServerResponse(format!( + "Token response lacks fields: {:?}", + token + ))) + } else { + Ok(token.to_oauth_token()) } - }, - ) + } + }) } } @@ -329,7 +317,7 @@ where fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator, @@ -348,14 +336,10 @@ where if !token.expired() { return Ok(token); } - return Err(Box::new(StringError::new("expired token in cache", None)) - as Box); - } - Err(e) => return Err(Box::new(e) as Box), - Ok(None) => { - return Err(Box::new(StringError::new("no token in cache", None)) - as Box) + return Err(StringError::new("expired token in cache", None)); } + Err(e) => return Err(StringError::new(format!("cache lookup error: {}", e), None)), + Ok(None) => return Err(StringError::new("no token in cache", None)), } }); @@ -380,9 +364,10 @@ where Box::new(cache_lookup.then(|r| match r { Ok(t) => Box::new(Ok(t).into_future()) - as Box> + Send>, - Err(_) => Box::new(req_token) - as Box> + Send>, + as Box + Send>, + Err(_) => { + Box::new(req_token) as Box + Send> + } })) } diff --git a/src/types.rs b/src/types.rs index 4c670a0..a4d200e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, TimeZone, Utc}; use hyper; use std::error::Error; use std::fmt; +use std::io; use std::str::FromStr; use futures::prelude::*; @@ -18,13 +19,37 @@ pub struct JsonError { pub error_uri: Option, } -/// Encapsulates all possible results of the `request_token(...)` operation +/// All possible outcomes of the refresh flow +#[derive(Debug)] +pub enum RefreshResult { + /// Indicates connection failure + Error(hyper::Error), + /// The server did not answer with a new token, providing the server message + RefreshError(String, Option), + /// The refresh operation finished successfully, providing a new `Token` + Success(Token), +} + +/// Encapsulates all possible results of a `poll_token(...)` operation in the Device flow. +#[derive(Debug)] +pub enum PollError { + /// Connection failure - retry if you think it's worth it + HttpError(hyper::Error), + /// Indicates we are expired, including the expiration date + Expired(DateTime), + /// Indicates that the user declined access. String is server response + AccessDenied, + /// Indicates that too many attempts failed. + TimedOut, + /// Other type of error. + Other(String), +} + +/// Encapsulates all possible results of the `token(...)` operation #[derive(Debug)] pub enum RequestError { /// Indicates connection failure ClientError(hyper::Error), - /// Indicates HTTP status failure - HttpError(hyper::http::Error), /// The OAuth client was not found InvalidClient, /// Some requested scopes were invalid. String contains the scopes as part of @@ -33,6 +58,20 @@ pub enum RequestError { /// A 'catch-all' variant containing the server error and description /// First string is the error code, the second may be a more detailed description NegativeServerResponse(String, Option), + /// A malformed server response. + BadServerResponse(String), + /// Error while decoding a JSON response. + JSONError(serde_json::error::Error), + /// Error within user input. + UserError(String), + /// A lower level IO error. + LowLevelError(io::Error), + /// A poll error occurred in the DeviceFlow. + Poll(PollError), + /// An error occurred while refreshing tokens. + Refresh(RefreshResult), + /// Error in token cache layer + Cache(Box), } impl From for RequestError { @@ -41,12 +80,6 @@ impl From for RequestError { } } -impl From for RequestError { - fn from(error: hyper::http::Error) -> RequestError { - RequestError::HttpError(error) - } -} - impl From for RequestError { fn from(value: JsonError) -> RequestError { match &*value.error { @@ -65,7 +98,6 @@ impl fmt::Display for RequestError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { RequestError::ClientError(ref err) => err.fmt(f), - RequestError::HttpError(ref err) => err.fmt(f), RequestError::InvalidClient => "Invalid Client".fmt(f), RequestError::InvalidScope(ref scope) => writeln!(f, "Invalid Scope: '{}'", scope), RequestError::NegativeServerResponse(ref error, ref desc) => { @@ -75,6 +107,17 @@ impl fmt::Display for RequestError { } "\n".fmt(f) } + RequestError::BadServerResponse(ref s) => s.fmt(f), + RequestError::JSONError(ref e) => format!( + "JSON Error; this might be a bug with unexpected server responses! {}", + e + ) + .fmt(f), + RequestError::UserError(ref s) => s.fmt(f), + RequestError::LowLevelError(ref e) => e.fmt(f), + RequestError::Poll(ref pe) => pe.fmt(f), + RequestError::Refresh(ref rr) => format!("{:?}", rr).fmt(f), + RequestError::Cache(ref e) => e.fmt(f), } } } @@ -83,7 +126,8 @@ impl Error for RequestError { fn source(&self) -> Option<&(dyn Error + 'static)> { match *self { RequestError::ClientError(ref err) => Some(err), - RequestError::HttpError(ref err) => Some(err), + RequestError::LowLevelError(ref err) => Some(err), + RequestError::JSONError(ref err) => Some(err), _ => None, } } @@ -199,7 +243,7 @@ pub trait GetToken { fn token<'b, I, T>( &mut self, scopes: I, - ) -> Box> + Send> + ) -> Box + Send> where T: AsRef + Ord + 'b, I: Iterator;