Merge pull request #3379 from ethcore/new-token
Signer new-token generates a link and opens browser
This commit is contained in:
commit
25950345db
@ -25,6 +25,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import injectTapEventPlugin from 'react-tap-event-plugin';
|
import injectTapEventPlugin from 'react-tap-event-plugin';
|
||||||
import { createHashHistory } from 'history';
|
import { createHashHistory } from 'history';
|
||||||
import { Redirect, Router, Route, useRouterHistory } from 'react-router';
|
import { Redirect, Router, Route, useRouterHistory } from 'react-router';
|
||||||
|
import qs from 'querystring';
|
||||||
|
|
||||||
import SecureApi from './secureApi';
|
import SecureApi from './secureApi';
|
||||||
import ContractInstances from './contracts';
|
import ContractInstances from './contracts';
|
||||||
@ -45,6 +46,7 @@ import './index.html';
|
|||||||
|
|
||||||
injectTapEventPlugin();
|
injectTapEventPlugin();
|
||||||
|
|
||||||
|
const AUTH_HASH = '#/auth?';
|
||||||
const parityUrl = process.env.PARITY_URL ||
|
const parityUrl = process.env.PARITY_URL ||
|
||||||
(
|
(
|
||||||
process.env.NODE_ENV === 'production'
|
process.env.NODE_ENV === 'production'
|
||||||
@ -52,7 +54,12 @@ const parityUrl = process.env.PARITY_URL ||
|
|||||||
: '127.0.0.1:8180'
|
: '127.0.0.1:8180'
|
||||||
);
|
);
|
||||||
|
|
||||||
const api = new SecureApi(`ws://${parityUrl}`);
|
let token = null;
|
||||||
|
if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
|
||||||
|
token = qs.parse(window.location.hash.substr(AUTH_HASH.length)).token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new SecureApi(`ws://${parityUrl}`, token);
|
||||||
ContractInstances.create(api);
|
ContractInstances.create(api);
|
||||||
|
|
||||||
const store = initStore(api);
|
const store = initStore(api);
|
||||||
@ -67,6 +74,7 @@ ReactDOM.render(
|
|||||||
<ContextProvider api={ api } muiTheme={ muiTheme } store={ store }>
|
<ContextProvider api={ api } muiTheme={ muiTheme } store={ store }>
|
||||||
<Router className={ styles.reset } history={ routerHistory }>
|
<Router className={ styles.reset } history={ routerHistory }>
|
||||||
<Redirect from='/' to='/accounts' />
|
<Redirect from='/' to='/accounts' />
|
||||||
|
<Redirect from='/auth' to='/accounts' query={ {} } />
|
||||||
<Redirect from='/settings' to='/settings/views' />
|
<Redirect from='/settings' to='/settings/views' />
|
||||||
<Route path='/' component={ Application }>
|
<Route path='/' component={ Application }>
|
||||||
<Route path='accounts' component={ Accounts } />
|
<Route path='accounts' component={ Accounts } />
|
||||||
|
@ -19,12 +19,13 @@ import Api from './api';
|
|||||||
const sysuiToken = window.localStorage.getItem('sysuiToken');
|
const sysuiToken = window.localStorage.getItem('sysuiToken');
|
||||||
|
|
||||||
export default class SecureApi extends Api {
|
export default class SecureApi extends Api {
|
||||||
constructor (url) {
|
constructor (url, nextToken) {
|
||||||
super(new Api.Transport.Ws(url, sysuiToken));
|
super(new Api.Transport.Ws(url, sysuiToken));
|
||||||
|
|
||||||
this._isConnecting = true;
|
this._isConnecting = true;
|
||||||
this._connectState = sysuiToken === 'initial' ? 1 : 0;
|
this._connectState = sysuiToken === 'initial' ? 1 : 0;
|
||||||
this._needsToken = false;
|
this._needsToken = false;
|
||||||
|
this._nextToken = nextToken;
|
||||||
this._dappsPort = 8080;
|
this._dappsPort = 8080;
|
||||||
this._dappsInterface = null;
|
this._dappsInterface = null;
|
||||||
this._signerPort = 8180;
|
this._signerPort = 8180;
|
||||||
@ -57,7 +58,11 @@ export default class SecureApi extends Api {
|
|||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
return this.connectSuccess();
|
return this.connectSuccess();
|
||||||
} else if (lastError) {
|
} else if (lastError) {
|
||||||
this.updateToken('initial', 1);
|
const nextToken = this._nextToken || 'initial';
|
||||||
|
const nextState = this._nextToken ? 0 : 1;
|
||||||
|
|
||||||
|
this._nextToken = null;
|
||||||
|
this.updateToken(nextToken, nextState);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -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,9 +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
|
|
||||||
})
|
|
||||||
} 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 {
|
||||||
@ -690,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};
|
||||||
@ -827,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,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,13 +93,29 @@ 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_url(signer_conf));
|
||||||
|
// Open a browser
|
||||||
|
url::open(&token.url);
|
||||||
|
// Print a message
|
||||||
|
println!("{}", token.message);
|
||||||
|
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(());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +328,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(),
|
||||||
@ -366,10 +382,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
|
||||||
|
@ -27,7 +27,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,
|
||||||
@ -53,6 +53,12 @@ pub struct Dependencies {
|
|||||||
pub apis: Arc<rpc_apis::Dependencies>,
|
pub apis: Arc<rpc_apis::Dependencies>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct NewToken {
|
||||||
|
pub token: String,
|
||||||
|
pub url: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start(conf: Configuration, deps: Dependencies) -> Result<Option<SignerServer>, String> {
|
pub fn start(conf: Configuration, deps: Dependencies) -> Result<Option<SignerServer>, String> {
|
||||||
if !conf.enabled {
|
if !conf.enabled {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@ -68,20 +74,33 @@ fn codes_path(path: String) -> PathBuf {
|
|||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
pub fn execute(cmd: Configuration) -> Result<String, String> {
|
||||||
pub struct SignerCommand {
|
Ok(try!(generate_token_and_url(&cmd)).message)
|
||||||
pub path: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(cmd: SignerCommand) -> Result<String, String> {
|
pub fn generate_token_and_url(conf: &Configuration) -> Result<NewToken, String> {
|
||||||
generate_new_token(cmd.path)
|
let code = try!(generate_new_token(conf.signer_path.clone()).map_err(|err| format!("Error generating token: {:?}", err)));
|
||||||
.map(|code| format!("This key code will authorise your System Signer UI: {}", Colour::White.bold().paint(code)))
|
let auth_url = format!("http://{}:{}/#/auth?token={}", conf.interface, conf.port, code);
|
||||||
.map_err(|err| format!("Error generating token: {:?}", err))
|
// And print in to the console
|
||||||
|
Ok(NewToken {
|
||||||
|
token: code.clone(),
|
||||||
|
url: auth_url.clone(),
|
||||||
|
message: format!(
|
||||||
|
r#"
|
||||||
|
Open: {}
|
||||||
|
to authorize your browser.
|
||||||
|
Or use the generated token:
|
||||||
|
{}"#,
|
||||||
|
Colour::White.bold().paint(auth_url),
|
||||||
|
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[..]));
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user