1
1
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:
Wez Furlong 2021-03-28 07:11:14 -07:00
parent e96011bc26
commit 369888c94e
6 changed files with 210 additions and 220 deletions

3
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<()> {