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:
parent
5aef725171
commit
e103653923
25
Cargo.lock
generated
25
Cargo.lock
generated
@ -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",
|
||||
|
@ -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]
|
||||
|
675
mux/src/ssh.rs
675
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<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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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"] }
|
||||
|
@ -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"]}
|
||||
|
@ -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<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);
|
||||
|
@ -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"
|
||||
|
@ -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,20 +69,21 @@ 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 {
|
||||
@ -84,7 +91,7 @@ impl crate::session::SessionInner {
|
||||
instructions: "".to_string(),
|
||||
prompts: vec![AuthenticationPrompt {
|
||||
prompt: format!(
|
||||
"Passphrase to decrypt {} for {}@{}: ",
|
||||
"Passphrase to decrypt {} for {}@{}:\n> ",
|
||||
file.display(),
|
||||
user,
|
||||
host
|
||||
@ -105,14 +112,13 @@ impl crate::session::SessionInner {
|
||||
let passphrase = &answers[0];
|
||||
|
||||
match sess.userauth_pubkey_file(user, pubkey, &file, Some(passphrase)) {
|
||||
Ok(_) => return Ok(true),
|
||||
Ok(_) => {
|
||||
return Ok(true);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)) => {
|
||||
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)
|
||||
}
|
||||
Ok(SessionRequest::ResizePty(resize)) => {
|
||||
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,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user