grin/servers/tests/framework/mod.rs
hashmap 4a6cae0fe6
Allow TLS for Wallet APIs (#1626)
* Allow TLS for Wallet APIs

This PR adds an optional support of TLS for wallet APIs. Only PKCS12 format is supported, will address .pem support in next PR and provide some documentation.
Address #1425
2018-10-02 09:49:36 +02:00

611 lines
18 KiB
Rust

// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
extern crate grin_api as api;
extern crate grin_chain as chain;
extern crate grin_core as core;
extern crate grin_keychain as keychain;
extern crate grin_p2p as p2p;
extern crate grin_servers as servers;
extern crate grin_util as util;
extern crate grin_wallet as wallet;
extern crate blake2_rfc as blake2;
use std::default::Default;
use std::ops::Deref;
use std::sync::{Arc, Mutex};
use std::{fs, thread, time};
use wallet::{FileWallet, HTTPWalletClient, WalletConfig};
/// Just removes all results from previous runs
pub fn clean_all_output(test_name_dir: &str) {
let target_dir = format!("target/tmp/{}", test_name_dir);
if let Err(e) = fs::remove_dir_all(target_dir) {
println!("can't remove output from previous test :{}, may be ok", e);
}
}
/// Errors that can be returned by LocalServerContainer
#[derive(Debug)]
#[allow(dead_code)]
pub enum Error {
Internal(String),
Argument(String),
NotFound,
}
/// All-in-one server configuration struct, for convenience
///
#[derive(Clone)]
pub struct LocalServerContainerConfig {
// user friendly name for the server, also denotes what dir
// the data files will appear in
pub name: String,
// Base IP address
pub base_addr: String,
// Port the server (p2p) is running on
pub p2p_server_port: u16,
// Port the API server is running on
pub api_server_port: u16,
// Port the wallet server is running on
pub wallet_port: u16,
// Whether we're going to mine
pub start_miner: bool,
// time in millis by which to artificially slow down the mining loop
// in this container
pub miner_slowdown_in_millis: u64,
// Whether we're going to run a wallet as well,
// can use same server instance as a validating node for convenience
pub start_wallet: bool,
// address of a server to use as a seed
pub seed_addr: String,
// keep track of whether this server is supposed to be seeding
pub is_seeding: bool,
// Whether to burn mining rewards
pub burn_mining_rewards: bool,
// full address to send coinbase rewards to
pub coinbase_wallet_address: String,
// When running a wallet, the address to check inputs and send
// finalised transactions to,
pub wallet_validating_node_url: String,
}
/// Default server config
impl Default for LocalServerContainerConfig {
fn default() -> LocalServerContainerConfig {
LocalServerContainerConfig {
name: String::from("test_host"),
base_addr: String::from("127.0.0.1"),
api_server_port: 13413,
p2p_server_port: 13414,
wallet_port: 13415,
seed_addr: String::from(""),
is_seeding: false,
start_miner: false,
start_wallet: false,
burn_mining_rewards: false,
coinbase_wallet_address: String::from(""),
wallet_validating_node_url: String::from(""),
miner_slowdown_in_millis: 0,
}
}
}
/// A top-level container to hold everything that might be running
/// on a server, i.e. server, wallet in send or receive mode
pub struct LocalServerContainer {
// Configuration
config: LocalServerContainerConfig,
// Structure of references to the
// internal server data
pub p2p_server_stats: Option<servers::ServerStats>,
// The API server instance
api_server: Option<api::ApiServer>,
// whether the server is running
pub server_is_running: bool,
// Whether the server is mining
pub server_is_mining: bool,
// Whether the server is also running a wallet
// Not used if running wallet without server
pub wallet_is_running: bool,
// the list of peers to connect to
pub peer_list: Vec<String>,
// base directory for the server instance
pub working_dir: String,
// Wallet configuration
pub wallet_config: WalletConfig,
}
impl LocalServerContainer {
/// Create a new local server container with defaults, with the given name
/// all related files will be created in the directory
/// target/tmp/{name}
pub fn new(config: LocalServerContainerConfig) -> Result<LocalServerContainer, Error> {
let working_dir = format!("target/tmp/{}", config.name);
let mut wallet_config = WalletConfig::default();
wallet_config.api_listen_port = config.wallet_port;
wallet_config.check_node_api_http_addr = config.wallet_validating_node_url.clone();
wallet_config.data_file_dir = working_dir.clone();
Ok(LocalServerContainer {
config: config,
p2p_server_stats: None,
api_server: None,
server_is_running: false,
server_is_mining: false,
wallet_is_running: false,
working_dir: working_dir,
peer_list: Vec::new(),
wallet_config: wallet_config,
})
}
pub fn run_server(&mut self, duration_in_seconds: u64) -> servers::Server {
let api_addr = format!("{}:{}", self.config.base_addr, self.config.api_server_port);
let mut seeding_type = p2p::Seeding::None;
let mut seeds = Vec::new();
if self.config.seed_addr.len() > 0 {
seeding_type = p2p::Seeding::List;
seeds = vec![self.config.seed_addr.to_string()];
}
let s = servers::Server::new(servers::ServerConfig {
api_http_addr: api_addr,
api_secret_path: None,
db_root: format!("{}/.grin", self.working_dir),
p2p_config: p2p::P2PConfig {
port: self.config.p2p_server_port,
seeds: Some(seeds),
seeding_type: seeding_type,
..p2p::P2PConfig::default()
},
chain_type: core::global::ChainTypes::AutomatedTesting,
skip_sync_wait: Some(true),
stratum_mining_config: None,
..Default::default()
}).unwrap();
self.p2p_server_stats = Some(s.get_server_stats().unwrap());
let mut wallet_url = None;
if self.config.start_wallet == true {
self.run_wallet(duration_in_seconds + 5);
// give a second to start wallet before continuing
thread::sleep(time::Duration::from_millis(1000));
wallet_url = Some(format!(
"http://{}:{}",
self.config.base_addr, self.config.wallet_port
));
}
if self.config.start_miner == true {
println!(
"starting test Miner on port {}",
self.config.p2p_server_port
);
s.start_test_miner(wallet_url);
}
for p in &mut self.peer_list {
println!("{} connecting to peer: {}", self.config.p2p_server_port, p);
let _ = s.connect_peer(p.parse().unwrap());
}
if self.wallet_is_running {
self.stop_wallet();
}
s
}
/// Starts a wallet daemon to receive and returns the
/// listening server url
pub fn run_wallet(&mut self, _duration_in_mills: u64) {
// URL on which to start the wallet listener (i.e. api server)
let _url = format!("{}:{}", self.config.base_addr, self.config.wallet_port);
// Just use the name of the server for a seed for now
let seed = format!("{}", self.config.name);
let _seed = blake2::blake2b::blake2b(32, &[], seed.as_bytes());
println!(
"Starting the Grin wallet receiving daemon on {} ",
self.config.wallet_port
);
self.wallet_config = WalletConfig::default();
self.wallet_config.api_listen_port = self.config.wallet_port;
self.wallet_config.check_node_api_http_addr =
self.config.wallet_validating_node_url.clone();
self.wallet_config.data_file_dir = self.working_dir.clone();
let _ = fs::create_dir_all(self.wallet_config.clone().data_file_dir);
let r = wallet::WalletSeed::init_file(&self.wallet_config);
let client = HTTPWalletClient::new(&self.wallet_config.check_node_api_http_addr, None);
if let Err(e) = r {
//panic!("Error initializing wallet seed: {}", e);
}
let wallet: FileWallet<HTTPWalletClient, keychain::ExtKeychain> =
FileWallet::new(self.wallet_config.clone(), "", client).unwrap_or_else(|e| {
panic!(
"Error creating wallet: {:?} Config: {:?}",
e, self.wallet_config
)
});
wallet::controller::foreign_listener(
Box::new(wallet),
&self.wallet_config.api_listen_addr(),
None,
).unwrap_or_else(|e| {
panic!(
"Error creating wallet listener: {:?} Config: {:?}",
e, self.wallet_config
)
});
self.wallet_is_running = true;
}
pub fn get_wallet_seed(config: &WalletConfig) -> wallet::WalletSeed {
let _ = fs::create_dir_all(config.clone().data_file_dir);
wallet::WalletSeed::init_file(config).unwrap();
let wallet_seed =
wallet::WalletSeed::from_file(config).expect("Failed to read wallet seed file.");
wallet_seed
}
pub fn get_wallet_info(
config: &WalletConfig,
wallet_seed: &wallet::WalletSeed,
) -> wallet::WalletInfo {
let keychain: keychain::ExtKeychain = wallet_seed
.derive_keychain("")
.expect("Failed to derive keychain from seed file and passphrase.");
let client = HTTPWalletClient::new(&config.check_node_api_http_addr, None);
let mut wallet = FileWallet::new(config.clone(), "", client)
.unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config));
wallet.keychain = Some(keychain);
let _ = wallet::libwallet::internal::updater::refresh_outputs(&mut wallet);
wallet::libwallet::internal::updater::retrieve_info(&mut wallet).unwrap()
}
pub fn send_amount_to(
config: &WalletConfig,
amount: &str,
minimum_confirmations: u64,
selection_strategy: &str,
dest: &str,
fluff: bool,
) {
let amount = core::core::amount_from_hr_string(amount)
.expect("Could not parse amount as a number with optional decimal point.");
let wallet_seed =
wallet::WalletSeed::from_file(config).expect("Failed to read wallet seed file.");
let keychain: keychain::ExtKeychain = wallet_seed
.derive_keychain("")
.expect("Failed to derive keychain from seed file and passphrase.");
let client = HTTPWalletClient::new(&config.check_node_api_http_addr, None);
let max_outputs = 500;
let change_outputs = 1;
let mut wallet = FileWallet::new(config.clone(), "", client)
.unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config));
wallet.keychain = Some(keychain);
let _ =
wallet::controller::owner_single_use(Arc::new(Mutex::new(Box::new(wallet))), |api| {
let result = api.issue_send_tx(
amount,
minimum_confirmations,
dest,
max_outputs,
change_outputs,
selection_strategy == "all",
);
match result {
Ok(_) => println!(
"Tx sent: {} grin to {} (strategy '{}')",
core::core::amount_to_hr_string(amount, false),
dest,
selection_strategy,
),
Err(e) => {
println!("Tx not sent to {}: {:?}", dest, e);
}
};
Ok(())
}).unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config));
}
/// Stops the running wallet server
pub fn stop_wallet(&mut self) {
println!("Stop wallet!");
let api_server = self.api_server.as_mut().unwrap();
api_server.stop();
}
/// Adds a peer to this server to connect to upon running
pub fn add_peer(&mut self, addr: String) {
self.peer_list.push(addr);
}
}
/// Configuration values for container pool
pub struct LocalServerContainerPoolConfig {
// Base name to append to all the servers in this pool
pub base_name: String,
// Base http address for all of the servers in this pool
pub base_http_addr: String,
// Base port server for all of the servers in this pool
// Increment the number by 1 for each new server
pub base_p2p_port: u16,
// Base api port for all of the servers in this pool
// Increment this number by 1 for each new server
pub base_api_port: u16,
// Base wallet port for this server
//
pub base_wallet_port: u16,
// How long the servers in the pool are going to run
pub run_length_in_seconds: u64,
}
/// Default server config
///
impl Default for LocalServerContainerPoolConfig {
fn default() -> LocalServerContainerPoolConfig {
LocalServerContainerPoolConfig {
base_name: String::from("test_pool"),
base_http_addr: String::from("127.0.0.1"),
base_p2p_port: 10000,
base_api_port: 11000,
base_wallet_port: 12000,
run_length_in_seconds: 30,
}
}
}
/// A convenience pool for running many servers simultaneously
/// without necessarily having to configure each one manually
pub struct LocalServerContainerPool {
// configuration
pub config: LocalServerContainerPoolConfig,
// keep ahold of all the created servers thread-safely
server_containers: Vec<LocalServerContainer>,
// Keep track of what the last ports a server was opened on
next_p2p_port: u16,
next_api_port: u16,
next_wallet_port: u16,
// keep track of whether a seed exists, and pause a bit if so
is_seeding: bool,
}
impl LocalServerContainerPool {
pub fn new(config: LocalServerContainerPoolConfig) -> LocalServerContainerPool {
(LocalServerContainerPool {
next_api_port: config.base_api_port,
next_p2p_port: config.base_p2p_port,
next_wallet_port: config.base_wallet_port,
config: config,
server_containers: Vec::new(),
is_seeding: false,
})
}
/// adds a single server on the next available port
/// overriding passed-in values as necessary. Config object is an OUT value
/// with
/// ports/addresses filled in
///
pub fn create_server(&mut self, server_config: &mut LocalServerContainerConfig) {
// If we're calling it this way, need to override these
server_config.p2p_server_port = self.next_p2p_port;
server_config.api_server_port = self.next_api_port;
server_config.wallet_port = self.next_wallet_port;
server_config.name = String::from(format!(
"{}/{}-{}",
self.config.base_name, self.config.base_name, server_config.p2p_server_port
));
// Use self as coinbase wallet
server_config.coinbase_wallet_address = String::from(format!(
"http://{}:{}",
server_config.base_addr, server_config.wallet_port
));
self.next_p2p_port += 1;
self.next_api_port += 1;
self.next_wallet_port += 1;
if server_config.is_seeding {
self.is_seeding = true;
}
let _server_address = format!(
"{}:{}",
server_config.base_addr, server_config.p2p_server_port
);
let server_container = LocalServerContainer::new(server_config.clone()).unwrap();
// self.server_containers.push(server_arc);
// Create a future that runs the server for however many seconds
// collect them all and run them in the run_all_servers
let _run_time = self.config.run_length_in_seconds;
self.server_containers.push(server_container);
}
/// adds n servers, ready to run
///
///
#[allow(dead_code)]
pub fn create_servers(&mut self, number: u16) {
for _ in 0..number {
// self.create_server();
}
}
/// runs all servers, and returns a vector of references to the servers
/// once they've all been run
///
pub fn run_all_servers(self) -> Arc<Mutex<Vec<servers::Server>>> {
let run_length = self.config.run_length_in_seconds;
let mut handles = vec![];
// return handles to all of the servers, wrapped in mutexes, handles, etc
let return_containers = Arc::new(Mutex::new(Vec::new()));
let is_seeding = self.is_seeding.clone();
for mut s in self.server_containers {
let return_container_ref = return_containers.clone();
let handle = thread::spawn(move || {
if is_seeding && !s.config.is_seeding {
// there's a seed and we're not it, so hang around longer and give the seed
// a chance to start
thread::sleep(time::Duration::from_millis(2000));
}
let server_ref = s.run_server(run_length);
return_container_ref.lock().unwrap().push(server_ref);
});
// Not a big fan of sleeping hack here, but there appears to be a
// concurrency issue when creating files in rocksdb that causes
// failure if we don't pause a bit before starting the next server
thread::sleep(time::Duration::from_millis(500));
handles.push(handle);
}
for handle in handles {
match handle.join() {
Ok(_) => {}
Err(e) => {
println!("Error starting server thread: {:?}", e);
panic!(e);
}
}
}
// return a much simplified version of the results
return_containers.clone()
}
pub fn connect_all_peers(&mut self) {
// just pull out all currently active servers, build a list,
// and feed into all servers
let mut server_addresses: Vec<String> = Vec::new();
for s in &self.server_containers {
let server_address = format!("{}:{}", s.config.base_addr, s.config.p2p_server_port);
server_addresses.push(server_address);
}
for a in server_addresses {
for s in &mut self.server_containers {
if format!("{}:{}", s.config.base_addr, s.config.p2p_server_port) != a {
s.add_peer(a.clone());
}
}
}
}
}
pub fn stop_all_servers(servers: Arc<Mutex<Vec<servers::Server>>>) {
let locked_servs = servers.lock().unwrap();
for s in locked_servs.deref() {
s.stop();
}
}
/// Create and return a ServerConfig
pub fn config(n: u16, test_name_dir: &str, seed_n: u16) -> servers::ServerConfig {
servers::ServerConfig {
api_http_addr: format!("127.0.0.1:{}", 20000 + n),
api_secret_path: None,
db_root: format!("target/tmp/{}/grin-sync-{}", test_name_dir, n),
p2p_config: p2p::P2PConfig {
port: 10000 + n,
seeding_type: p2p::Seeding::List,
seeds: Some(vec![format!("127.0.0.1:{}", 10000 + seed_n)]),
..p2p::P2PConfig::default()
},
chain_type: core::global::ChainTypes::AutomatedTesting,
archive_mode: Some(true),
skip_sync_wait: Some(true),
..Default::default()
}
}
/// return stratum mining config
pub fn stratum_config() -> servers::common::types::StratumServerConfig {
servers::common::types::StratumServerConfig {
enable_stratum_server: Some(true),
stratum_server_addr: Some(String::from("127.0.0.1:13416")),
attempt_time_per_block: 60,
minimum_share_difficulty: 1,
wallet_listener_url: String::from("http://127.0.0.1:13415"),
burn_reward: false,
}
}