use chrono::{DateTime, TimeZone, Utc};
use hyper;
use std::error::Error;
use std::fmt;
use std::io;
use std::str::FromStr;
use futures::prelude::*;
pub trait Flow {
fn type_id() -> FlowType;
}
#[derive(Deserialize, Debug)]
pub struct JsonError {
pub error: String,
pub error_description: Option<String>,
pub error_uri: Option<String>,
}
#[derive(Debug)]
pub enum RefreshResult {
Error(hyper::Error),
RefreshError(String, Option<String>),
Success(Token),
}
#[derive(Debug)]
pub enum PollError {
HttpError(hyper::Error),
Expired(DateTime<Utc>),
AccessDenied,
TimedOut,
Other(String),
}
#[derive(Debug)]
pub enum RequestError {
ClientError(hyper::Error),
InvalidClient,
InvalidScope(String),
NegativeServerResponse(String, Option<String>),
BadServerResponse(String),
JSONError(serde_json::error::Error),
UserError(String),
LowLevelError(io::Error),
Poll(PollError),
Refresh(RefreshResult),
Cache(Box<dyn Error + Send>),
}
impl From<hyper::Error> for RequestError {
fn from(error: hyper::Error) -> RequestError {
RequestError::ClientError(error)
}
}
impl From<JsonError> for RequestError {
fn from(value: JsonError) -> RequestError {
match &*value.error {
"invalid_client" => RequestError::InvalidClient,
"invalid_scope" => RequestError::InvalidScope(
value
.error_description
.unwrap_or("no description provided".to_string()),
),
_ => RequestError::NegativeServerResponse(value.error, value.error_description),
}
}
}
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::InvalidClient => "Invalid Client".fmt(f),
RequestError::InvalidScope(ref scope) => writeln!(f, "Invalid Scope: '{}'", scope),
RequestError::NegativeServerResponse(ref error, ref desc) => {
error.fmt(f)?;
if let &Some(ref desc) = desc {
write!(f, ": {}", desc)?;
}
"\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),
}
}
}
impl Error for RequestError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match *self {
RequestError::ClientError(ref err) => Some(err),
RequestError::LowLevelError(ref err) => Some(err),
RequestError::JSONError(ref err) => Some(err),
_ => None,
}
}
}
#[derive(Debug)]
pub struct StringError {
error: String,
}
impl fmt::Display for StringError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
self.description().fmt(f)
}
}
impl StringError {
pub fn new<S: AsRef<str>>(error: S, desc: Option<S>) -> StringError {
let mut error = error.as_ref().to_string();
if let Some(d) = desc {
error.push_str(": ");
error.push_str(d.as_ref());
}
StringError { error: error }
}
}
impl<'a> From<&'a dyn Error> for StringError {
fn from(err: &'a dyn Error) -> StringError {
StringError::new(err.description().to_string(), None)
}
}
impl From<String> for StringError {
fn from(value: String) -> StringError {
StringError::new(value, None)
}
}
impl Error for StringError {
fn description(&self) -> &str {
&self.error
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum TokenType {
Bearer,
}
impl AsRef<str> for TokenType {
fn as_ref(&self) -> &'static str {
match *self {
TokenType::Bearer => "Bearer",
}
}
}
impl FromStr for TokenType {
type Err = ();
fn from_str(s: &str) -> Result<TokenType, ()> {
match s {
"Bearer" => Ok(TokenType::Bearer),
_ => Err(()),
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct Scheme {
pub token_type: TokenType,
pub access_token: String,
}
impl std::convert::Into<hyper::header::HeaderValue> for Scheme {
fn into(self) -> hyper::header::HeaderValue {
hyper::header::HeaderValue::from_str(&format!(
"{} {}",
self.token_type.as_ref(),
self.access_token
))
.expect("Invalid Scheme header value")
}
}
impl FromStr for Scheme {
type Err = &'static str;
fn from_str(s: &str) -> Result<Scheme, &'static str> {
let parts: Vec<&str> = s.split(' ').collect();
if parts.len() != 2 {
return Err("Expected two parts: <token_type> <token>");
}
match <TokenType as FromStr>::from_str(parts[0]) {
Ok(t) => Ok(Scheme {
token_type: t,
access_token: parts[1].to_string(),
}),
Err(_) => Err("Couldn't parse token type"),
}
}
}
pub trait GetToken {
fn token<'b, I, T>(
&mut self,
scopes: I,
) -> Box<dyn Future<Item = Token, Error = RequestError> + Send>
where
T: AsRef<str> + Ord + 'b,
I: Iterator<Item = &'b T>;
fn api_key(&mut self) -> Option<String>;
fn application_secret(&self) -> ApplicationSecret;
}
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
pub struct Token {
pub access_token: String,
pub refresh_token: String,
pub token_type: String,
pub expires_in: Option<i64>,
pub expires_in_timestamp: Option<i64>,
}
impl Token {
pub fn expired(&self) -> bool {
if self.access_token.len() == 0 {
panic!("called expired() on unset token");
}
self.expiry_date() - chrono::Duration::minutes(1) <= Utc::now()
}
pub fn expiry_date(&self) -> DateTime<Utc> {
Utc.timestamp(
self.expires_in_timestamp
.expect("Tokens without an absolute expiry are invalid"),
0,
)
}
pub fn set_expiry_absolute(&mut self) -> &mut Token {
if self.expires_in_timestamp.is_some() {
assert!(self.expires_in.is_none());
return self;
}
self.expires_in_timestamp = Some(Utc::now().timestamp() + self.expires_in.unwrap());
self.expires_in = None;
self
}
}
#[derive(Clone)]
pub enum FlowType {
Device(String),
InstalledInteractive,
InstalledRedirect(u16),
}
#[derive(Deserialize, Serialize, Clone, Default)]
pub struct ApplicationSecret {
pub client_id: String,
pub client_secret: String,
pub token_uri: String,
pub auth_uri: String,
pub redirect_uris: Vec<String>,
pub project_id: Option<String>,
pub client_email: Option<String>,
pub auth_provider_x509_cert_url: Option<String>,
pub client_x509_cert_url: Option<String>,
}
#[derive(Deserialize, Serialize, Default)]
pub struct ConsoleApplicationSecret {
pub web: Option<ApplicationSecret>,
pub installed: Option<ApplicationSecret>,
}
#[cfg(test)]
pub mod tests {
use super::*;
use hyper;
pub const SECRET: &'static str =
"{\"installed\":{\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\
\"client_secret\":\"UqkDJd5RFwnHoiG5x5Rub8SI\",\"token_uri\":\"https://accounts.google.\
com/o/oauth2/token\",\"client_email\":\"\",\"redirect_uris\":[\"urn:ietf:wg:oauth:2.0:\
oob\",\"oob\"],\"client_x509_cert_url\":\"\",\"client_id\":\
\"14070749909-vgip2f1okm7bkvajhi9jugan6126io9v.apps.googleusercontent.com\",\
\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\"}}";
#[test]
fn console_secret() {
use serde_json as json;
match json::from_str::<ConsoleApplicationSecret>(SECRET) {
Ok(s) => assert!(s.installed.is_some() && s.web.is_none()),
Err(err) => panic!(err),
}
}
#[test]
fn schema() {
let s = Scheme {
token_type: TokenType::Bearer,
access_token: "foo".to_string(),
};
let mut headers = hyper::HeaderMap::new();
headers.insert(hyper::header::AUTHORIZATION, s.into());
assert_eq!(
format!("{:?}", headers),
"{\"authorization\": \"Bearer foo\"}".to_string()
);
}
#[test]
fn parse_schema() {
let auth = Scheme::from_str("Bearer foo").unwrap();
assert_eq!(auth.token_type, TokenType::Bearer);
assert_eq!(auth.access_token, "foo".to_string());
}
}