mod auth;
mod channel;
mod protocol;
mod server;
use std::{
io::{self, IsTerminal},
net::SocketAddr,
path::PathBuf,
};
use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "shex", version, about = "Small encrypted remote shell")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Init {
#[arg(long, default_value = ".shex")]
data_dir: PathBuf,
},
Serve {
#[arg(long, default_value = "127.0.0.1:8022")]
bind: SocketAddr,
#[arg(long, default_value = ".shex")]
data_dir: PathBuf,
},
Connect {
#[arg(default_value = "127.0.0.1:8022")]
address: SocketAddr,
#[arg(long)]
session: Option<String>,
#[arg(long)]
code_stdin: bool,
},
Exec {
#[arg(long, default_value = "127.0.0.1:8022")]
address: SocketAddr,
#[arg(long)]
session: String,
#[arg(long)]
code_stdin: bool,
#[arg(required = true, trailing_var_arg = true)]
command: Vec<String>,
},
}
fn read_code(force_stdin: bool, confirm: bool) -> Result<Vec<u8>> {
let code = if force_stdin || !io::stdin().is_terminal() {
let mut value = String::new();
io::stdin().read_line(&mut value)?;
value.trim_end_matches(['\r', '\n']).to_owned()
} else {
rpassword::prompt_password("Authentication code: ")?
};
if code.is_empty() {
bail!("the authentication code cannot be empty");
}
if confirm && io::stdin().is_terminal() && !force_stdin {
let second = rpassword::prompt_password("Confirm code: ")?;
if code != second {
bail!("codes do not match");
}
}
Ok(code.into_bytes())
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Init { data_dir } => {
let code = read_code(false, true)?;
auth::initialize(&data_dir, &code)?;
println!("initialized {}", data_dir.display());
}
Command::Serve { bind, data_dir } => server::serve(bind, &data_dir)?,
Command::Connect {
address,
session,
code_stdin,
} => {
let code = read_code(code_stdin, false)?;
run_client(address, &code, session, None)?;
}
Command::Exec {
address,
session,
code_stdin,
command,
} => {
let code = read_code(code_stdin, false)?;
run_client(address, &code, Some(session), Some(command.join(" ")))?;
}
}
Ok(())
}
fn run_client(
address: SocketAddr,
code: &[u8],
session: Option<String>,
command: Option<String>,
) -> Result<()> {
use protocol::{ClientRequest, ServerReply};
use std::io::Write;
let stream = std::net::TcpStream::connect(address)
.with_context(|| format!("could not connect to {address}"))?;
stream.set_nodelay(true)?;
let (stream, key) = auth::client_login(stream, code).context("authentication failed")?;
let mut channel = channel::SecureChannel::client(stream, &key)?;
channel.send(&ClientRequest::Open { session })?;
let session_id = match channel.recv::<ServerReply>()? {
ServerReply::Opened { session } => session,
ServerReply::Error { message } => bail!("server: {message}"),
other => bail!("unexpected server response: {other:?}"),
};
eprintln!("session: {session_id}");
if let Some(command) = command {
channel.send(&ClientRequest::Run { command })?;
match channel.recv::<ServerReply>()? {
ServerReply::Output { data, status } => {
print!("{data}");
io::stdout().flush()?;
if status != 0 {
std::process::exit(status.clamp(1, 255));
}
}
ServerReply::Error { message } => bail!("server: {message}"),
other => bail!("unexpected server response: {other:?}"),
}
return Ok(());
}
let mut line = String::new();
loop {
eprint!("shex> ");
io::stderr().flush()?;
line.clear();
if io::stdin().read_line(&mut line)? == 0 {
break;
}
let command = line.trim_end().to_owned();
if command == "exit" {
break;
}
if command.is_empty() {
continue;
}
channel.send(&ClientRequest::Run { command })?;
match channel.recv::<ServerReply>()? {
ServerReply::Output { data, .. } => {
print!("{data}");
io::stdout().flush()?;
}
ServerReply::Error { message } => eprintln!("server: {message}"),
other => bail!("unexpected server response: {other:?}"),
}
}
Ok(())
}