use super::*;
#[cfg(unix)]
pub(crate) fn setup_askpass_script(credential: &str) -> Result<std::path::PathBuf> {
use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
let path = std::env::temp_dir().join(format!("susshi-askpass-{}", std::process::id()));
let escaped = credential.replace('\'', r"'\''");
let script = format!("#!/bin/sh\nprintf '%s\\n' '{}'\n", escaped);
let mut f = std::fs::File::create(&path)?;
f.write_all(script.as_bytes())?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))?;
Ok(path)
}
#[cfg(unix)]
pub(crate) fn wallix_connected_directly(buffer: &str) -> bool {
let clean = crate::wallix::strip_ansi(buffer);
let lower = clean.to_ascii_lowercase();
lower.contains("account successfully checked out")
&& !lower.contains("tapez h pour")
&& !lower.contains("type h for")
}
#[cfg(unix)]
pub(crate) fn current_winsize() -> Option<Winsize> {
let mut winsize = Winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
let rc = unsafe { libc::ioctl(libc::STDIN_FILENO, libc::TIOCGWINSZ, &mut winsize) };
if rc == 0 { Some(winsize) } else { None }
}
#[cfg(unix)]
pub(crate) fn contains_ssh_auth_prompt(buffer: &str) -> bool {
let lower = buffer.to_ascii_lowercase();
lower.contains("enter passphrase for key") || lower.contains("password:")
}
#[cfg(unix)]
pub(crate) fn contains_wallix_prompt(buffer: &str) -> bool {
let clean = crate::wallix::strip_ansi(buffer);
let trimmed = clean.trim_end();
trimmed.ends_with(" >")
|| trimmed.ends_with(">")
|| trimmed.lines().rev().find(|line| !line.trim().is_empty()) == Some(">")
}
#[cfg(unix)]
pub(crate) fn contains_wallix_target_address_prompt(buffer: &str) -> bool {
let clean = crate::wallix::strip_ansi(buffer);
let lowered = clean.to_ascii_lowercase();
lowered.contains("adresse cible")
|| lowered.contains("target address")
|| lowered.contains("destination address")
}
#[cfg(unix)]
pub(crate) fn contains_wallix_return_selector_prompt(buffer: &str) -> bool {
let clean = crate::wallix::strip_ansi(buffer);
let lowered = clean.to_lowercase();
lowered.contains("retour au sélecteur")
|| lowered.contains("retour au selecteur")
|| lowered.contains("return to selector")
}
#[cfg(unix)]
pub(crate) fn parse_wallix_page_position(buffer: &str) -> Option<(u32, u32)> {
let clean = crate::wallix::strip_ansi(buffer);
let lowered = clean.to_ascii_lowercase();
let marker = "page ";
let start = lowered.rfind(marker)? + marker.len();
let tail = &lowered[start..];
let mut current = String::new();
let mut total = String::new();
let mut seen_slash = false;
for character in tail.chars() {
if character.is_ascii_digit() {
if seen_slash {
total.push(character);
} else {
current.push(character);
}
} else if character == '/' && !seen_slash {
seen_slash = true;
} else if !current.is_empty() {
break;
}
}
if current.is_empty() || total.is_empty() {
return None;
}
Some((current.parse().ok()?, total.parse().ok()?))
}
#[cfg(unix)]
pub(crate) fn is_wallix_menu_matching_error(err: &anyhow::Error) -> bool {
let message = err.to_string();
message.contains("No menu entry found with target")
|| message.contains("No menu entry found for matching targets")
|| message.contains("No menu entry found for target")
}
#[cfg(unix)]
pub(crate) fn spawn_wallix_pty(
args: &[String],
) -> Result<(nix::unistd::Pid, std::fs::File, std::fs::File)> {
let mut argv = Vec::with_capacity(args.len() + 2);
argv.push(CString::new("ssh")?);
for arg in args {
argv.push(CString::new(arg.as_str())?);
}
let mut argv_ptrs: Vec<*const libc::c_char> = argv.iter().map(|arg| arg.as_ptr()).collect();
argv_ptrs.push(std::ptr::null());
let winsize = current_winsize();
let fork = unsafe { forkpty(winsize.as_ref(), None) }
.map_err(|err| anyhow::anyhow!("Failed to create PTY for Wallix session: {err}"))?;
match fork {
ForkptyResult::Child => unsafe {
libc::execvp(argv[0].as_ptr(), argv_ptrs.as_ptr());
libc::_exit(127);
},
ForkptyResult::Parent { child, master } => {
let master_reader = std::fs::File::from(master);
let master_writer = master_reader.try_clone()?;
Ok((child, master_reader, master_writer))
}
}
}
#[cfg(unix)]
pub(crate) fn connect_wallix_via_pty_with_selection(
server: &ResolvedServer,
verbose: bool,
selected_id: Option<&str>,
auth: Option<&str>,
) -> Result<()> {
let args = build_wallix_bastion_args(server, verbose)?;
let (child, mut master_reader, mut master_writer) = spawn_wallix_pty(&args)?;
let mut stdout = std::io::stdout().lock();
let mut stdin = std::io::stdin().lock();
let mut transcript = String::new();
let mut selection_completed = false;
let mut target_address_sent = false;
let mut return_selector_prompt_handled = false;
let mut stdin_closed = false;
let hide_menu_output = selected_id.is_some();
let mut auth_prompted = false;
let master_fd = master_reader.as_raw_fd();
let stdin_fd = std::io::stdin().as_raw_fd();
loop {
let mut pollfds = [
libc::pollfd {
fd: master_fd,
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: if stdin_closed || !(selection_completed || auth_prompted && auth.is_none()) {
-1
} else {
stdin_fd
},
events: libc::POLLIN,
revents: 0,
},
];
let rc = unsafe { libc::poll(pollfds.as_mut_ptr(), pollfds.len() as _, 100) };
if rc < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(err.into());
}
if pollfds[0].revents & libc::POLLIN != 0 {
let mut buf = [0_u8; 4096];
let read = master_reader.read(&mut buf)?;
if read == 0 {
break;
}
let chunk = String::from_utf8_lossy(&buf[..read]);
transcript.push_str(&chunk);
if transcript.len() > 64 * 1024 {
let drain = transcript.len().saturating_sub(64 * 1024);
transcript.drain(..drain);
}
if !auth_prompted && !selection_completed && contains_ssh_auth_prompt(&transcript) {
auth_prompted = true;
if let Some(cred) = auth {
master_writer.write_all(cred.as_bytes())?;
master_writer.write_all(b"\n")?;
master_writer.flush()?;
}
}
if !hide_menu_output || selection_completed || (auth_prompted && auth.is_none()) {
stdout.write_all(&buf[..read])?;
stdout.flush()?;
}
if !selection_completed {
if contains_wallix_prompt(&transcript) {
let selection = if let Some(id) = selected_id {
Ok(id.to_string())
} else {
parse_wallix_menu(&transcript, &server.wallix_header_columns)
.and_then(|entries| select_id_for_server(&entries, server))
};
match selection {
Ok(id) => {
master_writer.write_all(id.as_bytes())?;
master_writer.write_all(b"\n")?;
master_writer.flush()?;
selection_completed = true;
}
Err(err) if server.wallix_fail_if_menu_match_error => {
if is_wallix_menu_matching_error(&err) {
if let Some((current, total)) =
parse_wallix_page_position(&transcript)
&& current < total
{
master_writer.write_all(b"n\n")?;
master_writer.flush()?;
transcript.clear();
continue;
}
selection_completed = true;
continue;
}
unsafe {
libc::kill(child.as_raw(), libc::SIGTERM);
}
let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
return Err(err);
}
Err(_) => {
selection_completed = true;
}
}
}
} else if !target_address_sent && contains_wallix_target_address_prompt(&transcript) {
master_writer.write_all(server.host.as_bytes())?;
master_writer.write_all(b"\n")?;
master_writer.flush()?;
target_address_sent = true;
}
if selection_completed
&& !return_selector_prompt_handled
&& contains_wallix_return_selector_prompt(&transcript)
{
master_writer.write_all(b"n\n")?;
master_writer.flush()?;
return_selector_prompt_handled = true;
}
}
if pollfds[1].revents & libc::POLLIN != 0 {
let mut buf = [0_u8; 4096];
let read = stdin.read(&mut buf)?;
if read == 0 {
master_writer.write_all(&[0x04])?;
master_writer.flush()?;
stdin_closed = true;
} else {
master_writer.write_all(&buf[..read])?;
master_writer.flush()?;
}
}
match waitpid(child, Some(WaitPidFlag::WNOHANG)) {
Ok(WaitStatus::StillAlive) => {}
Ok(_) => return Ok(()),
Err(err) => {
return Err(anyhow::anyhow!("Failed to wait for Wallix session: {err}"));
}
}
}
if !selection_completed {
return Err(anyhow::anyhow!(
"Wallix session exited before menu auto-selection completed"
));
}
Ok(())
}