use crate::{data::RequestPayload, subcommand};
use derive_more::{Display, Error, From, IsVariant};
use lazy_static::lazy_static;
use std::{
env,
net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::PathBuf,
str::FromStr,
};
use structopt::StructOpt;
use strum::{EnumString, EnumVariantNames, IntoStaticStr, VariantNames};
lazy_static! {
static ref USERNAME: String = whoami::username();
}
#[derive(Debug, StructOpt)]
#[structopt(name = "distant")]
pub struct Opt {
#[structopt(flatten)]
pub common: CommonOpt,
#[structopt(subcommand)]
pub subcommand: Subcommand,
}
impl Opt {
pub fn load() -> Self {
Self::from_args()
}
}
#[derive(Debug, StructOpt)]
pub struct CommonOpt {
#[structopt(short, long, parse(from_occurrences), global = true)]
pub verbose: u8,
#[structopt(short, long, global = true)]
pub quiet: bool,
#[structopt(long, global = true)]
pub log_file: Option<PathBuf>,
}
#[derive(Debug, StructOpt)]
pub enum Subcommand {
Action(ActionSubcommand),
Launch(LaunchSubcommand),
Listen(ListenSubcommand),
Session(SessionSubcommand),
}
impl Subcommand {
pub fn run(self, opt: CommonOpt) -> Result<(), Box<dyn std::error::Error>> {
match self {
Self::Action(cmd) => subcommand::action::run(cmd, opt)?,
Self::Launch(cmd) => subcommand::launch::run(cmd, opt)?,
Self::Listen(cmd) => subcommand::listen::run(cmd, opt)?,
Self::Session(cmd) => subcommand::session::run(cmd, opt)?,
}
Ok(())
}
}
#[derive(Debug, StructOpt)]
pub enum SessionSubcommand {
Clear,
Exists,
Info {
#[structopt(
short,
long,
case_insensitive = true,
default_value = Mode::Shell.into(),
possible_values = Mode::VARIANTS
)]
mode: Mode,
},
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
IsVariant,
IntoStaticStr,
EnumString,
EnumVariantNames,
)]
#[strum(serialize_all = "snake_case")]
pub enum Mode {
Json,
Shell,
}
#[derive(Debug, StructOpt)]
#[structopt(verbatim_doc_comment)]
pub struct ActionSubcommand {
#[structopt(
short,
long,
case_insensitive = true,
default_value = Mode::Shell.into(),
possible_values = Mode::VARIANTS
)]
pub mode: Mode,
#[structopt(
long,
default_value = SessionInput::File.into(),
possible_values = SessionInput::VARIANTS
)]
pub session: SessionInput,
#[structopt(short, long)]
pub interactive: bool,
#[structopt(subcommand)]
pub operation: Option<RequestPayload>,
}
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, IsVariant)]
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 {
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>()?)),
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
IntoStaticStr,
IsVariant,
EnumString,
EnumVariantNames,
)]
#[strum(serialize_all = "snake_case")]
pub enum SessionOutput {
File,
Keep,
Pipe,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
IntoStaticStr,
IsVariant,
EnumString,
EnumVariantNames,
)]
#[strum(serialize_all = "snake_case")]
pub enum SessionInput {
Environment,
File,
Pipe,
}
#[derive(Debug, StructOpt)]
pub struct LaunchSubcommand {
#[structopt(
long,
default_value = SessionOutput::File.into(),
possible_values = SessionOutput::VARIANTS
)]
pub session: SessionOutput,
#[structopt(
short,
long,
case_insensitive = true,
default_value = Mode::Shell.into(),
possible_values = Mode::VARIANTS
)]
pub mode: Mode,
#[structopt(short, long, default_value = "distant")]
pub remote_program: String,
#[structopt(short, long, default_value = "ssh")]
pub ssh_program: String,
#[structopt(long, value_name = "ssh|any|IP", default_value = "ssh")]
pub bind_server: BindAddress,
#[structopt(long, allow_hyphen_values(true))]
pub extra_server_args: Option<String>,
#[structopt(short, long, default_value = &USERNAME)]
pub username: String,
#[structopt(short, long)]
pub identity_file: Option<PathBuf>,
#[structopt(short, long, default_value = "22")]
pub port: u16,
#[structopt(name = "HOST")]
pub host: String,
}
#[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 {
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;
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 })
}
}
#[derive(Debug, StructOpt)]
pub struct ListenSubcommand {
#[structopt(short, long)]
pub daemon: bool,
#[structopt(short, long, value_name = "ssh|any|IP", default_value = "localhost")]
pub host: BindAddress,
#[structopt(short = "6", long)]
pub use_ipv6: bool,
#[structopt(long, default_value = "1000")]
pub max_msg_capacity: u16,
#[structopt(long)]
pub current_dir: Option<PathBuf>,
#[structopt(short, long, value_name = "PORT[:PORT2]", default_value = "8080:8099")]
pub port: PortRange,
}