diff --git a/Cargo.lock b/Cargo.lock index ffa638d..23291d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,15 +34,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "bumpalo" version = "3.7.0" @@ -90,50 +81,10 @@ dependencies = [ ] [[package]] -name = "const-oid" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22" - -[[package]] -name = "cpufeatures" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-bigint" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32a398eb1ccfbe7e4f452bc749c44d38dd732e9a253f19da224c416f00ee7f4" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-mac" -version = "0.11.1" +name = "ct-codecs" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "der" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f215f706081a44cb702c71c39a52c05da637822e9c1645a50b7202689e982d" -dependencies = [ - "const-oid", -] +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" [[package]] name = "derive_more" @@ -147,12 +98,23 @@ dependencies = [ ] [[package]] -name = "digest" -version = "0.9.0" +name = "directories" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e69600ff1703123957937708eb27f7a564e48885c537782722ed0ba3189ce1d7" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" dependencies = [ - "generic-array", + "libc", + "redox_users", + "winapi", ] [[package]] @@ -160,14 +122,14 @@ name = "distant" version = "0.1.0" dependencies = [ "derive_more", - "hkdf", - "hmac", - "k256", + "directories", + "fork", + "hex", "lazy_static", "log", + "orion", "serde", "serde_cbor", - "sha2", "stderrlog", "structopt", "tokio", @@ -175,51 +137,12 @@ dependencies = [ ] [[package]] -name = "ecdsa" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713c32426287891008edb98f8b5c6abb2130aa043c93a818728fcda78606f274" -dependencies = [ - "der", - "elliptic-curve", - "hmac", - "signature", -] - -[[package]] -name = "elliptic-curve" -version = "0.10.5" +name = "fork" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069397e10739989e400628cbc0556a817a8a64119d7a2315767f4456e1332c23" +checksum = "e4c5b9b0bce249a456f83ac4404e8baad0d2ba81cf651949719a4f74eb7323bb" dependencies = [ - "crypto-bigint", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "ff" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63eec06c61e487eecf0f7e6e6372e596a81922c28d33e645d6983ca6493a1af0" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "generic-array" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" -dependencies = [ - "typenum", - "version_check", + "libc", ] [[package]] @@ -233,17 +156,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "group" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - [[package]] name = "half" version = "1.7.1" @@ -269,24 +181,10 @@ dependencies = [ ] [[package]] -name = "hkdf" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" -dependencies = [ - "digest", - "hmac", -] - -[[package]] -name = "hmac" -version = "0.11.0" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac", - "digest", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "instant" @@ -306,18 +204,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "k256" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "sha2", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -421,10 +307,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] -name = "opaque-debug" -version = "0.3.0" +name = "orion" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "118f94b4ca56d1eb99466a26a216b87fad822a51af8b308264c88a9337eb0a15" +dependencies = [ + "ct-codecs", + "getrandom", + "subtle", + "zeroize", +] [[package]] name = "parking_lot" @@ -457,16 +349,6 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" -[[package]] -name = "pkcs8" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b4b8dc4b54af53088ef3b303052d92be5e4e9d5fa0acd4555ea4ad84ea1d72" -dependencies = [ - "der", - "spki", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -510,21 +392,22 @@ dependencies = [ ] [[package]] -name = "rand_core" -version = "0.6.3" +name = "redox_syscall" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" dependencies = [ - "getrandom", + "bitflags", ] [[package]] -name = "redox_syscall" -version = "0.2.9" +name = "redox_users" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ - "bitflags", + "getrandom", + "redox_syscall", ] [[package]] @@ -563,19 +446,6 @@ dependencies = [ "syn", ] -[[package]] -name = "sha2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" -dependencies = [ - "block-buffer", - "cfg-if", - "cpufeatures", - "digest", - "opaque-debug", -] - [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -585,31 +455,12 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335" -dependencies = [ - "digest", - "rand_core", -] - [[package]] name = "smallvec" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" -[[package]] -name = "spki" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987637c5ae6b3121aba9d513f869bd2bff11c4cc086c22473befd6649c0bd521" -dependencies = [ - "der", -] - [[package]] name = "stderrlog" version = "0.5.1" @@ -738,12 +589,6 @@ dependencies = [ "syn", ] -[[package]] -name = "typenum" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" - [[package]] name = "unicode-segmentation" version = "1.8.0" diff --git a/Cargo.toml b/Cargo.toml index 96fb944..cdf9d25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,13 @@ codegen-units = 1 [dependencies] derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error"] } -hkdf = "0.11.0" -hmac = "0.11.0" -k256 = "0.9.6" +directories = "3.0.2" +fork = "0.1.18" +hex = "0.4.3" log = "0.4.14" +orion = "0.16.0" serde = { version = "1.0.126", features = ["derive"] } serde_cbor = "0.11.1" -sha2 = "0.9.5" tokio = { version = "1.9.0", features = ["full"] } # Binary-specific dependencies diff --git a/src/lib.rs b/src/lib.rs index d1b1c0d..5755ea6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,17 @@ mod opt; mod subcommand; pub use opt::Opt; +use std::path::PathBuf; + +lazy_static::lazy_static! { + static ref PROJECT_DIRS: directories::ProjectDirs = + directories::ProjectDirs::from( + "org", + "senkbeil", + "distant", + ).expect("Failed to find valid home directory path"); + static ref SESSION_PATH: PathBuf = PROJECT_DIRS.cache_dir().join("session"); +} pub fn init_logging(opt: &opt::CommonOpt) { stderrlog::new() diff --git a/src/main.rs b/src/main.rs index c0a985a..d7a450f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,10 @@ use distant::Opt; -fn main() { +#[tokio::main] +async fn main() { let opt = Opt::load(); distant::init_logging(&opt.common); - println!("Hello, world!"); + if let Err(x) = opt.subcommand.run().await { + eprintln!("{}", x); + } } diff --git a/src/opt.rs b/src/opt.rs index 326062a..95496bc 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -1,5 +1,6 @@ +use crate::subcommand; use lazy_static::lazy_static; -use std::{net::SocketAddr, path::PathBuf}; +use std::path::PathBuf; use structopt::StructOpt; lazy_static! { @@ -38,18 +39,24 @@ pub struct CommonOpt { #[derive(Debug, StructOpt)] pub enum Subcommand { - #[structopt(visible_aliases = &["conn", "c"])] - Connect(ConnectSubcommand), #[structopt(visible_aliases = &["exec", "x"])] Execute(ExecuteSubcommand), Launch(LaunchSubcommand), - #[structopt(visible_aliases = &["l"])] Listen(ListenSubcommand), } -/// Represents subcommand to connect to an already-running remote server -#[derive(Debug, StructOpt)] -pub struct ConnectSubcommand {} +impl Subcommand { + /// Runs the subcommand, returning the result + pub async fn run(self) -> Result<(), Box> { + match self { + Self::Execute(cmd) => subcommand::execute::run(cmd).await?, + Self::Launch(cmd) => subcommand::launch::run(cmd).await?, + Self::Listen(cmd) => subcommand::listen::run(cmd).await?, + } + + Ok(()) + } +} /// Represents subcommand to execute some operation remotely #[derive(Debug, StructOpt)] @@ -58,6 +65,14 @@ pub struct ExecuteSubcommand {} /// 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_info: bool, + + /// Path to remote program to execute via ssh + #[structopt(short, long, default_value = "distant")] + pub remote_program: String, + /// Username to use when sshing into remote machine #[structopt(short, long, default_value = &USERNAME)] pub username: String, @@ -66,11 +81,37 @@ pub struct LaunchSubcommand { #[structopt(short, long)] pub identity_file: Option, - /// Destination of remote machine to launch binary - #[structopt(name = "DESTINATION")] - pub destination: SocketAddr, + /// 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, } /// Represents subcommand to operate in listen mode for incoming requests #[derive(Debug, StructOpt)] -pub struct ListenSubcommand {} +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)] + pub no_print_startup_info: bool, + + /// Represents the host to bind to when listening + #[structopt(short, long, default_value = "localhost")] + pub host: String, + + /// Represents the port to bind to when listening + #[structopt(short, long, default_value = "60000")] + pub port: u16, + + /// Represents total range of ports to try if a port is already taken + /// when binding, applying range incrementally against the specified + /// port (e.g. 60000-61000 inclusively if range is 1000) + #[structopt(long, default_value = "1000")] + pub port_range: u16, +} diff --git a/src/subcommand/execute.rs b/src/subcommand/execute.rs new file mode 100644 index 0000000..7cd5a87 --- /dev/null +++ b/src/subcommand/execute.rs @@ -0,0 +1,56 @@ +use crate::{opt::ExecuteSubcommand, SESSION_PATH}; +use derive_more::{Display, Error, From}; +use orion::aead::SecretKey; +use tokio::io; + +pub type Result = std::result::Result<(), Error>; + +#[derive(Debug, Display, Error, From)] +pub enum Error { + #[display(fmt = "Invalid key for session")] + InvalidSessionKey, + + #[display(fmt = "Invalid port for session")] + InvalidSessionPort, + + IoError(io::Error), + + #[display(fmt = "Missing key for session")] + MissingSessionKey, + + #[display(fmt = "Missing port for session")] + MissingSessionPort, + + #[display(fmt = "No session file: {:?}", SESSION_PATH.as_path())] + NoSessionFile, +} + +pub async fn run(_cmd: ExecuteSubcommand) -> Result { + // Load our session file's port and key + let (port, key) = { + let text = tokio::fs::read_to_string(SESSION_PATH.as_path()) + .await + .map_err(|_| Error::NoSessionFile)?; + let mut tokens = text.split(' ').take(2); + let port = tokens + .next() + .ok_or(Error::MissingSessionPort)? + .parse::() + .map_err(|_| Error::InvalidSessionPort)?; + let key = SecretKey::from_slice( + &hex::decode(tokens.next().ok_or(Error::MissingSessionKey)?.to_string()) + .map_err(|_| Error::InvalidSessionKey)?, + ) + .map_err(|_| Error::InvalidSessionKey)?; + (port, key) + }; + + println!( + "PORT:{}; KEY:{}", + port, + hex::encode(key.unprotected_as_bytes()) + ); + + // Encrypt -> MAC + Ok(()) +} diff --git a/src/subcommand/launch.rs b/src/subcommand/launch.rs index 8c9664a..61fabf8 100644 --- a/src/subcommand/launch.rs +++ b/src/subcommand/launch.rs @@ -1,22 +1,28 @@ -use crate::opt::{CommonOpt, LaunchSubcommand}; +use crate::{opt::LaunchSubcommand, PROJECT_DIRS, SESSION_PATH}; use derive_more::{Display, Error, From}; +use hex::FromHexError; +use orion::{aead::SecretKey, errors::UnknownCryptoError}; use std::string::FromUtf8Error; use tokio::{io, process::Command}; -pub type Result = std::result::Result<(), Error>; - #[derive(Debug, Display, Error, From)] pub enum Error { + #[display(fmt = "Missing data for session")] + MissingSessionData, + + BadKey(UnknownCryptoError), + HexError(FromHexError), IoError(io::Error), Utf8Error(FromUtf8Error), } -pub async fn run(cmd: LaunchSubcommand, opt: CommonOpt) -> Result { - let remote_command = r#"distant listen --print-port"#; +pub async fn run(cmd: LaunchSubcommand) -> Result<(), Error> { + let remote_command = format!("{} listen --daemon --host 0.0.0.0", cmd.remote_program); let ssh_command = format!( - "ssh -o StrictHostKeyChecking=no ssh://{}@{} {} {}", + "ssh -o StrictHostKeyChecking=no ssh://{}@{}:{} {} {}", cmd.username, - cmd.destination, + cmd.host, + cmd.port, cmd.identity_file .map(|f| format!("-i {}", f.as_path().display())) .unwrap_or_default(), @@ -27,8 +33,51 @@ pub async fn run(cmd: LaunchSubcommand, opt: CommonOpt) -> Result { .arg(ssh_command) .output() .await?; + + // If our attempt to run the program via ssh failed, report it + if !out.status.success() { + return Err(Error::from(io::Error::new( + io::ErrorKind::Other, + String::from_utf8(out.stderr)?.trim().to_string(), + ))); + } + + // Parse our output for the specific info line let out = String::from_utf8(out.stdout)?.trim().to_string(); - println!("{}", out); + let result = out + .lines() + .find_map(|line| { + let tokens: Vec<&str> = line.split(' ').take(4).collect(); + let is_data_line = tokens.len() == 4 && tokens[0] == "DISTANT" && tokens[1] == "DATA"; + match tokens[2].parse::() { + Ok(port) if is_data_line => { + let key = hex::decode(tokens[3]) + .map_err(Error::from) + .and_then(|bytes| SecretKey::from_slice(&bytes).map_err(Error::from)); + match key { + Ok(key) => Some(Ok((port, key))), + Err(x) => Some(Err(x)), + } + } + _ => None, + } + }) + .unwrap_or(Err(Error::MissingSessionData)); + + // Write a session file containing our data for use in subsequent calls + let (port, key) = result?; + let key_hex_str = hex::encode(key.unprotected_as_bytes()); + + if cmd.print_startup_info { + println!("DISTANT DATA {} {}", port, key_hex_str); + } + + // Ensure our cache directory exists + let cache_dir = PROJECT_DIRS.cache_dir(); + tokio::fs::create_dir_all(cache_dir).await?; + + // Write our session file + tokio::fs::write(SESSION_PATH.as_path(), format!("{} {}", port, key_hex_str)).await?; Ok(()) } diff --git a/src/subcommand/listen.rs b/src/subcommand/listen.rs new file mode 100644 index 0000000..bdcb109 --- /dev/null +++ b/src/subcommand/listen.rs @@ -0,0 +1,30 @@ +use crate::opt::ListenSubcommand; +use derive_more::{Display, Error, From}; +use orion::aead; +use std::string::FromUtf8Error; +use tokio::io; + +pub type Result = std::result::Result<(), Error>; + +#[derive(Debug, Display, Error, From)] +pub enum Error { + IoError(io::Error), + Utf8Error(FromUtf8Error), +} + +pub async fn run(cmd: ListenSubcommand) -> Result { + let port = cmd.port; + let key = aead::SecretKey::default(); + + // TODO: We have to share the key in some manner (maybe use k256 to arrive at the same key?) + // For now, we do what mosh does and print out the key knowing that this is shared over + // ssh, which should provide security + print!( + "DISTANT DATA {} {}", + port, + hex::encode(key.unprotected_as_bytes()) + ); + + // MAC -> Decrypt + Ok(()) +} diff --git a/src/subcommand/mod.rs b/src/subcommand/mod.rs index d779117..1eeaafb 100644 --- a/src/subcommand/mod.rs +++ b/src/subcommand/mod.rs @@ -1 +1,3 @@ +pub mod execute; pub mod launch; +pub mod listen;