Skip to content

Commit

Permalink
add wezterm ssh user@host some command
Browse files Browse the repository at this point in the history
This is mostly useful for folks on Windows, as you can also run
`wezterm start ssh user@host some command` to run the `ssh` binary in a
local pty and let it manage your ssh session.

On Windows the local pty currently breaks mouse reporting
(see microsoft/terminal#376) so it is
desirable to avoid using a local pty if we're going to talk to a
remote system.

This commit makes it a bit more convenient to establish an ad-hoc ssh
session with a pty on a remote host.  This method uses libssh2 under the
covers and thus doesn't support gssapi, which is potentially frustrating
for kerberized environments, but works ok for the majority of users.

There are two issues that I want to resolve in follow up work:

* The TERM has to be sent very early in the channel establishment,
  before we "know" the TERM in the `portable-pty` interface.  Will need to
  figure out how to specify that earlier in an appropriate way.
* Similarly, if no command is specified, we should request the use
  of the shell subsystem but we don't have a way to capture this
  intend with the cmdbuilder. Will need to solve this too.
  • Loading branch information
wez committed Aug 5, 2019
1 parent c294b4d commit 127b2a5
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 5 deletions.
47 changes: 44 additions & 3 deletions pty/src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
//! initiate a connection somewhere and to authenticate that session
//! before we can get to a point where `openpty` will be able to run.
use crate::{Child, CommandBuilder, ExitStatus, MasterPty, PtyPair, PtySize, PtySystem, SlavePty};
use failure::Fallible;
use failure::{format_err, Fallible};
use filedescriptor::AsRawSocketDescriptor;
use ssh2::{Channel, Session};
use std::collections::HashMap;
use std::io::Result as IoResult;
Expand Down Expand Up @@ -131,6 +132,12 @@ impl PtyHandle {
let mut inner = self.inner.lock().unwrap();
f(&mut inner.ptys.get_mut(&self.id).unwrap())
}

fn as_socket_descriptor(&self) -> filedescriptor::SocketDescriptor {
let inner = self.inner.lock().unwrap();
let stream = inner.session.tcp_stream();
stream.as_ref().unwrap().as_socket_descriptor()
}
}

