1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 05:12:40 +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",
"serde",
"smol",
"ssh2",
"sysinfo",
"terminfo",
"termwiz",
@ -4399,12 +4398,12 @@ dependencies = [
"rangeset",
"ratelim",
"smol",
"ssh2",
"termwiz",
"textwrap 0.13.4",
"thiserror",
"uds_windows",
"url",
"wezterm-ssh",
"wezterm-term",
]

View File

@ -26,7 +26,6 @@ ratelim= { path = "../ratelim" }
regex = "1"
serde = {version="1.0", features = ["rc", "derive"]}
smol = "1.2"
ssh2 = "0.9"
terminfo = "0.7"
termwiz = { path = "../termwiz" }
textwrap = "0.13"

View File

@ -11,10 +11,8 @@ use filedescriptor::{socketpair, FileDescriptor};
use portable_pty::cmdbuilder::CommandBuilder;
use portable_pty::{ExitStatus, MasterPty, PtySize};
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::net::TcpStream;
use std::path::Path;
use std::rc::Rc;
use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
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(
remote_address: &str,
username: &str,
ui: &mut ConnectionUI,
) -> anyhow::Result<ssh2::Session> {
) -> anyhow::Result<Session> {
let cloned_ui = ui.clone();
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();
if parts.len() == 2 {
(remote_address.to_string(), parts[0], parts[1].parse()?)
(parts[0], Some(parts[1].parse::<u16>()?))
} 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));
let (session, events) = Session::connect(ssh_config.clone())?;
let tcp = TcpStream::connect(&remote_address)
.with_context(|| format!("ssh connecting to {}", remote_address))?;
ui.output_str("SSH: Connected OK!\n");
tcp.set_nodelay(true)?;
sess.set_tcp_stream(tcp);
sess.handshake()
.with_context(|| format!("ssh handshake with {}", remote_address))?;
if let Ok(mut known_hosts) = sess.known_hosts() {
let varname = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
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]> ")?;
while let Ok(event) = smol::block_on(events.recv()) {
match event {
SessionEvent::Banner(banner) => {
if let Some(banner) = banner {
ui.output_str(&format!("{}\n", banner));
}
}
SessionEvent::HostVerify(verify) => {
ui.output_str(&format!("{}\n", verify.message));
let ok = if let Ok(line) = ui.input("Enter [y/n]> ") {
match line.as_ref() {
"y" | "Y" | "yes" | "YES" => break,
"n" | "N" | "no" | "NO" => bail!("user declined to trust host"),
_ => continue,
"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() {
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");
}
}
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()))?;
smol::block_on(auth.answer(answers))?;
}
CheckResult::Mismatch => {
ui.output_str(&format!(
"🛑 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::Error(err) => {
anyhow::bail!("Error: {}", err);
}
SessionEvent::Authenticated => return Ok(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)
bail!("unable to authenticate session");
})
}

View File

@ -19,16 +19,16 @@ lru = "0.6"
metrics = { version="0.14", features=["std"]}
mux = { path = "../mux" }
openssl = "0.10"
portable-pty = { path = "../pty", features = ["serde_support", "ssh"]}
portable-pty = { path = "../pty", features = ["serde_support"]}
promise = { path = "../promise" }
rangeset = { path = "../rangeset" }
ratelim= { path = "../ratelim" }
smol = "1.2"
ssh2 = "0.9"
termwiz = { path = "../termwiz" }
textwrap = "0.13"
thiserror = "1.0"
url = "2"
wezterm-ssh = { path = "../wezterm-ssh" }
wezterm-term = { path = "../term", features=["use_serde"] }
[target."cfg(windows)".dependencies]

View File

@ -6,6 +6,7 @@ use async_ossl::AsyncSslStream;
use async_trait::async_trait;
use codec::*;
use config::{configuration, SshDomain, TlsDomainClient, UnixDomain};
use filedescriptor::FileDescriptor;
use futures::FutureExt;
use mux::connui::ConnectionUI;
use mux::domain::{alloc_domain_id, DomainId};
@ -18,7 +19,6 @@ use smol::channel::{bounded, unbounded, Receiver, Sender};
use smol::prelude::*;
use smol::{block_on, Async};
use std::collections::HashMap;
use std::convert::TryInto;
use std::io::{Read, Write};
use std::marker::Unpin;
use std::net::TcpStream;
@ -312,8 +312,9 @@ struct Reconnectable {
}
struct SshStream {
chan: ssh2::Channel,
sess: ssh2::Session,
stdin: FileDescriptor,
stdout: FileDescriptor,
_child: wezterm_ssh::SshChildProcess,
}
impl std::fmt::Debug for SshStream {
@ -325,60 +326,29 @@ impl std::fmt::Debug for SshStream {
#[cfg(unix)]
impl std::os::unix::io::AsRawFd for SshStream {
fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
self.sess.as_raw_fd()
self.stdout.as_raw_fd()
}
}
#[cfg(windows)]
impl std::os::windows::io::AsRawSocket for SshStream {
fn as_raw_socket(&self) -> std::os::windows::io::RawSocket {
self.sess.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);
self.stdout.as_raw_socket()
}
}
impl Read for SshStream {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
// Take the opportunity to read and show data from stderr
self.process_stderr();
self.chan.read(buf)
self.stdout.read(buf)
}
}
impl Write for SshStream {
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> {
self.chan.flush()
self.stdin.flush()
}
}
@ -456,10 +426,6 @@ impl Reconnectable {
ui: &mut ConnectionUI,
) -> anyhow::Result<()> {
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 cmd = if initial {
@ -469,9 +435,27 @@ impl Reconnectable {
};
ui.output_str(&format!("Running: {}\n", 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);
Ok(())
}
@ -593,8 +577,6 @@ impl Reconnectable {
ssh_connect_with_ui(&ssh_params.host_and_port, &ssh_params.username, ui)?;
let creds = ui.run_and_log_error(|| {
let mut chan = sess.channel_session()?;
// The `tlscreds` command will start the server if needed and then
// obtain client credentials that we can use for tls.
let cmd = format!(
@ -603,20 +585,20 @@ impl Reconnectable {
);
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))?;
// stdout holds an encoded pdu
let mut buf = Vec::new();
chan.read_to_end(&mut buf)
exec.stdout
.read_to_end(&mut buf)
.context("reading tlscreds response to buffer")?;
chan.send_eof()?;
chan.wait_eof()?;
drop(exec.stdin);
// stderr is ideally empty
let mut err = String::new();
chan.stderr()
exec.stderr
.read_to_string(&mut err)
.context("reading tlscreds stderr")?;
if !err.is_empty() {
@ -624,11 +606,11 @@ impl Reconnectable {
}
let creds = match Pdu::decode(buf.as_slice())
.with_context(|| format!("reading tlscreds response. stderr={}", err))?
.context("reading tlscreds response")?
.pdu
{
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

View File

@ -53,6 +53,14 @@ impl SessionSender {
pub(crate) enum SessionRequest {
NewPty(NewPty),
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 {
@ -328,12 +336,89 @@ impl SessionInner {
}
Ok(true)
}
SessionRequest::Exec(exec) => {
if let Err(err) = self.exec(&sess, &exec) {
log::error!("{:?} -> error: {:#}", exec, err);
}
Ok(true)
}
};
sess.set_blocking(false);
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)]
@ -394,6 +479,32 @@ impl Session {
child.tx.replace(self.tx.clone());
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<()> {