use crate::node::{Error, Result};
use crate::routing::NetworkConfig;
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeSet,
io::{self},
net::SocketAddr,
path::PathBuf,
time::Duration,
};
use structopt::StructOpt;
use tokio::{
fs::{self, File},
io::AsyncWriteExt,
};
use tracing::{debug, warn, Level};
const CONFIG_FILE: &str = "node.config";
const CONNECTION_INFO_FILE: &str = "node_connection_info.config";
const DEFAULT_ROOT_DIR_NAME: &str = "root_dir";
const DEFAULT_MAX_CAPACITY: u64 = 2 * 1024 * 1024 * 1024;
#[derive(Default, Clone, Debug, Serialize, Deserialize, StructOpt)]
#[structopt(rename_all = "kebab-case", bin_name = "sn_node")]
#[structopt(global_settings = &[structopt::clap::AppSettings::ColoredHelp])]
pub struct Config {
#[structopt(short, long, parse(try_from_str))]
pub wallet_id: Option<String>,
#[structopt(short, long)]
pub max_capacity: Option<u64>,
#[structopt(short, long, parse(from_os_str))]
pub root_dir: Option<PathBuf>,
#[structopt(short, long, parse(from_occurrences))]
pub verbose: u8,
#[structopt(long)]
pub completions: Option<String>,
#[structopt(long)]
pub log_dir: Option<PathBuf>,
#[structopt(long)]
pub update: bool,
#[structopt(long)]
pub update_only: bool,
#[structopt(short, long)]
pub json_logs: bool,
#[structopt(long)]
pub resource_logs: bool,
#[structopt(long)]
pub clear_data: bool,
#[structopt(long)]
pub first: bool,
#[structopt(long)]
pub local_addr: Option<SocketAddr>,
#[structopt(long, parse(try_from_str = parse_public_addr))]
pub public_addr: Option<SocketAddr>,
#[structopt(long)]
pub skip_auto_port_forwarding: bool,
#[structopt(
short,
long,
default_value = "[]",
parse(try_from_str = serde_json::from_str)
)]
pub hard_coded_contacts: BTreeSet<SocketAddr>,
#[structopt(long)]
pub genesis_key: Option<String>,
#[structopt(long)]
pub max_msg_size_allowed: Option<u32>,
#[structopt(long)]
pub idle_timeout_msec: Option<u64>,
#[structopt(long)]
pub keep_alive_interval_msec: Option<u32>,
#[structopt(long)]
pub upnp_lease_duration: Option<u32>,
#[structopt(skip)]
#[allow(missing_docs)]
pub network_config: NetworkConfig,
}
impl Config {
pub async fn new() -> Result<Self, Error> {
let mut config = Config::default();
let mut command_line_args = Config::from_args();
command_line_args.validate().map_err(Error::Configuration)?;
if command_line_args.hard_coded_contacts.is_empty() {
debug!("Using node connection config file as no hard coded contacts were passed in");
if let Ok((_, info)) = read_conn_info_from_file().await {
command_line_args.hard_coded_contacts = info;
}
}
if command_line_args.genesis_key.is_none() {
debug!("Using node connection config file as no genesis key was passed in");
if let Ok((genesis_key, _)) = read_conn_info_from_file().await {
command_line_args.genesis_key = Some(genesis_key);
}
}
config.merge(command_line_args);
config.clear_data_from_disk().await.unwrap_or_else(|_| {
tracing::error!("Error deleting data file from disk");
});
info!("Node config to be used: {:?}", config);
Ok(config)
}
fn validate(&self) -> Result<(), String> {
if let Some(local_addr) = self.local_addr {
if local_addr.ip().is_loopback() && self.public_addr.is_some() {
return Err(
"Cannot specify --public-addr when --local-addr uses a loopback IP. \
When local-addr uses a loopback IP, the node will never be reachable publicly. \
You can drop public-addr if this is a local-only node, or change local-addr to \
a public or unspecified IP."
.to_string(),
);
}
}
let local_ip_unspecified = self
.local_addr
.map(|addr| addr.ip().is_unspecified())
.unwrap_or(true);
if local_ip_unspecified && self.first && self.public_addr.is_none() {
return Err("Must specify public address for --first node. \
The first node cannot query its public address from peers, so one must be \
specifed. This can be specified with --public-addr, or by setting a concrete IP \
for --local-addr."
.to_string());
}
Ok(())
}
fn merge(&mut self, config: Config) {
if let Some(wallet_id) = config.wallet_id() {
self.wallet_id = Some(wallet_id.clone());
}
if let Some(max_capacity) = &config.max_capacity {
self.max_capacity = Some(*max_capacity);
}
if let Some(root_dir) = &config.root_dir {
self.root_dir = Some(root_dir.clone());
}
self.json_logs = config.json_logs;
self.resource_logs = config.resource_logs;
if config.verbose > 0 {
self.verbose = config.verbose;
}
if let Some(completions) = &config.completions {
self.completions = Some(completions.clone());
}
if let Some(log_dir) = &config.log_dir {
self.log_dir = Some(log_dir.clone());
}
self.update = config.update || self.update;
self.update_only = config.update_only || self.update_only;
self.clear_data = config.clear_data || self.clear_data;
self.first = config.first || self.first;
if let Some(local_addr) = config.local_addr {
self.local_addr = Some(local_addr);
}
if let Some(public_addr) = config.public_addr {
self.public_addr = config.public_addr;
self.network_config.external_port = Some(public_addr.port());
self.network_config.external_ip = Some(public_addr.ip());
}
self.network_config.forward_port = !config.skip_auto_port_forwarding;
if !config.hard_coded_contacts.is_empty() {
self.hard_coded_contacts = config.hard_coded_contacts;
}
if config.genesis_key.is_some() {
self.genesis_key = config.genesis_key;
}
if let Some(max_msg_size) = config.max_msg_size_allowed {
self.max_msg_size_allowed = Some(max_msg_size);
}
if let Some(idle_timeout) = config.idle_timeout_msec {
self.idle_timeout_msec = Some(idle_timeout);
}
if let Some(keep_alive) = config.keep_alive_interval_msec {
self.keep_alive_interval_msec = Some(keep_alive);
}
if let Some(upnp_lease_duration) = config.upnp_lease_duration {
self.network_config.upnp_lease_duration =
Some(Duration::from_millis(upnp_lease_duration as u64));
}
}
pub fn wallet_id(&self) -> Option<&String> {
self.wallet_id.as_ref()
}
pub fn is_first(&self) -> bool {
self.first
}
pub fn max_capacity(&self) -> u64 {
self.max_capacity.unwrap_or(DEFAULT_MAX_CAPACITY)
}
pub fn root_dir(&self) -> Result<PathBuf> {
Ok(match &self.root_dir {
Some(root_dir) => root_dir.clone(),
None => project_dirs()?.join(DEFAULT_ROOT_DIR_NAME),
})
}
pub fn set_root_dir<P: Into<PathBuf>>(&mut self, path: P) {
self.root_dir = Some(path.into())
}
pub fn set_log_dir<P: Into<PathBuf>>(&mut self, path: P) {
self.log_dir = Some(path.into())
}
pub fn verbose(&self) -> Level {
match self.verbose {
0 => Level::ERROR,
1 => Level::WARN,
2 => Level::INFO,
3 => Level::DEBUG,
_ => Level::TRACE,
}
}
pub fn network_config(&self) -> &NetworkConfig {
&self.network_config
}
pub fn set_network_config(&mut self, config: NetworkConfig) {
self.network_config = config;
}
pub fn completions(&self) -> &Option<String> {
&self.completions
}
pub fn log_dir(&self) -> &Option<PathBuf> {
&self.log_dir
}
pub fn update(&self) -> bool {
self.update
}
pub fn update_only(&self) -> bool {
self.update_only
}
async fn clear_data_from_disk(&self) -> Result<()> {
if self.clear_data {
let path = project_dirs()?.join(self.root_dir()?);
if path.exists() {
fs::remove_dir_all(&path).await?;
}
}
Ok(())
}
#[allow(unused)]
async fn read_from_file() -> Result<Option<Config>> {
let path = project_dirs()?.join(CONFIG_FILE);
match fs::read(path.clone()).await {
Ok(content) => {
debug!("Reading settings from {}", path.display());
serde_json::from_slice(&content).map_err(|err| {
warn!(
"Could not parse content of config file '{:?}': {:?}",
path, err
);
err.into()
})
}
Err(error) => {
if error.kind() == std::io::ErrorKind::NotFound {
debug!("No config file available at {:?}", path);
Ok(None)
} else {
Err(error.into())
}
}
}
}
pub async fn write_to_disk(&self) -> Result<()> {
write_file(CONFIG_FILE, self).await
}
}
fn parse_public_addr(public_addr: &str) -> Result<SocketAddr, String> {
let public_addr: SocketAddr = public_addr.parse().map_err(|err| format!("{}", err))?;
if public_addr.ip().is_unspecified() {
return Err("Cannot use unspecified IP for public address. \
You can drop this option to query the public IP from a peer instead."
.to_string());
}
if public_addr.ip().is_loopback() {
return Err("Cannot use loopback IP for public address. \
You can drop this option for a local-only network."
.to_string());
}
if public_addr.port() == 0 {
return Err("Cannot use unspecified port for public address. \
You must specify the concrete port on which the node will be reachable."
.to_string());
}
Ok(public_addr)
}
pub async fn set_connection_info(genesis_key: bls::PublicKey, contact: SocketAddr) -> Result<()> {
let genesis_key_hex = hex::encode(genesis_key.to_bytes());
write_file(CONNECTION_INFO_FILE, &(genesis_key_hex, vec![contact])).await
}
pub async fn add_connection_info(contact: SocketAddr) -> Result<()> {
let (genesis_key_hex, mut bootstrap_nodes) = read_conn_info_from_file().await?;
let _prev = bootstrap_nodes.insert(contact);
write_file(CONNECTION_INFO_FILE, &(genesis_key_hex, bootstrap_nodes)).await
}
async fn read_conn_info_from_file() -> Result<(String, BTreeSet<SocketAddr>)> {
let path = project_dirs()?.join(CONNECTION_INFO_FILE);
match fs::read(&path).await {
Ok(content) => {
debug!("Reading connection info from {}", path.display());
let config = serde_json::from_slice(&content)?;
Ok(config)
}
Err(error) => {
if error.kind() == std::io::ErrorKind::NotFound {
debug!("No connection info file available at {}", path.display());
}
Err(error.into())
}
}
}
async fn write_file<T: ?Sized>(file: &str, config: &T) -> Result<()>
where
T: Serialize,
{
let project_dirs = project_dirs()?;
fs::create_dir_all(project_dirs.clone()).await?;
let path = project_dirs.join(file);
let mut file = File::create(&path).await?;
let serialized = serde_json::to_string_pretty(config)?;
file.write_all(serialized.as_bytes()).await?;
file.sync_all().await?;
Ok(())
}
fn project_dirs() -> Result<PathBuf> {
let mut home_dir = dirs_next::home_dir()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found"))?;
home_dir.push(".safe");
home_dir.push("node");
Ok(home_dir)
}
#[test]
fn smoke() {
let expected_size = 440;
assert_eq!(std::mem::size_of::<Config>(), expected_size);
}