use super::*;
pub fn is_control_master_active(server: &ResolvedServer) -> bool {
if !server.control_master || server.control_path.is_empty() {
return false;
}
let path = shellexpand::tilde(&server.control_path).into_owned();
let path = path
.replace("%h", &server.host)
.replace("%p", &server.port.to_string())
.replace("%r", &server.user);
if !std::path::Path::new(&path).exists() {
return false;
}
Command::new("ssh")
.args(["-O", "check", "-S", &path, &server.host])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn connect(
server: &ResolvedServer,
mode: ConnectionMode,
verbose: bool,
credential: Option<&str>,
) -> Result<()> {
if let Some(cred) = credential {
return connect_blocking(server, mode, verbose, Some(cred));
}
let args = build_ssh_args(server, mode, verbose)?;
let mut command = Command::new("ssh");
command.args(&args);
#[cfg(unix)]
{
let err = command.exec();
Err(anyhow::Error::new(err).context("Failed to exec ssh command"))
}
#[cfg(not(unix))]
{
command
.status()
.map(|_| ())
.map_err(|e| anyhow::Error::new(e).context("Failed to spawn ssh command"))
}
}
pub fn connect_blocking(
server: &ResolvedServer,
mode: ConnectionMode,
verbose: bool,
credential: Option<&str>,
) -> Result<()> {
let args = build_ssh_args(server, mode, verbose)?;
let mut command = Command::new("ssh");
command.args(&args);
#[cfg(unix)]
if !server.ssh_agent_sock.is_empty() {
command.env("SSH_AUTH_SOCK", &server.ssh_agent_sock);
}
#[cfg(unix)]
let askpass_path = if let Some(cred) = credential {
let p = setup_askpass_script(cred)?;
command.env("SSH_ASKPASS", &p);
command.env("SSH_ASKPASS_REQUIRE", "force");
Some(p)
} else {
None
};
let result = command
.status()
.map(|_| ())
.map_err(|e| anyhow::Error::new(e).context("Failed to spawn ssh command"));
#[cfg(unix)]
if let Some(p) = askpass_path {
let _ = std::fs::remove_file(p);
}
result
}
#[cfg(unix)]
pub fn fetch_wallix_menu_entries(
server: &ResolvedServer,
verbose: bool,
auth: Option<&str>,
) -> Result<Vec<WallixMenuEntry>> {
let args = build_wallix_bastion_args(server, verbose)?;
let (child, mut master_reader, mut master_writer) = spawn_wallix_pty(&args)?;
let master_fd = master_reader.as_raw_fd();
let mut transcript = String::new();
let mut auth_injected = false;
let timeout_secs = server.wallix_selection_timeout_secs;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
'outer: loop {
loop {
if std::time::Instant::now() >= deadline {
unsafe { libc::kill(child.as_raw(), libc::SIGTERM) };
let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
return Err(anyhow::anyhow!(
"Wallix menu fetch timed out after {}s without showing a selection prompt",
timeout_secs
));
}
let mut pfd = libc::pollfd {
fd: master_fd,
events: libc::POLLIN,
revents: 0,
};
let rc = unsafe { libc::poll(&mut pfd, 1, 200) };
if rc < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
unsafe { libc::kill(child.as_raw(), libc::SIGTERM) };
let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
return Err(err.into());
}
if rc == 0 {
continue;
}
let mut buf = [0_u8; 4096];
let read = match master_reader.read(&mut buf) {
Ok(0) => break 'outer, Ok(n) => n,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => {
unsafe { libc::kill(child.as_raw(), libc::SIGTERM) };
let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
return Err(e.into());
}
};
let chunk = String::from_utf8_lossy(&buf[..read]);
transcript.push_str(&chunk);
if transcript.len() > 128 * 1024 {
let drain = transcript.len().saturating_sub(128 * 1024);
transcript.drain(..drain);
}
if !auth_injected && contains_ssh_auth_prompt(&transcript) {
if let Some(cred) = auth {
master_writer.write_all(cred.as_bytes())?;
master_writer.write_all(b"\n")?;
master_writer.flush()?;
auth_injected = true;
transcript.clear();
} else {
unsafe { libc::kill(child.as_raw(), libc::SIGTERM) };
let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
return Err(anyhow::anyhow!("SSH_AUTH_REQUIRED: {}", transcript.trim()));
}
continue;
}
if wallix_connected_directly(&transcript) {
unsafe { libc::kill(child.as_raw(), libc::SIGTERM) };
let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
return Err(anyhow::anyhow!("WALLIX_DIRECT_CONNECTION"));
}
if contains_wallix_prompt(&transcript) {
match parse_wallix_page_position(&transcript) {
Some((current, total)) if current < total => {
master_writer.write_all(b"n\n")?;
master_writer.flush()?;
break; }
_ => break 'outer,
}
}
}
}
unsafe {
libc::kill(child.as_raw(), libc::SIGTERM);
}
let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
if std::env::var("SUSSHI_WALLIX_DEBUG").is_ok() {
let _ = std::fs::write("/tmp/susshi-wallix-debug.txt", &transcript);
}
parse_wallix_menu(&transcript, &server.wallix_header_columns)
}
#[cfg(not(unix))]
pub fn fetch_wallix_menu_entries(
_server: &ResolvedServer,
_verbose: bool,
_auth: Option<&str>,
) -> Result<Vec<WallixMenuEntry>> {
anyhow::bail!("Wallix menu fetching is only supported on Unix")
}
pub fn connect_wallix_with_selection(
server: &ResolvedServer,
verbose: bool,
selected_id: &str,
auth: Option<&str>,
) -> Result<()> {
if selected_id == "WALLIX_DIRECT" {
#[cfg(unix)]
{
let args = build_wallix_bastion_args(server, verbose)?;
let mut command = Command::new("ssh");
command.args(&args);
if let Some(cred) = auth {
let p = setup_askpass_script(cred)?;
command.env("SSH_ASKPASS", &p);
command.env("SSH_ASKPASS_REQUIRE", "force");
let result = command.status().map(|_| ()).map_err(anyhow::Error::from);
let _ = std::fs::remove_file(p);
return result;
}
let err = std::os::unix::process::CommandExt::exec(&mut command);
return Err(anyhow::Error::new(err).context("Failed to exec ssh command"));
}
#[cfg(not(unix))]
return connect(server, ConnectionMode::Wallix, verbose, auth);
}
#[cfg(unix)]
{
connect_wallix_via_pty_with_selection(server, verbose, Some(selected_id), auth)
}
#[cfg(not(unix))]
{
let _ = (server, verbose, selected_id, auth);
anyhow::bail!("Wallix menu automation is only supported on Unix")
}
}
pub fn connect_blocking_wallix_with_selection(
server: &ResolvedServer,
verbose: bool,
selected_id: &str,
auth: Option<&str>,
) -> Result<()> {
connect_wallix_with_selection(server, verbose, selected_id, auth)
}