refactor(errors): Move almost everything to RequestError.

This is nicer than stupid Box<dyn Error+Send> everywhere.
This commit is contained in:
Lewin Bormann
2019-06-22 21:53:55 +02:00
parent 8d6085375f
commit 602ea1565d
8 changed files with 178 additions and 198 deletions

View File

@@ -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<dyn Future<Item = Token, Error = Box<dyn Error + Send>> + Send>
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
where
T: AsRef<str> + Ord + 'b,
I: Iterator<Item = &'b T>,
@@ -100,7 +100,7 @@ impl<
let appsecret = self.inner.lock().unwrap().application_secret();
let gettoken = self.inner.clone();
let loopfn = move |()| -> Box<
dyn Future<Item = future::Loop<Token, ()>, Error = Box<dyn Error + Send>> + Send,
dyn Future<Item = future::Loop<Token, ()>, 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<dyn Future<Item=future::Loop<Token, ()>, Error=Box<dyn Error+Send>> + Send> {
.and_then(move |rr| -> Box<dyn Future<Item=future::Loop<Token, ()>, 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<dyn Error + Send>).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<dyn Error + Send>).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<dyn Error + Send>).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<Token, ()>,
Error = Box<dyn Error + Send>,
> + 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<dyn Error + Send>).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<Token, ()>,
Error = Box<dyn Error + Send>,
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<dyn Error + Send>))
return Box::new(Err(RequestError::Cache(Box::new(err))).into_future())
}
Retry::After(d) => {
return Box::new(

View File

@@ -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<Utc>),
/// 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.

View File

@@ -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<dyn Future<Item = Token, Error = Box<dyn Error + Send>> + Send>
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
where
T: AsRef<str> + Ord + 'b,
I: Iterator<Item = &'b T>,
@@ -97,7 +96,7 @@ where
pub fn retrieve_device_token<'a>(
&mut self,
scopes: Vec<String>,
) -> Box<dyn Future<Item = Token, Error = Box<dyn Error + Send>> + Send> {
) -> Box<dyn Future<Item = Token, Error = RequestError> + 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<dyn Error + Send>)
.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<Token, u64>,
Error = Box<dyn Error + Send>,
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<dyn Error + Send>).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<dyn Error + Send>)
.into_future(),
),
Ok(None) | Err(_) => {
Box::new(Err(RequestError::Poll(PollError::TimedOut)).into_future())
}
})
})
}))
@@ -188,8 +182,7 @@ where
client: hyper::Client<C>,
device_code_url: String,
scopes: Vec<String>,
) -> impl Future<Item = (PollInformation, String), Error = Box<dyn 'static + Error + Send>>
{
) -> impl Future<Item = (PollInformation, String), Error = RequestError> {
// 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::Response<hyper::Body>, hyper::error::Error>| {
match r {
Err(err) => {
return Err(
Box::new(RequestError::ClientError(err)) as Box<dyn Error + Send>
);
return Err(RequestError::ClientError(err));
}
Ok(res) => {
#[derive(Deserialize)]
@@ -246,11 +237,7 @@ where
// check for error
match json::from_str::<JsonError>(&json_str) {
Err(_) => {} // ignore, move on
Ok(res) => {
return Err(
Box::new(RequestError::from(res)) as Box<dyn Error + Send>
)
}
Ok(res) => return Err(RequestError::from(res)),
}
let decoded: JsonData = json::from_str(&json_str).unwrap();

View File

@@ -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<FD: FlowDelegate + 'static + Send + Clone, C: hyper::client::connect::Conne
fn token<'b, I, T>(
&mut self,
scopes: I,
) -> Box<dyn Future<Item = Token, Error = Box<dyn Error + Send>> + Send>
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
where
T: AsRef<str> + Ord + 'b,
I: Iterator<Item = &'b T>,
@@ -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<String>, // Note: I haven't found a better way to give a list of strings here, due to ownership issues with futures.
) -> impl 'a + Future<Item = Token, Error = Box<dyn 'a + Error + Send>> + Send {
) -> impl 'a + Future<Item = Token, Error = RequestError> + 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<dyn Error + Send>),
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<dyn Error + Send>)
.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<dyn Error + Send>),
.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<JSONTokenResponse, error::Error> =
let token_resp: Result<JSONTokenResponse, serde_json::Error> =
serde_json::from_str(&resp);
match token_resp {
Err(e) => {
return Err(Box::new(e) as Box<dyn Error + Send>);
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<JSONTokenResponse, Box<dyn Error + Send>>,
}
})
})
@@ -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("<unknown err>".to_string()),
tokens.error_description.unwrap_or("".to_string())
)
.as_str(),
)) as Box<dyn Error + Send>;
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<dyn Future<Item = String, Error = Box<dyn Error + Send>> + Send>
) -> Box<dyn Future<Item = String, Error = RequestError> + Send>
where
T: AsRef<str> + 'a,
S: Iterator<Item = &'a T>,
@@ -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<dyn Error + Send>),
_ => 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<dyn Error + Send>),
.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();
}

