1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-24 22:01:47 +03:00

ssh: use a single window for authenticating a session

This makes it so that we preserve context while showing the connection
status and authentication prompts.
This commit is contained in:
Wez Furlong 2020-01-25 13:00:16 -08:00
parent 1ef95b917a
commit fd8f28960f
8 changed files with 256 additions and 139 deletions

16
Cargo.lock generated
View File

@ -539,6 +539,20 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crossbeam"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e"
dependencies = [
"cfg-if",
"crossbeam-channel 0.4.0",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils 0.7.0",
]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.3.9" version = "0.3.9"
@ -3401,7 +3415,7 @@ dependencies = [
"core-foundation 0.7.0", "core-foundation 0.7.0",
"core-graphics 0.19.0", "core-graphics 0.19.0",
"core-text 15.0.0", "core-text 15.0.0",
"crossbeam-channel 0.3.9", "crossbeam",
"daemonize", "daemonize",
"dirs 1.0.5", "dirs 1.0.5",
"downcast-rs", "downcast-rs",

View File

@ -21,7 +21,7 @@ base64 = "0.10"
base91 = { path = "base91" } base91 = { path = "base91" }
rangeset = { path = "rangeset" } rangeset = { path = "rangeset" }
bitflags = "1.0" bitflags = "1.0"
crossbeam-channel = "0.3" crossbeam = "0.7"
dirs = "1.0" dirs = "1.0"
downcast-rs = "1.0" downcast-rs = "1.0"
euclid = "0.20" euclid = "0.20"

View File

@ -6,7 +6,7 @@ use crate::mux::window::WindowId;
use crate::mux::Mux; use crate::mux::Mux;
use crate::server::listener::spawn_listener; use crate::server::listener::spawn_listener;
use anyhow::{bail, Error}; use anyhow::{bail, Error};
use crossbeam_channel::{unbounded as channel, Receiver}; use crossbeam::channel::{unbounded as channel, Receiver};
use log::info; use log::info;
use promise::*; use promise::*;
use std::rc::Rc; use std::rc::Rc;

View File

@ -9,7 +9,7 @@ use crate::server::tab::ClientTab;
use crate::server::UnixStream; use crate::server::UnixStream;
use crate::ssh::ssh_connect; use crate::ssh::ssh_connect;
use anyhow::{anyhow, bail, Context, Error}; use anyhow::{anyhow, bail, Context, Error};
use crossbeam_channel::TryRecvError; use crossbeam::channel::TryRecvError;
use filedescriptor::{pollfd, AsRawSocketDescriptor}; use filedescriptor::{pollfd, AsRawSocketDescriptor};
use log::info; use log::info;
use portable_pty::{CommandBuilder, NativePtySystem, PtySystem}; use portable_pty::{CommandBuilder, NativePtySystem, PtySystem};

View File

@ -4,7 +4,7 @@ use crate::mux::{Mux, MuxNotification, MuxSubscriber};
use crate::server::codec::*; use crate::server::codec::*;
use crate::server::pollable::*; use crate::server::pollable::*;
use anyhow::{anyhow, bail, Context, Error}; use anyhow::{anyhow, bail, Context, Error};
use crossbeam_channel::TryRecvError; use crossbeam::channel::TryRecvError;
use log::error; use log::error;
use portable_pty::PtySize; use portable_pty::PtySize;
use promise::spawn::spawn_into_main_thread; use promise::spawn::spawn_into_main_thread;

View File

@ -1,6 +1,6 @@
use crate::server::UnixStream; use crate::server::UnixStream;
use anyhow::Error; use anyhow::Error;
use crossbeam_channel::{unbounded as channel, Receiver, Sender, TryRecvError}; use crossbeam::channel::{unbounded as channel, Receiver, Sender, TryRecvError};
use filedescriptor::*; use filedescriptor::*;
use std::cell::RefCell; use std::cell::RefCell;
use std::io::{Read, Write}; use std::io::{Read, Write};

View File

@ -6,6 +6,7 @@ use crate::mux::Mux;
use crate::termwiztermtab; use crate::termwiztermtab;
use anyhow::{anyhow, bail, Context, Error}; use anyhow::{anyhow, bail, Context, Error};
use async_trait::async_trait; use async_trait::async_trait;
use crossbeam::channel::{bounded, Receiver, Sender};
use portable_pty::cmdbuilder::CommandBuilder; use portable_pty::cmdbuilder::CommandBuilder;
use portable_pty::{PtySize, PtySystem}; use portable_pty::{PtySize, PtySystem};
use promise::{Future, Promise}; use promise::{Future, Promise};
@ -14,25 +15,12 @@ use std::io::Write;
use std::net::TcpStream; use std::net::TcpStream;
use std::path::Path; use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use termwiz::cell::{unicode_column_width, AttributeChange, Intensity}; use std::time::Duration;
use termwiz::cell::unicode_column_width;
use termwiz::lineedit::*; use termwiz::lineedit::*;
use termwiz::surface::Change; use termwiz::surface::Change;
use termwiz::terminal::*; use termwiz::terminal::*;
fn password_prompt(
instructions: &str,
prompt: &str,
username: &str,
remote_address: &str,
) -> Option<String> {
let title = "🔐 wezterm: SSH authentication".to_string();
let text = format!(
"🔐 SSH Authentication for {} @ {}\n{}\n",
username, remote_address, instructions
)
.replace("\n", "\r\n");
let prompt = prompt.to_string();
#[derive(Default)] #[derive(Default)]
struct PasswordPromptHost { struct PasswordPromptHost {
history: BasicHistory, history: BasicHistory,
@ -44,11 +32,7 @@ fn password_prompt(
// Rewrite the input so that we can obscure the password // Rewrite the input so that we can obscure the password
// characters when output to the terminal widget // characters when output to the terminal widget
fn highlight_line( fn highlight_line(&self, line: &str, cursor_position: usize) -> (Vec<OutputElement>, usize) {
&self,
line: &str,
cursor_position: usize,
) -> (Vec<OutputElement>, usize) {
let placeholder = "🔑"; let placeholder = "🔑";
let grapheme_count = unicode_column_width(line); let grapheme_count = unicode_column_width(line);
let mut output = vec![]; let mut output = vec![];
@ -58,52 +42,72 @@ fn password_prompt(
(output, unicode_column_width(placeholder) * cursor_position) (output, unicode_column_width(placeholder) * cursor_position)
} }
} }
match promise::spawn::block_on(termwiztermtab::run(60, 10, move |mut term| {
term.render(&[
// Change::Attribute(AttributeChange::Intensity(Intensity::Bold)),
Change::Title(title.to_string()),
Change::Text(text.to_string()),
Change::Attribute(AttributeChange::Intensity(Intensity::Normal)),
])?;
let mut editor = LineEditor::new(term); enum UIRequest {
editor.set_prompt(&format!("{}: ", prompt)); /// Display something
Output(Vec<Change>),
/// Request input
Input {
prompt: String,
echo: bool,
respond: Promise<String>,
},
Close,
}
struct SshUIImpl {
term: termwiztermtab::TermWizTerminal,
rx: Receiver<UIRequest>,
}
impl SshUIImpl {
fn run(&mut self) -> anyhow::Result<()> {
let title = "🔐 wezterm: SSH authentication".to_string();
self.term.render(&[Change::Title(title)])?;
loop {
match self.rx.recv_timeout(Duration::from_millis(200)) {
Ok(UIRequest::Close) => break,
Ok(UIRequest::Output(changes)) => self.term.render(&changes)?,
Ok(UIRequest::Input {
prompt,
echo: true,
mut respond,
}) => {
respond.result(self.input_prompt(&prompt));
}
Ok(UIRequest::Input {
prompt,
echo: false,
mut respond,
}) => {
respond.result(self.password_prompt(&prompt));
}
Err(err) if err.is_timeout() => {}
Err(err) => bail!("recv_timeout: {}", err),
}
}
std::thread::sleep(Duration::new(2, 0));
Ok(())
}
fn password_prompt(&mut self, prompt: &str) -> anyhow::Result<String> {
let mut editor = LineEditor::new(&mut self.term);
editor.set_prompt(prompt);
let mut host = PasswordPromptHost::default(); let mut host = PasswordPromptHost::default();
if let Some(line) = editor.read_line(&mut host)? { if let Some(line) = editor.read_line(&mut host)? {
Ok(line) Ok(line)
} else { } else {
bail!("prompt cancelled"); bail!("password entry was cancelled");
}
})) {
Ok(p) => Some(p),
Err(p) => {
log::error!("failed to prompt for pw: {}", p);
None
}
} }
} }
fn input_prompt( fn input_prompt(&mut self, prompt: &str) -> anyhow::Result<String> {
instructions: &str, let mut editor = LineEditor::new(&mut self.term);
prompt: &str, editor.set_prompt(prompt);
username: &str,
remote_address: &str,
) -> Option<String> {
let title = "🔐 wezterm: SSH authentication".to_string();
let text = format!(
"SSH Authentication for {} @ {}\n{}\n{}\n",
username, remote_address, instructions, prompt
)
.replace("\n", "\r\n");
match promise::spawn::block_on(termwiztermtab::run(60, 10, move |mut term| {
term.render(&[
Change::Title(title.to_string()),
Change::Text(text.to_string()),
Change::Attribute(AttributeChange::Intensity(Intensity::Normal)),
])?;
let mut editor = LineEditor::new(term);
let mut host = NopLineEditorHost::default(); let mut host = NopLineEditorHost::default();
if let Some(line) = editor.read_line(&mut host)? { if let Some(line) = editor.read_line(&mut host)? {
@ -111,21 +115,72 @@ fn input_prompt(
} else { } else {
bail!("prompt cancelled"); bail!("prompt cancelled");
} }
})) {
Ok(p) => Some(p),
Err(p) => {
log::error!("failed to prompt for pw: {}", p);
None
}
} }
} }
struct Prompt<'a> { struct SshUI {
username: &'a str, tx: Sender<UIRequest>,
remote_address: &'a str,
} }
impl<'a> ssh2::KeyboardInteractivePrompt for Prompt<'a> { impl SshUI {
fn new() -> Self {
let (tx, rx) = bounded(16);
promise::spawn::spawn_into_main_thread(termwiztermtab::run(70, 15, move |term| {
let mut ui = SshUIImpl { term, rx };
ui.run()
}));
Self { tx }
}
fn output(&self, changes: Vec<Change>) {
self.tx
.send(UIRequest::Output(changes))
.expect("send to SShUI failed");
}
fn output_str(&self, s: &str) {
let s = s.replace("\n", "\r\n");
self.output(vec![Change::Text(s)]);
}
fn input(&self, prompt: &str) -> anyhow::Result<String> {
let mut promise = Promise::new();
let future = promise.get_future().unwrap();
self.tx
.send(UIRequest::Input {
prompt: prompt.replace("\n", "\r\n"),
echo: true,
respond: promise,
})
.expect("send to SshUI failed");
future.wait()
}
fn password(&self, prompt: &str) -> anyhow::Result<String> {
let mut promise = Promise::new();
let future = promise.get_future().unwrap();
self.tx
.send(UIRequest::Input {
prompt: prompt.replace("\n", "\r\n"),
echo: false,
respond: promise,
})
.expect("send to SshUI failed");
future.wait()
}
fn close(&self) {
self.tx
.send(UIRequest::Close)
.expect("send to SshUI failed");
}
}
impl ssh2::KeyboardInteractivePrompt for SshUI {
fn prompt<'b>( fn prompt<'b>(
&mut self, &mut self,
_username: &str, _username: &str,
@ -135,14 +190,13 @@ impl<'a> ssh2::KeyboardInteractivePrompt for Prompt<'a> {
prompts prompts
.iter() .iter()
.map(|p| { .map(|p| {
let func = if p.echo { self.output_str(&format!("{}\n", instructions));
input_prompt if p.echo {
self.input(&p.text)
} else { } else {
password_prompt self.password(&p.text)
}; }
.unwrap_or_else(|_| String::new())
func(instructions, &p.text, &self.username, &self.remote_address)
.unwrap_or_else(String::new)
}) })
.collect() .collect()
} }
@ -157,7 +211,11 @@ pub fn async_ssh_connect(remote_address: &str, username: &str) -> Future<ssh2::S
future future
} }
pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2::Session> { fn ssh_connect_with_ui(
remote_address: &str,
username: &str,
ui: &mut SshUI,
) -> anyhow::Result<ssh2::Session> {
let mut sess = ssh2::Session::new()?; let mut sess = ssh2::Session::new()?;
let (remote_address, remote_host_name, port) = { let (remote_address, remote_host_name, port) = {
@ -170,8 +228,11 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
} }
}; };
ui.output_str(&format!("Connecting to {}\n", remote_address));
let tcp = TcpStream::connect(&remote_address) let tcp = TcpStream::connect(&remote_address)
.with_context(|| format!("ssh connecting to {}", remote_address))?; .with_context(|| format!("ssh connecting to {}", remote_address))?;
ui.output_str("Connected OK!\n");
tcp.set_nodelay(true)?; tcp.set_nodelay(true)?;
sess.set_tcp_stream(tcp); sess.set_tcp_stream(tcp);
sess.handshake() sess.handshake()
@ -221,38 +282,22 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
match known_hosts.check_port(&remote_host_name, port, key) { match known_hosts.check_port(&remote_host_name, port, key) {
CheckResult::Match => {} CheckResult::Match => {}
CheckResult::NotFound => { CheckResult::NotFound => {
let message = format!( ui.output_str(&format!(
"SSH host {} is not yet trusted.\r\n\ "SSH host {} is not yet trusted.\n\
{:?} Fingerprint: {}.\r\n\ {:?} Fingerprint: {}.\n\
Trust and continue connecting?\r\n", Trust and continue connecting?\n",
remote_address, key_type, fingerprint remote_address, key_type, fingerprint
); ));
let allow =
promise::spawn::block_on(termwiztermtab::run(80, 10, move |mut term| {
let title = "🔐 wezterm: SSH authentication".to_string();
term.render(&[Change::Title(title), Change::Text(message.to_string())])?;
let mut editor = LineEditor::new(term);
editor.set_prompt("Enter [Y/n]> ");
let mut host = NopLineEditorHost::default();
loop { loop {
let line = match editor.read_line(&mut host) { let line = ui.input("Enter [Y/n]> ")?;
Ok(Some(line)) => line,
Ok(None) | Err(_) => return Ok(false),
};
match line.as_ref() { match line.as_ref() {
"y" | "Y" | "yes" | "YES" => return Ok(true), "y" | "Y" | "yes" | "YES" => break,
"n" | "N" | "no" | "NO" => return Ok(false), "n" | "N" | "no" | "NO" => bail!("user declined to trust host"),
_ => continue, _ => continue,
} }
} }
}))?;
if !allow {
bail!("user declined to trust host");
}
known_hosts known_hosts
.add(remote_host_name, key, &remote_address, key_type.into()) .add(remote_host_name, key, &remote_address, key_type.into())
@ -263,11 +308,11 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
.with_context(|| format!("writing known_hosts file {}", file.display()))?; .with_context(|| format!("writing known_hosts file {}", file.display()))?;
} }
CheckResult::Mismatch => { CheckResult::Mismatch => {
termwiztermtab::message_box_ok(&format!( ui.output_str(&format!(
"🛑 host key mismatch for ssh server {}.\n\ "🛑 host key mismatch for ssh server {}.\n\
Got fingerprint {} instead of expected value from known_hosts\n\ Got fingerprint {} instead of expected value from known_hosts\n\
file {}.\n\ file {}.\n\
Refusing to connect.", Refusing to connect.\n",
remote_address, remote_address,
fingerprint, fingerprint,
file.display() file.display()
@ -275,7 +320,7 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
bail!("host mismatch, man in the middle attack?!"); bail!("host mismatch, man in the middle attack?!");
} }
CheckResult::Failure => { CheckResult::Failure => {
termwiztermtab::message_box_ok("🛑 Failed to load and check known ssh hosts"); ui.output_str("🛑 Failed to load and check known ssh hosts\n");
bail!("failed to check the known hosts"); bail!("failed to check the known hosts");
} }
} }
@ -295,24 +340,24 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
if !sess.authenticated() && methods.contains("publickey") { if !sess.authenticated() && methods.contains("publickey") {
if let Err(err) = sess.userauth_agent(&username) { if let Err(err) = sess.userauth_agent(&username) {
log::info!("while attempting agent auth: {}", err); log::info!("while attempting agent auth: {}", err);
} else if sess.authenticated() {
ui.output_str("publickey auth successful!\n");
} }
} }
if !sess.authenticated() && methods.contains("password") { if !sess.authenticated() && methods.contains("password") {
let pass = password_prompt("", "Password", username, &remote_address) ui.output_str(&format!(
.ok_or_else(|| anyhow!("password entry was cancelled"))?; "Password authentication for {}@{}\n",
username, remote_address
));
let pass = ui.password("Password: ")?;
if let Err(err) = sess.userauth_password(username, &pass) { if let Err(err) = sess.userauth_password(username, &pass) {
log::error!("while attempting password auth: {}", err); log::error!("while attempting password auth: {}", err);
} }
} }
if !sess.authenticated() && methods.contains("keyboard-interactive") { if !sess.authenticated() && methods.contains("keyboard-interactive") {
let mut prompt = Prompt { if let Err(err) = sess.userauth_keyboard_interactive(&username, ui) {
username,
remote_address: &remote_address,
};
if let Err(err) = sess.userauth_keyboard_interactive(&username, &mut prompt) {
log::error!("while attempting keyboard-interactive auth: {}", err); log::error!("while attempting keyboard-interactive auth: {}", err);
} }
} }
@ -325,6 +370,21 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
Ok(sess) Ok(sess)
} }
pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2::Session> {
let mut ui = SshUI::new();
let res = ssh_connect_with_ui(remote_address, username, &mut ui);
match res {
Ok(sess) => {
ui.close();
Ok(sess)
}
Err(err) => {
ui.output_str(&format!("\nFailed: {}", err));
Err(err)
}
}
}
pub struct RemoteSshDomain { pub struct RemoteSshDomain {
pty_system: Box<dyn PtySystem>, pty_system: Box<dyn PtySystem>,
id: DomainId, id: DomainId,

View File

@ -13,7 +13,7 @@ use crate::mux::window::WindowId;
use crate::mux::Mux; use crate::mux::Mux;
use anyhow::{bail, Error}; use anyhow::{bail, Error};
use async_trait::async_trait; use async_trait::async_trait;
use crossbeam_channel::{unbounded as channel, Receiver, Sender}; use crossbeam::channel::{unbounded as channel, Receiver, Sender};
use filedescriptor::Pipe; use filedescriptor::Pipe;
use portable_pty::*; use portable_pty::*;
use rangeset::RangeSet; use rangeset::RangeSet;
@ -370,6 +370,48 @@ impl termwiz::terminal::Terminal for TermWizTerminal {
} }
} }
impl termwiz::terminal::Terminal for &mut TermWizTerminal {
fn set_raw_mode(&mut self) -> anyhow::Result<()> {
(**self).set_raw_mode()
}
fn set_cooked_mode(&mut self) -> anyhow::Result<()> {
(**self).set_cooked_mode()
}
fn enter_alternate_screen(&mut self) -> anyhow::Result<()> {
(**self).enter_alternate_screen()
}
fn exit_alternate_screen(&mut self) -> anyhow::Result<()> {
(**self).exit_alternate_screen()
}
fn get_screen_size(&mut self) -> anyhow::Result<ScreenSize> {
(**self).get_screen_size()
}
fn set_screen_size(&mut self, size: ScreenSize) -> anyhow::Result<()> {
(**self).set_screen_size(size)
}
fn render(&mut self, changes: &[Change]) -> anyhow::Result<()> {
(**self).render(changes)
}
fn flush(&mut self) -> anyhow::Result<()> {
(**self).flush()
}
fn poll_input(&mut self, wait: Option<Duration>) -> anyhow::Result<Option<InputEvent>> {
(**self).poll_input(wait)
}
fn waker(&self) -> TerminalWaker {
(**self).waker()
}
}
/// This function spawns a thread and constructs a GUI window with an /// This function spawns a thread and constructs a GUI window with an
/// associated termwiz Terminal object to execute the provided function. /// associated termwiz Terminal object to execute the provided function.
/// The function is expected to run in a loop to manage input and output /// The function is expected to run in a loop to manage input and output
@ -378,7 +420,7 @@ impl termwiz::terminal::Terminal for TermWizTerminal {
/// the return value from the function. /// the return value from the function.
pub async fn run< pub async fn run<
T: Send + 'static, T: Send + 'static,
F: Send + 'static + Fn(TermWizTerminal) -> anyhow::Result<T>, F: Send + 'static + FnOnce(TermWizTerminal) -> anyhow::Result<T>,
>( >(
width: usize, width: usize,
height: usize, height: usize,
@ -457,6 +499,7 @@ pub async fn run<
result result
} }
#[allow(unused)]
pub fn message_box_ok(message: &str) { pub fn message_box_ok(message: &str) {
let title = "wezterm"; let title = "wezterm";
let message = message.to_string(); let message = message.to_string();
@ -497,7 +540,7 @@ pub fn show_configuration_error_message(err: &str) {
]) ])
.map_err(Error::msg)?; .map_err(Error::msg)?;
let mut editor = LineEditor::new(term); let mut editor = LineEditor::new(&mut term);
editor.set_prompt("(press enter to close this window)"); editor.set_prompt("(press enter to close this window)");
let mut host = NopLineEditorHost::default(); let mut host = NopLineEditorHost::default();