You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
distant/src/opt.rs

340 lines
10 KiB
Rust

use crate::{subcommand, data::Operation};
use derive_more::{Display, Error, From};
3 years ago
use lazy_static::lazy_static;
use std::{
env,
net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::PathBuf,
str::FromStr,
};
3 years ago
use structopt::StructOpt;
lazy_static! {
static ref USERNAME: String = whoami::username();
}
/// Options and commands to apply to binary
#[derive(Debug, StructOpt)]
#[structopt(name = "distant")]
pub struct Opt {
#[structopt(flatten)]
pub common: CommonOpt,
#[structopt(subcommand)]
pub subcommand: Subcommand,
}
impl Opt {
/// Loads options from CLI arguments
pub fn load() -> Self {
Self::from_args()
}
}
/// Contains options that are common across subcommands
#[derive(Debug, StructOpt)]
pub struct CommonOpt {
/// Verbose mode (-v, -vv, -vvv, etc.)
#[structopt(short, long, parse(from_occurrences), global = true)]
pub verbose: u8,
/// Quiet mode
#[structopt(short, long, global = true)]
pub quiet: bool,
/// Log output to disk instead of stderr
#[structopt(long, global = true)]
pub log_file: Option<PathBuf>,
3 years ago
}
#[derive(Debug, StructOpt)]
pub enum Subcommand {
/// Clears the global session file
ClearSession,
3 years ago
#[structopt(visible_aliases = &["exec", "x"])]
Execute(ExecuteSubcommand),
Launch(LaunchSubcommand),
Listen(ListenSubcommand),
}
impl Subcommand {
/// Runs the subcommand, returning the result
pub fn run(self, opt: CommonOpt) -> Result<(), Box<dyn std::error::Error>> {
match self {
Self::ClearSession => subcommand::clear_session::run()?,
Self::Execute(cmd) => subcommand::execute::run(cmd, opt)?,
Self::Launch(cmd) => subcommand::launch::run(cmd, opt)?,
Self::Listen(cmd) => subcommand::listen::run(cmd, opt)?,
}
Ok(())
}
}
3 years ago
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq)]
pub enum ExecuteFormat {
#[display(fmt = "shell")]
Shell,
#[display(fmt = "json")]
Json,
}
#[derive(Clone, Debug, Display, From, Error, PartialEq, Eq)]
pub enum ExecuteFormatParseError {
InvalidVariant(#[error(not(source))] String),
}
impl FromStr for ExecuteFormat {
type Err = ExecuteFormatParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"shell" => Ok(Self::Shell),
"json" => Ok(Self::Json),
x => Err(ExecuteFormatParseError::InvalidVariant(x.to_string())),
}
}
}
3 years ago
/// Represents subcommand to execute some operation remotely
#[derive(Debug, StructOpt)]
#[structopt(verbatim_doc_comment)]
pub struct ExecuteSubcommand {
/// Represents the format that results should be returned
///
/// Currently, there are two possible formats:
/// 1. "shell": printing out human-readable results for interactive shell usage
/// 2. "json": printing our JSON for external program usage
#[structopt(
short,
long,
value_name = "shell|json",
default_value = "shell",
possible_values = &["shell", "json"]
)]
pub format: ExecuteFormat,
#[structopt(subcommand)]
pub operation: Operation,
}
3 years ago
/// Represents options for binding a server to an IP address
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq)]
pub enum BindAddress {
#[display(fmt = "ssh")]
Ssh,
#[display(fmt = "any")]
Any,
Ip(IpAddr),
}
#[derive(Clone, Debug, Display, From, Error, PartialEq, Eq)]
pub enum ConvertToIpAddrError {
AddrParseError(AddrParseError),
#[display(fmt = "SSH_CONNECTION missing 3rd argument (host ip)")]
MissingSshAddr,
VarError(env::VarError),
}
impl BindAddress {
/// Converts address into valid IP; in the case of "any", will leverage the
/// `use_ipv6` flag to determine if binding should use ipv4 or ipv6
pub fn to_ip_addr(&self, use_ipv6: bool) -> Result<IpAddr, ConvertToIpAddrError> {
match self {
Self::Ssh => {
let ssh_connection = env::var("SSH_CONNECTION")?;
let ip_str = ssh_connection
.split(' ')
.skip(2)
.next()
.ok_or(ConvertToIpAddrError::MissingSshAddr)?;
let ip = ip_str.parse::<IpAddr>()?;
Ok(ip)
}
Self::Any if use_ipv6 => Ok(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
Self::Any => Ok(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
Self::Ip(addr) => Ok(*addr),
}
}
}
impl FromStr for BindAddress {
type Err = AddrParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"ssh" => Ok(Self::Ssh),
"any" => Ok(Self::Any),
"localhost" => Ok(Self::Ip(IpAddr::V4(Ipv4Addr::LOCALHOST))),
x => Ok(Self::Ip(x.parse::<IpAddr>()?)),
}
}
}
3 years ago
/// Represents subcommand to launch a remote server
#[derive(Debug, StructOpt)]
pub struct LaunchSubcommand {
/// Outputs port and key of remotely-started binary
#[structopt(long)]
3 years ago
pub print_startup_data: bool,
/// Path to remote program to execute via ssh
#[structopt(short, long, default_value = "distant")]
pub remote_program: String,
/// Path to ssh program to execute
#[structopt(short, long, default_value = "ssh")]
pub ssh_program: String,
/// Control the IP address that the mosh-server binds to.
///
/// The default is `ssh', in which case the server will reply from the IP address that the SSH
/// connection came from (as found in the SSH_CONNECTION environment variable). This is
/// useful for multihomed servers.
///
/// With --bind-server=any, the server will reply on the default interface and will not bind to
/// a particular IP address. This can be useful if the connection is made through sslh or
/// another tool that makes the SSH connection appear to come from localhost.
///
/// With --bind-server=IP, the server will attempt to bind to the specified IP address.
3 years ago
#[structopt(long, value_name = "ssh|any|IP", default_value = "ssh")]
pub bind_server: BindAddress,
/// If specified, will write server logs to a file instead of discarding them
#[structopt(long)]
pub server_log_file: Option<PathBuf>,
/// If specified, will set the server's log level (0 is warning and above, 1 is info, 2 is
/// debug, and 3 or higher is trace)
#[structopt(long, default_value = "0")]
pub server_log_level: u8,
/// If specified, will bind server to the ipv6 interface if host is "any" instead of ipv4
#[structopt(short = "6", long)]
pub use_ipv6: bool,
3 years ago
/// Username to use when sshing into remote machine
#[structopt(short, long, default_value = &USERNAME)]
pub username: String,
/// Explicit identity file to use with ssh
#[structopt(short, long)]
pub identity_file: Option<PathBuf>,
/// Port to use for sshing into the remote machine
#[structopt(short, long, default_value = "22")]
pub port: u16,
/// Host to use for sshing into the remote machine
#[structopt(name = "ADDRESS")]
pub host: String,
3 years ago
}
/// Represents some range of ports
#[derive(Clone, Debug, Display, PartialEq, Eq)]
#[display(
fmt = "{}{}",
start,
"end.as_ref().map(|end| format!(\"[:{}]\", end)).unwrap_or_default()"
)]
pub struct PortRange {
pub start: u16,
pub end: Option<u16>,
}
impl PortRange {
/// Builds a collection of `SocketAddr` instances from the port range and given ip address
pub fn make_socket_addrs(&self, addr: impl Into<IpAddr>) -> Vec<SocketAddr> {
let mut socket_addrs = Vec::new();
let addr = addr.into();
for port in self.start..=self.end.unwrap_or(self.start) {
socket_addrs.push(SocketAddr::from((addr, port)));
}
socket_addrs
}
}
#[derive(Copy, Clone, Debug, Display, Error, PartialEq, Eq)]
pub enum PortRangeParseError {
InvalidPort,
MissingPort,
}
impl FromStr for PortRange {
type Err = PortRangeParseError;
/// Parses PORT into single range or PORT1:PORTN into full range
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut tokens = s.trim().split(':');
let start = tokens
.next()
.ok_or(PortRangeParseError::MissingPort)?
.parse::<u16>()
.map_err(|_| PortRangeParseError::InvalidPort)?;
let end = if let Some(token) = tokens.next() {
Some(
token
.parse::<u16>()
.map_err(|_| PortRangeParseError::InvalidPort)?,
)
} else {
None
};
if tokens.next().is_some() {
return Err(PortRangeParseError::InvalidPort);
}
Ok(Self { start, end })
}
}
3 years ago
/// Represents subcommand to operate in listen mode for incoming requests
#[derive(Debug, StructOpt)]
pub struct ListenSubcommand {
/// Runs in background via daemon-mode (does nothing on windows)
#[structopt(short, long)]
pub daemon: bool,
/// Prevents output of selected port, key, and other info
#[structopt(long)]
3 years ago
pub no_print_startup_data: bool,
/// Control the IP address that the distant binds to
///
/// There are three options here:
///
/// 1. `ssh`: the server will reply from the IP address that the SSH
/// connection came from (as found in the SSH_CONNECTION environment variable). This is
/// useful for multihomed servers.
///
/// 2. `any`: the server will reply on the default interface and will not bind to
/// a particular IP address. This can be useful if the connection is made through sslh or
/// another tool that makes the SSH connection appear to come from localhost.
///
/// 3. `IP`: the server will attempt to bind to the specified IP address.
3 years ago
#[structopt(short, long, value_name = "ssh|any|IP", default_value = "localhost")]
pub host: BindAddress,
/// If specified, will bind to the ipv6 interface if host is "any" instead of ipv4
#[structopt(short = "6", long)]
pub use_ipv6: bool,
3 years ago
/// Set the port(s) that the server will attempt to bind to
///
/// This can be in the form of PORT1 or PORT1:PORTN to provide a range of ports.
/// With -p 0, the server will let the operating system pick an available TCP port.
///
/// Please note that this option does not affect the server-side port used by SSH
#[structopt(
short,
long,
value_name = "PORT[:PORT2]",
default_value = "60000:61000"
)]
pub port: PortRange,
}