use std::process::Command;
use anyhow::{Context, Result};
use crate::model::Auth;
use crate::remote::{self, Host};
use crate::shell::Dialect;
pub const ENV_CLIENT: &str = "TURNOUT_SSH";
pub fn run(name: Option<String>, credential: Option<String>) -> Result<()> {
let host = remote::resolve_host(name, credential)?;
let client = std::env::var(ENV_CLIENT).unwrap_or_else(|_| "ssh".to_string());
let mut args: Vec<String> = vec!["-p".into(), host.server.port.to_string()];
if host.credential.auth == Auth::Key
&& let Some(key) = &host.credential.key
{
args.push("-i".into());
args.push(key.clone());
}
args.push(format!("{}@{}", host.credential.user, host.server.ssh_host()));
if let Some(dir) = &host.dir {
match landing_shell(&host) {
Some(dialect) => {
remote::check_quotable(dialect, &[dir])?;
args.push("-t".into());
args.push(landing_command(dialect, dir));
}
None => eprintln!(
"note: could not tell which shell '{}' runs - the session opens in the home directory",
host.server.name
),
}
}
if let Ok(secret) = crate::secrets::get(&host.credential.name) {
let what = match host.credential.auth {
Auth::Password => "Password",
Auth::Key => "Passphrase",
Auth::Agent => "Secret",
};
match arboard::Clipboard::new().and_then(|mut clipboard| clipboard.set_text(secret)) {
Ok(()) => eprintln!("{what} for '{}' copied to the clipboard - paste it at the prompt.", host.credential.name),
Err(_) => eprintln!("{what} for '{}' is in the keyring: `turnout pass copy {0}`.", host.credential.name),
}
}
eprintln!("[{}] {client} {}", host.label(), args.join(" "));
crate::journal::record("ssh", Some(&host.server.name), None, None);
let mut child = Command::new(&client)
.args(&args)
.spawn()
.with_context(|| format!("cannot run '{client}' - is the OpenSSH client installed? ({ENV_CLIENT} names another one)"))?;
crate::term::child_begin();
let status = child.wait();
crate::term::child_end();
let status = status.with_context(|| format!("cannot wait for '{client}'"))?;
std::process::exit(status.code().unwrap_or(1));
}
fn landing_shell(host: &Host) -> Option<Dialect> {
if let Some(known) = host.server.shell {
return Some(known);
}
let session = remote::connect(&host.server, &host.credential).ok()?;
Some(remote::dialect(&session, &host.server))
}
pub fn landing_command(dialect: Dialect, dir: &str) -> String {
match dialect {
Dialect::Posix => format!("cd {} && exec $SHELL -l", dialect.quote(dir)),
Dialect::Windows => format!("cd /d {} && cmd", dialect.quote(dir)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_landing_command_speaks_the_servers_shell() {
assert_eq!(landing_command(Dialect::Posix, "/srv/app"), "cd '/srv/app' && exec $SHELL -l");
assert_eq!(landing_command(Dialect::Windows, r"C:\site"), r#"cd /d "C:\site" && cmd"#);
}
#[test]
fn an_unquotable_directory_is_refused() {
assert!(remote::check_quotable(Dialect::Windows, &[r#"C:\odd"dir"#]).is_err());
assert!(remote::check_quotable(Dialect::Posix, &["/srv/it's"]).is_ok());
}
}