parent
449b4b8278
commit
72ef070ef3
|
@ -982,6 +982,7 @@ name = "shared"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"atty",
|
||||||
"colored",
|
"colored",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"indoc",
|
"indoc",
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
use std::{collections::HashMap, fmt, fs::OpenOptions, io::{self, BufRead, BufReader, ErrorKind, Write}, net::IpAddr, path::{Path, PathBuf}, result};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fmt,
|
||||||
|
fs::OpenOptions,
|
||||||
|
io::{self, BufRead, BufReader, ErrorKind, Write},
|
||||||
|
net::IpAddr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
|
||||||
pub type Result<T> = result::Result<T, Box<dyn std::error::Error>>;
|
pub type Result<T> = result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ version = "1.3.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
atty = "0.2"
|
||||||
colored = "2.0"
|
colored = "2.0"
|
||||||
dialoguer = "0.8"
|
dialoguer = "0.8"
|
||||||
indoc = "1"
|
indoc = "1"
|
||||||
|
|
|
@ -9,13 +9,65 @@ use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use publicip::Preference;
|
use publicip::Preference;
|
||||||
use std::{net::SocketAddr, time::SystemTime};
|
use std::{
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
io,
|
||||||
|
net::SocketAddr,
|
||||||
|
str::FromStr,
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
use wgctrl::{InterfaceName, KeyPair};
|
use wgctrl::{InterfaceName, KeyPair};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref THEME: ColorfulTheme = ColorfulTheme::default();
|
pub static ref THEME: ColorfulTheme = ColorfulTheme::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ensure_interactive(prompt: &str) -> Result<(), io::Error> {
|
||||||
|
if atty::is(atty::Stream::Stdin) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(io::ErrorKind::BrokenPipe, format!("Prompt \"{}\" failed because TTY isn't connected.", prompt)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn confirm(prompt: &str) -> Result<bool, io::Error> {
|
||||||
|
ensure_interactive(prompt)?;
|
||||||
|
Confirm::with_theme(&*THEME)
|
||||||
|
.wait_for_newline(true)
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.default(false)
|
||||||
|
.interact()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select<'a, T: ToString>(prompt: &str, items: &'a [T]) -> Result<(usize, &'a T), io::Error> {
|
||||||
|
ensure_interactive(prompt)?;
|
||||||
|
let choice = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.items(items)
|
||||||
|
.interact()?;
|
||||||
|
Ok((choice, &items[choice]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Prefill<T> {
|
||||||
|
Default(T),
|
||||||
|
Editable(String),
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn input<T>(prompt: &str, prefill: Prefill<T>) -> Result<T, io::Error>
|
||||||
|
where
|
||||||
|
T: Clone + FromStr + Display,
|
||||||
|
T::Err: Display + Debug,
|
||||||
|
{
|
||||||
|
ensure_interactive(prompt)?;
|
||||||
|
let mut input = Input::with_theme(&*THEME);
|
||||||
|
match prefill {
|
||||||
|
Prefill::Default(value) => input.default(value),
|
||||||
|
Prefill::Editable(value) => input.with_initial_text(value),
|
||||||
|
_ => &mut input
|
||||||
|
}.with_prompt(prompt).interact()
|
||||||
|
}
|
||||||
|
|
||||||
/// Bring up a prompt to create a new CIDR. Returns the peer request.
|
/// Bring up a prompt to create a new CIDR. Returns the peer request.
|
||||||
pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrOpts) -> Result<Option<CidrContents>, Error> {
|
pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrOpts) -> Result<Option<CidrContents>, Error> {
|
||||||
let parent_cidr = if let Some(ref parent_name) = request.parent {
|
let parent_cidr = if let Some(ref parent_name) = request.parent {
|
||||||
|
@ -30,13 +82,13 @@ pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrOpts) -> Result<Option<CidrCont
|
||||||
let name = if let Some(ref name) = request.name {
|
let name = if let Some(ref name) = request.name {
|
||||||
name.clone()
|
name.clone()
|
||||||
} else {
|
} else {
|
||||||
Input::with_theme(&*THEME).with_prompt("Name").interact()?
|
input("Name", Prefill::None)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let cidr = if let Some(cidr) = request.cidr {
|
let cidr = if let Some(cidr) = request.cidr {
|
||||||
cidr
|
cidr
|
||||||
} else {
|
} else {
|
||||||
Input::with_theme(&*THEME).with_prompt("CIDR").interact()?
|
input("CIDR", Prefill::None)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let cidr_request = CidrContents {
|
let cidr_request = CidrContents {
|
||||||
|
@ -46,13 +98,7 @@ pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrOpts) -> Result<Option<CidrCont
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
if request.yes
|
if request.yes || confirm(&format!("Create CIDR \"{}\"?", cidr_request.name))? {
|
||||||
|| Confirm::with_theme(&*THEME)
|
|
||||||
.wait_for_newline(true)
|
|
||||||
.with_prompt(&format!("Create CIDR \"{}\"?", cidr_request.name))
|
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
{
|
|
||||||
Some(cidr_request)
|
Some(cidr_request)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -77,20 +123,10 @@ pub fn delete_cidr(cidrs: &[Cidr], peers: &[Peer], request: &DeleteCidrOpts) ->
|
||||||
.find(|cidr| &cidr.name == name)
|
.find(|cidr| &cidr.name == name)
|
||||||
.ok_or_else(|| anyhow!("CIDR {} doesn't exist or isn't eligible for deletion", name))?
|
.ok_or_else(|| anyhow!("CIDR {} doesn't exist or isn't eligible for deletion", name))?
|
||||||
} else {
|
} else {
|
||||||
let cidr_index = Select::with_theme(&*THEME)
|
select("Delete CIDR", &eligible_cidrs)?.1
|
||||||
.with_prompt("Delete CIDR")
|
|
||||||
.items(&eligible_cidrs)
|
|
||||||
.interact()?;
|
|
||||||
&eligible_cidrs[cidr_index]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if request.yes
|
if request.yes || confirm(&format!("Delete CIDR \"{}\"?", cidr.name))? {
|
||||||
|| Confirm::with_theme(&*THEME)
|
|
||||||
.wait_for_newline(true)
|
|
||||||
.with_prompt(&format!("Delete CIDR \"{}\"?", cidr.name))
|
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
{
|
|
||||||
Ok(cidr.id)
|
Ok(cidr.id)
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("Canceled"))
|
Err(anyhow!("Canceled"))
|
||||||
|
@ -102,11 +138,7 @@ pub fn choose_cidr<'a>(cidrs: &'a [Cidr], text: &'static str) -> Result<&'a Cidr
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|cidr| cidr.name != "innernet-server")
|
.filter(|cidr| cidr.name != "innernet-server")
|
||||||
.collect();
|
.collect();
|
||||||
let cidr_index = Select::with_theme(&*THEME)
|
Ok(select(text, &eligible_cidrs)?.1)
|
||||||
.with_prompt(text)
|
|
||||||
.items(&eligible_cidrs)
|
|
||||||
.interact()?;
|
|
||||||
Ok(&eligible_cidrs[cidr_index])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn choose_association<'a>(
|
pub fn choose_association<'a>(
|
||||||
|
@ -132,10 +164,7 @@ pub fn choose_association<'a>(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let index = Select::with_theme(&*THEME)
|
let (index, _) = select("Association", &names)?;
|
||||||
.with_prompt("Association")
|
|
||||||
.items(&names)
|
|
||||||
.interact()?;
|
|
||||||
|
|
||||||
Ok(&associations[index])
|
Ok(&associations[index])
|
||||||
}
|
}
|
||||||
|
@ -145,16 +174,11 @@ pub fn add_association(cidrs: &[Cidr]) -> Result<Option<(&Cidr, &Cidr)>, Error>
|
||||||
let cidr2 = choose_cidr(cidrs, "Second CIDR")?;
|
let cidr2 = choose_cidr(cidrs, "Second CIDR")?;
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
if Confirm::with_theme(&*THEME)
|
if confirm(&format!(
|
||||||
.wait_for_newline(true)
|
|
||||||
.with_prompt(&format!(
|
|
||||||
"Add association: {} <=> {}?",
|
"Add association: {} <=> {}?",
|
||||||
cidr1.name.yellow().bold(),
|
cidr1.name.yellow().bold(),
|
||||||
cidr2.name.yellow().bold()
|
cidr2.name.yellow().bold()
|
||||||
))
|
))? {
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
{
|
|
||||||
Some((cidr1, cidr2))
|
Some((cidr1, cidr2))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -169,12 +193,7 @@ pub fn delete_association<'a>(
|
||||||
let association = choose_association(associations, cidrs)?;
|
let association = choose_association(associations, cidrs)?;
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
if Confirm::with_theme(&*THEME)
|
if confirm(&format!("Delete association #{}?", association.id))? {
|
||||||
.wait_for_newline(true)
|
|
||||||
.with_prompt(&format!("Delete association #{}?", association.id))
|
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
{
|
|
||||||
Some(association)
|
Some(association)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -215,35 +234,25 @@ pub fn add_peer(
|
||||||
} else if args.auto_ip {
|
} else if args.auto_ip {
|
||||||
available_ip
|
available_ip
|
||||||
} else {
|
} else {
|
||||||
Input::with_theme(&*THEME)
|
input("IP", Prefill::Default(available_ip))?
|
||||||
.with_prompt("IP")
|
|
||||||
.default(available_ip)
|
|
||||||
.interact()?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = if let Some(ref name) = args.name {
|
let name = if let Some(ref name) = args.name {
|
||||||
name.clone()
|
name.clone()
|
||||||
} else {
|
} else {
|
||||||
Input::with_theme(&*THEME).with_prompt("Name").interact()?
|
input("Name", Prefill::None)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_admin = if let Some(is_admin) = args.admin {
|
let is_admin = if let Some(is_admin) = args.admin {
|
||||||
is_admin
|
is_admin
|
||||||
} else {
|
} else {
|
||||||
Confirm::with_theme(&*THEME)
|
confirm(&format!("Make {} an admin?", name))?
|
||||||
.wait_for_newline(true)
|
|
||||||
.with_prompt(&format!("Make {} an admin?", name))
|
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let invite_expires = if let Some(ref invite_expires) = args.invite_expires {
|
let invite_expires = if let Some(ref invite_expires) = args.invite_expires {
|
||||||
invite_expires.clone()
|
invite_expires.clone()
|
||||||
} else {
|
} else {
|
||||||
Input::with_theme(&*THEME)
|
input("Invite expires after", Prefill::Default("14d".parse().map_err(|s: &str| anyhow!(s))?))?
|
||||||
.with_prompt("Invite expires after")
|
|
||||||
.default("14d".parse().map_err(|s: &str| anyhow!(s))?)
|
|
||||||
.interact()?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let default_keypair = KeyPair::generate();
|
let default_keypair = KeyPair::generate();
|
||||||
|
@ -261,13 +270,7 @@ pub fn add_peer(
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
if args.yes
|
if args.yes || confirm(&format!("Create peer {}?", peer_request.name.yellow()))? {
|
||||||
|| Confirm::with_theme(&*THEME)
|
|
||||||
.wait_for_newline(true)
|
|
||||||
.with_prompt(&format!("Create peer {}?", peer_request.name.yellow()))
|
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
{
|
|
||||||
Some((peer_request, default_keypair))
|
Some((peer_request, default_keypair))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -291,24 +294,20 @@ pub fn rename_peer(
|
||||||
.ok_or_else(|| anyhow!("Peer '{}' does not exist", name))?
|
.ok_or_else(|| anyhow!("Peer '{}' does not exist", name))?
|
||||||
.clone()
|
.clone()
|
||||||
} else {
|
} else {
|
||||||
let peer_index = Select::with_theme(&*THEME)
|
let (peer_index, _) = select(
|
||||||
.with_prompt("Peer to rename")
|
"Peer to rename",
|
||||||
.items(
|
|
||||||
&eligible_peers
|
&eligible_peers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ep| ep.name.clone())
|
.map(|ep| ep.name.clone())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)?;
|
||||||
.interact()?;
|
|
||||||
eligible_peers[peer_index].clone()
|
eligible_peers[peer_index].clone()
|
||||||
};
|
};
|
||||||
let old_name = old_peer.name.clone();
|
let old_name = old_peer.name.clone();
|
||||||
let new_name = if let Some(ref name) = args.new_name {
|
let new_name = if let Some(ref name) = args.new_name {
|
||||||
name.clone()
|
name.clone()
|
||||||
} else {
|
} else {
|
||||||
Input::with_theme(&*THEME)
|
input("New Name", Prefill::None)?
|
||||||
.with_prompt("New Name")
|
|
||||||
.interact()?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut new_peer = old_peer;
|
let mut new_peer = old_peer;
|
||||||
|
@ -316,14 +315,11 @@ pub fn rename_peer(
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
if args.yes
|
if args.yes
|
||||||
|| Confirm::with_theme(&*THEME)
|
|| confirm(&format!(
|
||||||
.with_prompt(&format!(
|
|
||||||
"Rename peer {} to {}?",
|
"Rename peer {} to {}?",
|
||||||
old_name.yellow(),
|
old_name.yellow(),
|
||||||
new_name.yellow()
|
new_name.yellow()
|
||||||
))
|
))?
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
{
|
{
|
||||||
Some((new_peer.contents, old_name))
|
Some((new_peer.contents, old_name))
|
||||||
} else {
|
} else {
|
||||||
|
@ -344,26 +340,18 @@ pub fn enable_or_disable_peer(peers: &[Peer], enable: bool) -> Result<Option<Pee
|
||||||
.iter()
|
.iter()
|
||||||
.map(|peer| format!("{} ({})", &peer.name, &peer.ip))
|
.map(|peer| format!("{} ({})", &peer.name, &peer.ip))
|
||||||
.collect();
|
.collect();
|
||||||
let index = Select::with_theme(&*THEME)
|
let (index, _) = select(
|
||||||
.with_prompt(&format!(
|
&format!("Peer to {}able", if enable { "en" } else { "dis" }),
|
||||||
"Peer to {}able",
|
&peer_selection,
|
||||||
if enable { "en" } else { "dis" }
|
)?;
|
||||||
))
|
|
||||||
.items(&peer_selection)
|
|
||||||
.interact()?;
|
|
||||||
let peer = enabled_peers[index];
|
let peer = enabled_peers[index];
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
if Confirm::with_theme(&*THEME)
|
if confirm(&format!(
|
||||||
.wait_for_newline(true)
|
|
||||||
.with_prompt(&format!(
|
|
||||||
"{}able peer {}?",
|
"{}able peer {}?",
|
||||||
if enable { "En" } else { "Dis" },
|
if enable { "En" } else { "Dis" },
|
||||||
peer.name.yellow()
|
peer.name.yellow()
|
||||||
))
|
))? {
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
{
|
|
||||||
Some(peer.clone())
|
Some(peer.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -401,10 +389,7 @@ pub fn save_peer_invitation(
|
||||||
let invitation_save_path = if let Some(location) = config_location {
|
let invitation_save_path = if let Some(location) = config_location {
|
||||||
location.clone()
|
location.clone()
|
||||||
} else {
|
} else {
|
||||||
Input::with_theme(&*THEME)
|
input("Save peer invitation file as", Prefill::Default(format!("{}.toml", peer.name)))?
|
||||||
.with_prompt("Save peer invitation file as")
|
|
||||||
.default(format!("{}.toml", peer.name))
|
|
||||||
.interact()?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
peer_invitation.write_to_path(&invitation_save_path, true, None)?;
|
peer_invitation.write_to_path(&invitation_save_path, true, None)?;
|
||||||
|
@ -427,10 +412,7 @@ pub fn set_listen_port(
|
||||||
) -> Result<Option<Option<u16>>, Error> {
|
) -> Result<Option<Option<u16>>, Error> {
|
||||||
let listen_port = (!unset)
|
let listen_port = (!unset)
|
||||||
.then(|| {
|
.then(|| {
|
||||||
Input::with_theme(&*THEME)
|
input("Listen port", Prefill::Default(interface.listen_port.unwrap_or(51820)))
|
||||||
.with_prompt("Listen port")
|
|
||||||
.default(interface.listen_port.unwrap_or(51820))
|
|
||||||
.interact()
|
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
|
@ -469,32 +451,23 @@ pub fn ask_endpoint() -> Result<Endpoint, Error> {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut endpoint_builder = Input::with_theme(&*THEME);
|
Ok(input("External endpoint", match external_ip {
|
||||||
if let Some(ip) = external_ip {
|
Some(ip) => Prefill::Editable(SocketAddr::new(ip, 51820).to_string()),
|
||||||
endpoint_builder.with_initial_text(SocketAddr::new(ip, 51820).to_string());
|
None => Prefill::None
|
||||||
}
|
})?)
|
||||||
endpoint_builder
|
|
||||||
.with_prompt("External endpoint")
|
|
||||||
.interact()
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn override_endpoint(unset: bool) -> Result<Option<Option<Endpoint>>, Error> {
|
pub fn override_endpoint(unset: bool) -> Result<Option<Option<Endpoint>>, Error> {
|
||||||
let endpoint = if !unset { Some(ask_endpoint()?) } else { None };
|
let endpoint = if !unset { Some(ask_endpoint()?) } else { None };
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
if Confirm::with_theme(&*THEME)
|
if confirm(
|
||||||
.wait_for_newline(true)
|
|
||||||
.with_prompt(
|
|
||||||
&(if let Some(endpoint) = &endpoint {
|
&(if let Some(endpoint) = &endpoint {
|
||||||
format!("Set external endpoint to {}?", endpoint)
|
format!("Set external endpoint to {}?", endpoint)
|
||||||
} else {
|
} else {
|
||||||
"Unset external endpoint to enable automatic endpoint discovery?".to_string()
|
"Unset external endpoint to enable automatic endpoint discovery?".to_string()
|
||||||
}),
|
}),
|
||||||
)
|
)? {
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
{
|
|
||||||
Some(endpoint)
|
Some(endpoint)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
@ -137,7 +137,9 @@ pub fn add_route(interface: &InterfaceName, cidr: IpNetwork) -> Result<bool, Err
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
Err(anyhow::anyhow!(
|
Err(anyhow::anyhow!(
|
||||||
"failed to add route for device {} ({}): {}",
|
"failed to add route for device {} ({}): {}",
|
||||||
&interface, real_interface, stderr
|
&interface,
|
||||||
|
real_interface,
|
||||||
|
stderr
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(!stderr.contains("File exists"))
|
Ok(!stderr.contains("File exists"))
|
||||||
|
|
Loading…
Reference in New Issue