From e103653923dc07cd8c3ae58faa043a737b62c617 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Sat, 27 Mar 2021 17:49:30 -0700 Subject: [PATCH] RemoteSshDomain now uses wezterm-ssh crate There are a few notable changes as a result: * A number of `.ssh/config` options are now respected; host matching and aliasing and identity file are the main things * The authentication prompt is inline in the window, rather than popping up a separate authentication window Refs: https://github.com/wez/wezterm/issues/457 --- Cargo.lock | 25 +- mux/Cargo.toml | 4 +- mux/src/ssh.rs | 675 +++++++++++++++++++++++++++++++++++-- mux/src/termwiztermtab.rs | 2 +- wezterm-client/Cargo.toml | 2 +- wezterm-gui/Cargo.toml | 1 + wezterm-gui/src/main.rs | 21 +- wezterm-ssh/Cargo.toml | 2 +- wezterm-ssh/src/auth.rs | 100 +++--- wezterm-ssh/src/pty.rs | 2 +- wezterm-ssh/src/session.rs | 43 ++- 11 files changed, 764 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 424c2fa00..330460486 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" [[package]] name = "approx" @@ -872,7 +872,7 @@ dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", "lazy_static", - "memoffset 0.6.1", + "memoffset 0.6.2", "scopeguard", ] @@ -1847,7 +1847,7 @@ dependencies = [ [[package]] name = "libssh2-sys" version = "0.2.21" -source = "git+https://github.com/wez/ssh2-rs.git?branch=win32ssl#200d08770f11a438a0f24070db2ea6db2c37c847" +source = "git+https://github.com/wez/ssh2-rs.git?branch=win32ssl#c65067040c97a0cf7f96c69d6fc87764a32c34ae" dependencies = [ "cc", "libc", @@ -2030,9 +2030,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" +checksum = "cc14fc54a812b4472b4113facc3e44d099fbc0ea2ce0551fa5c703f8edfbfd38" dependencies = [ "autocfg", ] @@ -2164,6 +2164,7 @@ dependencies = [ "ratelim", "regex", "serde", + "smol", "ssh2", "sysinfo", "terminfo", @@ -2173,6 +2174,7 @@ dependencies = [ "tmux-cc", "unicode-segmentation", "url", + "wezterm-ssh", "wezterm-term", ] @@ -3194,9 +3196,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287f3c3f8236abb92d8b7e36797f19159df4b58f0a658cc3fb6dd3004b1f3bd3" +checksum = "8fddb3b23626145d1776addfc307e1a1851f60ef6ca64f376bcb889697144cf0" dependencies = [ "bytemuck", ] @@ -3606,7 +3608,7 @@ checksum = "1fccf17fd09e2455ea796d2ad267b64fa2c5cbd8701b2a93b555d2aa73449f7d" [[package]] name = "ssh2" version = "0.9.1" -source = "git+https://github.com/wez/ssh2-rs.git?branch=win32ssl#200d08770f11a438a0f24070db2ea6db2c37c847" +source = "git+https://github.com/wez/ssh2-rs.git?branch=win32ssl#c65067040c97a0cf7f96c69d6fc87764a32c34ae" dependencies = [ "bitflags", "libc", @@ -3678,9 +3680,9 @@ checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" [[package]] name = "syn" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" +checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663" dependencies = [ "proc-macro2", "quote", @@ -4496,6 +4498,7 @@ dependencies = [ "wezterm-font", "wezterm-gui-subcommands", "wezterm-mux-server-impl", + "wezterm-ssh", "wezterm-term", "wezterm-toast-notification", "winapi 0.3.9", diff --git a/mux/Cargo.toml b/mux/Cargo.toml index 463a07689..290b01bca 100644 --- a/mux/Cargo.toml +++ b/mux/Cargo.toml @@ -25,7 +25,8 @@ rangeset = { path = "../rangeset" } ratelim= { path = "../ratelim" } regex = "1" serde = {version="1.0", features = ["rc", "derive"]} -ssh2 = {version="0.9", features=["openssl-on-win32"]} +smol = "1.2" +ssh2 = "0.9" terminfo = "0.7" termwiz = { path = "../termwiz" } textwrap = "0.13" @@ -33,6 +34,7 @@ thiserror = "1.0" tmux-cc = { path = "../tmux-cc" } unicode-segmentation = "1.7" url = "2" +wezterm-ssh = { path = "../wezterm-ssh" } wezterm-term = { path = "../term", features=["use_serde"] } [target.'cfg(all(windows, target_os="linux", target_os="macos"))'.dependencies] diff --git a/mux/src/ssh.rs b/mux/src/ssh.rs index 757ebe040..ceadd53b5 100644 --- a/mux/src/ssh.rs +++ b/mux/src/ssh.rs @@ -7,14 +7,52 @@ use crate::window::WindowId; use crate::Mux; use anyhow::{anyhow, bail, Context, Error}; use async_trait::async_trait; +use filedescriptor::{socketpair, FileDescriptor}; use portable_pty::cmdbuilder::CommandBuilder; -use portable_pty::{PtySize, PtySystem}; +use portable_pty::{ExitStatus, MasterPty, PtySize}; use promise::{Future, Promise}; -use std::collections::HashSet; -use std::io::Write; +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; +use std::io::{BufWriter, Read, Write}; use std::net::TcpStream; use std::path::Path; use std::rc::Rc; +use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError}; +use std::time::Duration; +use termwiz::cell::unicode_column_width; +use termwiz::input::{InputEvent, InputParser}; +use termwiz::lineedit::*; +use termwiz::render::terminfo::TerminfoRenderer; +use termwiz::surface::Change; +use termwiz::terminal::{ScreenSize, Terminal, TerminalWaker}; +use wezterm_ssh::{Session, SessionEvent, SshChildProcess, SshPty}; + +#[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, 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 = "🔑"; + let grapheme_count = unicode_column_width(line); + let mut output = vec![]; + for _ in 0..grapheme_count { + output.push(OutputElement::Text(placeholder.to_string())); + } + (output, unicode_column_width(placeholder) * cursor_position) + } + } +} impl ssh2::KeyboardInteractivePrompt for ConnectionUI { fn prompt<'b>( @@ -217,21 +255,284 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result, + ssh_config: BTreeMap, + session: Session, id: DomainId, name: String, + events: RefCell>>, } impl RemoteSshDomain { - pub fn with_pty_system(name: &str, pty_system: Box) -> Self { + pub fn with_ssh_config( + name: &str, + ssh_config: BTreeMap, + ) -> anyhow::Result { let id = alloc_domain_id(); - Self { - pty_system, + let (session, events) = Session::connect(ssh_config.clone())?; + Ok(Self { + ssh_config, id, name: format!("SSH to {}", name), + session, + events: RefCell::new(Some(events)), + }) + } +} + +/// Carry out the authentication process and create the initial pty. +fn connect_ssh_session( + session: Session, + events: smol::channel::Receiver, + mut stdin_read: BoxedReader, + stdin_tx: Sender, + stdout_write: &mut BufWriter, + stdout_tx: Sender, + child_tx: Sender, + pty_tx: Sender, + ssh_config: BTreeMap, + size: PtySize, + command_line: Option, + env: HashMap, +) -> anyhow::Result<()> { + struct StdoutShim<'a> { + size: PtySize, + stdout: &'a mut BufWriter, + } + + impl<'a> Write for StdoutShim<'a> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.stdout.write(buf) + } + fn flush(&mut self) -> std::io::Result<()> { + self.stdout.flush() } } + + impl<'a> termwiz::render::RenderTty for StdoutShim<'a> { + fn get_size_in_cells(&mut self) -> termwiz::Result<(usize, usize)> { + Ok((self.size.cols as _, self.size.rows as _)) + } + } + + /// a termwiz Terminal for use with the line editor + struct TerminalShim<'a> { + stdout: &'a mut StdoutShim<'a>, + stdin: &'a mut BoxedReader, + size: PtySize, + renderer: TerminfoRenderer, + parser: InputParser, + input_queue: VecDeque, + } + + impl<'a> termwiz::terminal::Terminal for TerminalShim<'a> { + fn set_raw_mode(&mut self) -> termwiz::Result<()> { + use termwiz::escape::csi::{DecPrivateMode, DecPrivateModeCode, Mode, CSI}; + + macro_rules! decset { + ($variant:ident) => { + write!( + self.stdout, + "{}", + CSI::Mode(Mode::SetDecPrivateMode(DecPrivateMode::Code( + DecPrivateModeCode::$variant + ))) + )?; + }; + } + + decset!(BracketedPaste); + self.flush()?; + + Ok(()) + } + + fn flush(&mut self) -> termwiz::Result<()> { + self.stdout.flush()?; + Ok(()) + } + + fn set_cooked_mode(&mut self) -> termwiz::Result<()> { + Ok(()) + } + + fn enter_alternate_screen(&mut self) -> termwiz::Result<()> { + termwiz::bail!("TerminalShim has no alt screen"); + } + + fn exit_alternate_screen(&mut self) -> termwiz::Result<()> { + termwiz::bail!("TerminalShim has no alt screen"); + } + + fn get_screen_size(&mut self) -> termwiz::Result { + Ok(ScreenSize { + cols: self.size.cols as _, + rows: self.size.rows as _, + xpixel: self.size.pixel_width as _, + ypixel: self.size.pixel_height as _, + }) + } + + fn set_screen_size(&mut self, _size: ScreenSize) -> termwiz::Result<()> { + termwiz::bail!("TerminalShim cannot set screen size"); + } + + fn render(&mut self, changes: &[Change]) -> termwiz::Result<()> { + self.renderer.render_to(changes, self.stdout)?; + Ok(()) + } + + fn poll_input(&mut self, _wait: Option) -> termwiz::Result> { + if let Some(event) = self.input_queue.pop_front() { + return Ok(Some(event)); + } + + let mut buf = [0u8; 64]; + let n = self.stdin.read(&mut buf)?; + let input_queue = &mut self.input_queue; + self.parser + .parse(&buf[0..n], |evt| input_queue.push_back(evt), n == buf.len()); + Ok(self.input_queue.pop_front()) + } + + fn waker(&self) -> TerminalWaker { + // TODO: TerminalWaker assumes that we're a SystemTerminal but that + // isn't the case here. + panic!("TerminalShim::waker called!?"); + } + } + + let renderer = crate::termwiztermtab::new_wezterm_terminfo_renderer(); + let mut shim = TerminalShim { + stdout: &mut StdoutShim { + stdout: stdout_write, + size, + }, + size, + renderer, + stdin: &mut stdin_read, + parser: InputParser::new(), + input_queue: VecDeque::new(), + }; + + impl<'a> TerminalShim<'a> { + fn output_line(&mut self, s: &str) -> termwiz::Result<()> { + let mut s = s.replace("\n", "\r\n"); + s.push_str("\r\n"); + self.render(&[Change::Text(s)]) + } + } + + // Process authentication related events + while let Ok(event) = smol::block_on(events.recv()) { + match event { + SessionEvent::Banner(banner) => { + if let Some(banner) = banner { + shim.output_line(&banner)?; + } + } + SessionEvent::HostVerify(verify) => { + shim.output_line(&verify.message)?; + let mut editor = LineEditor::new(&mut shim); + 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 + }; + smol::block_on(verify.answer(ok)).context("send verify response")?; + } + SessionEvent::Authenticate(auth) => { + if !auth.username.is_empty() { + shim.output_line(&format!("Authentication for {}", auth.username))?; + } + if !auth.instructions.is_empty() { + shim.output_line(&auth.instructions)?; + } + let mut answers = vec![]; + for prompt in &auth.prompts { + let mut prompt_lines = prompt.prompt.split('\n').collect::>(); + let editor_prompt = prompt_lines.pop().unwrap(); + for line in &prompt_lines { + shim.output_line(line)?; + } + let mut editor = LineEditor::new(&mut shim); + let mut host = PasswordPromptHost::default(); + editor.set_prompt(editor_prompt); + host.echo = prompt.echo; + if let Some(line) = editor.read_line(&mut host)? { + answers.push(line); + } else { + anyhow::bail!("Authentication was cancelled"); + } + } + smol::block_on(auth.answer(answers))?; + } + SessionEvent::Error(err) => { + shim.output_line(&format!("Error: {}", err))?; + } + SessionEvent::Authenticated => { + // Our session has been authenticated: we can now + // set up the real pty for the pane + match smol::block_on(session.request_pty( + &config::configuration().term, + size, + command_line.as_ref().map(|s| s.as_str()), + Some(env), + )) { + Err(err) => { + shim.output_line(&format!("Failed to spawn command: {:#}", err))?; + break; + } + Ok((pty, child)) => { + drop(shim); + + // Obtain the real stdin/stdout for the pty + let reader = pty.try_clone_reader()?; + let writer = pty.try_clone_writer()?; + + // And send them to the wrapped reader/writer + stdin_tx + .send(Box::new(writer)) + .map_err(|e| anyhow!("{:#}", e))?; + stdout_tx + .send(Box::new(reader)) + .map_err(|e| anyhow!("{:#}", e))?; + + // Likewise, send the real pty and child to + // the wrappers + pty_tx.send(pty)?; + child_tx.send(child)?; + + // Now when we return, our stdin_read and + // stdout_write will close and that will cause + // the PtyReader and PtyWriter to recv the + // the new reader/writer above and continue. + // + // The pty and child will be picked up when + // they are next polled or resized. + + return Ok(()); + } + } + } + } + } + + Ok(()) } #[async_trait(?Send)] @@ -243,34 +544,127 @@ impl Domain for RemoteSshDomain { _command_dir: Option, window: WindowId, ) -> Result, Error> { - let mut cmd = match command { + let pane_id = alloc_pane_id(); + + let cmd = match command { Some(c) => c, None => CommandBuilder::new_default_prog(), }; - let pair = self.pty_system.openpty(size)?; - let pane_id = alloc_pane_id(); - cmd.env("WEZTERM_PANE", pane_id.to_string()); - let child = pair.slave.spawn_command(cmd)?; - log::trace!("spawned: {:?}", child); - let writer = pair.master.try_clone_writer()?; + let command_line = if cmd.is_default_prog() { + None + } else { + Some(cmd.as_unix_command_line()?) + }; + let mut env: HashMap = cmd + .iter_env_as_str() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + env.insert("WEZTERM_PANE".to_string(), pane_id.to_string()); + + let pty: Box; + let child: Box; + let writer: BoxedWriter; + + if let Some(events) = self.events.borrow_mut().take() { + // We get to establish the session! + // + // Since we want spawn to return the Pane in which + // we'll carry out interactive auth, we generate + // some shim/wrapper versions of the pty, child + // and reader/writer. + + let (stdout_read, stdout_write) = socketpair()?; + let (reader_tx, reader_rx) = channel(); + let (stdin_read, stdin_write) = socketpair()?; + let (writer_tx, writer_rx) = channel(); + + let pty_reader = PtyReader { + reader: Box::new(stdout_read), + rx: reader_rx, + }; + + let pty_writer = PtyWriter { + writer: Box::new(stdin_write), + rx: writer_rx, + }; + writer = Box::new(pty_writer); + + let (child_tx, child_rx) = channel(); + + child = Box::new(WrappedSshChild { + child: None, + rx: child_rx, + exited: None, + }); + + let (pty_tx, pty_rx) = channel(); + + pty = Box::new(WrappedSshPty { + inner: RefCell::new(WrappedSshPtyInner::Connecting { + size, + reader: Some(pty_reader), + connected: pty_rx, + }), + }); + + // And with those created, we can now spawn a new thread + // to perform the blocking (from its perspective) terminal + // UI to carry out any authentication. + let ssh_config = self.ssh_config.clone(); + let session = self.session.clone(); + let stdin_read: BoxedReader = Box::new(stdin_read); + let mut stdout_write = BufWriter::new(stdout_write); + std::thread::spawn(move || { + if let Err(err) = connect_ssh_session( + session, + events, + stdin_read, + writer_tx, + &mut stdout_write, + reader_tx, + child_tx, + pty_tx, + ssh_config, + size, + command_line, + env, + ) { + let _ = write!(stdout_write, "{:#}", err); + log::error!("Failed to connect ssh: {:#}", err); + } + let _ = stdout_write.flush(); + }); + } else { + let (concrete_pty, concrete_child) = self + .session + .request_pty( + &config::configuration().term, + size, + command_line.as_ref().map(|s| s.as_str()), + Some(env), + ) + .await?; + + pty = Box::new(concrete_pty); + child = Box::new(concrete_child); + writer = Box::new(pty.try_clone_writer()?); + }; + + // Wrap up the pty etc. in a LocalPane. That allows for + // eg: tmux integration to be tunnelled via the remote + // session without duplicating a lot of logic over here. let terminal = wezterm_term::Terminal::new( crate::pty_size_to_terminal_size(size), std::sync::Arc::new(config::TermConfig {}), "WezTerm", config::wezterm_version(), - Box::new(writer), + writer, ); let mux = Mux::get().unwrap(); - let pane: Rc = Rc::new(LocalPane::new( - pane_id, - terminal, - child, - pair.master, - self.id, - )); + let pane: Rc = Rc::new(LocalPane::new(pane_id, terminal, child, pty, self.id)); let tab = Rc::new(Tab::new(&size)); tab.assign_pane(&pane); @@ -311,3 +705,240 @@ impl Domain for RemoteSshDomain { DomainState::Attached } } + +#[derive(Debug)] +struct WrappedSshChild { + child: Option, + rx: Receiver, + exited: Option, +} + +impl WrappedSshChild { + fn check_connected(&mut self) { + if self.child.is_none() { + match self.rx.try_recv() { + Ok(c) => { + self.child.replace(c); + } + Err(TryRecvError::Empty) => {} + Err(err) => { + log::error!("WrappedSshChild err: {:#?}", err); + self.exited.replace(ExitStatus::with_exit_code(1)); + } + } + } + } +} + +impl portable_pty::Child for WrappedSshChild { + fn try_wait(&mut self) -> std::io::Result> { + if let Some(status) = self.exited.as_ref() { + return Ok(Some(status.clone())); + } + + self.check_connected(); + + if let Some(child) = self.child.as_mut() { + child.try_wait() + } else if let Some(status) = self.exited.as_ref() { + Ok(Some(status.clone())) + } else { + Ok(None) + } + } + + fn kill(&mut self) -> std::io::Result<()> { + // There is no way to send a signal via libssh2. + // Just pretend that we did. :-/ + Ok(()) + } + + fn wait(&mut self) -> std::io::Result { + if let Some(status) = self.exited.as_ref() { + return Ok(status.clone()); + } + + self.check_connected(); + + if let Some(child) = self.child.as_mut() { + child.wait() + } else { + match self.rx.recv() { + Ok(c) => { + self.child.replace(c); + self.child.as_mut().unwrap().wait() + } + Err(_) => { + self.exited.replace(ExitStatus::with_exit_code(1)); + return Ok(self.exited.as_ref().cloned().unwrap()); + } + } + } + } + + fn process_id(&self) -> Option { + None + } +} + +type BoxedReader = Box<(dyn Read + Send + 'static)>; +type BoxedWriter = Box<(dyn Write + Send + 'static)>; + +struct WrappedSshPty { + inner: RefCell, +} + +enum WrappedSshPtyInner { + Connecting { + reader: Option, + connected: Receiver, + size: PtySize, + }, + Connected { + reader: Option, + pty: SshPty, + }, +} + +struct PtyReader { + reader: BoxedReader, + rx: Receiver, +} + +struct PtyWriter { + writer: BoxedWriter, + rx: Receiver, +} + +impl std::io::Write for WrappedSshPty { + fn write(&mut self, _buf: &[u8]) -> std::io::Result { + log::error!("boo"); + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "you are expected to write via try_clone_writer", + )) + } + + fn flush(&mut self) -> std::io::Result<()> { + log::error!("boo"); + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "you are expected to write via try_clone_writer", + )) + } +} + +impl WrappedSshPtyInner { + fn check_connected(&mut self) -> anyhow::Result<()> { + match self { + Self::Connecting { + reader, + connected, + size, + .. + } => { + if let Ok(pty) = connected.try_recv() { + let res = pty.resize(*size); + *self = Self::Connected { + pty, + reader: reader.take(), + }; + res + } else { + Ok(()) + } + } + _ => Ok(()), + } + } +} + +impl portable_pty::MasterPty for WrappedSshPty { + fn resize(&self, new_size: PtySize) -> anyhow::Result<()> { + let mut inner = self.inner.borrow_mut(); + match &mut *inner { + WrappedSshPtyInner::Connecting { ref mut size, .. } => { + *size = new_size; + inner.check_connected() + } + WrappedSshPtyInner::Connected { pty, .. } => pty.resize(new_size), + } + } + + fn get_size(&self) -> anyhow::Result { + let mut inner = self.inner.borrow_mut(); + match &*inner { + WrappedSshPtyInner::Connecting { size, .. } => { + let size = *size; + inner.check_connected()?; + Ok(size) + } + WrappedSshPtyInner::Connected { pty, .. } => pty.get_size(), + } + } + + fn try_clone_reader(&self) -> anyhow::Result> { + let mut inner = self.inner.borrow_mut(); + inner.check_connected()?; + match &mut *inner { + WrappedSshPtyInner::Connected { ref mut reader, .. } + | WrappedSshPtyInner::Connecting { ref mut reader, .. } => match reader.take() { + Some(r) => Ok(Box::new(r)), + None => anyhow::bail!("reader already taken"), + }, + } + } + + fn try_clone_writer(&self) -> anyhow::Result> { + anyhow::bail!("writer must be created during bootstrap"); + } + + fn process_group_leader(&self) -> Option { + let mut inner = self.inner.borrow_mut(); + let _ = inner.check_connected(); + None + } +} + +impl std::io::Write for PtyWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + match self.writer.write(buf) { + Ok(len) if len > 0 => Ok(len), + res => match self.rx.recv() { + Ok(writer) => { + self.writer = writer; + self.writer.write(buf) + } + _ => res, + }, + } + } + + fn flush(&mut self) -> std::io::Result<()> { + match self.writer.flush() { + Ok(_) => Ok(()), + res => match self.rx.recv() { + Ok(writer) => { + self.writer = writer; + self.writer.flush() + } + _ => res, + }, + } + } +} + +impl std::io::Read for PtyReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self.reader.read(buf) { + Ok(len) if len > 0 => Ok(len), + res => match self.rx.recv() { + Ok(reader) => { + self.reader = reader; + self.reader.read(buf) + } + _ => res, + }, + } + } +} diff --git a/mux/src/termwiztermtab.rs b/mux/src/termwiztermtab.rs index 492259b7c..74d2bad51 100644 --- a/mux/src/termwiztermtab.rs +++ b/mux/src/termwiztermtab.rs @@ -415,7 +415,7 @@ pub fn allocate(size: PtySize) -> (TermWizTerminal, Rc) { (tw_term, pane) } -fn new_wezterm_terminfo_renderer() -> TerminfoRenderer { +pub(crate) fn new_wezterm_terminfo_renderer() -> TerminfoRenderer { let data = include_bytes!("../../termwiz/data/xterm-256color"); let db = terminfo::Database::from_buffer(&data[..]).unwrap(); diff --git a/wezterm-client/Cargo.toml b/wezterm-client/Cargo.toml index 5d464a13f..07cae93e8 100644 --- a/wezterm-client/Cargo.toml +++ b/wezterm-client/Cargo.toml @@ -20,7 +20,7 @@ promise = { path = "../promise" } rangeset = { path = "../rangeset" } ratelim= { path = "../ratelim" } smol = "1.2" -ssh2 = {version="0.9", features=["openssl-on-win32"]} +ssh2 = "0.9" thiserror = "1.0" url = "2" wezterm-term = { path = "../term", features=["use_serde"] } diff --git a/wezterm-gui/Cargo.toml b/wezterm-gui/Cargo.toml index 5e8b3e9fd..f8e1092fb 100644 --- a/wezterm-gui/Cargo.toml +++ b/wezterm-gui/Cargo.toml @@ -65,6 +65,7 @@ wezterm-client = { path = "../wezterm-client" } wezterm-font = { path = "../wezterm-font" } wezterm-gui-subcommands = { path = "../wezterm-gui-subcommands" } wezterm-mux-server-impl = { path = "../wezterm-mux-server-impl" } +wezterm-ssh = { path = "../wezterm-ssh" } wezterm-term = { path = "../term", features=["use_serde"] } wezterm-toast-notification = { path = "../wezterm-toast-notification" } window = { path = "../window", features=["wayland"]} diff --git a/wezterm-gui/src/main.rs b/wezterm-gui/src/main.rs index 2121c16d9..067b9ccfb 100644 --- a/wezterm-gui/src/main.rs +++ b/wezterm-gui/src/main.rs @@ -15,6 +15,7 @@ use std::sync::Arc; use structopt::StructOpt; use wezterm_client::domain::{ClientDomain, ClientDomainConfig}; use wezterm_gui_subcommands::*; +use wezterm_ssh::*; use wezterm_toast_notification::*; mod frontend; @@ -86,11 +87,14 @@ enum SubCommand { } async fn async_run_ssh(opts: SshCommand) -> anyhow::Result<()> { - // Establish the connection; it may show UI for authentication - let params = &opts.user_at_host_and_port; - let sess = mux::ssh::async_ssh_connect(¶ms.host_and_port, ¶ms.username).await?; - // Now we have a connected session, set up the ssh domain and make it - // the default domain + let mut ssh_config = Config::new(); + ssh_config.add_default_config_files(); + let mut ssh_config = ssh_config.for_host(&opts.user_at_host_and_port.host_and_port); + ssh_config.insert( + "user".to_string(), + opts.user_at_host_and_port.username.to_string(), + ); + let _gui = front_end().unwrap(); let cmd = if !opts.prog.is_empty() { @@ -101,11 +105,10 @@ async fn async_run_ssh(opts: SshCommand) -> anyhow::Result<()> { }; let config = config::configuration(); - let pty_system = Box::new(portable_pty::ssh::SshSession::new(sess, &config.term)); - let domain: Arc = Arc::new(mux::ssh::RemoteSshDomain::with_pty_system( + let domain: Arc = Arc::new(mux::ssh::RemoteSshDomain::with_ssh_config( &opts.user_at_host_and_port.to_string(), - pty_system, - )); + ssh_config, + )?); let mux = Mux::get().unwrap(); mux.add_domain(&domain); diff --git a/wezterm-ssh/Cargo.toml b/wezterm-ssh/Cargo.toml index d25c835da..957484e28 100644 --- a/wezterm-ssh/Cargo.toml +++ b/wezterm-ssh/Cargo.toml @@ -15,7 +15,7 @@ log = "0.4" portable-pty = { path = "../pty" } regex = "1" smol = "1.2" -ssh2 = "0.9" +ssh2 = {version="0.9", features=["openssl-on-win32"]} [dev-dependencies] k9 = "0.11.0" diff --git a/wezterm-ssh/src/auth.rs b/wezterm-ssh/src/auth.rs index b264b1516..4425ca35d 100644 --- a/wezterm-ssh/src/auth.rs +++ b/wezterm-ssh/src/auth.rs @@ -32,12 +32,17 @@ impl crate::session::SessionInner { fn agent_auth(&mut self, sess: &ssh2::Session, user: &str) -> anyhow::Result { if let Some(only) = self.config.get("identitiesonly") { if only == "yes" { + log::trace!("Skipping agent auth because identitiesonly=yes"); return Ok(false); } } let mut agent = sess.agent()?; - agent.connect()?; + if agent.connect().is_err() { + // If the agent is around, we can proceed with other methods + return Ok(false); + } + agent.list_identities()?; let identities = agent.identities()?; for identity in identities { @@ -45,6 +50,7 @@ impl crate::session::SessionInner { return Ok(true); } } + Ok(false) } @@ -63,55 +69,55 @@ impl crate::session::SessionInner { continue; } - let pubkey = if pubkey.exists() && false { + let pubkey = if pubkey.exists() { Some(pubkey.as_ref()) } else { None }; + // We try with no passphrase first, in case the key is unencrypted match sess.userauth_pubkey_file(user, pubkey, &file, None) { - Ok(_) => return Ok(true), - Err(err) => { - if err.code() == ssh2::ErrorCode::Session(-16) - || err.code() == ssh2::ErrorCode::Session(-18) - { - // Need a passphrase to decrypt the key + Ok(_) => { + log::info!("pubkey_file immediately ok for {}", file.display()); + return Ok(true); + } + Err(_) => { + // Most likely cause of error is that we need a passphrase + // to decrypt the key, so let's prompt the user for one. + let (reply, answers) = bounded(1); + self.tx_event + .try_send(SessionEvent::Authenticate(AuthenticationEvent { + username: "".to_string(), + instructions: "".to_string(), + prompts: vec![AuthenticationPrompt { + prompt: format!( + "Passphrase to decrypt {} for {}@{}:\n> ", + file.display(), + user, + host + ), + echo: false, + }], + reply, + })) + .context("sending Authenticate request to user")?; - let (reply, answers) = bounded(1); - self.tx_event - .try_send(SessionEvent::Authenticate(AuthenticationEvent { - username: "".to_string(), - instructions: "".to_string(), - prompts: vec![AuthenticationPrompt { - prompt: format!( - "Passphrase to decrypt {} for {}@{}: ", - file.display(), - user, - host - ), - echo: false, - }], - reply, - })) - .context("sending Authenticate request to user")?; + let answers = smol::block_on(answers.recv()) + .context("waiting for authentication answers from user")?; - let answers = smol::block_on(answers.recv()) - .context("waiting for authentication answers from user")?; + if answers.is_empty() { + anyhow::bail!("user cancelled authentication"); + } - if answers.is_empty() { - anyhow::bail!("user cancelled authentication"); + let passphrase = &answers[0]; + + match sess.userauth_pubkey_file(user, pubkey, &file, Some(passphrase)) { + Ok(_) => { + return Ok(true); } - - let passphrase = &answers[0]; - - match sess.userauth_pubkey_file(user, pubkey, &file, Some(passphrase)) { - Ok(_) => return Ok(true), - Err(err) => { - log::warn!("pubkey auth: {:#}", err); - } + Err(err) => { + log::warn!("pubkey auth: {:#}", err); } - } else { - log::warn!("pubkey auth: {:#}", err); } } } @@ -138,20 +144,12 @@ impl crate::session::SessionInner { log::trace!("ssh auth methods: {:?}", methods); if !sess.authenticated() && methods.contains("publickey") { - match self.agent_auth(sess, user) { - Ok(true) => continue, - Ok(false) => {} - Err(err) => { - log::warn!("while attempting agent auth: {}", err) - } + if self.agent_auth(sess, user)? { + continue; } - match self.pubkey_auth(sess, user, host) { - Ok(true) => continue, - Ok(false) => {} - Err(err) => { - log::warn!("while attempting auth: {}", err) - } + if self.pubkey_auth(sess, user, host)? { + continue; } } diff --git a/wezterm-ssh/src/pty.rs b/wezterm-ssh/src/pty.rs index a79bd2a48..b5c57be99 100644 --- a/wezterm-ssh/src/pty.rs +++ b/wezterm-ssh/src/pty.rs @@ -160,7 +160,7 @@ impl crate::session::SessionInner { // Depending on the server configuration, a given // setenv request may not succeed, but that doesn't // prevent the connection from being set up. - log::error!("ssh: setenv {}={} failed: {}", key, val, err); + log::warn!("ssh: setenv {}={} failed: {}", key, val, err); } } } diff --git a/wezterm-ssh/src/session.rs b/wezterm-ssh/src/session.rs index f038eb01f..d92900b4b 100644 --- a/wezterm-ssh/src/session.rs +++ b/wezterm-ssh/src/session.rs @@ -115,6 +115,7 @@ impl SessionInner { let mut sess = ssh2::Session::new()?; // sess.trace(ssh2::TraceFlags::all()); + sess.set_blocking(true); sess.set_tcp_stream(tcp); sess.handshake() .with_context(|| format!("ssh handshake with {}", remote_address))?; @@ -138,6 +139,8 @@ impl SessionInner { } fn request_loop(&mut self, sess: ssh2::Session) -> anyhow::Result<()> { + let mut sleep_delay = Duration::from_millis(100); + loop { self.tick_io()?; self.drain_request_pipe(); @@ -181,9 +184,13 @@ impl SessionInner { } } - poll(&mut poll_array, Some(Duration::from_secs(1))).context("poll")?; + poll(&mut poll_array, Some(sleep_delay)).context("poll")?; + sleep_delay += sleep_delay; for (idx, poll) in poll_array.iter().enumerate() { + if poll.revents != 0 { + sleep_delay = Duration::from_millis(100); + } if idx == 0 || idx == 1 { // Dealt with at the top of the loop } else if poll.revents != 0 { @@ -229,7 +236,7 @@ impl SessionInner { fn tick_io(&mut self) -> anyhow::Result<()> { for chan in self.channels.values_mut() { if chan.exit.is_some() { - if chan.channel.wait_close().is_ok() { + if chan.channel.eof() && chan.channel.wait_close().is_ok() { fn has_signal(chan: &ssh2::Channel) -> Option { if let Ok(sig) = chan.exit_signal() { if sig.exit_signal.is_some() { @@ -298,9 +305,7 @@ impl SessionInner { } fn dispatch_pending_requests(&mut self, sess: &ssh2::Session) -> anyhow::Result<()> { - sess.set_blocking(true); while self.dispatch_one_request(sess)? {} - sess.set_blocking(false); Ok(()) } @@ -308,22 +313,30 @@ impl SessionInner { match self.rx_req.try_recv() { Err(TryRecvError::Closed) => anyhow::bail!("all clients are closed"), Err(TryRecvError::Empty) => Ok(false), - Ok(SessionRequest::NewPty(newpty)) => { - if let Err(err) = self.new_pty(&sess, &newpty) { - log::error!("{:?} -> error: {:#}", newpty, err); - } - Ok(true) - } - Ok(SessionRequest::ResizePty(resize)) => { - if let Err(err) = self.resize_pty(&sess, &resize) { - log::error!("{:?} -> error: {:#}", resize, err); - } - Ok(true) + Ok(req) => { + sess.set_blocking(true); + let res = match req { + SessionRequest::NewPty(newpty) => { + if let Err(err) = self.new_pty(&sess, &newpty) { + log::error!("{:?} -> error: {:#}", newpty, err); + } + Ok(true) + } + SessionRequest::ResizePty(resize) => { + if let Err(err) = self.resize_pty(&sess, &resize) { + log::error!("{:?} -> error: {:#}", resize, err); + } + Ok(true) + } + }; + sess.set_blocking(false); + res } } } } +#[derive(Clone)] pub struct Session { tx: SessionSender, }