From 2cb5250e72f53bc020cad298b5689dcc88e6347f Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Fri, 15 Apr 2016 18:16:36 +0000 Subject: [PATCH 1/3] feat(storage): Implement DiskTokenStorage DiskTokenStorage is a TokenStorage that stores its tokens in a JSON file on disk. That file can be read in later, and the tokens in it reused. (The idea for a cache file is from here: https://developers.google.com/drive/v3/web/quickstart/go) --- src/helper.rs | 66 +---------------- src/lib.rs.in | 9 ++- src/storage.rs | 198 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 68 deletions(-) create mode 100644 src/storage.rs diff --git a/src/helper.rs b/src/helper.rs index 83f232e20..9d8578c2f 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -11,75 +11,12 @@ use std::convert::From; use common::{Token, FlowType, ApplicationSecret}; use device::{PollInformation, RequestError, DeviceFlow, PollError}; use refresh::{RefreshResult, RefreshFlow}; +use storage::{TokenStorage}; use chrono::{DateTime, UTC, Local}; use std::time::Duration; use hyper; -/// Implements a specialized storage to set and retrieve `Token` instances. -/// The `scope_hash` represents the signature of the scopes for which the given token -/// should be stored or retrieved. -/// For completeness, the underlying, sorted scopes are provided as well. They might be -/// useful for presentation to the user. -pub trait TokenStorage { - type Error: 'static + Error; - - /// If `token` is None, it is invalid or revoked and should be removed from storage. - /// Otherwise, it should be saved. - fn set(&mut self, scope_hash: u64, scopes: &Vec<&str>, token: Option) -> Result<(), Self::Error>; - /// A `None` result indicates that there is no token for the given scope_hash. - fn get(&self, scope_hash: u64, scopes: &Vec<&str>) -> Result, Self::Error>; -} - -/// A storage that remembers nothing. -#[derive(Default)] -pub struct NullStorage; -#[derive(Debug)] -pub struct NullError; - -impl Error for NullError { - fn description(&self) -> &str { - "NULL" - } -} - -impl fmt::Display for NullError { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - "NULL-ERROR".fmt(f) - } -} - -impl TokenStorage for NullStorage { - type Error = NullError; - fn set(&mut self, _: u64, _: &Vec<&str>, _: Option) -> Result<(), NullError> { Ok(()) } - fn get(&self, _: u64, _: &Vec<&str>) -> Result, NullError> { Ok(None) } -} - -/// A storage that remembers values for one session only. -#[derive(Default)] -pub struct MemoryStorage { - pub tokens: HashMap -} - -impl TokenStorage for MemoryStorage { - type Error = NullError; - - fn set(&mut self, scope_hash: u64, _: &Vec<&str>, token: Option) -> Result<(), NullError> { - match token { - Some(t) => self.tokens.insert(scope_hash, t), - None => self.tokens.remove(&scope_hash), - }; - Ok(()) - } - - fn get(&self, scope_hash: u64, _: &Vec<&str>) -> Result, NullError> { - match self.tokens.get(&scope_hash) { - Some(t) => Ok(Some(t.clone())), - None => Ok(None), - } - } -} - /// A generalized authenticator which will keep tokens valid and store them. /// /// It is the go-to helper to deal with any kind of supported authentication flow, @@ -477,6 +414,7 @@ mod tests { use super::super::device::tests::MockGoogleAuth; use super::super::common::tests::SECRET; use super::super::common::{ConsoleApplicationSecret}; + use storage::MemoryStorage; use std::default::Default; use hyper; diff --git a/src/lib.rs.in b/src/lib.rs.in index 17d510388..007d7f57b 100644 --- a/src/lib.rs.in +++ b/src/lib.rs.in @@ -12,13 +12,14 @@ extern crate mime; extern crate url; extern crate itertools; -mod device; -mod refresh; mod common; +mod device; mod helper; +mod refresh; +mod storage; pub use device::{DeviceFlow, PollInformation, PollError}; pub use refresh::{RefreshFlow, RefreshResult}; pub use common::{Token, FlowType, ApplicationSecret, ConsoleApplicationSecret, Scheme, TokenType}; -pub use helper::{TokenStorage, NullStorage, MemoryStorage, Authenticator, - AuthenticatorDelegate, Retry, DefaultAuthenticatorDelegate, GetToken}; +pub use helper::{Authenticator, AuthenticatorDelegate, Retry, DefaultAuthenticatorDelegate, GetToken}; +pub use storage::{TokenStorage, DiskTokenStorage, NullStorage, MemoryStorage}; diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 000000000..30afbbed2 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,198 @@ +/* + * partially (c) 2016 Google Inc. (Lewin Bormann, lewinb@google.com) + * + * See project root for licensing information. + */ + +extern crate serde_json; + +use std::collections::HashMap; +use std::hash::Hasher; +use std::error::Error; +use std::fmt; +use std::fs; +use std::io; +use std::io::{Read, Write}; + +use common::Token; + +/// Implements a specialized storage to set and retrieve `Token` instances. +/// The `scope_hash` represents the signature of the scopes for which the given token +/// should be stored or retrieved. +/// For completeness, the underlying, sorted scopes are provided as well. They might be +/// useful for presentation to the user. +pub trait TokenStorage { + type Error: 'static + Error; + + /// If `token` is None, it is invalid or revoked and should be removed from storage. + /// Otherwise, it should be saved. + fn set(&mut self, + scope_hash: u64, + scopes: &Vec<&str>, + token: Option) + -> Result<(), Self::Error>; + /// A `None` result indicates that there is no token for the given scope_hash. + fn get(&self, scope_hash: u64, scopes: &Vec<&str>) -> Result, Self::Error>; +} + +/// A storage that remembers nothing. +#[derive(Default)] +pub struct NullStorage; +#[derive(Debug)] +pub struct NullError; + +impl Error for NullError { + fn description(&self) -> &str { + "NULL" + } +} + +impl fmt::Display for NullError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + "NULL-ERROR".fmt(f) + } +} + +impl TokenStorage for NullStorage { + type Error = NullError; + fn set(&mut self, _: u64, _: &Vec<&str>, _: Option) -> Result<(), NullError> { + Ok(()) + } + fn get(&self, _: u64, _: &Vec<&str>) -> Result, NullError> { + Ok(None) + } +} + +/// A storage that remembers values for one session only. +#[derive(Default)] +pub struct MemoryStorage { + pub tokens: HashMap, +} + +impl TokenStorage for MemoryStorage { + type Error = NullError; + + fn set(&mut self, + scope_hash: u64, + _: &Vec<&str>, + token: Option) + -> Result<(), NullError> { + match token { + Some(t) => self.tokens.insert(scope_hash, t), + None => self.tokens.remove(&scope_hash), + }; + Ok(()) + } + + fn get(&self, scope_hash: u64, _: &Vec<&str>) -> Result, NullError> { + match self.tokens.get(&scope_hash) { + Some(t) => Ok(Some(t.clone())), + None => Ok(None), + } + } +} + +/// A single stored token. +#[derive(Serialize, Deserialize)] +struct JSONToken { + pub hash: u64, + pub token: Token, +} + +/// List of tokens in a JSON object +#[derive(Serialize, Deserialize)] +struct JSONTokens { + pub tokens: Vec, +} + +/// Serializes tokens to a JSON file on disk. +#[derive(Default)] +pub struct DiskTokenStorage { + location: String, + tokens: HashMap, +} + +impl DiskTokenStorage { + pub fn new(location: &String) -> DiskTokenStorage { + use std::fs; + let mut dts = DiskTokenStorage { + location: location.clone(), + tokens: HashMap::new(), + }; + + // best-effort + let _ = dts.load_from_file(); + dts + } + + fn load_from_file(&mut self) -> Result<(), io::Error> { + let mut f = try!(fs::OpenOptions::new().read(true).open(&self.location)); + let mut contents = String::new(); + + match f.read_to_string(&mut contents) { + Result::Err(e) => return Result::Err(e), + Result::Ok(_sz) => (), + } + + let tokens: JSONTokens; + + match serde_json::from_str(&contents) { + Result::Err(e) => return Result::Err(io::Error::new(io::ErrorKind::InvalidData, e)), + Result::Ok(t) => tokens = t, + } + + for t in tokens.tokens { + self.tokens.insert(t.hash, t.token); + } + return Result::Ok(()); + } + + pub fn dump_to_file(&mut self) -> Result<(), io::Error> { + let mut jsontokens = JSONTokens { tokens: Vec::new() }; + + for (hash, token) in self.tokens.iter() { + jsontokens.tokens.push(JSONToken { + hash: *hash, + token: token.clone(), + }); + } + + let serialized;; + + match serde_json::to_string(&jsontokens) { + Result::Err(e) => return Result::Err(io::Error::new(io::ErrorKind::InvalidData, e)), + Result::Ok(s) => serialized = s, + } + + let mut f = try!(fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&self.location)); + f.write(serialized.as_ref()).map(|_| ()) + } +} + +impl TokenStorage for DiskTokenStorage { + type Error = io::Error; + fn set(&mut self, + scope_hash: u64, + _: &Vec<&str>, + token: Option) + -> Result<(), Self::Error> { + match token { + None => { + self.tokens.remove(&scope_hash); + () + } + Some(t) => { + self.tokens.insert(scope_hash, t.clone()); + () + } + } + self.dump_to_file() + } + fn get(&self, scope_hash: u64, _: &Vec<&str>) -> Result, Self::Error> { + Result::Ok(self.tokens.get(&scope_hash).map(|tok| tok.clone())) + } +} From ae5e94bf7b15318a15b2885fa0d4b5fded8bcadd Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Mon, 25 Apr 2016 19:14:53 +0200 Subject: [PATCH 2/3] fix(DiskTokenStorage): make DiskTokenStorage::new() return a Result<> --- src/storage.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/storage.rs b/src/storage.rs index 30afbbed2..2fc8dff64 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -113,7 +113,7 @@ pub struct DiskTokenStorage { } impl DiskTokenStorage { - pub fn new(location: &String) -> DiskTokenStorage { + pub fn new(location: &String) -> Result { use std::fs; let mut dts = DiskTokenStorage { location: location.clone(), @@ -121,8 +121,17 @@ impl DiskTokenStorage { }; // best-effort - let _ = dts.load_from_file(); - dts + let read_result = dts.load_from_file(); + + match read_result { + Result::Ok(()) => Result::Ok(dts), + Result::Err(e) => { + match e.kind() { + io::ErrorKind::NotFound => Result::Ok(dts), // File not found; ignore and create new one + _ => Result::Err(e), // e.g. PermissionDenied + } + } + } } fn load_from_file(&mut self) -> Result<(), io::Error> { From 95ecb5c3887bd51367724da4e9c862aa1c3154a9 Mon Sep 17 00:00:00 2001 From: Lewin Bormann Date: Mon, 25 Apr 2016 19:39:11 +0200 Subject: [PATCH 3/3] fix(dependencies): use url = 0.5; the bump to 1.0 broke us Fixes #23. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c5d362aae..d904ade69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ build = "src/build.rs" chrono = ">= 0.2" log = ">= 0.3" mime = ">= 0.1" -url = ">= 0.5" +url = "= 0.5" hyper = ">= 0.8.0" itertools = ">= 0.4" serde = "0.6.0"