2019-01-07 11:33:07 +01:00
|
|
|
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
|
|
|
|
// This file is part of Parity Ethereum.
|
2016-06-20 10:06:49 +02:00
|
|
|
|
2019-01-07 11:33:07 +01:00
|
|
|
// Parity Ethereum is free software: you can redistribute it and/or modify
|
2016-06-20 10:06:49 +02:00
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
2019-01-07 11:33:07 +01:00
|
|
|
// Parity Ethereum is distributed in the hope that it will be useful,
|
2016-06-20 10:06:49 +02:00
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
|
|
|
|
// You should have received a copy of the GNU General Public License
|
2019-01-07 11:33:07 +01:00
|
|
|
// along with Parity Ethereum. If not, see <http://www.gnu.org/licenses/>.
|
2016-06-20 10:06:49 +02:00
|
|
|
|
2017-12-24 09:34:43 +01:00
|
|
|
extern crate dir;
|
2016-06-20 00:10:34 +02:00
|
|
|
extern crate docopt;
|
2017-12-01 09:40:07 +01:00
|
|
|
extern crate ethstore;
|
|
|
|
extern crate num_cpus;
|
|
|
|
extern crate panic_hook;
|
|
|
|
extern crate parking_lot;
|
|
|
|
extern crate rustc_hex;
|
2017-07-06 11:36:15 +02:00
|
|
|
extern crate serde;
|
2017-12-01 09:40:07 +01:00
|
|
|
|
2019-01-03 14:07:27 +01:00
|
|
|
extern crate env_logger;
|
|
|
|
|
2017-07-06 11:36:15 +02:00
|
|
|
#[macro_use]
|
|
|
|
extern crate serde_derive;
|
2016-06-20 00:10:34 +02:00
|
|
|
|
2017-12-01 09:40:07 +01:00
|
|
|
use std::collections::VecDeque;
|
2016-06-22 17:02:40 +02:00
|
|
|
use std::io::Read;
|
2017-12-01 09:40:07 +01:00
|
|
|
use std::{env, process, fs, fmt};
|
|
|
|
|
2016-06-20 00:10:34 +02:00
|
|
|
use docopt::Docopt;
|
2017-12-24 09:34:43 +01:00
|
|
|
use ethstore::accounts_dir::{KeyDirectory, RootDiskDirectory};
|
2018-06-22 15:09:15 +02:00
|
|
|
use ethstore::ethkey::{Address, Password};
|
2017-12-01 09:40:07 +01:00
|
|
|
use ethstore::{EthStore, SimpleSecretStore, SecretStore, import_accounts, PresaleWallet, SecretVaultRef, StoreAccountRef};
|
|
|
|
|
|
|
|
mod crack;
|
2016-06-20 00:10:34 +02:00
|
|
|
|
|
|
|
pub const USAGE: &'static str = r#"
|
2018-08-30 19:57:27 +02:00
|
|
|
Parity Ethereum key management tool.
|
2019-01-23 10:26:36 +01:00
|
|
|
Copyright 2015-2019 Parity Technologies (UK) Ltd.
|
2016-06-20 00:10:34 +02:00
|
|
|
|
|
|
|
Usage:
|
2017-02-16 17:42:01 +01:00
|
|
|
ethstore insert <secret> <password> [--dir DIR] [--vault VAULT] [--vault-pwd VAULTPWD]
|
|
|
|
ethstore change-pwd <address> <old-pwd> <new-pwd> [--dir DIR] [--vault VAULT] [--vault-pwd VAULTPWD]
|
|
|
|
ethstore list [--dir DIR] [--vault VAULT] [--vault-pwd VAULTPWD]
|
2019-01-03 14:07:27 +01:00
|
|
|
ethstore import [<password>] [--src DIR] [--dir DIR]
|
2017-02-16 17:42:01 +01:00
|
|
|
ethstore import-wallet <path> <password> [--dir DIR] [--vault VAULT] [--vault-pwd VAULTPWD]
|
2017-12-01 09:40:07 +01:00
|
|
|
ethstore find-wallet-pass <path> <password>
|
2017-02-16 17:42:01 +01:00
|
|
|
ethstore remove <address> <password> [--dir DIR] [--vault VAULT] [--vault-pwd VAULTPWD]
|
|
|
|
ethstore sign <address> <password> <message> [--dir DIR] [--vault VAULT] [--vault-pwd VAULTPWD]
|
|
|
|
ethstore public <address> <password> [--dir DIR] [--vault VAULT] [--vault-pwd VAULTPWD]
|
|
|
|
ethstore list-vaults [--dir DIR]
|
|
|
|
ethstore create-vault <vault> <password> [--dir DIR]
|
|
|
|
ethstore change-vault-pwd <vault> <old-pwd> <new-pwd> [--dir DIR]
|
|
|
|
ethstore move-to-vault <address> <vault> <password> [--dir DIR] [--vault VAULT] [--vault-pwd VAULTPWD]
|
|
|
|
ethstore move-from-vault <address> <vault> <password> [--dir DIR]
|
2016-06-20 00:10:34 +02:00
|
|
|
ethstore [-h | --help]
|
|
|
|
|
|
|
|
Options:
|
2017-02-16 17:42:01 +01:00
|
|
|
-h, --help Display this message and exit.
|
|
|
|
--dir DIR Specify the secret store directory. It may be either
|
2017-03-23 13:23:03 +01:00
|
|
|
parity, parity-(chain), geth, geth-test
|
2017-02-16 17:42:01 +01:00
|
|
|
or a path [default: parity].
|
|
|
|
--vault VAULT Specify vault to use in this operation.
|
|
|
|
--vault-pwd VAULTPWD Specify vault password to use in this operation. Please note
|
|
|
|
that this option is required when vault option is set.
|
|
|
|
Otherwise it is ignored.
|
|
|
|
--src DIR Specify import source. It may be either
|
2018-08-30 19:57:27 +02:00
|
|
|
parity, parity-(chain), geth, geth-test
|
2017-02-16 17:42:01 +01:00
|
|
|
or a path [default: geth].
|
2016-06-20 00:10:34 +02:00
|
|
|
|
|
|
|
Commands:
|
|
|
|
insert Save account with password.
|
|
|
|
change-pwd Change password.
|
|
|
|
list List accounts.
|
|
|
|
import Import accounts from src.
|
2016-06-21 15:04:36 +02:00
|
|
|
import-wallet Import presale wallet.
|
2017-12-01 09:40:07 +01:00
|
|
|
find-wallet-pass Tries to open a wallet with list of passwords given.
|
2016-06-20 00:10:34 +02:00
|
|
|
remove Remove account.
|
|
|
|
sign Sign message.
|
2016-10-15 14:44:08 +02:00
|
|
|
public Displays public key for an address.
|
2017-02-16 17:42:01 +01:00
|
|
|
list-vaults List vaults.
|
|
|
|
create-vault Create new vault.
|
|
|
|
change-vault-pwd Change vault password.
|
|
|
|
move-to-vault Move account to vault from another vault/root directory.
|
|
|
|
move-from-vault Move account to root directory from given vault.
|
2016-06-20 00:10:34 +02:00
|
|
|
"#;
|
|
|
|
|
2017-07-06 11:36:15 +02:00
|
|
|
#[derive(Debug, Deserialize)]
|
2016-06-20 00:10:34 +02:00
|
|
|
struct Args {
|
|
|
|
cmd_insert: bool,
|
|
|
|
cmd_change_pwd: bool,
|
|
|
|
cmd_list: bool,
|
|
|
|
cmd_import: bool,
|
2016-06-21 15:04:36 +02:00
|
|
|
cmd_import_wallet: bool,
|
2017-12-01 09:40:07 +01:00
|
|
|
cmd_find_wallet_pass: bool,
|
2016-06-20 00:10:34 +02:00
|
|
|
cmd_remove: bool,
|
|
|
|
cmd_sign: bool,
|
2016-10-15 14:44:08 +02:00
|
|
|
cmd_public: bool,
|
2017-02-16 17:42:01 +01:00
|
|
|
cmd_list_vaults: bool,
|
|
|
|
cmd_create_vault: bool,
|
|
|
|
cmd_change_vault_pwd: bool,
|
|
|
|
cmd_move_to_vault: bool,
|
|
|
|
cmd_move_from_vault: bool,
|
2016-06-20 00:10:34 +02:00
|
|
|
arg_secret: String,
|
|
|
|
arg_password: String,
|
|
|
|
arg_old_pwd: String,
|
|
|
|
arg_new_pwd: String,
|
|
|
|
arg_address: String,
|
|
|
|
arg_message: String,
|
2016-06-21 15:04:36 +02:00
|
|
|
arg_path: String,
|
2017-02-16 17:42:01 +01:00
|
|
|
arg_vault: String,
|
2016-06-20 00:10:34 +02:00
|
|
|
flag_src: String,
|
|
|
|
flag_dir: String,
|
2017-02-16 17:42:01 +01:00
|
|
|
flag_vault: String,
|
|
|
|
flag_vault_pwd: String,
|
2016-06-20 00:10:34 +02:00
|
|
|
}
|
|
|
|
|
2017-07-06 11:36:15 +02:00
|
|
|
enum Error {
|
|
|
|
Ethstore(ethstore::Error),
|
|
|
|
Docopt(docopt::Error),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<ethstore::Error> for Error {
|
|
|
|
fn from(err: ethstore::Error) -> Self {
|
|
|
|
Error::Ethstore(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<docopt::Error> for Error {
|
|
|
|
fn from(err: docopt::Error) -> Self {
|
|
|
|
Error::Docopt(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for Error {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
match *self {
|
|
|
|
Error::Ethstore(ref err) => fmt::Display::fmt(err, f),
|
|
|
|
Error::Docopt(ref err) => fmt::Display::fmt(err, f),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-20 00:10:34 +02:00
|
|
|
fn main() {
|
2018-07-02 11:53:50 +02:00
|
|
|
panic_hook::set_abort();
|
2019-01-03 14:07:27 +01:00
|
|
|
if env::var("RUST_LOG").is_err() {
|
|
|
|
env::set_var("RUST_LOG", "warn")
|
|
|
|
}
|
|
|
|
env_logger::try_init().expect("Logger initialized only once.");
|
2017-07-03 07:31:29 +02:00
|
|
|
|
2016-06-20 00:10:34 +02:00
|
|
|
match execute(env::args()) {
|
|
|
|
Ok(result) => println!("{}", result),
|
2018-06-11 20:38:01 +02:00
|
|
|
Err(Error::Docopt(ref e)) => e.exit(),
|
2016-06-20 00:10:34 +02:00
|
|
|
Err(err) => {
|
2019-01-03 14:07:27 +01:00
|
|
|
eprintln!("{}", err);
|
2016-06-20 00:10:34 +02:00
|
|
|
process::exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-02 10:55:31 +02:00
|
|
|
fn key_dir(location: &str, password: Option<Password>) -> Result<Box<dyn KeyDirectory>, Error> {
|
2019-01-03 14:07:27 +01:00
|
|
|
let dir: RootDiskDirectory = match location {
|
|
|
|
"geth" => RootDiskDirectory::create(dir::geth(false))?,
|
|
|
|
"geth-test" => RootDiskDirectory::create(dir::geth(true))?,
|
2017-03-23 13:23:03 +01:00
|
|
|
path if path.starts_with("parity") => {
|
|
|
|
let chain = path.split('-').nth(1).unwrap_or("ethereum");
|
2017-12-24 09:34:43 +01:00
|
|
|
let path = dir::parity(chain);
|
2019-01-03 14:07:27 +01:00
|
|
|
RootDiskDirectory::create(path)?
|
2017-03-23 13:23:03 +01:00
|
|
|
},
|
2019-01-03 14:07:27 +01:00
|
|
|
path => RootDiskDirectory::create(path)?,
|
2016-06-20 00:10:34 +02:00
|
|
|
};
|
|
|
|
|
2019-01-03 14:07:27 +01:00
|
|
|
Ok(Box::new(dir.with_password(password)))
|
2016-06-20 00:10:34 +02:00
|
|
|
}
|
|
|
|
|
2017-02-16 17:42:01 +01:00
|
|
|
fn open_args_vault(store: &EthStore, args: &Args) -> Result<SecretVaultRef, Error> {
|
|
|
|
if args.flag_vault.is_empty() {
|
|
|
|
return Ok(SecretVaultRef::Root);
|
|
|
|
}
|
|
|
|
|
|
|
|
let vault_pwd = load_password(&args.flag_vault_pwd)?;
|
|
|
|
store.open_vault(&args.flag_vault, &vault_pwd)?;
|
|
|
|
Ok(SecretVaultRef::Vault(args.flag_vault.clone()))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn open_args_vault_account(store: &EthStore, address: Address, args: &Args) -> Result<StoreAccountRef, Error> {
|
|
|
|
match open_args_vault(store, args)? {
|
|
|
|
SecretVaultRef::Root => Ok(StoreAccountRef::root(address)),
|
|
|
|
SecretVaultRef::Vault(name) => Ok(StoreAccountRef::vault(&name, address)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-20 00:10:34 +02:00
|
|
|
fn format_accounts(accounts: &[Address]) -> String {
|
|
|
|
accounts.iter()
|
|
|
|
.enumerate()
|
2018-04-02 13:12:52 +02:00
|
|
|
.map(|(i, a)| format!("{:2}: 0x{:x}", i, a))
|
2016-06-20 00:10:34 +02:00
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join("\n")
|
|
|
|
}
|
|
|
|
|
2017-02-16 17:42:01 +01:00
|
|
|
fn format_vaults(vaults: &[String]) -> String {
|
|
|
|
vaults.join("\n")
|
|
|
|
}
|
|
|
|
|
2018-06-22 15:09:15 +02:00
|
|
|
fn load_password(path: &str) -> Result<Password, Error> {
|
2019-01-03 14:07:27 +01:00
|
|
|
let mut file = fs::File::open(path).map_err(|e| ethstore::Error::Custom(format!("Error opening password file '{}': {}", path, e)))?;
|
2016-06-22 17:02:40 +02:00
|
|
|
let mut password = String::new();
|
2019-01-03 14:07:27 +01:00
|
|
|
file.read_to_string(&mut password).map_err(|e| ethstore::Error::Custom(format!("Error reading password file '{}': {}", path, e)))?;
|
2016-06-22 17:02:40 +02:00
|
|
|
// drop EOF
|
|
|
|
let _ = password.pop();
|
2018-06-22 15:09:15 +02:00
|
|
|
Ok(password.into())
|
2016-06-22 17:02:40 +02:00
|
|
|
}
|
|
|
|
|
2016-06-20 00:10:34 +02:00
|
|
|
fn execute<S, I>(command: I) -> Result<String, Error> where I: IntoIterator<Item=S>, S: AsRef<str> {
|
|
|
|
let args: Args = Docopt::new(USAGE)
|
2017-07-06 11:36:15 +02:00
|
|
|
.and_then(|d| d.argv(command).deserialize())?;
|
2016-06-20 00:10:34 +02:00
|
|
|
|
2019-01-03 14:07:27 +01:00
|
|
|
let store = EthStore::open(key_dir(&args.flag_dir, None)?)?;
|
2016-06-20 00:10:34 +02:00
|
|
|
|
|
|
|
return if args.cmd_insert {
|
2017-07-06 11:36:15 +02:00
|
|
|
let secret = args.arg_secret.parse().map_err(|_| ethstore::Error::InvalidSecret)?;
|
2016-12-27 12:53:56 +01:00
|
|
|
let password = load_password(&args.arg_password)?;
|
2017-02-16 17:42:01 +01:00
|
|
|
let vault_ref = open_args_vault(&store, &args)?;
|
2017-11-14 12:33:05 +01:00
|
|
|
let account_ref = store.insert_account(vault_ref, secret, &password)?;
|
2018-04-02 13:12:52 +02:00
|
|
|
Ok(format!("0x{:x}", account_ref.address))
|
2016-06-20 00:10:34 +02:00
|
|
|
} else if args.cmd_change_pwd {
|
2017-07-06 11:36:15 +02:00
|
|
|
let address = args.arg_address.parse().map_err(|_| ethstore::Error::InvalidAccount)?;
|
2016-12-27 12:53:56 +01:00
|
|
|
let old_pwd = load_password(&args.arg_old_pwd)?;
|
|
|
|
let new_pwd = load_password(&args.arg_new_pwd)?;
|
2017-02-16 17:42:01 +01:00
|
|
|
let account_ref = open_args_vault_account(&store, address, &args)?;
|
|
|
|
let ok = store.change_password(&account_ref, &old_pwd, &new_pwd).is_ok();
|
2016-06-20 00:10:34 +02:00
|
|
|
Ok(format!("{}", ok))
|
|
|
|
} else if args.cmd_list {
|
2017-02-16 17:42:01 +01:00
|
|
|
let vault_ref = open_args_vault(&store, &args)?;
|
2016-12-27 12:53:56 +01:00
|
|
|
let accounts = store.accounts()?;
|
2017-02-16 17:42:01 +01:00
|
|
|
let accounts: Vec<_> = accounts
|
|
|
|
.into_iter()
|
|
|
|
.filter(|a| &a.vault == &vault_ref)
|
|
|
|
.map(|a| a.address)
|
|
|
|
.collect();
|
2016-06-20 00:10:34 +02:00
|
|
|
Ok(format_accounts(&accounts))
|
|
|
|
} else if args.cmd_import {
|
2019-01-03 14:07:27 +01:00
|
|
|
let password = match args.arg_password.as_ref() {
|
|
|
|
"" => None,
|
|
|
|
_ => Some(load_password(&args.arg_password)?)
|
|
|
|
};
|
|
|
|
let src = key_dir(&args.flag_src, password)?;
|
|
|
|
let dst = key_dir(&args.flag_dir, None)?;
|
|
|
|
|
2016-12-27 12:53:56 +01:00
|
|
|
let accounts = import_accounts(&*src, &*dst)?;
|
2016-06-20 00:10:34 +02:00
|
|
|
Ok(format_accounts(&accounts))
|
2016-06-21 15:04:36 +02:00
|
|
|
} else if args.cmd_import_wallet {
|
2016-12-27 12:53:56 +01:00
|
|
|
let wallet = PresaleWallet::open(&args.arg_path)?;
|
|
|
|
let password = load_password(&args.arg_password)?;
|
|
|
|
let kp = wallet.decrypt(&password)?;
|
2017-02-16 17:42:01 +01:00
|
|
|
let vault_ref = open_args_vault(&store, &args)?;
|
2017-11-14 12:33:05 +01:00
|
|
|
let account_ref = store.insert_account(vault_ref, kp.secret().clone(), &password)?;
|
2018-04-02 13:12:52 +02:00
|
|
|
Ok(format!("0x{:x}", account_ref.address))
|
2017-12-01 09:40:07 +01:00
|
|
|
} else if args.cmd_find_wallet_pass {
|
|
|
|
let passwords = load_password(&args.arg_password)?;
|
2018-06-22 15:09:15 +02:00
|
|
|
let passwords = passwords.as_str().lines().map(|line| str::to_owned(line).into()).collect::<VecDeque<_>>();
|
2017-12-01 09:40:07 +01:00
|
|
|
crack::run(passwords, &args.arg_path)?;
|
|
|
|
Ok(format!("Password not found."))
|
2016-06-20 00:10:34 +02:00
|
|
|
} else if args.cmd_remove {
|
2017-07-06 11:36:15 +02:00
|
|
|
let address = args.arg_address.parse().map_err(|_| ethstore::Error::InvalidAccount)?;
|
2016-12-27 12:53:56 +01:00
|
|
|
let password = load_password(&args.arg_password)?;
|
2017-02-16 17:42:01 +01:00
|
|
|
let account_ref = open_args_vault_account(&store, address, &args)?;
|
|
|
|
let ok = store.remove_account(&account_ref, &password).is_ok();
|
2016-06-20 00:10:34 +02:00
|
|
|
Ok(format!("{}", ok))
|
|
|
|
} else if args.cmd_sign {
|
2017-07-06 11:36:15 +02:00
|
|
|
let address = args.arg_address.parse().map_err(|_| ethstore::Error::InvalidAccount)?;
|
|
|
|
let message = args.arg_message.parse().map_err(|_| ethstore::Error::InvalidMessage)?;
|
2016-12-27 12:53:56 +01:00
|
|
|
let password = load_password(&args.arg_password)?;
|
2017-02-16 17:42:01 +01:00
|
|
|
let account_ref = open_args_vault_account(&store, address, &args)?;
|
|
|
|
let signature = store.sign(&account_ref, &password, &message)?;
|
2018-03-01 14:20:11 +01:00
|
|
|
Ok(format!("0x{}", signature))
|
2016-10-15 14:44:08 +02:00
|
|
|
} else if args.cmd_public {
|
2017-07-06 11:36:15 +02:00
|
|
|
let address = args.arg_address.parse().map_err(|_| ethstore::Error::InvalidAccount)?;
|
2016-12-27 12:53:56 +01:00
|
|
|
let password = load_password(&args.arg_password)?;
|
2017-02-16 17:42:01 +01:00
|
|
|
let account_ref = open_args_vault_account(&store, address, &args)?;
|
|
|
|
let public = store.public(&account_ref, &password)?;
|
2018-04-02 13:12:52 +02:00
|
|
|
Ok(format!("0x{:x}", public))
|
2017-02-16 17:42:01 +01:00
|
|
|
} else if args.cmd_list_vaults {
|
|
|
|
let vaults = store.list_vaults()?;
|
|
|
|
Ok(format_vaults(&vaults))
|
|
|
|
} else if args.cmd_create_vault {
|
|
|
|
let password = load_password(&args.arg_password)?;
|
|
|
|
store.create_vault(&args.arg_vault, &password)?;
|
|
|
|
Ok("OK".to_owned())
|
|
|
|
} else if args.cmd_change_vault_pwd {
|
|
|
|
let old_pwd = load_password(&args.arg_old_pwd)?;
|
|
|
|
let new_pwd = load_password(&args.arg_new_pwd)?;
|
|
|
|
store.open_vault(&args.arg_vault, &old_pwd)?;
|
|
|
|
store.change_vault_password(&args.arg_vault, &new_pwd)?;
|
|
|
|
Ok("OK".to_owned())
|
|
|
|
} else if args.cmd_move_to_vault {
|
2017-07-06 11:36:15 +02:00
|
|
|
let address = args.arg_address.parse().map_err(|_| ethstore::Error::InvalidAccount)?;
|
2017-02-16 17:42:01 +01:00
|
|
|
let password = load_password(&args.arg_password)?;
|
|
|
|
let account_ref = open_args_vault_account(&store, address, &args)?;
|
|
|
|
store.open_vault(&args.arg_vault, &password)?;
|
|
|
|
store.change_account_vault(SecretVaultRef::Vault(args.arg_vault), account_ref)?;
|
|
|
|
Ok("OK".to_owned())
|
|
|
|
} else if args.cmd_move_from_vault {
|
2017-07-06 11:36:15 +02:00
|
|
|
let address = args.arg_address.parse().map_err(|_| ethstore::Error::InvalidAccount)?;
|
2017-02-16 17:42:01 +01:00
|
|
|
let password = load_password(&args.arg_password)?;
|
|
|
|
store.open_vault(&args.arg_vault, &password)?;
|
|
|
|
store.change_account_vault(SecretVaultRef::Root, StoreAccountRef::vault(&args.arg_vault, address))?;
|
|
|
|
Ok("OK".to_owned())
|
2016-06-20 00:10:34 +02:00
|
|
|
} else {
|
2016-08-23 19:28:21 +02:00
|
|
|
Ok(format!("{}", USAGE))
|
2016-06-20 00:10:34 +02:00
|
|
|
}
|
|
|
|
}
|