Tokens retention policy

This commit is contained in:
Tomasz Drwięga 2016-11-14 11:56:01 +01:00
parent 6957634ee7
commit 7f011afacb
5 changed files with 181 additions and 43 deletions

View File

@ -35,7 +35,7 @@ use params::{ResealPolicy, AccountsConfig, GasPricerConfig, MinerExtras};
use ethcore_logger::Config as LogConfig; use ethcore_logger::Config as LogConfig;
use dir::Directories; use dir::Directories;
use dapps::Configuration as DappsConfiguration; use dapps::Configuration as DappsConfiguration;
use signer::{Configuration as SignerConfiguration, SignerCommand}; use signer::{Configuration as SignerConfiguration};
use run::RunCmd; use run::RunCmd;
use blockchain::{BlockchainCmd, ImportBlockchain, ExportBlockchain, DataFormat}; use blockchain::{BlockchainCmd, ImportBlockchain, ExportBlockchain, DataFormat};
use presale::ImportWallet; use presale::ImportWallet;
@ -49,7 +49,7 @@ pub enum Cmd {
Account(AccountCmd), Account(AccountCmd),
ImportPresaleWallet(ImportWallet), ImportPresaleWallet(ImportWallet),
Blockchain(BlockchainCmd), Blockchain(BlockchainCmd),
SignerToken(SignerCommand), SignerToken(SignerConfiguration),
Snapshot(SnapshotCommand), Snapshot(SnapshotCommand),
Hash(Option<String>), Hash(Option<String>),
} }
@ -103,11 +103,7 @@ impl Configuration {
let cmd = if self.args.flag_version { let cmd = if self.args.flag_version {
Cmd::Version Cmd::Version
} else if self.args.cmd_signer && self.args.cmd_new_token { } else if self.args.cmd_signer && self.args.cmd_new_token {
Cmd::SignerToken(SignerCommand { Cmd::SignerToken(signer_conf)
path: dirs.signer,
signer_interface: signer_conf.interface,
signer_port: signer_conf.port,
})
} else if self.args.cmd_tools && self.args.cmd_hash { } else if self.args.cmd_tools && self.args.cmd_hash {
Cmd::Hash(self.args.arg_file) Cmd::Hash(self.args.arg_file)
} else if self.args.cmd_account { } else if self.args.cmd_account {
@ -692,7 +688,7 @@ mod tests {
use ethcore::miner::{MinerOptions, PrioritizationStrategy}; use ethcore::miner::{MinerOptions, PrioritizationStrategy};
use helpers::{replace_home, default_network_config}; use helpers::{replace_home, default_network_config};
use run::RunCmd; use run::RunCmd;
use signer::{Configuration as SignerConfiguration, SignerCommand}; use signer::{Configuration as SignerConfiguration};
use blockchain::{BlockchainCmd, ImportBlockchain, ExportBlockchain, DataFormat}; use blockchain::{BlockchainCmd, ImportBlockchain, ExportBlockchain, DataFormat};
use presale::ImportWallet; use presale::ImportWallet;
use account::{AccountCmd, NewAccount, ImportAccounts}; use account::{AccountCmd, NewAccount, ImportAccounts};
@ -829,8 +825,12 @@ mod tests {
let args = vec!["parity", "signer", "new-token"]; let args = vec!["parity", "signer", "new-token"];
let conf = parse(&args); let conf = parse(&args);
let expected = replace_home("$HOME/.parity/signer"); let expected = replace_home("$HOME/.parity/signer");
assert_eq!(conf.into_command().unwrap().cmd, Cmd::SignerToken(SignerCommand { assert_eq!(conf.into_command().unwrap().cmd, Cmd::SignerToken(SignerConfiguration {
path: expected, enabled: true,
signer_path: expected,
interface: "127.0.0.1".into(),
port: 8180,
skip_origin_validation: false,
})); }));
} }

View File

@ -48,7 +48,6 @@ use signer;
use modules; use modules;
use rpc_apis; use rpc_apis;
use rpc; use rpc;
use url;
// how often to take periodic snapshots. // how often to take periodic snapshots.
const SNAPSHOT_PERIOD: u64 = 10000; const SNAPSHOT_PERIOD: u64 = 10000;
@ -93,13 +92,26 @@ pub struct RunCmd {
pub check_seal: bool, pub check_seal: bool,
} }
pub fn open_ui(dapps_conf: &dapps::Configuration, signer_conf: &signer::Configuration) -> Result<(), String> {
if !dapps_conf.enabled {
return Err("Cannot use UI command with Dapps turned off.".into())
}
if !signer_conf.enabled {
return Err("Cannot use UI command with UI turned off.".into())
}
let token = try!(signer::generate_token_and_open_ui(signer_conf));
println!("{}", token);
Ok(())
}
pub fn execute(cmd: RunCmd, logger: Arc<RotatingLogger>) -> Result<(), String> { pub fn execute(cmd: RunCmd, logger: Arc<RotatingLogger>) -> Result<(), String> {
if cmd.ui && cmd.dapps_conf.enabled { if cmd.ui && cmd.dapps_conf.enabled {
// Check if Parity is already running // Check if Parity is already running
let addr = format!("{}:{}", cmd.dapps_conf.interface, cmd.dapps_conf.port); let addr = format!("{}:{}", cmd.dapps_conf.interface, cmd.dapps_conf.port);
if !TcpListener::bind(&addr as &str).is_ok() { if !TcpListener::bind(&addr as &str).is_ok() {
url::open(&format!("http://{}:{}/", cmd.dapps_conf.interface, cmd.dapps_conf.port)); return open_ui(&cmd.dapps_conf, &cmd.signer_conf);
return Ok(());
} }
} }
@ -309,7 +321,7 @@ pub fn execute(cmd: RunCmd, logger: Arc<RotatingLogger>) -> Result<(), String> {
}; };
// start signer server // start signer server
let signer_server = try!(signer::start(cmd.signer_conf, signer_deps)); let signer_server = try!(signer::start(cmd.signer_conf.clone(), signer_deps));
let informant = Arc::new(Informant::new( let informant = Arc::new(Informant::new(
service.client(), service.client(),
@ -363,10 +375,7 @@ pub fn execute(cmd: RunCmd, logger: Arc<RotatingLogger>) -> Result<(), String> {
// start ui // start ui
if cmd.ui { if cmd.ui {
if !cmd.dapps_conf.enabled { try!(open_ui(&cmd.dapps_conf, &cmd.signer_conf));
return Err("Cannot use UI command with Dapps turned off.".into())
}
url::open(&format!("http://{}:{}/", cmd.dapps_conf.interface, cmd.dapps_conf.port));
} }
// Handle exit // Handle exit

View File

@ -28,7 +28,7 @@ pub use ethcore_signer::Server as SignerServer;
const CODES_FILENAME: &'static str = "authcodes"; const CODES_FILENAME: &'static str = "authcodes";
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
pub struct Configuration { pub struct Configuration {
pub enabled: bool, pub enabled: bool,
pub port: u16, pub port: u16,
@ -69,16 +69,13 @@ fn codes_path(path: String) -> PathBuf {
p p
} }
#[derive(Debug, PartialEq)] pub fn execute(cmd: Configuration) -> Result<String, String> {
pub struct SignerCommand { generate_token_and_open_ui(&cmd)
pub path: String,
pub signer_interface: String,
pub signer_port: u16,
} }
pub fn execute(cmd: SignerCommand) -> Result<String, String> { pub fn generate_token_and_open_ui(conf: &Configuration) -> Result<String, String> {
let code = try!(generate_new_token(cmd.path).map_err(|err| format!("Error generating token: {:?}", err))); let code = try!(generate_new_token(conf.signer_path.clone()).map_err(|err| format!("Error generating token: {:?}", err)));
let auth_url = format!("http://{}:{}/#/auth?token={}", cmd.signer_interface, cmd.signer_port, code); let auth_url = format!("http://{}:{}/#/auth?token={}", conf.interface, conf.port, code);
// Open a browser // Open a browser
url::open(&auth_url); url::open(&auth_url);
// And print in to the console // And print in to the console
@ -96,6 +93,7 @@ Or use the code:
pub fn generate_new_token(path: String) -> io::Result<String> { pub fn generate_new_token(path: String) -> io::Result<String> {
let path = codes_path(path); let path = codes_path(path);
let mut codes = try!(signer::AuthCodes::from_file(&path)); let mut codes = try!(signer::AuthCodes::from_file(&path));
codes.clear_garbage();
let code = try!(codes.generate_new()); let code = try!(codes.generate_new());
try!(codes.to_file(&path)); try!(codes.to_file(&path));
trace!("New key code created: {}", Colour::White.bold().paint(&code[..])); trace!("New key code created: {}", Colour::White.bold().paint(&code[..]));

View File

@ -16,12 +16,10 @@
use rand::Rng; use rand::Rng;
use rand::os::OsRng; use rand::os::OsRng;
use std::io; use std::io::{self, Read, Write};
use std::io::{Read, Write};
use std::fs;
use std::path::Path; use std::path::Path;
use std::time; use std::{fs, time, mem};
use util::{H256, Hashable}; use util::{H256, Hashable, Itertools};
/// Providing current time in seconds /// Providing current time in seconds
pub trait TimeProvider { pub trait TimeProvider {
@ -47,12 +45,35 @@ impl TimeProvider for DefaultTimeProvider {
/// No of seconds the hash is valid /// No of seconds the hash is valid
const TIME_THRESHOLD: u64 = 7; const TIME_THRESHOLD: u64 = 7;
/// minimal length of hash
const TOKEN_LENGTH: usize = 16; const TOKEN_LENGTH: usize = 16;
/// special "initial" token used for authorization when there are no tokens yet.
const INITIAL_TOKEN: &'static str = "initial"; const INITIAL_TOKEN: &'static str = "initial";
/// Separator between fields in serialized tokens file.
const SEPARATOR: &'static str = ";";
/// Number of seconds to keep unused tokens.
const UNUSED_TOKEN_TIMEOUT: u64 = 3600 * 24; // a day
struct Code {
code: String,
/// Duration since unix_epoch
created_at: time::Duration,
/// Duration since unix_epoch
last_used_at: Option<time::Duration>,
}
fn decode_time(val: &str) -> Option<time::Duration> {
let time = val.parse::<u64>().ok();
time.map(time::Duration::from_secs)
}
fn encode_time(time: time::Duration) -> String {
format!("{}", time.as_secs())
}
/// Manages authorization codes for `SignerUIs` /// Manages authorization codes for `SignerUIs`
pub struct AuthCodes<T: TimeProvider = DefaultTimeProvider> { pub struct AuthCodes<T: TimeProvider = DefaultTimeProvider> {
codes: Vec<String>, codes: Vec<Code>,
now: T, now: T,
} }
@ -69,13 +90,32 @@ impl AuthCodes<DefaultTimeProvider> {
"".into() "".into()
} }
}; };
let time_provider = DefaultTimeProvider::default();
let codes = content.lines() let codes = content.lines()
.filter(|f| f.len() >= TOKEN_LENGTH) .filter_map(|line| {
.map(String::from) let mut parts = line.split(SEPARATOR);
let token = parts.next();
let created = parts.next();
let used = parts.next();
match token {
None => None,
Some(token) if token.len() < TOKEN_LENGTH => None,
Some(token) => {
Some(Code {
code: token.into(),
last_used_at: used.and_then(decode_time),
created_at: created.and_then(decode_time)
.unwrap_or_else(|| time::Duration::from_secs(time_provider.now())),
})
}
}
})
.collect(); .collect();
Ok(AuthCodes { Ok(AuthCodes {
codes: codes, codes: codes,
now: DefaultTimeProvider::default(), now: time_provider,
}) })
} }
@ -86,19 +126,30 @@ impl<T: TimeProvider> AuthCodes<T> {
/// Writes all `AuthCodes` to a disk. /// Writes all `AuthCodes` to a disk.
pub fn to_file(&self, file: &Path) -> io::Result<()> { pub fn to_file(&self, file: &Path) -> io::Result<()> {
let mut file = try!(fs::File::create(file)); let mut file = try!(fs::File::create(file));
let content = self.codes.join("\n"); let content = self.codes.iter().map(|code| {
let mut data = vec![code.code.clone(), encode_time(code.created_at.clone())];
if let Some(used_at) = code.last_used_at.clone() {
data.push(encode_time(used_at));
}
data.join(SEPARATOR)
}).join("\n");
file.write_all(content.as_bytes()) file.write_all(content.as_bytes())
} }
/// Creates a new `AuthCodes` store with given `TimeProvider`. /// Creates a new `AuthCodes` store with given `TimeProvider`.
pub fn new(codes: Vec<String>, now: T) -> Self { pub fn new(codes: Vec<String>, now: T) -> Self {
AuthCodes { AuthCodes {
codes: codes, codes: codes.into_iter().map(|code| Code {
code: code,
created_at: time::Duration::from_secs(now.now()),
last_used_at: None,
}).collect(),
now: now, now: now,
} }
} }
/// Checks if given hash is correct identifier of `SignerUI` /// Checks if given hash is correct authcode of `SignerUI`
/// Updates this hash last used field in case it's valid.
#[cfg_attr(feature="dev", allow(wrong_self_convention))] #[cfg_attr(feature="dev", allow(wrong_self_convention))]
pub fn is_valid(&mut self, hash: &H256, time: u64) -> bool { pub fn is_valid(&mut self, hash: &H256, time: u64) -> bool {
let now = self.now.now(); let now = self.now.now();
@ -121,8 +172,14 @@ impl<T: TimeProvider> AuthCodes<T> {
} }
// look for code // look for code
self.codes.iter() for mut code in &mut self.codes {
.any(|code| &as_token(code) == hash) if &as_token(&code.code) == hash {
code.last_used_at = Some(time::Duration::from_secs(now));
return true;
}
}
false
} }
/// Generates and returns a new code that can be used by `SignerUIs` /// Generates and returns a new code that can be used by `SignerUIs`
@ -135,7 +192,11 @@ impl<T: TimeProvider> AuthCodes<T> {
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("-"); .join("-");
trace!(target: "signer", "New authentication token generated."); trace!(target: "signer", "New authentication token generated.");
self.codes.push(code); self.codes.push(Code {
code: code,
created_at: time::Duration::from_secs(self.now.now()),
last_used_at: None,
});
Ok(readable_code) Ok(readable_code)
} }
@ -143,12 +204,31 @@ impl<T: TimeProvider> AuthCodes<T> {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.codes.is_empty() self.codes.is_empty()
} }
}
/// Removes old tokens that have not been used since creation.
pub fn clear_garbage(&mut self) {
let now = self.now.now();
let threshold = time::Duration::from_secs(now.saturating_sub(UNUSED_TOKEN_TIMEOUT));
let codes = mem::replace(&mut self.codes, Vec::new());
for code in codes {
// Skip codes that are old and were never used.
if code.last_used_at.is_none() && code.created_at <= threshold {
continue;
}
self.codes.push(code);
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use devtools;
use std::io::{Read, Write};
use std::{time, fs};
use std::cell::Cell;
use util::{H256, Hashable}; use util::{H256, Hashable};
use super::*; use super::*;
@ -217,6 +297,54 @@ mod tests {
assert_eq!(res2, false); assert_eq!(res2, false);
} }
#[test]
fn should_read_old_format_from_file() {
// given
let path = devtools::RandomTempPath::new();
let code = "23521352asdfasdfadf";
{
let mut file = fs::File::create(&path).unwrap();
file.write_all(b"a\n23521352asdfasdfadf\nb\n").unwrap();
}
// when
let mut authcodes = AuthCodes::from_file(&path).unwrap();
let time = time::UNIX_EPOCH.elapsed().unwrap().as_secs();
// then
assert!(authcodes.is_valid(&generate_hash(code, time), time), "Code should be read from file");
}
#[test]
fn should_remove_old_unused_tokens() {
// given
let path = devtools::RandomTempPath::new();
let code1 = "11111111asdfasdf111";
let code2 = "22222222asdfasdf222";
let code3 = "33333333asdfasdf333";
let time = Cell::new(100);
let mut codes = AuthCodes::new(vec![code1.into(), code2.into(), code3.into()], || time.get());
// `code2` should not be removed (we never remove tokens that were used)
codes.is_valid(&generate_hash(code2, time.get()), time.get());
// when
time.set(100 + 10_000_000);
// mark `code1` as used now
codes.is_valid(&generate_hash(code1, time.get()), time.get());
let new_code = codes.generate_new().unwrap().replace('-', "");
codes.clear_garbage();
codes.to_file(&path).unwrap();
// then
let mut content = String::new();
let mut file = fs::File::open(&path).unwrap();
file.read_to_string(&mut content).unwrap();
assert_eq!(content, format!("{};100;10000100\n{};100;100\n{};10000100", code1, code2, new_code));
}
} }

View File

@ -94,6 +94,9 @@ fn auth_is_valid(codes_path: &Path, protocols: ws::Result<Vec<&str>>) -> bool {
// Check if the code is valid // Check if the code is valid
AuthCodes::from_file(codes_path) AuthCodes::from_file(codes_path)
.map(|mut codes| { .map(|mut codes| {
// remove old tokens
codes.clear_garbage();
let res = codes.is_valid(&auth, time); let res = codes.is_valid(&auth, time);
// make sure to save back authcodes - it might have been modified // make sure to save back authcodes - it might have been modified
if let Err(_) = codes.to_file(codes_path) { if let Err(_) = codes.to_file(codes_path) {