Skip to content

Commit

Permalink
Refactor codebase with breaking changes
Browse files Browse the repository at this point in the history
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
  • Loading branch information
chipsenkbeil committed Aug 3, 2021
1 parent 9353248 commit a7dd0eb
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 170 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/net/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
2 changes: 1 addition & 1 deletion src/net/transport/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
46 changes: 38 additions & 8 deletions src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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:
Expand Down
222 changes: 222 additions & 0 deletions src/session.rs
Original file line number Diff line number Diff line change
@@ -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()?))
}
}
12 changes: 8 additions & 4 deletions src/subcommand/action.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand All @@ -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")]
Expand All @@ -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() {
Expand Down
Loading

0 comments on commit a7dd0eb

Please sign in to comment.