Refactor codebase with breaking changes

1. Add --session argument to launch and action
   subcommands that accepts file or pipe for
   launch and environment, file, or pipe for action
2. Unify session string as "DISTANT DATA <host> <port> <auth key>"
3. Rename utils to session
4. Split out Session file functionality to SessionFile
5. Remove SessionError in favor of io::Error
6. Bump version to 0.4.0 in preparation for that release
pull/38/head
Chip Senkbeil 3 years ago
parent 93532480d7
commit a7dd0eb435
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

2
Cargo.lock generated

@ -199,7 +199,7 @@ dependencies = [
[[package]]
name = "distant"
version = "0.3.2"
version = "0.4.0"
dependencies = [
"bytes",
"derive_more",

@ -2,7 +2,7 @@
name = "distant"
description = "Operate on a remote computer through file and process manipulation"
categories = ["command-line-utilities"]
version = "0.3.2"
version = "0.4.0"
authors = ["Chip Senkbeil <chip@senkbeil.org>"]
edition = "2018"
homepage = "https://github.com/chipsenkbeil/distant"

@ -2,8 +2,8 @@ mod constants;
mod data;
mod net;
mod opt;
mod session;
mod subcommand;
mod utils;
use log::error;
pub use opt::Opt;

@ -4,7 +4,7 @@ pub use transport::{Transport, TransportError, TransportReadHalf, TransportWrite
use crate::{
constants::CLIENT_BROADCAST_CHANNEL_CAPACITY,
data::{Request, Response},
utils::Session,
session::Session,
};
use log::*;
use std::{

@ -1,4 +1,4 @@
use crate::{constants::SALT_LEN, utils::Session};
use crate::{constants::SALT_LEN, session::Session};
use codec::DistantCodec;
use derive_more::{Display, Error, From};
use futures::SinkExt;

@ -8,7 +8,7 @@ use std::{
str::FromStr,
};
use structopt::StructOpt;
use strum::{EnumString, EnumVariantNames, VariantNames};
use strum::{EnumString, EnumVariantNames, VariantNames, IntoStaticStr};
lazy_static! {
static ref USERNAME: String = whoami::username();
@ -134,6 +134,10 @@ pub struct ActionSubcommand {
)]
pub mode: Mode,
/// Represents the medium for retrieving a session for use in performing the action
#[structopt(long, default_value = "file", possible_values = SessionSharing::VARIANTS)]
pub session: SessionSharing,
/// If specified, commands to send are sent over stdin and responses are received
/// over stdout (and stderr if mode is shell)
#[structopt(short, long)]
@ -197,12 +201,42 @@ impl FromStr for BindAddress {
}
}
/// Represents the means by which to share the session from launching on a remote machine
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, IntoStaticStr, IsVariant, EnumString, EnumVariantNames)]
#[strum(serialize_all = "snake_case")]
pub enum SessionSharing {
/// Session is in a environment variables
///
/// * `DISTANT_HOST=<host>`
/// * `DISTANT_PORT=<port>`
/// * `DISTANT_AUTH_KEY=<auth key>`
Environment,
/// Session is in a file in the form of `DISTANT DATA <host> <port> <auth key>`
File,
/// Session is stored and retrieved over anonymous pipes (stdout/stdin)
/// in form of `DISTANT DATA <host> <port> <auth key>`
Pipe,
}
impl SessionSharing {
/// Represents session configurations that can be used for output
pub fn output_variants() -> Vec<&'static str> {
vec![Self::File.into(), Self::Pipe.into()]
}
}
/// Represents subcommand to launch a remote server
#[derive(Debug, StructOpt)]
pub struct LaunchSubcommand {
/// Outputs port and key of remotely-started binary
#[structopt(long)]
pub print_startup_data: bool,
/// Represents the medium for sharing the session upon launching on a remote machine
#[structopt(
long,
default_value = SessionSharing::File.into(),
possible_values = &SessionSharing::output_variants()
)]
pub session: SessionSharing,
/// Path to remote program to execute via ssh
#[structopt(short, long, default_value = "distant")]
@ -315,10 +349,6 @@ pub struct ListenSubcommand {
#[structopt(short, long)]
pub daemon: bool,
/// Prevents output of selected port, key, and other info
#[structopt(long)]
pub no_print_startup_data: bool,
/// Control the IP address that the distant binds to
///
/// There are three options here:

@ -0,0 +1,222 @@
use crate::{PROJECT_DIRS, SESSION_PATH};
use derive_more::{Display, Error};
use orion::aead::SecretKey;
use std::{
env,
net::{IpAddr, SocketAddr},
ops::Deref,
path::Path,
str::FromStr,
};
use tokio::{io, net::lookup_host};
#[derive(Debug, PartialEq, Eq)]
pub struct Session {
pub host: String,
pub port: u16,
pub auth_key: SecretKey,
}
#[derive(Copy, Clone, Debug, Display, Error, PartialEq, Eq)]
pub enum SessionParseError {
#[display(fmt = "Prefix of string is invalid")]
BadPrefix,
#[display(fmt = "Bad hex key for session")]
BadSessionHexKey,
#[display(fmt = "Invalid key for session")]
InvalidSessionKey,
#[display(fmt = "Invalid port for session")]
InvalidSessionPort,
#[display(fmt = "Missing address for session")]
MissingSessionAddr,
#[display(fmt = "Missing key for session")]
MissingSessionKey,
#[display(fmt = "Missing port for session")]
MissingSessionPort,
}
impl From<SessionParseError> for io::Error {
fn from(x: SessionParseError) -> Self {
io::Error::new(io::ErrorKind::InvalidData, x)
}
}
impl FromStr for Session {
type Err = SessionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut tokens = s.split(' ').take(5);
// First, validate that we have the appropriate prefix
if tokens.next().ok_or(SessionParseError::BadPrefix)? != "DISTANT" {
return Err(SessionParseError::BadPrefix);
}
if tokens.next().ok_or(SessionParseError::BadPrefix)? != "DATA" {
return Err(SessionParseError::BadPrefix);
}
// Second, load up the address without parsing it
let host = tokens
.next()
.ok_or(SessionParseError::MissingSessionAddr)?
.trim()
.to_string();
// Third, load up the port and parse it into a number
let port = tokens
.next()
.ok_or(SessionParseError::MissingSessionPort)?
.trim()
.parse::<u16>()
.map_err(|_| SessionParseError::InvalidSessionPort)?;
// Fourth, load up the key and convert it back into a secret key from a hex slice
let auth_key = SecretKey::from_slice(
&hex::decode(
tokens
.next()
.ok_or(SessionParseError::MissingSessionKey)?
.trim(),
)
.map_err(|_| SessionParseError::BadSessionHexKey)?,
)
.map_err(|_| SessionParseError::InvalidSessionKey)?;
Ok(Session {
host,
port,
auth_key,
})
}
}
impl Session {
/// Loads session from environment variables
pub fn from_environment() -> io::Result<Self> {
fn to_err(x: env::VarError) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, x)
}
let host = env::var("DISTANT_HOST").map_err(to_err)?;
let port = env::var("DISTANT_PORT").map_err(to_err)?;
let auth_key = env::var("DISTANT_AUTH_KEY").map_err(to_err)?;
Ok(format!("DISTANT DATA {} {} {}", host, port, auth_key).parse()?)
}
/// Loads session from the next line available in this program's stdin
pub fn from_stdin() -> io::Result<Self> {
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
line.parse()
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))
}
/// Returns the ip address associated with the session based on the host
pub async fn to_ip_addr(&self) -> io::Result<IpAddr> {
let addr = match self.host.parse::<IpAddr>() {
Ok(addr) => addr,
Err(_) => lookup_host((self.host.as_str(), self.port))
.await?
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Failed to lookup_host"))?
.ip(),
};
Ok(addr)
}
/// Returns socket address associated with the session
pub async fn to_socket_addr(&self) -> io::Result<SocketAddr> {
let addr = self.to_ip_addr().await?;
Ok(SocketAddr::from((addr, self.port)))
}
/// Returns a string representing the auth key as hex
pub fn to_unprotected_hex_auth_key(&self) -> String {
hex::encode(self.auth_key.unprotected_as_bytes())
}
/// Converts to unprotected string that exposes the auth key in the form of
/// `DISTANT DATA <addr> <port> <auth key>`
pub async fn to_unprotected_string(&self) -> io::Result<String> {
Ok(format!(
"DISTANT DATA {} {} {}",
self.to_ip_addr().await?,
self.port,
self.to_unprotected_hex_auth_key()
))
}
}
/// Provides operations related to working with a session that is disk-based
pub struct SessionFile(Session);
impl AsRef<Session> for SessionFile {
fn as_ref(&self) -> &Session {
&self.0
}
}
impl Deref for SessionFile {
type Target = Session;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<SessionFile> for Session {
fn from(sf: SessionFile) -> Self {
sf.0
}
}
impl From<Session> for SessionFile {
fn from(session: Session) -> Self {
Self(session)
}
}
impl SessionFile {
/// Clears the global session file
pub async fn clear() -> io::Result<()> {
tokio::fs::remove_file(SESSION_PATH.as_path()).await
}
/// Returns true if the global session file exists
pub fn exists() -> bool {
SESSION_PATH.exists()
}
/// Saves a session to the global session file
pub async fn save(&self) -> io::Result<()> {
// Ensure our cache directory exists
let cache_dir = PROJECT_DIRS.cache_dir();
tokio::fs::create_dir_all(cache_dir).await?;
self.save_to(SESSION_PATH.as_path()).await
}
/// Saves a session to to a file at the specified path
pub async fn save_to(&self, path: impl AsRef<Path>) -> io::Result<()> {
tokio::fs::write(path.as_ref(), self.0.to_unprotected_string().await?).await
}
/// Loads a session from the global session file
pub async fn load() -> io::Result<Self> {
Self::load_from(SESSION_PATH.as_path()).await
}
/// Loads a session from a file at the specified path
pub async fn load_from(path: impl AsRef<Path>) -> io::Result<Self> {
let text = tokio::fs::read_to_string(path.as_ref()).await?;
Ok(Self(text.parse()?))
}
}

@ -1,8 +1,8 @@
use crate::{
data::{Request, RequestPayload, Response, ResponsePayload},
net::{Client, TransportError},
opt::{ActionSubcommand, CommonOpt, Mode},
utils::{Session, SessionError},
opt::{ActionSubcommand, CommonOpt, Mode, SessionSharing},
session::{Session, SessionFile},
};
use derive_more::{Display, Error, From};
use log::*;
@ -19,7 +19,6 @@ use tokio_stream::StreamExt;
#[derive(Debug, Display, Error, From)]
pub enum Error {
IoError(io::Error),
SessionError(SessionError),
TransportError(TransportError),
#[display(fmt = "Non-interactive but no operation supplied")]
@ -33,7 +32,12 @@ pub fn run(cmd: ActionSubcommand, opt: CommonOpt) -> Result<(), Error> {
}
async fn run_async(cmd: ActionSubcommand, _opt: CommonOpt) -> Result<(), Error> {
let session = Session::load().await?;
let session = match cmd.session {
SessionSharing::Environment => Session::from_environment()?,
SessionSharing::File => SessionFile::load().await?.into(),
SessionSharing::Pipe => Session::from_stdin()?,
};
let mut client = Client::connect(session).await?;
if !cmd.interactive && cmd.operation.is_none() {

@ -1,6 +1,6 @@
use crate::{
opt::{CommonOpt, LaunchSubcommand},
utils::Session,
opt::{CommonOpt, LaunchSubcommand, SessionSharing},
session::{Session, SessionFile},
};
use derive_more::{Display, Error, From};
use hex::FromHexError;
@ -86,14 +86,13 @@ async fn run_async(cmd: LaunchSubcommand, _opt: CommonOpt) -> Result<(), Error>
auth_key,
};
session.save().await?;
if cmd.print_startup_data {
println!(
"DISTANT DATA {} {}",
port,
session.to_unprotected_hex_auth_key()
);
// Handle sharing resulting session in different ways
// NOTE: Environment is unreachable here as we disallow it from the defined options since
// there is no way to set the shell's environment variables, only this running process
match cmd.session {
SessionSharing::Environment => unreachable!(),
SessionSharing::File => SessionFile::from(session).save().await?,
SessionSharing::Pipe => println!("{}", session.to_unprotected_string().await?),
}
Ok(())

@ -102,10 +102,8 @@ async fn run_async(cmd: ListenSubcommand, _opt: CommonOpt, is_forked: bool) -> R
let key = Arc::new(SecretKey::default());
// Print information about port, key, etc. unless told not to
if !cmd.no_print_startup_data {
publish_data(port, &key);
}
// Print information about port, key, etc.
publish_data(port, &key);
// For the child, we want to fully disconnect it from pipes, which we do now
if is_forked {

@ -1,6 +1,6 @@
use crate::{
opt::{CommonOpt, Mode, SessionSubcommand},
utils::Session,
session::SessionFile,
};
use tokio::io;
@ -8,9 +8,9 @@ pub fn run(cmd: SessionSubcommand, _opt: CommonOpt) -> Result<(), io::Error> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
match cmd {
SessionSubcommand::Clear => Session::clear().await,
SessionSubcommand::Clear => SessionFile::clear().await,
SessionSubcommand::Exists => {
if Session::exists() {
if SessionFile::exists() {
Ok(())
} else {
Err(io::Error::new(
@ -20,7 +20,7 @@ pub fn run(cmd: SessionSubcommand, _opt: CommonOpt) -> Result<(), io::Error> {
}
}
SessionSubcommand::Info { mode } => {
let session = Session::load()
let session = SessionFile::load()
.await
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?;
match mode {

@ -1,135 +0,0 @@
use crate::{PROJECT_DIRS, SESSION_PATH};
use derive_more::{Display, Error, From};
use orion::aead::SecretKey;
use std::net::{IpAddr, SocketAddr};
use tokio::{io, net::lookup_host};
#[derive(Debug, Display, Error, From)]
pub enum SessionError {
#[display(fmt = "Bad hex key for session")]
BadSessionHexKey,
#[display(fmt = "Invalid address for session")]
InvalidSessionAddr,
#[display(fmt = "Invalid key for session")]
InvalidSessionKey,
#[display(fmt = "Invalid port for session")]
InvalidSessionPort,
IoError(io::Error),
#[display(fmt = "Missing address for session")]
MissingSessionAddr,
#[display(fmt = "Missing key for session")]
MissingSessionKey,
#[display(fmt = "Missing port for session")]
MissingSessionPort,
#[display(fmt = "No session file: {:?}", SESSION_PATH.as_path())]
NoSessionFile,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Session {
pub host: String,
pub port: u16,
pub auth_key: SecretKey,
}
impl Session {
/// Returns a string representing the secret key as hex
pub fn to_unprotected_hex_auth_key(&self) -> String {
hex::encode(self.auth_key.unprotected_as_bytes())
}
/// Returns the ip address associated with the session based on the host
pub async fn to_ip_addr(&self) -> io::Result<IpAddr> {
let addr = match self.host.parse::<IpAddr>() {
Ok(addr) => addr,
Err(_) => lookup_host((self.host.as_str(), self.port))
.await?
.next()
.ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, SessionError::InvalidSessionAddr)
})?
.ip(),
};
Ok(addr)
}
/// Returns socket address associated with the session
pub async fn to_socket_addr(&self) -> io::Result<SocketAddr> {
let addr = self.to_ip_addr().await?;
Ok(SocketAddr::from((addr, self.port)))
}
/// Clears the global session file
pub async fn clear() -> io::Result<()> {
tokio::fs::remove_file(SESSION_PATH.as_path()).await
}
/// Returns true if a session is available
pub fn exists() -> bool {
SESSION_PATH.exists()
}
/// Saves a session to disk
pub async fn save(&self) -> io::Result<()> {
let key_hex_str = self.to_unprotected_hex_auth_key();
// Ensure our cache directory exists
let cache_dir = PROJECT_DIRS.cache_dir();
tokio::fs::create_dir_all(cache_dir).await?;
// Write our session file
let addr = self.to_ip_addr().await?;
tokio::fs::write(
SESSION_PATH.as_path(),
format!("{} {} {}", addr, self.port, key_hex_str),
)
.await?;
Ok(())
}
/// Loads a session's information into memory
pub async fn load() -> Result<Self, SessionError> {
let text = tokio::fs::read_to_string(SESSION_PATH.as_path())
.await
.map_err(|_| SessionError::NoSessionFile)?;
let mut tokens = text.split(' ').take(3);
// First, load up the address without parsing it
let host = tokens
.next()
.ok_or(SessionError::MissingSessionAddr)?
.trim()
.to_string();
// Second, load up the port and parse it into a number
let port = tokens
.next()
.ok_or(SessionError::MissingSessionPort)?
.trim()
.parse::<u16>()
.map_err(|_| SessionError::InvalidSessionPort)?;
// Third, load up the key and convert it back into a secret key from a hex slice
let auth_key = SecretKey::from_slice(
&hex::decode(tokens.next().ok_or(SessionError::MissingSessionKey)?.trim())
.map_err(|_| SessionError::BadSessionHexKey)?,
)
.map_err(|_| SessionError::InvalidSessionKey)?;
Ok(Session {
host,
port,
auth_key,
})
}
}
Loading…
Cancel
Save