1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 13:21:38 +03:00

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
This commit is contained in:
Wez Furlong 2021-03-27 17:49:30 -07:00
parent 5aef725171
commit e103653923
11 changed files with 764 additions and 113 deletions

25
Cargo.lock generated
View File

@ -107,9 +107,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.39" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767" checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
[[package]] [[package]]
name = "approx" name = "approx"
@ -872,7 +872,7 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"crossbeam-utils", "crossbeam-utils",
"lazy_static", "lazy_static",
"memoffset 0.6.1", "memoffset 0.6.2",
"scopeguard", "scopeguard",
] ]
@ -1847,7 +1847,7 @@ dependencies = [
[[package]] [[package]]
name = "libssh2-sys" name = "libssh2-sys"
version = "0.2.21" 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 = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -2030,9 +2030,9 @@ dependencies = [
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.1" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" checksum = "cc14fc54a812b4472b4113facc3e44d099fbc0ea2ce0551fa5c703f8edfbfd38"
dependencies = [ dependencies = [
"autocfg", "autocfg",
] ]
@ -2164,6 +2164,7 @@ dependencies = [
"ratelim", "ratelim",
"regex", "regex",
"serde", "serde",
"smol",
"ssh2", "ssh2",
"sysinfo", "sysinfo",
"terminfo", "terminfo",
@ -2173,6 +2174,7 @@ dependencies = [
"tmux-cc", "tmux-cc",
"unicode-segmentation", "unicode-segmentation",
"url", "url",
"wezterm-ssh",
"wezterm-term", "wezterm-term",
] ]
@ -3194,9 +3196,9 @@ dependencies = [
[[package]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.25" version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "287f3c3f8236abb92d8b7e36797f19159df4b58f0a658cc3fb6dd3004b1f3bd3" checksum = "8fddb3b23626145d1776addfc307e1a1851f60ef6ca64f376bcb889697144cf0"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
] ]
@ -3606,7 +3608,7 @@ checksum = "1fccf17fd09e2455ea796d2ad267b64fa2c5cbd8701b2a93b555d2aa73449f7d"
[[package]] [[package]]
name = "ssh2" name = "ssh2"
version = "0.9.1" 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 = [ dependencies = [
"bitflags", "bitflags",
"libc", "libc",
@ -3678,9 +3680,9 @@ checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.64" version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4496,6 +4498,7 @@ dependencies = [
"wezterm-font", "wezterm-font",
"wezterm-gui-subcommands", "wezterm-gui-subcommands",
"wezterm-mux-server-impl", "wezterm-mux-server-impl",
"wezterm-ssh",
"wezterm-term", "wezterm-term",
"wezterm-toast-notification", "wezterm-toast-notification",
"winapi 0.3.9", "winapi 0.3.9",

View File

@ -25,7 +25,8 @@ rangeset = { path = "../rangeset" }
ratelim= { path = "../ratelim" } ratelim= { path = "../ratelim" }
regex = "1" regex = "1"
serde = {version="1.0", features = ["rc", "derive"]} serde = {version="1.0", features = ["rc", "derive"]}
ssh2 = {version="0.9", features=["openssl-on-win32"]} smol = "1.2"
ssh2 = "0.9"
terminfo = "0.7" terminfo = "0.7"
termwiz = { path = "../termwiz" } termwiz = { path = "../termwiz" }
textwrap = "0.13" textwrap = "0.13"
@ -33,6 +34,7 @@ thiserror = "1.0"
tmux-cc = { path = "../tmux-cc" } tmux-cc = { path = "../tmux-cc" }
unicode-segmentation = "1.7" unicode-segmentation = "1.7"
url = "2" url = "2"
wezterm-ssh = { path = "../wezterm-ssh" }
wezterm-term = { path = "../term", features=["use_serde"] } wezterm-term = { path = "../term", features=["use_serde"] }
[target.'cfg(all(windows, target_os="linux", target_os="macos"))'.dependencies] [target.'cfg(all(windows, target_os="linux", target_os="macos"))'.dependencies]

View File

@ -7,14 +7,52 @@ use crate::window::WindowId;
use crate::Mux; use crate::Mux;
use anyhow::{anyhow, bail, Context, Error}; use anyhow::{anyhow, bail, Context, Error};
use async_trait::async_trait; use async_trait::async_trait;
use filedescriptor::{socketpair, FileDescriptor};
use portable_pty::cmdbuilder::CommandBuilder; use portable_pty::cmdbuilder::CommandBuilder;
use portable_pty::{PtySize, PtySystem}; use portable_pty::{ExitStatus, MasterPty, PtySize};
use promise::{Future, Promise}; use promise::{Future, Promise};
use std::collections::HashSet; use std::cell::RefCell;
use std::io::Write; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::io::{BufWriter, Read, 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 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<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 = "🔑";
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 { impl ssh2::KeyboardInteractivePrompt for ConnectionUI {
fn prompt<'b>( fn prompt<'b>(
@ -217,21 +255,284 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
Ok(sess) Ok(sess)
} }
/// Represents a connection to remote host via ssh.
/// The domain is created with the ssh config prior to making the
/// connection. The connection is established by the first spawn()
/// call.
/// In order to show the authentication dialog inline in that spawned
/// pane, we play some tricks with wrapped versions of the pty, child
/// and the reader and writer instances so that we can inject the
/// interactive setup. The bulk of that is driven by `connect_ssh_session`.
pub struct RemoteSshDomain { pub struct RemoteSshDomain {
pty_system: Box<dyn PtySystem>, ssh_config: BTreeMap<String, String>,
session: Session,
id: DomainId, id: DomainId,
name: String, name: String,
events: RefCell<Option<smol::channel::Receiver<SessionEvent>>>,
} }
impl RemoteSshDomain { impl RemoteSshDomain {
pub fn with_pty_system(name: &str, pty_system: Box<dyn PtySystem>) -> Self { pub fn with_ssh_config(
name: &str,
ssh_config: BTreeMap<String, String>,
) -> anyhow::Result<Self> {
let id = alloc_domain_id(); let id = alloc_domain_id();
Self { let (session, events) = Session::connect(ssh_config.clone())?;
pty_system, Ok(Self {
ssh_config,
id, id,
name: format!("SSH to {}", name), 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<SessionEvent>,
mut stdin_read: BoxedReader,
stdin_tx: Sender<BoxedWriter>,
stdout_write: &mut BufWriter<FileDescriptor>,
stdout_tx: Sender<BoxedReader>,
child_tx: Sender<SshChildProcess>,
pty_tx: Sender<SshPty>,
ssh_config: BTreeMap<String, String>,
size: PtySize,
command_line: Option<String>,
env: HashMap<String, String>,
) -> anyhow::Result<()> {
struct StdoutShim<'a> {
size: PtySize,
stdout: &'a mut BufWriter<FileDescriptor>,
}
impl<'a> Write for StdoutShim<'a> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<InputEvent>,
}
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<ScreenSize> {
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<Duration>) -> termwiz::Result<Option<InputEvent>> {
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::<Vec<_>>();
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)] #[async_trait(?Send)]
@ -243,34 +544,127 @@ impl Domain for RemoteSshDomain {
_command_dir: Option<String>, _command_dir: Option<String>,
window: WindowId, window: WindowId,
) -> Result<Rc<Tab>, Error> { ) -> Result<Rc<Tab>, Error> {
let mut cmd = match command { let pane_id = alloc_pane_id();
let cmd = match command {
Some(c) => c, Some(c) => c,
None => CommandBuilder::new_default_prog(), 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<String, String> = 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<dyn portable_pty::MasterPty>;
let child: Box<dyn portable_pty::Child>;
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( let terminal = wezterm_term::Terminal::new(
crate::pty_size_to_terminal_size(size), crate::pty_size_to_terminal_size(size),
std::sync::Arc::new(config::TermConfig {}), std::sync::Arc::new(config::TermConfig {}),
"WezTerm", "WezTerm",
config::wezterm_version(), config::wezterm_version(),
Box::new(writer), writer,
); );
let mux = Mux::get().unwrap(); let mux = Mux::get().unwrap();
let pane: Rc<dyn Pane> = Rc::new(LocalPane::new( let pane: Rc<dyn Pane> = Rc::new(LocalPane::new(pane_id, terminal, child, pty, self.id));
pane_id,
terminal,
child,
pair.master,
self.id,
));
let tab = Rc::new(Tab::new(&size)); let tab = Rc::new(Tab::new(&size));
tab.assign_pane(&pane); tab.assign_pane(&pane);
@ -311,3 +705,240 @@ impl Domain for RemoteSshDomain {
DomainState::Attached DomainState::Attached
} }
} }
#[derive(Debug)]
struct WrappedSshChild {
child: Option<SshChildProcess>,
rx: Receiver<SshChildProcess>,
exited: Option<ExitStatus>,
}
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<Option<ExitStatus>> {
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<portable_pty::ExitStatus> {
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<u32> {
None
}
}
type BoxedReader = Box<(dyn Read + Send + 'static)>;
type BoxedWriter = Box<(dyn Write + Send + 'static)>;
struct WrappedSshPty {
inner: RefCell<WrappedSshPtyInner>,
}
enum WrappedSshPtyInner {
Connecting {
reader: Option<PtyReader>,
connected: Receiver<SshPty>,
size: PtySize,
},
Connected {
reader: Option<PtyReader>,
pty: SshPty,
},
}
struct PtyReader {
reader: BoxedReader,
rx: Receiver<BoxedReader>,
}
struct PtyWriter {
writer: BoxedWriter,
rx: Receiver<BoxedWriter>,
}
impl std::io::Write for WrappedSshPty {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
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<PtySize> {
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<Box<(dyn Read + Send + 'static)>> {
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<Box<(dyn Write + Send + 'static)>> {
anyhow::bail!("writer must be created during bootstrap");
}
fn process_group_leader(&self) -> Option<i32> {
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<usize> {
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<usize> {
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,
},
}
}
}

View File

@ -415,7 +415,7 @@ pub fn allocate(size: PtySize) -> (TermWizTerminal, Rc<dyn Pane>) {
(tw_term, pane) (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 data = include_bytes!("../../termwiz/data/xterm-256color");
let db = terminfo::Database::from_buffer(&data[..]).unwrap(); let db = terminfo::Database::from_buffer(&data[..]).unwrap();

View File

@ -20,7 +20,7 @@ promise = { path = "../promise" }
rangeset = { path = "../rangeset" } rangeset = { path = "../rangeset" }
ratelim= { path = "../ratelim" } ratelim= { path = "../ratelim" }
smol = "1.2" smol = "1.2"
ssh2 = {version="0.9", features=["openssl-on-win32"]} ssh2 = "0.9"
thiserror = "1.0" thiserror = "1.0"
url = "2" url = "2"
wezterm-term = { path = "../term", features=["use_serde"] } wezterm-term = { path = "../term", features=["use_serde"] }

View File

@ -65,6 +65,7 @@ wezterm-client = { path = "../wezterm-client" }
wezterm-font = { path = "../wezterm-font" } wezterm-font = { path = "../wezterm-font" }
wezterm-gui-subcommands = { path = "../wezterm-gui-subcommands" } wezterm-gui-subcommands = { path = "../wezterm-gui-subcommands" }
wezterm-mux-server-impl = { path = "../wezterm-mux-server-impl" } wezterm-mux-server-impl = { path = "../wezterm-mux-server-impl" }
wezterm-ssh = { path = "../wezterm-ssh" }
wezterm-term = { path = "../term", features=["use_serde"] } wezterm-term = { path = "../term", features=["use_serde"] }
wezterm-toast-notification = { path = "../wezterm-toast-notification" } wezterm-toast-notification = { path = "../wezterm-toast-notification" }
window = { path = "../window", features=["wayland"]} window = { path = "../window", features=["wayland"]}

View File

@ -15,6 +15,7 @@ use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
use wezterm_client::domain::{ClientDomain, ClientDomainConfig}; use wezterm_client::domain::{ClientDomain, ClientDomainConfig};
use wezterm_gui_subcommands::*; use wezterm_gui_subcommands::*;
use wezterm_ssh::*;
use wezterm_toast_notification::*; use wezterm_toast_notification::*;
mod frontend; mod frontend;
@ -86,11 +87,14 @@ enum SubCommand {
} }
async fn async_run_ssh(opts: SshCommand) -> anyhow::Result<()> { async fn async_run_ssh(opts: SshCommand) -> anyhow::Result<()> {
// Establish the connection; it may show UI for authentication let mut ssh_config = Config::new();
let params = &opts.user_at_host_and_port; ssh_config.add_default_config_files();
let sess = mux::ssh::async_ssh_connect(&params.host_and_port, &params.username).await?; let mut ssh_config = ssh_config.for_host(&opts.user_at_host_and_port.host_and_port);
// Now we have a connected session, set up the ssh domain and make it ssh_config.insert(
// the default domain "user".to_string(),
opts.user_at_host_and_port.username.to_string(),
);
let _gui = front_end().unwrap(); let _gui = front_end().unwrap();
let cmd = if !opts.prog.is_empty() { 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 config = config::configuration();
let pty_system = Box::new(portable_pty::ssh::SshSession::new(sess, &config.term)); let domain: Arc<dyn Domain> = Arc::new(mux::ssh::RemoteSshDomain::with_ssh_config(
let domain: Arc<dyn Domain> = Arc::new(mux::ssh::RemoteSshDomain::with_pty_system(
&opts.user_at_host_and_port.to_string(), &opts.user_at_host_and_port.to_string(),
pty_system, ssh_config,
)); )?);
let mux = Mux::get().unwrap(); let mux = Mux::get().unwrap();
mux.add_domain(&domain); mux.add_domain(&domain);

View File

@ -15,7 +15,7 @@ log = "0.4"
portable-pty = { path = "../pty" } portable-pty = { path = "../pty" }
regex = "1" regex = "1"
smol = "1.2" smol = "1.2"
ssh2 = "0.9" ssh2 = {version="0.9", features=["openssl-on-win32"]}
[dev-dependencies] [dev-dependencies]
k9 = "0.11.0" k9 = "0.11.0"

View File

@ -32,12 +32,17 @@ impl crate::session::SessionInner {
fn agent_auth(&mut self, sess: &ssh2::Session, user: &str) -> anyhow::Result<bool> { fn agent_auth(&mut self, sess: &ssh2::Session, user: &str) -> anyhow::Result<bool> {
if let Some(only) = self.config.get("identitiesonly") { if let Some(only) = self.config.get("identitiesonly") {
if only == "yes" { if only == "yes" {
log::trace!("Skipping agent auth because identitiesonly=yes");
return Ok(false); return Ok(false);
} }
} }
let mut agent = sess.agent()?; 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()?; agent.list_identities()?;
let identities = agent.identities()?; let identities = agent.identities()?;
for identity in identities { for identity in identities {
@ -45,6 +50,7 @@ impl crate::session::SessionInner {
return Ok(true); return Ok(true);
} }
} }
Ok(false) Ok(false)
} }
@ -63,20 +69,21 @@ impl crate::session::SessionInner {
continue; continue;
} }
let pubkey = if pubkey.exists() && false { let pubkey = if pubkey.exists() {
Some(pubkey.as_ref()) Some(pubkey.as_ref())
} else { } else {
None None
}; };
// We try with no passphrase first, in case the key is unencrypted
match sess.userauth_pubkey_file(user, pubkey, &file, None) { match sess.userauth_pubkey_file(user, pubkey, &file, None) {
Ok(_) => return Ok(true), Ok(_) => {
Err(err) => { log::info!("pubkey_file immediately ok for {}", file.display());
if err.code() == ssh2::ErrorCode::Session(-16) return Ok(true);
|| err.code() == ssh2::ErrorCode::Session(-18) }
{ Err(_) => {
// Need a passphrase to decrypt the key // 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); let (reply, answers) = bounded(1);
self.tx_event self.tx_event
.try_send(SessionEvent::Authenticate(AuthenticationEvent { .try_send(SessionEvent::Authenticate(AuthenticationEvent {
@ -84,7 +91,7 @@ impl crate::session::SessionInner {
instructions: "".to_string(), instructions: "".to_string(),
prompts: vec![AuthenticationPrompt { prompts: vec![AuthenticationPrompt {
prompt: format!( prompt: format!(
"Passphrase to decrypt {} for {}@{}: ", "Passphrase to decrypt {} for {}@{}:\n> ",
file.display(), file.display(),
user, user,
host host
@ -105,14 +112,13 @@ impl crate::session::SessionInner {
let passphrase = &answers[0]; let passphrase = &answers[0];
match sess.userauth_pubkey_file(user, pubkey, &file, Some(passphrase)) { match sess.userauth_pubkey_file(user, pubkey, &file, Some(passphrase)) {
Ok(_) => return Ok(true), Ok(_) => {
return Ok(true);
}
Err(err) => { Err(err) => {
log::warn!("pubkey auth: {:#}", 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); log::trace!("ssh auth methods: {:?}", methods);
if !sess.authenticated() && methods.contains("publickey") { if !sess.authenticated() && methods.contains("publickey") {
match self.agent_auth(sess, user) { if self.agent_auth(sess, user)? {
Ok(true) => continue, continue;
Ok(false) => {}
Err(err) => {
log::warn!("while attempting agent auth: {}", err)
}
} }
match self.pubkey_auth(sess, user, host) { if self.pubkey_auth(sess, user, host)? {
Ok(true) => continue, continue;
Ok(false) => {}
Err(err) => {
log::warn!("while attempting auth: {}", err)
}
} }
} }

View File

@ -160,7 +160,7 @@ impl crate::session::SessionInner {
// Depending on the server configuration, a given // Depending on the server configuration, a given
// setenv request may not succeed, but that doesn't // setenv request may not succeed, but that doesn't
// prevent the connection from being set up. // prevent the connection from being set up.
log::error!("ssh: setenv {}={} failed: {}", key, val, err); log::warn!("ssh: setenv {}={} failed: {}", key, val, err);
} }
} }
} }

View File

@ -115,6 +115,7 @@ impl SessionInner {
let mut sess = ssh2::Session::new()?; let mut sess = ssh2::Session::new()?;
// sess.trace(ssh2::TraceFlags::all()); // sess.trace(ssh2::TraceFlags::all());
sess.set_blocking(true);
sess.set_tcp_stream(tcp); sess.set_tcp_stream(tcp);
sess.handshake() sess.handshake()
.with_context(|| format!("ssh handshake with {}", remote_address))?; .with_context(|| format!("ssh handshake with {}", remote_address))?;
@ -138,6 +139,8 @@ impl SessionInner {
} }
fn request_loop(&mut self, sess: ssh2::Session) -> anyhow::Result<()> { fn request_loop(&mut self, sess: ssh2::Session) -> anyhow::Result<()> {
let mut sleep_delay = Duration::from_millis(100);
loop { loop {
self.tick_io()?; self.tick_io()?;
self.drain_request_pipe(); 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() { for (idx, poll) in poll_array.iter().enumerate() {
if poll.revents != 0 {
sleep_delay = Duration::from_millis(100);
}
if idx == 0 || idx == 1 { if idx == 0 || idx == 1 {
// Dealt with at the top of the loop // Dealt with at the top of the loop
} else if poll.revents != 0 { } else if poll.revents != 0 {
@ -229,7 +236,7 @@ impl SessionInner {
fn tick_io(&mut self) -> anyhow::Result<()> { fn tick_io(&mut self) -> anyhow::Result<()> {
for chan in self.channels.values_mut() { for chan in self.channels.values_mut() {
if chan.exit.is_some() { 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<ssh2::ExitSignal> { fn has_signal(chan: &ssh2::Channel) -> Option<ssh2::ExitSignal> {
if let Ok(sig) = chan.exit_signal() { if let Ok(sig) = chan.exit_signal() {
if sig.exit_signal.is_some() { if sig.exit_signal.is_some() {
@ -298,9 +305,7 @@ impl SessionInner {
} }
fn dispatch_pending_requests(&mut self, sess: &ssh2::Session) -> anyhow::Result<()> { fn dispatch_pending_requests(&mut self, sess: &ssh2::Session) -> anyhow::Result<()> {
sess.set_blocking(true);
while self.dispatch_one_request(sess)? {} while self.dispatch_one_request(sess)? {}
sess.set_blocking(false);
Ok(()) Ok(())
} }
@ -308,22 +313,30 @@ impl SessionInner {
match self.rx_req.try_recv() { match self.rx_req.try_recv() {
Err(TryRecvError::Closed) => anyhow::bail!("all clients are closed"), Err(TryRecvError::Closed) => anyhow::bail!("all clients are closed"),
Err(TryRecvError::Empty) => Ok(false), Err(TryRecvError::Empty) => Ok(false),
Ok(SessionRequest::NewPty(newpty)) => { Ok(req) => {
sess.set_blocking(true);
let res = match req {
SessionRequest::NewPty(newpty) => {
if let Err(err) = self.new_pty(&sess, &newpty) { if let Err(err) = self.new_pty(&sess, &newpty) {
log::error!("{:?} -> error: {:#}", newpty, err); log::error!("{:?} -> error: {:#}", newpty, err);
} }
Ok(true) Ok(true)
} }
Ok(SessionRequest::ResizePty(resize)) => { SessionRequest::ResizePty(resize) => {
if let Err(err) = self.resize_pty(&sess, &resize) { if let Err(err) = self.resize_pty(&sess, &resize) {
log::error!("{:?} -> error: {:#}", resize, err); log::error!("{:?} -> error: {:#}", resize, err);
} }
Ok(true) Ok(true)
} }
};
sess.set_blocking(false);
res
}
} }
} }
} }
#[derive(Clone)]
pub struct Session { pub struct Session {
tx: SessionSender, tx: SessionSender,
} }