View File

@@ -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,
};

View File

@@ -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<String>),
/// 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<C>,
client_secret: ApplicationSecret,
refresh_token: String,
) -> impl 'a + Future<Item = RefreshResult, Error = Box<dyn 'static + Error + Send>> {
) -> impl 'a + Future<Item = RefreshResult, Error = RequestError> {
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)
}
}

View File

@@ -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<T: AsRef<[u8]>>(s: T) -> String {
}
/// Decode a PKCS8 formatted RSA key.
fn decode_rsa_key(pem_pkcs8: &str) -> Result<PrivateKey, Box<dyn error::Error + Send>> {
fn decode_rsa_key(pem_pkcs8: &str) -> Result<PrivateKey, io::Error> {
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<PrivateKey, Box<dyn error::Error +
if pk.len() > 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<String, Box<dyn error::Error + Send>> {
fn sign(&self, private_key: &str) -> Result<String, io::Error> {
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<dyn error::Error + Send>
})?;
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<dyn error::Error + Send>)?;
))?;
let signature = signer
.sign(jwt_head.as_bytes())
.map_err(|e| Box::new(e) as Box<dyn error::Error + Send>)?;
.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<C> {
sub: Option<String>,
key: ServiceAccountKey,
scopes: Vec<String>,
) -> impl Future<Item = Token, Error = Box<dyn 'static + error::Error + Send>> {
) -> impl Future<Item = Token, Error = RequestError> {
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<C> {
.body(hyper::Body::from(rqbody))
.unwrap()
})
.and_then(move |request| {
client
.request(request)
.map_err(|e| Box::new(e) as Box<dyn error::Error + Send>)
})
.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<dyn error::Error + Send>)
.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::<JsonError>(&s) {
Err(
Box::new(StringError::new(&jse.error, jse.error_description.as_ref()))
as Box<dyn error::Error + Send>,
)
Err(RequestError::NegativeServerResponse(
jse.error,
jse.error_description,
))
} else {
serde_json::from_str(&s)
.map_err(|e| Box::new(e) as Box<dyn error::Error + Send>)
serde_json::from_str(&s).map_err(RequestError::JSONError)
}
})
.then(
|token: Result<TokenResponse, Box<dyn error::Error + Send>>| 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<dyn error::Error + Send>)
} else {
Ok(token.to_oauth_token())
}
.then(|token: Result<TokenResponse, RequestError>| 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<dyn Future<Item = Token, Error = Box<dyn error::Error + Send>> + Send>
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
where
T: AsRef<str> + Ord + 'b,
I: Iterator<Item = &'b T>,
@@ -348,14 +336,10 @@ where
if !token.expired() {
return Ok(token);
}
return Err(Box::new(StringError::new("expired token in cache", None))
as Box<dyn error::Error + Send>);
}
Err(e) => return Err(Box::new(e) as Box<dyn error::Error + Send>),
Ok(None) => {
return Err(Box::new(StringError::new("no token in cache", None))
as Box<dyn error::Error + Send>)
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<dyn Future<Item = Token, Error = Box<dyn error::Error + Send>> + Send>,
Err(_) => Box::new(req_token)
as Box<dyn Future<Item = Token, Error = Box<dyn error::Error + Send>> + Send>,
as Box<dyn Future<Item = Token, Error = RequestError> + Send>,
Err(_) => {
Box::new(req_token) as Box<dyn Future<Item = Token, Error = RequestError> + Send>
}
}))
}

View File

@@ -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<String>,
}
/// 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<String>),
/// 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<Utc>),
/// 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<String>),
/// 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<dyn Error + Send>),
}
impl From<hyper::Error> for RequestError {
@@ -41,12 +80,6 @@ impl From<hyper::Error> for RequestError {
}
}
impl From<hyper::http::Error> for RequestError {
fn from(error: hyper::http::Error) -> RequestError {
RequestError::HttpError(error)
}
}
impl From<JsonError> 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<dyn Future<Item = Token, Error = Box<dyn Error + Send>> + Send>
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
where
T: AsRef<str> + Ord + 'b,
I: Iterator<Item = &'b T>;