2021-03-19 18:07:44 +03:00
|
|
|
//! This is a really basic ssh client example intended
|
|
|
|
//! to test the guts of the ssh handling, rather than
|
|
|
|
//! to be a full fledged replacement for ssh.
|
|
|
|
use anyhow::Context;
|
|
|
|
use portable_pty::{Child, MasterPty, PtySize};
|
|
|
|
use std::io::{Read, Write};
|
|
|
|
use structopt::StructOpt;
|
|
|
|
use termwiz::cell::unicode_column_width;
|
|
|
|
use termwiz::lineedit::*;
|
|
|
|
use wezterm_ssh::{Config, Session, SessionEvent};
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
struct PasswordPromptHost {
|
|
|
|
history: BasicHistory,
|
|
|
|
echo: bool,
|
|
|
|
}
|
|
|
|
impl LineEditorHost for PasswordPromptHost {
|
|
|
|
fn history(&mut self) -> &mut dyn History {
|
|
|
|
&mut self.history
|
|
|
|
}
|
|
|
|
|
|
|
|
fn highlight_line(&self, line: &str, cursor_position: usize) -> (Vec<OutputElement>, usize) {
|
|
|
|
if self.echo {
|
|
|
|
(vec![OutputElement::Text(line.to_string())], cursor_position)
|
|
|
|
} else {
|
|
|
|
// Rewrite the input so that we can obscure the password
|
|
|
|
// characters when output to the terminal widget
|
|
|
|
let placeholder = "🔑";
|
2021-11-25 18:53:07 +03:00
|
|
|
let grapheme_count = unicode_column_width(line, None);
|
2021-03-19 18:07:44 +03:00
|
|
|
let mut output = vec![];
|
|
|
|
for _ in 0..grapheme_count {
|
|
|
|
output.push(OutputElement::Text(placeholder.to_string()));
|
|
|
|
}
|
2021-11-25 18:53:07 +03:00
|
|
|
(
|
|
|
|
output,
|
|
|
|
unicode_column_width(placeholder, None) * cursor_position,
|
|
|
|
)
|
2021-03-19 18:07:44 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, StructOpt, Default, Clone)]
|
|
|
|
struct Opt {
|
|
|
|
#[structopt(long = "user", short = "l")]
|
|
|
|
pub user: Option<String>,
|
|
|
|
pub destination: String,
|
|
|
|
pub cmd: Vec<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
2021-03-27 09:54:57 +03:00
|
|
|
pretty_env_logger::init();
|
2021-03-19 18:07:44 +03:00
|
|
|
let opts = Opt::from_args();
|
|
|
|
|
|
|
|
let mut config = Config::new();
|
|
|
|
config.add_default_config_files();
|
|
|
|
|
|
|
|
let mut config = config.for_host(&opts.destination);
|
|
|
|
if let Some(user) = opts.user.as_ref() {
|
|
|
|
config.insert("user".to_string(), user.to_string());
|
|
|
|
}
|
|
|
|
|
|
|
|
let res = smol::block_on(async move {
|
|
|
|
let (session, events) = Session::connect(config.clone())?;
|
|
|
|
|
|
|
|
while let Ok(event) = events.recv().await {
|
|
|
|
match event {
|
|
|
|
SessionEvent::Banner(banner) => {
|
|
|
|
if let Some(banner) = banner {
|
|
|
|
log::trace!("{}", banner);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
SessionEvent::HostVerify(verify) => {
|
|
|
|
eprintln!("{}", verify.message);
|
|
|
|
let mut terminal = line_editor_terminal()?;
|
|
|
|
let mut editor = LineEditor::new(&mut terminal);
|
|
|
|
let mut host = PasswordPromptHost::default();
|
|
|
|
host.echo = true;
|
|
|
|
editor.set_prompt("Enter [y/n]> ");
|
|
|
|
let ok = if let Some(line) = editor.read_line(&mut host)? {
|
|
|
|
match line.as_ref() {
|
|
|
|
"y" | "Y" | "yes" | "YES" => true,
|
|
|
|
"n" | "N" | "no" | "NO" | _ => false,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
};
|
|
|
|
verify.answer(ok).await.context("send verify response")?;
|
|
|
|
}
|
|
|
|
SessionEvent::Authenticate(auth) => {
|
|
|
|
if !auth.username.is_empty() {
|
|
|
|
eprintln!("Authentication for {}", auth.username);
|
|
|
|
}
|
|
|
|
if !auth.instructions.is_empty() {
|
|
|
|
eprintln!("{}", auth.instructions);
|
|
|
|
}
|
|
|
|
let mut terminal = line_editor_terminal()?;
|
|
|
|
let mut editor = LineEditor::new(&mut terminal);
|
|
|
|
let mut host = PasswordPromptHost::default();
|
|
|
|
let mut answers = vec![];
|
|
|
|
for prompt in &auth.prompts {
|
|
|
|
let mut prompt_lines = prompt.prompt.split('\n').collect::<Vec<_>>();
|
|
|
|
editor.set_prompt(prompt_lines.pop().unwrap());
|
|
|
|
host.echo = prompt.echo;
|
|
|
|
for line in &prompt_lines {
|
|
|
|
eprintln!("{}", line);
|
|
|
|
}
|
|
|
|
if let Some(line) = editor.read_line(&mut host)? {
|
|
|
|
answers.push(line);
|
|
|
|
} else {
|
|
|
|
anyhow::bail!("Authentication was cancelled");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
auth.answer(answers).await?;
|
|
|
|
}
|
|
|
|
SessionEvent::Error(err) => {
|
|
|
|
anyhow::bail!("{}", err);
|
|
|
|
}
|
|
|
|
SessionEvent::Authenticated => break,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let command_line = shell_words::join(&opts.cmd);
|
|
|
|
let command_line = if command_line.is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(command_line.as_str())
|
|
|
|
};
|
|
|
|
|
|
|
|
let (pty, mut child) = session
|
|
|
|
.request_pty("xterm-256color", PtySize::default(), command_line, None)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
let mut reader = pty.try_clone_reader()?;
|
|
|
|
let stdout = std::thread::spawn(move || {
|
|
|
|
let mut buf = [0u8; 8192];
|
|
|
|
let mut stdout = std::io::stdout();
|
|
|
|
while let Ok(len) = reader.read(&mut buf) {
|
|
|
|
if len == 0 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if stdout.write_all(&buf[0..len]).is_err() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Need to separate out the writer so that we can drop
|
|
|
|
// the pty which would otherwise keep the ssh session
|
|
|
|
// thread alive
|
|
|
|
let mut writer = pty.try_clone_writer()?;
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
let mut buf = [0u8; 8192];
|
|
|
|
let mut stdin = std::io::stdin();
|
|
|
|
while let Ok(len) = stdin.read(&mut buf) {
|
|
|
|
if len == 0 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if writer.write_all(&buf[0..len]).is_err() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let status = child.wait()?;
|
|
|
|
let _ = stdout.join();
|
|
|
|
if !status.success() {
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
});
|
|
|
|
|
|
|
|
if let Err(err) = res {
|
|
|
|
eprintln!("{:#}", err);
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
}
|