mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-01-08 12:06:59 +01:00
refactor(errors): Move almost everything to RequestError.
This is nicer than stupid Box<dyn Error+Send> everywhere.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
68
src/types.rs
68
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<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>;
|
||||
|
||||
Reference in New Issue
Block a user