1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 05:12:40 +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]]
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",

View File

@ -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]

View File

@ -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<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 {
fn prompt<'b>(
@ -217,21 +255,284 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> anyhow::Result<ssh2:
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 {
pty_system: Box<dyn PtySystem>,
ssh_config: BTreeMap<String, String>,
session: Session,
id: DomainId,
name: String,
events: RefCell<Option<smol::channel::Receiver<SessionEvent>>>,
}
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();
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<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)]
@ -243,34 +544,127 @@ impl Domain for RemoteSshDomain {
_command_dir: Option<String>,
window: WindowId,
) -> Result<Rc<Tab>, 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<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(
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<dyn Pane> = Rc::new(LocalPane::new(
pane_id,
terminal,
child,
pair.master,
self.id,
));
let pane: Rc<dyn Pane> = 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<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)
}
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();

View File

@ -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"] }

View File

@ -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"]}

View File

@ -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(&params.host_and_port, &params.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<dyn Domain> = Arc::new(mux::ssh::RemoteSshDomain::with_pty_system(
let domain: Arc<dyn Domain> = 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);

View File

@ -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"

View File

@ -32,12 +32,17 @@ impl crate::session::SessionInner {
fn agent_auth(&mut self, sess: &ssh2::Session, user: &str) -> anyhow::Result<bool> {
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;
}
}

View File

@ -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);
}
}
}

View File

@ -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<ssh2::ExitSignal> {
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,
}