struct SshMaster {
Expand Down Expand Up @@ -180,7 +187,9 @@ impl SlavePty for SshSlave {
fn spawn_command(&self, cmd: CommandBuilder) -> Fallible<Box<dyn Child>> {
self.pty.with_channel(|channel| {
for (key, val) in cmd.iter_env_as_str() {
channel.setenv(key, val)?;
channel
.setenv(key, val)
.map_err(|e| format_err!("ssh: setenv {}={} failed: {}", key, val, e))?;
}

let command = cmd.as_unix_command_line()?;
Expand Down Expand Up @@ -233,6 +242,38 @@ struct SshReader {

impl Read for SshReader {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
self.pty.with_channel(|channel| channel.read(buf))
// A blocking read, but we don't want to own the mutex while we
// sleep, so we manually poll the underlying socket descriptor
// and then use a non-blocking read to read the actual data
let socket = self.pty.as_socket_descriptor();
loop {
// Wait for input on the descriptor
let mut pfd = [filedescriptor::pollfd {
fd: socket,
events: filedescriptor::POLLIN,
revents: 0,
}];
filedescriptor::poll(&mut pfd, None).ok();

// a read won't block, so ask libssh2 for data from the
// associated channel, but do not block!
let res = {
let mut inner = self.pty.inner.lock().unwrap();
inner.session.set_blocking(false);
let res = inner.ptys.get_mut(&self.pty.id).unwrap().channel.read(buf);
inner.session.set_blocking(true);
res
};

// If we have data or an error, return it, otherwise let's
// try again!
match res {
Ok(len) => return Ok(len),
Err(err) => match err.kind() {
std::io::ErrorKind::WouldBlock => continue,
_ => return Err(err),
},
}
}
}
}
102 changes: 100 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Don't create a new standard console window when launched from the windows GUI.
#![windows_subsystem = "windows"]

use failure::{err_msg, Error, Fallible};
use failure::{err_msg, format_err, Error, Fallible};
use std::ffi::OsString;
use std::fs::DirBuilder;
use std::io::{Read, Write};
Expand Down Expand Up @@ -46,7 +46,6 @@ fn get_shell() -> Result<String, Error> {
if ent.is_null() {
Ok("/bin/sh".into())
} else {
use failure::format_err;
use std::ffi::CStr;
use std::str;
let shell = unsafe { CStr::from_ptr((*ent).pw_shell) };
Expand Down Expand Up @@ -123,6 +122,9 @@ enum SubCommand {
#[structopt(name = "start", about = "Start a front-end")]
Start(StartCommand),

#[structopt(name = "ssh", about = "Establish an ssh session")]
Ssh(SshCommand),

#[structopt(name = "cli", about = "Interact with experimental mux server")]
Cli(CliCommand),
}
Expand All @@ -142,6 +144,41 @@ enum CliSubCommand {
Proxy,
}

#[derive(Debug, StructOpt, Clone)]
struct SshCommand {
#[structopt(
long = "front-end",
raw(
possible_values = "&FrontEndSelection::variants()",
case_insensitive = "true"
)
)]
front_end: Option<FrontEndSelection>,

#[structopt(
long = "font-system",
raw(
possible_values = "&FontSystemSelection::variants()",
case_insensitive = "true"
)
)]
font_system: Option<FontSystemSelection>,

/// Specifies the remote system using the form:
/// `[username@]host[:port]`.
/// If `username@` is omitted, then your local $USER is used
/// instead.
/// If `:port` is omitted, then the standard ssh port (22) is
/// used instead.
user_at_host_and_port: String,

/// Instead of executing your shell, run PROG.
/// For example: `wezterm ssh user@host -- bash -l` will spawn bash
/// as if it were a login shell.
#[structopt(parse(from_os_str))]
prog: Vec<OsString>,
}

pub fn create_user_owned_dirs(p: &Path) -> Fallible<()> {
let mut builder = DirBuilder::new();
builder.recursive(true);
Expand Down Expand Up @@ -170,6 +207,66 @@ pub fn running_under_wsl() -> bool {
false
}

struct SshParameters {
username: String,
host_and_port: String,
}

impl SshParameters {
fn parse(host: &str) -> Fallible<Self> {
let parts: Vec<&str> = host.split('@').collect();

if parts.len() == 2 {
Ok(Self {
username: parts[0].to_string(),
host_and_port: parts[1].to_string(),
})
} else if parts.len() == 1 {
Ok(Self {
username: std::env::var("USER")?,
host_and_port: parts[0].to_string(),
})
} else {
failure::bail!("failed to parse ssh parameters from `{}`", host);
}
}
}

fn run_ssh(config: Arc<config::Config>, opts: &SshCommand) -> Fallible<()> {
let font_system = opts.font_system.unwrap_or(config.font_system);
font_system.set_default();

let fontconfig = Rc::new(FontConfiguration::new(Arc::clone(&config), font_system));
let cmd = if !opts.prog.is_empty() {
let argv: Vec<&std::ffi::OsStr> = opts.prog.iter().map(|x| x.as_os_str()).collect();
let mut builder = CommandBuilder::new(&argv[0]);
builder.args(&argv[1..]);
Some(builder)
} else {
None
};

let params = SshParameters::parse(&opts.user_at_host_and_port)?;

let sess = ssh::ssh_connect(&params.host_and_port, &params.username)?;
let pty_system = Box::new(portable_pty::ssh::SshSession::new(sess));
let domain: Arc<dyn Domain> =
Arc::new(ssh::RemoteSshDomain::with_pty_system(&config, pty_system));

let mux = Rc::new(mux::Mux::new(&config, &domain));
Mux::set_mux(&mux);

let front_end = opts.front_end.unwrap_or(config.front_end);
let gui = front_end.try_new(&mux)?;
domain.attach()?;

let window_id = mux.new_empty_window();
let tab = domain.spawn(PtySize::default(), cmd, window_id)?;
gui.spawn_new_window(mux.config(), &fontconfig, &tab, window_id)?;

gui.run_forever()
}

fn run_terminal_gui(config: Arc<config::Config>, opts: &StartCommand) -> Fallible<()> {
#[cfg(unix)]
{
Expand Down Expand Up @@ -344,6 +441,7 @@ fn run() -> Result<(), Error> {
log::info!("Using configuration: {:#?}\nopts: {:#?}", config, opts);
run_terminal_gui(config, &start)
}
SubCommand::Ssh(ssh) => run_ssh(config, &ssh),
SubCommand::Cli(cli) => {
let client = Client::new_default_unix_domain(&config)?;
match cli.sub {
Expand Down
82 changes: 82 additions & 0 deletions src/ssh.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
use crate::config::Config;
use crate::frontend::guicommon::localtab::LocalTab;
use crate::mux::domain::{alloc_domain_id, Domain, DomainId, DomainState};
use crate::mux::tab::Tab;
use crate::mux::window::WindowId;
use crate::mux::Mux;
use failure::Error;
use failure::{bail, format_err, Fallible};
use portable_pty::cmdbuilder::CommandBuilder;
use portable_pty::{PtySize, PtySystem};
use std::collections::HashSet;
use std::io::Write;
use std::net::TcpStream;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;

fn password_prompt(
instructions: &str,
Expand Down Expand Up @@ -211,3 +222,74 @@ pub fn ssh_connect(remote_address: &str, username: &str) -> Fallible<ssh2::Sessi

Ok(sess)
}

pub struct RemoteSshDomain {
pty_system: Box<dyn PtySystem>,
config: Arc<Config>,
id: DomainId,
}

impl RemoteSshDomain {
pub fn with_pty_system(config: &Arc<Config>, pty_system: Box<dyn PtySystem>) -> Self {
let config = Arc::clone(config);
let id = alloc_domain_id();
Self {
pty_system,
config,
id,
}
}
}

impl Domain for RemoteSshDomain {
fn spawn(
&self,
size: PtySize,
command: Option<CommandBuilder>,
window: WindowId,
) -> Result<Rc<dyn Tab>, Error> {
let cmd = match command {
Some(c) => c,
None => CommandBuilder::new("bash"),
};
let pair = self.pty_system.openpty(size)?;
let child = pair.slave.spawn_command(cmd)?;
log::info!("spawned: {:?}", child);

let mut terminal = term::Terminal::new(
size.rows as usize,
size.cols as usize,
self.config.scrollback_lines.unwrap_or(3500),
self.config.hyperlink_rules.clone(),
);

let mux = Mux::get().unwrap();

if let Some(palette) = mux.config().colors.as_ref() {
*terminal.palette_mut() = palette.clone().into();
}

let tab: Rc<dyn Tab> = Rc::new(LocalTab::new(terminal, child, pair.master, self.id));

mux.add_tab(&tab)?;
mux.add_tab_to_window(&tab, window)?;

Ok(tab)
}

fn domain_id(&self) -> DomainId {
self.id
}

fn attach(&self) -> Fallible<()> {
Ok(())
}

fn detach(&self) -> Fallible<()> {
failure::bail!("detach not implemented");
}

fn state(&self) -> DomainState {
DomainState::Attached
}
}

0 comments on commit 127b2a5

Please sign in to comment.