mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 13:21:38 +03:00
wezterm-client: cut over to new wezterm-ssh bits
refs: https://github.com/wez/wezterm/issues/507
This commit is contained in:
parent
e96011bc26
commit
369888c94e
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -2165,7 +2165,6 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"smol",
|
"smol",
|
||||||
"ssh2",
|
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"terminfo",
|
"terminfo",
|
||||||
"termwiz",
|
"termwiz",
|
||||||
@ -4399,12 +4398,12 @@ dependencies = [
|
|||||||
"rangeset",
|
"rangeset",
|
||||||
"ratelim",
|
"ratelim",
|
||||||
"smol",
|
"smol",
|
||||||
"ssh2",
|
|
||||||
"termwiz",
|
"termwiz",
|
||||||
"textwrap 0.13.4",
|
"textwrap 0.13.4",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uds_windows",
|
"uds_windows",
|
||||||
"url",
|
"url",
|
||||||
|
"wezterm-ssh",
|
||||||
"wezterm-term",
|
"wezterm-term",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -26,7 +26,6 @@ ratelim= { path = "../ratelim" }
|
|||||||
regex = "1"
|
regex = "1"
|
||||||
serde = {version="1.0", features = ["rc", "derive"]}
|
serde = {version="1.0", features = ["rc", "derive"]}
|
||||||
smol = "1.2"
|
smol = "1.2"
|
||||||
ssh2 = "0.9"
|
|
||||||
terminfo = "0.7"
|
terminfo = "0.7"
|
||||||
termwiz = { path = "../termwiz" }
|
termwiz = { path = "../termwiz" }
|
||||||
textwrap = "0.13"
|
textwrap = "0.13"
|
||||||
|
221
mux/src/ssh.rs
221
mux/src/ssh.rs
@ -11,10 +11,8 @@ use filedescriptor::{socketpair, FileDescriptor};
|
|||||||
use portable_pty::cmdbuilder::CommandBuilder;
|
use portable_pty::cmdbuilder::CommandBuilder;
|
||||||
use portable_pty::{ExitStatus, MasterPty, PtySize};
|
use portable_pty::{ExitStatus, MasterPty, PtySize};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
use std::collections::{BTreeMap, HashMap, VecDeque};
|
||||||
use std::io::{BufWriter, Read, Write};
|
use std::io::{BufWriter, Read, Write};
|
||||||
use std::net::TcpStream;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
|
use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@ -53,187 +51,88 @@ impl LineEditorHost for PasswordPromptHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ssh2::KeyboardInteractivePrompt for ConnectionUI {
|
|
||||||
fn prompt<'b>(
|
|
||||||
&mut self,
|
|
||||||
_username: &str,
|
|
||||||
instructions: &str,
|
|
||||||
prompts: &[ssh2::Prompt<'b>],
|
|
||||||
) -> Vec<String> {
|
|
||||||
prompts
|
|
||||||
.iter()
|
|
||||||
.map(|p| {
|
|
||||||
self.output_str(&format!("{}\n", instructions));
|
|
||||||
if p.echo {
|
|
||||||
self.input(&p.text)
|
|
||||||
} else {
|
|
||||||
self.password(&p.text)
|
|
||||||
}
|
|
||||||
.unwrap_or_else(|_| String::new())
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ssh_connect_with_ui(
|
pub fn ssh_connect_with_ui(
|
||||||
remote_address: &str,
|
remote_address: &str,
|
||||||
username: &str,
|
username: &str,
|
||||||
ui: &mut ConnectionUI,
|
ui: &mut ConnectionUI,
|
||||||
) -> anyhow::Result<ssh2::Session> {
|
) -> anyhow::Result<Session> {
|
||||||
let cloned_ui = ui.clone();
|
let cloned_ui = ui.clone();
|
||||||
cloned_ui.run_and_log_error(move || {
|
cloned_ui.run_and_log_error(move || {
|
||||||
let mut sess = ssh2::Session::new()?;
|
let mut ssh_config = wezterm_ssh::Config::new();
|
||||||
|
ssh_config.add_default_config_files();
|
||||||
|
|
||||||
let (remote_address, remote_host_name, port) = {
|
let (remote_host_name, port) = {
|
||||||
let parts: Vec<&str> = remote_address.split(':').collect();
|
let parts: Vec<&str> = remote_address.split(':').collect();
|
||||||
|
|
||||||
if parts.len() == 2 {
|
if parts.len() == 2 {
|
||||||
(remote_address.to_string(), parts[0], parts[1].parse()?)
|
(parts[0], Some(parts[1].parse::<u16>()?))
|
||||||
} else {
|
} else {
|
||||||
(format!("{}:22", remote_address), remote_address, 22)
|
(remote_address, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut ssh_config = ssh_config.for_host(&remote_host_name);
|
||||||
|
ssh_config.insert("user".to_string(), username.to_string());
|
||||||
|
if let Some(port) = port {
|
||||||
|
ssh_config.insert("port".to_string(), port.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
ui.output_str(&format!("Connecting to {} using SSH\n", remote_address));
|
ui.output_str(&format!("Connecting to {} using SSH\n", remote_address));
|
||||||
|
let (session, events) = Session::connect(ssh_config.clone())?;
|
||||||
|
|
||||||
let tcp = TcpStream::connect(&remote_address)
|
while let Ok(event) = smol::block_on(events.recv()) {
|
||||||
.with_context(|| format!("ssh connecting to {}", remote_address))?;
|
match event {
|
||||||
ui.output_str("SSH: Connected OK!\n");
|
SessionEvent::Banner(banner) => {
|
||||||
tcp.set_nodelay(true)?;
|
if let Some(banner) = banner {
|
||||||
sess.set_tcp_stream(tcp);
|
ui.output_str(&format!("{}\n", banner));
|
||||||
sess.handshake()
|
}
|
||||||
.with_context(|| format!("ssh handshake with {}", remote_address))?;
|
}
|
||||||
|
SessionEvent::HostVerify(verify) => {
|
||||||
if let Ok(mut known_hosts) = sess.known_hosts() {
|
ui.output_str(&format!("{}\n", verify.message));
|
||||||
let varname = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
|
let ok = if let Ok(line) = ui.input("Enter [y/n]> ") {
|
||||||
let var = std::env::var_os(varname)
|
|
||||||
.ok_or_else(|| anyhow!("environment variable {} is missing", varname))?;
|
|
||||||
let file = Path::new(&var).join(".ssh/known_hosts");
|
|
||||||
if file.exists() {
|
|
||||||
known_hosts
|
|
||||||
.read_file(&file, ssh2::KnownHostFileKind::OpenSSH)
|
|
||||||
.with_context(|| format!("reading known_hosts file {}", file.display()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (key, key_type) = sess
|
|
||||||
.host_key()
|
|
||||||
.ok_or_else(|| anyhow!("failed to get ssh host key"))?;
|
|
||||||
|
|
||||||
let fingerprint = sess
|
|
||||||
.host_key_hash(ssh2::HashType::Sha256)
|
|
||||||
.map(|fingerprint| {
|
|
||||||
format!(
|
|
||||||
"SHA256:{}",
|
|
||||||
base64::encode_config(
|
|
||||||
fingerprint,
|
|
||||||
base64::Config::new(base64::CharacterSet::Standard, false)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.or_else(|| {
|
|
||||||
// Querying for the Sha256 can fail if for example we were linked
|
|
||||||
// against libssh < 1.9, so let's fall back to Sha1 in that case.
|
|
||||||
sess.host_key_hash(ssh2::HashType::Sha1).map(|fingerprint| {
|
|
||||||
let mut res = vec![];
|
|
||||||
write!(&mut res, "SHA1").ok();
|
|
||||||
for b in fingerprint {
|
|
||||||
write!(&mut res, ":{:02x}", *b).ok();
|
|
||||||
}
|
|
||||||
String::from_utf8(res).unwrap()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.ok_or_else(|| anyhow!("failed to get host fingerprint"))?;
|
|
||||||
|
|
||||||
use ssh2::CheckResult;
|
|
||||||
match known_hosts.check_port(&remote_host_name, port, key) {
|
|
||||||
CheckResult::Match => {}
|
|
||||||
CheckResult::NotFound => {
|
|
||||||
ui.output_str(&format!(
|
|
||||||
"SSH host {} is not yet trusted.\n\
|
|
||||||
{:?} Fingerprint: {}.\n\
|
|
||||||
Trust and continue connecting?\n",
|
|
||||||
remote_address, key_type, fingerprint
|
|
||||||
));
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let line = ui.input("Enter [Y/n]> ")?;
|
|
||||||
|
|
||||||
match line.as_ref() {
|
match line.as_ref() {
|
||||||
"y" | "Y" | "yes" | "YES" => break,
|
"y" | "Y" | "yes" | "YES" => true,
|
||||||
"n" | "N" | "no" | "NO" => bail!("user declined to trust host"),
|
"n" | "N" | "no" | "NO" | _ => false,
|
||||||
_ => continue,
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
smol::block_on(verify.answer(ok)).context("send verify response")?;
|
||||||
|
}
|
||||||
|
SessionEvent::Authenticate(auth) => {
|
||||||
|
if !auth.username.is_empty() {
|
||||||
|
ui.output_str(&format!("Authentication for {}\n", auth.username));
|
||||||
|
}
|
||||||
|
if !auth.instructions.is_empty() {
|
||||||
|
ui.output_str(&format!("{}\n", 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 {
|
||||||
|
ui.output_str(&format!("{}\n", line));
|
||||||
|
}
|
||||||
|
let res = if prompt.echo {
|
||||||
|
ui.input(editor_prompt)
|
||||||
|
} else {
|
||||||
|
ui.password(editor_prompt)
|
||||||
|
};
|
||||||
|
if let Ok(line) = res {
|
||||||
|
answers.push(line);
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Authentication was cancelled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
smol::block_on(auth.answer(answers))?;
|
||||||
known_hosts
|
|
||||||
.add(remote_host_name, key, &remote_address, key_type.into())
|
|
||||||
.context("adding known_hosts entry in memory")?;
|
|
||||||
|
|
||||||
known_hosts
|
|
||||||
.write_file(&file, ssh2::KnownHostFileKind::OpenSSH)
|
|
||||||
.with_context(|| format!("writing known_hosts file {}", file.display()))?;
|
|
||||||
}
|
}
|
||||||
CheckResult::Mismatch => {
|
SessionEvent::Error(err) => {
|
||||||
ui.output_str(&format!(
|
anyhow::bail!("Error: {}", err);
|
||||||
"🛑 host key mismatch for ssh server {}.\n\
|
|
||||||
Got fingerprint {} instead of expected value from known_hosts\n\
|
|
||||||
file {}.\n\
|
|
||||||
Refusing to connect.\n",
|
|
||||||
remote_address,
|
|
||||||
fingerprint,
|
|
||||||
file.display()
|
|
||||||
));
|
|
||||||
bail!("host mismatch, man in the middle attack?!");
|
|
||||||
}
|
|
||||||
CheckResult::Failure => {
|
|
||||||
ui.output_str("🛑 Failed to load and check known ssh hosts\n");
|
|
||||||
bail!("failed to check the known hosts");
|
|
||||||
}
|
}
|
||||||
|
SessionEvent::Authenticated => return Ok(session),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bail!("unable to authenticate session");
|
||||||
for _ in 0..3 {
|
|
||||||
if sess.authenticated() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-query the auth methods on each loop as a successful method
|
|
||||||
// may unlock a new method on a subsequent iteration (eg: password
|
|
||||||
// auth may then unlock 2fac)
|
|
||||||
let methods: HashSet<&str> = sess.auth_methods(&username)?.split(',').collect();
|
|
||||||
log::trace!("ssh auth methods: {:?}", methods);
|
|
||||||
|
|
||||||
if !sess.authenticated() && methods.contains("publickey") {
|
|
||||||
if let Err(err) = sess.userauth_agent(&username) {
|
|
||||||
log::warn!("while attempting agent auth: {}", err);
|
|
||||||
} else if sess.authenticated() {
|
|
||||||
ui.output_str("publickey auth successful!\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !sess.authenticated() && methods.contains("password") {
|
|
||||||
ui.output_str(&format!(
|
|
||||||
"Password authentication for {}@{}\n",
|
|
||||||
username, remote_address
|
|
||||||
));
|
|
||||||
let pass = ui.password("🔐 Password: ")?;
|
|
||||||
if let Err(err) = sess.userauth_password(username, &pass) {
|
|
||||||
log::error!("while attempting password auth: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !sess.authenticated() && methods.contains("keyboard-interactive") {
|
|
||||||
if let Err(err) = sess.userauth_keyboard_interactive(&username, ui) {
|
|
||||||
log::error!("while attempting keyboard-interactive auth: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !sess.authenticated() {
|
|
||||||
bail!("unable to authenticate session");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(sess)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,16 +19,16 @@ lru = "0.6"
|
|||||||
metrics = { version="0.14", features=["std"]}
|
metrics = { version="0.14", features=["std"]}
|
||||||
mux = { path = "../mux" }
|
mux = { path = "../mux" }
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
portable-pty = { path = "../pty", features = ["serde_support", "ssh"]}
|
portable-pty = { path = "../pty", features = ["serde_support"]}
|
||||||
promise = { path = "../promise" }
|
promise = { path = "../promise" }
|
||||||
rangeset = { path = "../rangeset" }
|
rangeset = { path = "../rangeset" }
|
||||||
ratelim= { path = "../ratelim" }
|
ratelim= { path = "../ratelim" }
|
||||||
smol = "1.2"
|
smol = "1.2"
|
||||||
ssh2 = "0.9"
|
|
||||||
termwiz = { path = "../termwiz" }
|
termwiz = { path = "../termwiz" }
|
||||||
textwrap = "0.13"
|
textwrap = "0.13"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
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(windows)".dependencies]
|
[target."cfg(windows)".dependencies]
|
||||||
|
@ -6,6 +6,7 @@ use async_ossl::AsyncSslStream;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use codec::*;
|
use codec::*;
|
||||||
use config::{configuration, SshDomain, TlsDomainClient, UnixDomain};
|
use config::{configuration, SshDomain, TlsDomainClient, UnixDomain};
|
||||||
|
use filedescriptor::FileDescriptor;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use mux::connui::ConnectionUI;
|
use mux::connui::ConnectionUI;
|
||||||
use mux::domain::{alloc_domain_id, DomainId};
|
use mux::domain::{alloc_domain_id, DomainId};
|
||||||
@ -18,7 +19,6 @@ use smol::channel::{bounded, unbounded, Receiver, Sender};
|
|||||||
use smol::prelude::*;
|
use smol::prelude::*;
|
||||||
use smol::{block_on, Async};
|
use smol::{block_on, Async};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::marker::Unpin;
|
use std::marker::Unpin;
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
@ -312,8 +312,9 @@ struct Reconnectable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SshStream {
|
struct SshStream {
|
||||||
chan: ssh2::Channel,
|
stdin: FileDescriptor,
|
||||||
sess: ssh2::Session,
|
stdout: FileDescriptor,
|
||||||
|
_child: wezterm_ssh::SshChildProcess,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for SshStream {
|
impl std::fmt::Debug for SshStream {
|
||||||
@ -325,60 +326,29 @@ impl std::fmt::Debug for SshStream {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
impl std::os::unix::io::AsRawFd for SshStream {
|
impl std::os::unix::io::AsRawFd for SshStream {
|
||||||
fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
|
fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
|
||||||
self.sess.as_raw_fd()
|
self.stdout.as_raw_fd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
impl std::os::windows::io::AsRawSocket for SshStream {
|
impl std::os::windows::io::AsRawSocket for SshStream {
|
||||||
fn as_raw_socket(&self) -> std::os::windows::io::RawSocket {
|
fn as_raw_socket(&self) -> std::os::windows::io::RawSocket {
|
||||||
self.sess.as_raw_socket()
|
self.stdout.as_raw_socket()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SshStream {
|
|
||||||
fn process_stderr(&mut self) {
|
|
||||||
let blocking = self.sess.is_blocking();
|
|
||||||
self.sess.set_blocking(false);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let mut buf = [0u8; 1024];
|
|
||||||
match self.chan.stderr().read(&mut buf) {
|
|
||||||
Ok(size) => {
|
|
||||||
if size == 0 {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
let stderr = &buf[0..size];
|
|
||||||
log::error!("ssh stderr: {}", String::from_utf8_lossy(stderr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
if e.kind() != std::io::ErrorKind::WouldBlock {
|
|
||||||
log::error!("ssh error reading stderr: {}", e);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sess.set_blocking(blocking);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Read for SshStream {
|
impl Read for SshStream {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
||||||
// Take the opportunity to read and show data from stderr
|
self.stdout.read(buf)
|
||||||
self.process_stderr();
|
|
||||||
self.chan.read(buf)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Write for SshStream {
|
impl Write for SshStream {
|
||||||
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
|
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
|
||||||
self.chan.write(buf)
|
self.stdin.write(buf)
|
||||||
}
|
}
|
||||||
fn flush(&mut self) -> Result<(), std::io::Error> {
|
fn flush(&mut self) -> Result<(), std::io::Error> {
|
||||||
self.chan.flush()
|
self.stdin.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,10 +426,6 @@ impl Reconnectable {
|
|||||||
ui: &mut ConnectionUI,
|
ui: &mut ConnectionUI,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let sess = ssh_connect_with_ui(&ssh_dom.remote_address, &ssh_dom.username, ui)?;
|
let sess = ssh_connect_with_ui(&ssh_dom.remote_address, &ssh_dom.username, ui)?;
|
||||||
sess.set_timeout(ssh_dom.timeout.as_secs().try_into()?);
|
|
||||||
|
|
||||||
let mut chan = sess.channel_session()?;
|
|
||||||
|
|
||||||
let proxy_bin = Self::wezterm_bin_path(&ssh_dom.remote_wezterm_path);
|
let proxy_bin = Self::wezterm_bin_path(&ssh_dom.remote_wezterm_path);
|
||||||
|
|
||||||
let cmd = if initial {
|
let cmd = if initial {
|
||||||
@ -469,9 +435,27 @@ impl Reconnectable {
|
|||||||
};
|
};
|
||||||
ui.output_str(&format!("Running: {}\n", cmd));
|
ui.output_str(&format!("Running: {}\n", cmd));
|
||||||
log::error!("going to run {}", cmd);
|
log::error!("going to run {}", cmd);
|
||||||
chan.exec(&cmd)?;
|
|
||||||
|
|
||||||
let stream: Box<dyn AsyncReadAndWrite> = Box::new(Async::new(SshStream { sess, chan })?);
|
let exec = smol::block_on(sess.exec(&cmd, None))?;
|
||||||
|
|
||||||
|
let mut stderr = exec.stderr;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut buf = [0u8; 1024];
|
||||||
|
while let Ok(len) = stderr.read(&mut buf) {
|
||||||
|
if len == 0 {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
let stderr = &buf[0..len];
|
||||||
|
log::error!("ssh stderr: {}", String::from_utf8_lossy(stderr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let stream: Box<dyn AsyncReadAndWrite> = Box::new(Async::new(SshStream {
|
||||||
|
stdin: exec.stdin,
|
||||||
|
stdout: exec.stdout,
|
||||||
|
_child: exec.child,
|
||||||
|
})?);
|
||||||
self.stream.replace(stream);
|
self.stream.replace(stream);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -593,8 +577,6 @@ impl Reconnectable {
|
|||||||
ssh_connect_with_ui(&ssh_params.host_and_port, &ssh_params.username, ui)?;
|
ssh_connect_with_ui(&ssh_params.host_and_port, &ssh_params.username, ui)?;
|
||||||
|
|
||||||
let creds = ui.run_and_log_error(|| {
|
let creds = ui.run_and_log_error(|| {
|
||||||
let mut chan = sess.channel_session()?;
|
|
||||||
|
|
||||||
// The `tlscreds` command will start the server if needed and then
|
// The `tlscreds` command will start the server if needed and then
|
||||||
// obtain client credentials that we can use for tls.
|
// obtain client credentials that we can use for tls.
|
||||||
let cmd = format!(
|
let cmd = format!(
|
||||||
@ -603,20 +585,20 @@ impl Reconnectable {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ui.output_str(&format!("Running: {}\n", cmd));
|
ui.output_str(&format!("Running: {}\n", cmd));
|
||||||
chan.exec(&cmd)
|
let mut exec = smol::block_on(sess.exec(&cmd, None))
|
||||||
.with_context(|| format!("executing `{}` on remote host", cmd))?;
|
.with_context(|| format!("executing `{}` on remote host", cmd))?;
|
||||||
|
|
||||||
// stdout holds an encoded pdu
|
// stdout holds an encoded pdu
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
chan.read_to_end(&mut buf)
|
exec.stdout
|
||||||
|
.read_to_end(&mut buf)
|
||||||
.context("reading tlscreds response to buffer")?;
|
.context("reading tlscreds response to buffer")?;
|
||||||
|
|
||||||
chan.send_eof()?;
|
drop(exec.stdin);
|
||||||
chan.wait_eof()?;
|
|
||||||
|
|
||||||
// stderr is ideally empty
|
// stderr is ideally empty
|
||||||
let mut err = String::new();
|
let mut err = String::new();
|
||||||
chan.stderr()
|
exec.stderr
|
||||||
.read_to_string(&mut err)
|
.read_to_string(&mut err)
|
||||||
.context("reading tlscreds stderr")?;
|
.context("reading tlscreds stderr")?;
|
||||||
if !err.is_empty() {
|
if !err.is_empty() {
|
||||||
@ -624,11 +606,11 @@ impl Reconnectable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let creds = match Pdu::decode(buf.as_slice())
|
let creds = match Pdu::decode(buf.as_slice())
|
||||||
.with_context(|| format!("reading tlscreds response. stderr={}", err))?
|
.context("reading tlscreds response")?
|
||||||
.pdu
|
.pdu
|
||||||
{
|
{
|
||||||
Pdu::GetTlsCredsResponse(creds) => creds,
|
Pdu::GetTlsCredsResponse(creds) => creds,
|
||||||
_ => bail!("unexpected response to tlscreds, stderr={}", err),
|
_ => bail!("unexpected response to tlscreds"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save the credentials to disk, as that is currently the easiest
|
// Save the credentials to disk, as that is currently the easiest
|
||||||
|
@ -53,6 +53,14 @@ impl SessionSender {
|
|||||||
pub(crate) enum SessionRequest {
|
pub(crate) enum SessionRequest {
|
||||||
NewPty(NewPty),
|
NewPty(NewPty),
|
||||||
ResizePty(ResizePty),
|
ResizePty(ResizePty),
|
||||||
|
Exec(Exec),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Exec {
|
||||||
|
pub command_line: String,
|
||||||
|
pub env: Option<HashMap<String, String>>,
|
||||||
|
pub reply: Sender<ExecResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct DescriptorState {
|
pub(crate) struct DescriptorState {
|
||||||
@ -328,12 +336,89 @@ impl SessionInner {
|
|||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
SessionRequest::Exec(exec) => {
|
||||||
|
if let Err(err) = self.exec(&sess, &exec) {
|
||||||
|
log::error!("{:?} -> error: {:#}", exec, err);
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
sess.set_blocking(false);
|
sess.set_blocking(false);
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn exec(&mut self, sess: &ssh2::Session, exec: &Exec) -> anyhow::Result<()> {
|
||||||
|
sess.set_blocking(true);
|
||||||
|
|
||||||
|
let mut channel = sess.channel_session()?;
|
||||||
|
|
||||||
|
if let Some(env) = &exec.env {
|
||||||
|
for (key, val) in env {
|
||||||
|
if let Err(err) = channel.setenv(key, val) {
|
||||||
|
// Depending on the server configuration, a given
|
||||||
|
// setenv request may not succeed, but that doesn't
|
||||||
|
// prevent the connection from being set up.
|
||||||
|
log::warn!("ssh: setenv {}={} failed: {}", key, val, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.exec(&exec.command_line)?;
|
||||||
|
|
||||||
|
let channel_id = self.next_channel_id;
|
||||||
|
self.next_channel_id += 1;
|
||||||
|
|
||||||
|
let (write_to_stdin, mut read_from_stdin) = socketpair()?;
|
||||||
|
let (mut write_to_stdout, read_from_stdout) = socketpair()?;
|
||||||
|
let (mut write_to_stderr, read_from_stderr) = socketpair()?;
|
||||||
|
|
||||||
|
read_from_stdin.set_non_blocking(true)?;
|
||||||
|
write_to_stdout.set_non_blocking(true)?;
|
||||||
|
write_to_stderr.set_non_blocking(true)?;
|
||||||
|
|
||||||
|
let (exit_tx, exit_rx) = bounded(1);
|
||||||
|
|
||||||
|
let child = SshChildProcess {
|
||||||
|
channel: channel_id,
|
||||||
|
tx: None,
|
||||||
|
exit: exit_rx,
|
||||||
|
exited: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = ExecResult {
|
||||||
|
stdin: write_to_stdin,
|
||||||
|
stdout: read_from_stdout,
|
||||||
|
stderr: read_from_stderr,
|
||||||
|
child,
|
||||||
|
};
|
||||||
|
|
||||||
|
let info = ChannelInfo {
|
||||||
|
channel_id,
|
||||||
|
channel,
|
||||||
|
exit: Some(exit_tx),
|
||||||
|
descriptors: [
|
||||||
|
DescriptorState {
|
||||||
|
fd: Some(read_from_stdin),
|
||||||
|
buf: VecDeque::with_capacity(8192),
|
||||||
|
},
|
||||||
|
DescriptorState {
|
||||||
|
fd: Some(write_to_stdout),
|
||||||
|
buf: VecDeque::with_capacity(8192),
|
||||||
|
},
|
||||||
|
DescriptorState {
|
||||||
|
fd: Some(write_to_stderr),
|
||||||
|
buf: VecDeque::with_capacity(8192),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
exec.reply.try_send(result)?;
|
||||||
|
self.channels.insert(channel_id, info);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -394,6 +479,32 @@ impl Session {
|
|||||||
child.tx.replace(self.tx.clone());
|
child.tx.replace(self.tx.clone());
|
||||||
Ok((ssh_pty, child))
|
Ok((ssh_pty, child))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn exec(
|
||||||
|
&self,
|
||||||
|
command_line: &str,
|
||||||
|
env: Option<HashMap<String, String>>,
|
||||||
|
) -> anyhow::Result<ExecResult> {
|
||||||
|
let (reply, rx) = bounded(1);
|
||||||
|
self.tx
|
||||||
|
.send(SessionRequest::Exec(Exec {
|
||||||
|
command_line: command_line.to_string(),
|
||||||
|
env,
|
||||||
|
reply,
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
let mut exec = rx.recv().await?;
|
||||||
|
exec.child.tx.replace(self.tx.clone());
|
||||||
|
Ok(exec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ExecResult {
|
||||||
|
pub stdin: FileDescriptor,
|
||||||
|
pub stdout: FileDescriptor,
|
||||||
|
pub stderr: FileDescriptor,
|
||||||
|
pub child: SshChildProcess,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_from_buf<W: Write>(w: &mut W, buf: &mut VecDeque<u8>) -> std::io::Result<()> {
|
fn write_from_buf<W: Write>(w: &mut W, buf: &mut VecDeque<u8>) -> std::io::Result<()> {
|
||||||
|
Loading…
Reference in New Issue
Block a user