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]]
|
[[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",
|
||||||
|
@ -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]
|
||||||
|
675
mux/src/ssh.rs
675
mux/src/ssh.rs
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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"] }
|
||||||
|
@ -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"]}
|
||||||
|
@ -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(¶ms.host_and_port, ¶ms.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);
|
||||||
|
@ -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"
|
||||||
|
@ -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,55 +69,55 @@ 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);
|
||||||
|
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);
|
let answers = smol::block_on(answers.recv())
|
||||||
self.tx_event
|
.context("waiting for authentication answers from user")?;
|
||||||
.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())
|
if answers.is_empty() {
|
||||||
.context("waiting for authentication answers from user")?;
|
anyhow::bail!("user cancelled authentication");
|
||||||
|
}
|
||||||
|
|
||||||
if answers.is_empty() {
|
let passphrase = &answers[0];
|
||||||
anyhow::bail!("user cancelled authentication");
|
|
||||||
|
match sess.userauth_pubkey_file(user, pubkey, &file, Some(passphrase)) {
|
||||||
|
Ok(_) => {
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
let passphrase = &answers[0];
|
log::warn!("pubkey auth: {:#}", err);
|
||||||
|
|
||||||
match sess.userauth_pubkey_file(user, pubkey, &file, Some(passphrase)) {
|
|
||||||
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);
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
||||||
if let Err(err) = self.new_pty(&sess, &newpty) {
|
sess.set_blocking(true);
|
||||||
log::error!("{:?} -> error: {:#}", newpty, err);
|
let res = match req {
|
||||||
}
|
SessionRequest::NewPty(newpty) => {
|
||||||
Ok(true)
|
if let Err(err) = self.new_pty(&sess, &newpty) {
|
||||||
}
|
log::error!("{:?} -> error: {:#}", newpty, err);
|
||||||
Ok(SessionRequest::ResizePty(resize)) => {
|
}
|
||||||
if let Err(err) = self.resize_pty(&sess, &resize) {
|
Ok(true)
|
||||||
log::error!("{:?} -> error: {:#}", resize, err);
|
}
|
||||||
}
|
SessionRequest::ResizePty(resize) => {
|
||||||
Ok(true)
|
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 {
|
pub struct Session {
|
||||||
tx: SessionSender,
|
tx: SessionSender,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user