1
1
mirror of https://github.com/wez/wezterm.git synced 2024-09-19 18:57:59 +03:00

Support gating libssh-rs and ssh2 behind features of same name

This commit is contained in:
Chip Senkbeil 2021-12-17 22:01:34 -06:00 committed by Wez Furlong
parent b3987bec12
commit 1f17416e60
15 changed files with 335 additions and 133 deletions

View File

@ -11,6 +11,7 @@ documentation = "https://docs.rs/wezterm-ssh"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["libssh-rs", "ssh2"]
vendored-openssl = ["ssh2/vendored-openssl", "libssh-rs/vendored-openssl"]
[dependencies]
@ -26,9 +27,9 @@ log = "0.4"
portable-pty = { version="0.7", path = "../pty" }
regex = "1"
smol = "1.2"
ssh2 = {version="0.9.3", features=["openssl-on-win32"]}
libssh-rs = {version="0.1.2", features=["vendored"], git="https://github.com/wez/libssh-rs.git"}
#libssh-rs = {path="../../libssh-rs/libssh-rs", features=["vendored"]}
ssh2 = {version="0.9.3", features=["openssl-on-win32"], optional = true}
libssh-rs = {version="0.1.2", features=["vendored"], git="https://github.com/wez/libssh-rs.git", optional = true}
#libssh-rs = {path="../../libssh-rs/libssh-rs", features=["vendored"], optional = true}
thiserror = "1.0"
# Not used directly, but is used to centralize the openssl vendor feature selection

View File

@ -1,9 +1,6 @@
use crate::session::SessionEvent;
use anyhow::Context;
use libssh_rs as libssh;
use smol::channel::{bounded, Sender};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct AuthenticationPrompt {
@ -30,6 +27,7 @@ impl AuthenticationEvent {
}
impl crate::sessioninner::SessionInner {
#[cfg(feature = "ssh2")]
fn agent_auth(&mut self, sess: &ssh2::Session, user: &str) -> anyhow::Result<bool> {
if let Some(only) = self.config.get("identitiesonly") {
if only == "yes" {
@ -55,12 +53,15 @@ impl crate::sessioninner::SessionInner {
Ok(false)
}
#[cfg(feature = "ssh2")]
fn pubkey_auth(
&mut self,
sess: &ssh2::Session,
user: &str,
host: &str,
) -> anyhow::Result<bool> {
use std::path::{Path, PathBuf};
if let Some(files) = self.config.get("identityfile") {
for file in files.split_whitespace() {
let pubkey: PathBuf = format!("{}.pub", file).into();
@ -127,7 +128,9 @@ impl crate::sessioninner::SessionInner {
Ok(false)
}
pub fn authenticate_libssh(&mut self, sess: &libssh::Session) -> anyhow::Result<()> {
#[cfg(feature = "libssh-rs")]
pub fn authenticate_libssh(&mut self, sess: &libssh_rs::Session) -> anyhow::Result<()> {
use std::collections::HashMap;
let tx = self.tx_event.clone();
// Set the callback for pubkey auth
@ -153,7 +156,7 @@ impl crate::sessioninner::SessionInner {
Ok(answers.remove(0))
});
use libssh::{AuthMethods, AuthStatus};
use libssh_rs::{AuthMethods, AuthStatus};
match sess.userauth_none(None)? {
AuthStatus::Success => return Ok(()),
_ => {}
@ -249,12 +252,15 @@ impl crate::sessioninner::SessionInner {
}
}
#[cfg(feature = "ssh2")]
pub fn authenticate(
&mut self,
sess: &ssh2::Session,
user: &str,
host: &str,
) -> anyhow::Result<()> {
use std::collections::HashSet;
loop {
if sess.authenticated() {
return Ok(());

View File

@ -1,12 +1,15 @@
use crate::pty::{NewPty, ResizePty};
use libssh_rs as libssh;
use portable_pty::ExitStatus;
pub(crate) enum ChannelWrap {
#[cfg(feature = "ssh2")]
Ssh2(ssh2::Channel),
LibSsh(libssh::Channel),
#[cfg(feature = "libssh-rs")]
LibSsh(libssh_rs::Channel),
}
#[cfg(feature = "ssh2")]
fn has_signal(chan: &ssh2::Channel) -> Option<ssh2::ExitSignal> {
if let Ok(sig) = chan.exit_signal() {
if sig.exit_signal.is_some() {
@ -19,6 +22,7 @@ fn has_signal(chan: &ssh2::Channel) -> Option<ssh2::ExitSignal> {
impl ChannelWrap {
pub fn exit_status(&mut self) -> Option<ExitStatus> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(chan) => {
if chan.eof() && chan.wait_close().is_ok() {
if let Some(_sig) = has_signal(chan) {
@ -32,6 +36,8 @@ impl ChannelWrap {
None
}
}
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => {
if chan.is_eof() {
if let Some(status) = chan.get_exit_status() {
@ -45,7 +51,10 @@ impl ChannelWrap {
pub fn reader(&mut self, idx: usize) -> Box<dyn std::io::Read + '_> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(chan) => Box::new(chan.stream(idx as i32)),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => match idx {
0 => Box::new(chan.stdout()),
1 => Box::new(chan.stderr()),
@ -56,16 +65,22 @@ impl ChannelWrap {
pub fn writer(&mut self) -> Box<dyn std::io::Write + '_> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(chan) => Box::new(chan),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => Box::new(chan.stdin()),
}
}
pub fn close(&mut self) {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(chan) => {
let _ = chan.close();
}
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => {
let _ = chan.close();
}
@ -74,6 +89,7 @@ impl ChannelWrap {
pub fn request_pty(&mut self, newpty: &NewPty) -> anyhow::Result<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(chan) => Ok(chan.request_pty(
&newpty.term,
None,
@ -84,6 +100,8 @@ impl ChannelWrap {
newpty.size.pixel_height.into(),
)),
)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => Ok(chan.request_pty(
&newpty.term,
newpty.size.cols.into(),
@ -94,42 +112,60 @@ impl ChannelWrap {
pub fn request_env(&mut self, name: &str, value: &str) -> anyhow::Result<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(chan) => Ok(chan.setenv(name, value)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => Ok(chan.request_env(name, value)?),
}
}
pub fn request_exec(&mut self, command_line: &str) -> anyhow::Result<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(chan) => Ok(chan.exec(command_line)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => Ok(chan.request_exec(command_line)?),
}
}
pub fn request_shell(&mut self) -> anyhow::Result<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(chan) => Ok(chan.shell()?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => Ok(chan.request_shell()?),
}
}
pub fn resize_pty(&mut self, resize: &ResizePty) -> anyhow::Result<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(chan) => Ok(chan.request_pty_size(
resize.size.cols.into(),
resize.size.rows.into(),
Some(resize.size.pixel_width.into()),
Some(resize.size.pixel_height.into()),
)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => {
Ok(chan.change_pty_size(resize.size.cols.into(), resize.size.rows.into())?)
}
}
}
pub fn send_signal(&mut self, signame: &str) -> anyhow::Result<()> {
pub fn send_signal(
&mut self,
#[cfg_attr(not(feature = "libssh-rs"), allow(unused_variables))] signame: &str,
) -> anyhow::Result<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(_) => Ok(()),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(chan) => Ok(chan.request_send_signal(signame)?),
}
}

View File

@ -1,18 +1,21 @@
use crate::sftp::types::Metadata;
use crate::sftp::{SftpChannelError, SftpChannelResult};
use camino::Utf8PathBuf;
use libssh_rs as libssh;
use std::convert::TryFrom;
pub(crate) enum DirWrap {
#[cfg(feature = "ssh2")]
Ssh2(ssh2::File),
LibSsh(libssh::SftpDir),
#[cfg(feature = "libssh-rs")]
LibSsh(libssh_rs::SftpDir),
}
impl DirWrap {
pub fn read_dir(&mut self) -> SftpChannelResult<(Utf8PathBuf, Metadata)> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(file) => {
use std::convert::TryFrom;
file.readdir()
.map_err(SftpChannelError::from)
.and_then(|(path, stat)| match Utf8PathBuf::try_from(path) {
@ -23,6 +26,8 @@ impl DirWrap {
))),
})
}
#[cfg(feature = "libssh-rs")]
Self::LibSsh(dir) => match dir.read_dir() {
None => Err(SftpChannelError::from(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,

View File

@ -1,32 +1,45 @@
use crate::sftp::types::Metadata;
use crate::sftp::{SftpChannelError, SftpChannelResult};
use libssh_rs as libssh;
use std::io::Write;
pub(crate) enum FileWrap {
#[cfg(feature = "ssh2")]
Ssh2(ssh2::File),
LibSsh(libssh::SftpFile),
#[cfg(feature = "libssh-rs")]
LibSsh(libssh_rs::SftpFile),
}
impl FileWrap {
pub fn reader(&mut self) -> Box<dyn std::io::Read + '_> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(file) => Box::new(file),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(file) => Box::new(file),
}
}
pub fn writer(&mut self) -> Box<dyn std::io::Write + '_> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(file) => Box::new(file),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(file) => Box::new(file),
}
}
pub fn set_metadata(&mut self, metadata: Metadata) -> SftpChannelResult<()> {
pub fn set_metadata(
&mut self,
#[cfg_attr(not(feature = "ssh2"), allow(unused_variables))] metadata: Metadata,
) -> SftpChannelResult<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(file) => Ok(file.setstat(metadata.into())?),
Self::LibSsh(_file) => Err(libssh::Error::fatal(
#[cfg(feature = "libssh-rs")]
Self::LibSsh(_file) => Err(libssh_rs::Error::fatal(
"FileWrap::set_metadata not implemented for libssh::SftpFile",
)
.into()),
@ -35,7 +48,10 @@ impl FileWrap {
pub fn metadata(&mut self) -> SftpChannelResult<Metadata> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(file) => Ok(file.stat().map(Metadata::from)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(file) => file
.metadata()
.map(Metadata::from)
@ -45,8 +61,14 @@ impl FileWrap {
pub fn fsync(&mut self) -> SftpChannelResult<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(file) => file.fsync().map_err(SftpChannelError::from),
Self::LibSsh(file) => Ok(file.flush()?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(file) => {
use std::io::Write;
Ok(file.flush()?)
}
}
}
}

View File

@ -1,10 +1,6 @@
use crate::session::SessionEvent;
use anyhow::{anyhow, Context};
use libssh_rs as libssh;
use anyhow::Context;
use smol::channel::{bounded, Sender};
use ssh2::CheckResult;
use std::io::Write;
use std::path::Path;
#[derive(Debug)]
pub struct HostVerificationEvent {
@ -22,19 +18,20 @@ impl HostVerificationEvent {
}
impl crate::sessioninner::SessionInner {
#[cfg(feature = "libssh-rs")]
pub fn host_verification_libssh(
&mut self,
sess: &libssh::Session,
sess: &libssh_rs::Session,
hostname: &str,
port: u16,
) -> anyhow::Result<()> {
let key = sess
.get_server_public_key()?
.get_public_key_hash_hexa(libssh::PublicKeyHashType::Sha256)?;
.get_public_key_hash_hexa(libssh_rs::PublicKeyHashType::Sha256)?;
match sess.is_known_server()? {
libssh::KnownHosts::Ok => Ok(()),
libssh::KnownHosts::NotFound | libssh::KnownHosts::Unknown => {
libssh_rs::KnownHosts::Ok => Ok(()),
libssh_rs::KnownHosts::NotFound | libssh_rs::KnownHosts::Unknown => {
let (reply, confirm) = bounded(1);
self.tx_event
.try_send(SessionEvent::HostVerify(HostVerificationEvent {
@ -57,7 +54,7 @@ impl crate::sessioninner::SessionInner {
Ok(sess.update_known_hosts_file()?)
}
libssh::KnownHosts::Changed => {
libssh_rs::KnownHosts::Changed => {
anyhow::bail!(
"host key mismatch for ssh server {}:{}.\n\
Got fingerprint {} instead of expected value from known_hosts\n\
@ -68,7 +65,7 @@ impl crate::sessioninner::SessionInner {
key,
);
}
libssh::KnownHosts::Other => {
libssh_rs::KnownHosts::Other => {
anyhow::bail!(
"The host key for this server was not found, but another\n\
type of key exists. An attacker might change the default\n\
@ -78,6 +75,8 @@ impl crate::sessioninner::SessionInner {
}
}
}
#[cfg(feature = "ssh2")]
pub fn host_verification(
&mut self,
sess: &ssh2::Session,
@ -85,6 +84,10 @@ impl crate::sessioninner::SessionInner {
port: u16,
remote_address: &str,
) -> anyhow::Result<()> {
use anyhow::anyhow;
use std::io::Write;
use std::path::Path;
let mut known_hosts = sess.known_hosts().context("preparing known hosts")?;
let known_hosts_files = self
@ -135,8 +138,8 @@ impl crate::sessioninner::SessionInner {
.ok_or_else(|| anyhow!("failed to get host fingerprint"))?;
match known_hosts.check_port(&remote_host_name, port, key) {
CheckResult::Match => {}
CheckResult::NotFound => {
ssh2::CheckResult::Match => {}
ssh2::CheckResult::NotFound => {
let (reply, confirm) = bounded(1);
self.tx_event
.try_send(SessionEvent::HostVerify(HostVerificationEvent {
@ -171,7 +174,7 @@ impl crate::sessioninner::SessionInner {
.write_file(&file, ssh2::KnownHostFileKind::OpenSSH)
.with_context(|| format!("writing known_hosts file {}", file.display()))?;
}
CheckResult::Mismatch => {
ssh2::CheckResult::Mismatch => {
anyhow::bail!(
"host key mismatch for ssh server {}.\n\
Got fingerprint {} instead of expected value from known_hosts\n\
@ -182,7 +185,7 @@ impl crate::sessioninner::SessionInner {
file.display()
);
}
CheckResult::Failure => {
ssh2::CheckResult::Failure => {
anyhow::bail!("failed to check the known hosts");
}
}

View File

@ -1,3 +1,6 @@
#[cfg(not(any(feature = "libssh-rs", feature = "ssh2")))]
compile_error!("Either libssh-rs or ssh2 must be enabled!");
mod auth;
mod channelwrap;
mod config;

View File

@ -14,13 +14,10 @@ use camino::Utf8PathBuf;
use filedescriptor::{
poll, pollfd, socketpair, AsRawSocketDescriptor, FileDescriptor, POLLIN, POLLOUT,
};
use libssh_rs as libssh;
use portable_pty::ExitStatus;
use smol::channel::{bounded, Receiver, Sender, TryRecvError};
use std::collections::{HashMap, VecDeque};
use std::ffi::CStr;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
#[derive(Debug)]
@ -70,10 +67,31 @@ impl SessionInner {
.config
.get("wezterm_ssh_backend")
.map(|s| s.as_str())
.unwrap_or("libssh");
.unwrap_or(
#[cfg(feature = "libssh-rs")]
"libssh",
#[cfg(not(feature = "libssh-rs"))]
"ssh2",
);
match backend {
#[cfg(feature = "ssh2")]
"ssh2" => self.run_impl_ssh2(),
#[cfg(not(feature = "ssh2"))]
"ssh2" => anyhow::bail!(
"invalid wezterm_ssh_backend value: {}, not compiled with `ssh2`",
backend
),
#[cfg(feature = "libssh-rs")]
"libssh" => self.run_impl_libssh(),
#[cfg(not(feature = "libssh-rs"))]
"libssh" => anyhow::bail!(
"invalid wezterm_ssh_backend value: {}, not compiled with `libssh`",
backend
),
_ => anyhow::bail!(
"invalid wezterm_ssh_backend value: {}, expected either `ssh2` or `libssh`",
backend
@ -81,6 +99,7 @@ impl SessionInner {
}
}
#[cfg(feature = "libssh-rs")]
fn run_impl_libssh(&mut self) -> anyhow::Result<()> {
let hostname = self
.config
@ -105,7 +124,7 @@ impl SessionInner {
))))
.context("notifying user of banner")?;
let sess = libssh::Session::new()?;
let sess = libssh_rs::Session::new()?;
if self
.config
.get("wezterm_ssh_verbose")
@ -113,7 +132,7 @@ impl SessionInner {
.unwrap_or("false")
== "true"
{
sess.set_option(libssh::SshOption::LogLevel(libssh::LogLevel::Packet))?;
sess.set_option(libssh_rs::SshOption::LogLevel(libssh_rs::LogLevel::Packet))?;
/// libssh logs to stderr, but on Windows in the GUI there isn't a valid
/// stderr for it to log to.
@ -125,6 +144,7 @@ impl SessionInner {
message: *const std::os::raw::c_char,
_userdata: *mut std::os::raw::c_void,
) {
use std::ffi::CStr;
let function = CStr::from_ptr(function).to_string_lossy().to_string();
let message = CStr::from_ptr(message).to_string_lossy().to_string();
@ -147,32 +167,34 @@ impl SessionInner {
);
}
unsafe {
libssh::sys::ssh_set_log_callback(Some(log_callback));
libssh_rs::sys::ssh_set_log_callback(Some(log_callback));
}
}
sess.set_option(libssh::SshOption::Hostname(hostname.clone()))?;
sess.set_option(libssh::SshOption::User(Some(user)))?;
sess.set_option(libssh::SshOption::Port(port))?;
sess.set_option(libssh_rs::SshOption::Hostname(hostname.clone()))?;
sess.set_option(libssh_rs::SshOption::User(Some(user)))?;
sess.set_option(libssh_rs::SshOption::Port(port))?;
sess.options_parse_config(None)?; // FIXME: overridden config path?
if let Some(agent) = self.config.get("identityagent") {
sess.set_option(libssh::SshOption::IdentityAgent(Some(agent.clone())))?;
sess.set_option(libssh_rs::SshOption::IdentityAgent(Some(agent.clone())))?;
}
if let Some(files) = self.config.get("identityfile") {
for file in files.split_whitespace() {
sess.set_option(libssh::SshOption::AddIdentity(file.to_string()))?;
sess.set_option(libssh_rs::SshOption::AddIdentity(file.to_string()))?;
}
}
if let Some(kh) = self.config.get("userknownhostsfile") {
for file in kh.split_whitespace() {
sess.set_option(libssh::SshOption::KnownHosts(Some(file.to_string())))?;
sess.set_option(libssh_rs::SshOption::KnownHosts(Some(file.to_string())))?;
break;
}
}
if let Some(cmd) = self.config.get("proxycommand") {
sess.set_option(libssh::SshOption::ProxyCommand(Some(cmd.to_string())))?;
sess.set_option(libssh_rs::SshOption::ProxyCommand(Some(cmd.to_string())))?;
}
if let Some(types) = self.config.get("pubkeyacceptedtypes") {
sess.set_option(libssh::SshOption::PublicKeyAcceptedTypes(types.to_string()))?;
sess.set_option(libssh_rs::SshOption::PublicKeyAcceptedTypes(
types.to_string(),
))?;
}
sess.connect()?;
@ -200,7 +222,9 @@ impl SessionInner {
self.request_loop(&mut sess)
}
#[cfg(feature = "ssh2")]
fn run_impl_ssh2(&mut self) -> anyhow::Result<()> {
use std::net::TcpStream;
let hostname = self
.config
.get("hostname")
@ -795,12 +819,15 @@ impl SessionInner {
/// Initialize the sftp channel if not already created, returning a mutable reference to it
fn init_sftp<'a>(&mut self, sess: &'a mut SessionWrap) -> SftpChannelResult<&'a mut SftpWrap> {
match sess {
#[cfg(feature = "ssh2")]
SessionWrap::Ssh2(sess) => {
if sess.sftp.is_none() {
sess.sftp = Some(SftpWrap::Ssh2(sess.sess.sftp()?));
}
Ok(sess.sftp.as_mut().expect("sftp should have been set above"))
}
#[cfg(feature = "libssh-rs")]
SessionWrap::LibSsh(sess) => {
if sess.sftp.is_none() {
sess.sftp = Some(SftpWrap::LibSsh(sess.sess.sftp()?));

View File

@ -1,48 +1,59 @@
use crate::channelwrap::ChannelWrap;
use crate::sftpwrap::SftpWrap;
use filedescriptor::{AsRawSocketDescriptor, SocketDescriptor, POLLIN, POLLOUT};
use libssh_rs as libssh;
use ssh2::BlockDirections;
#[cfg(feature = "ssh2")]
pub(crate) struct Ssh2Session {
pub sess: ssh2::Session,
pub sftp: Option<SftpWrap>,
}
#[cfg(feature = "libssh-rs")]
pub(crate) struct LibSshSession {
pub sess: libssh::Session,
pub sess: libssh_rs::Session,
pub sftp: Option<SftpWrap>,
}
pub(crate) enum SessionWrap {
#[cfg(feature = "ssh2")]
Ssh2(Ssh2Session),
#[cfg(feature = "libssh-rs")]
LibSsh(LibSshSession),
}
impl SessionWrap {
#[cfg(feature = "ssh2")]
pub fn with_ssh2(sess: ssh2::Session) -> Self {
Self::Ssh2(Ssh2Session { sess, sftp: None })
}
pub fn with_libssh(sess: libssh::Session) -> Self {
#[cfg(feature = "libssh-rs")]
pub fn with_libssh(sess: libssh_rs::Session) -> Self {
Self::LibSsh(LibSshSession { sess, sftp: None })
}
pub fn set_blocking(&mut self, blocking: bool) {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sess) => sess.sess.set_blocking(blocking),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sess) => sess.sess.set_blocking(blocking),
}
}
pub fn get_poll_flags(&self) -> i16 {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sess) => match sess.sess.block_directions() {
BlockDirections::None => 0,
BlockDirections::Inbound => POLLIN,
BlockDirections::Outbound => POLLOUT,
BlockDirections::Both => POLLIN | POLLOUT,
ssh2::BlockDirections::None => 0,
ssh2::BlockDirections::Inbound => POLLIN,
ssh2::BlockDirections::Outbound => POLLOUT,
ssh2::BlockDirections::Both => POLLIN | POLLOUT,
},
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sess) => {
let (read, write) = sess.sess.get_poll_state();
match (read, write) {
@ -57,17 +68,23 @@ impl SessionWrap {
pub fn as_socket_descriptor(&self) -> SocketDescriptor {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sess) => sess.sess.as_socket_descriptor(),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sess) => sess.sess.as_socket_descriptor(),
}
}
pub fn open_session(&self) -> anyhow::Result<ChannelWrap> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sess) => {
let channel = sess.sess.channel_session()?;
Ok(ChannelWrap::Ssh2(channel))
}
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sess) => {
let channel = sess.sess.new_channel()?;
channel.open_session()?;

View File

@ -36,20 +36,28 @@ pub enum SftpError {
NoMedia = 13,
// Below are libssh2-specific errors
#[cfg(feature = "ssh2")]
#[error("No space available on filesystem")]
NoSpaceOnFilesystem = 14,
#[cfg(feature = "ssh2")]
#[error("Quota exceeded")]
QuotaExceeded = 15,
#[cfg(feature = "ssh2")]
#[error("Unknown principal")]
UnknownPrincipal = 16,
#[cfg(feature = "ssh2")]
#[error("Filesystem lock conflict")]
LockConflict = 17,
#[cfg(feature = "ssh2")]
#[error("Directory is not empty")]
DirNotEmpty = 18,
#[cfg(feature = "ssh2")]
#[error("Operation attempted against a path that is not a directory")]
NotADirectory = 19,
#[cfg(feature = "ssh2")]
#[error("Filename invalid")]
InvalidFilename = 20,
#[cfg(feature = "ssh2")]
#[error("Symlink loop encountered")]
LinkLoop = 21,
}
@ -90,13 +98,23 @@ impl TryFrom<i32> for SftpError {
11 => Ok(Self::FileAlreadyExists),
12 => Ok(Self::WriteProtect),
13 => Ok(Self::NoMedia),
// Errors only available with ssh2
#[cfg(feature = "ssh2")]
14 => Ok(Self::NoSpaceOnFilesystem),
#[cfg(feature = "ssh2")]
15 => Ok(Self::QuotaExceeded),
#[cfg(feature = "ssh2")]
16 => Ok(Self::UnknownPrincipal),
#[cfg(feature = "ssh2")]
17 => Ok(Self::LockConflict),
#[cfg(feature = "ssh2")]
18 => Ok(Self::DirNotEmpty),
#[cfg(feature = "ssh2")]
19 => Ok(Self::NotADirectory),
#[cfg(feature = "ssh2")]
20 => Ok(Self::InvalidFilename),
#[cfg(feature = "ssh2")]
21 => Ok(Self::LinkLoop),
// Unsupported codes get reflected back
@ -105,6 +123,7 @@ impl TryFrom<i32> for SftpError {
}
}
#[cfg(feature = "ssh2")]
impl TryFrom<ssh2::Error> for SftpError {
type Error = ssh2::Error;
@ -119,6 +138,7 @@ impl TryFrom<ssh2::Error> for SftpError {
}
}
#[cfg(feature = "ssh2")]
impl TryFrom<ssh2::ErrorCode> for SftpError {
type Error = ssh2::ErrorCode;

View File

@ -4,9 +4,7 @@ use crate::sftp::file::{File, FileRequest};
use crate::sftp::types::{Metadata, OpenFileType, OpenOptions, RenameOptions, WriteMode};
use camino::Utf8PathBuf;
use error::SftpError;
use libssh_rs as libssh;
use smol::channel::{bounded, RecvError, Sender};
use std::convert::TryFrom;
use std::convert::TryInto;
use std::io;
use thiserror::Error;
@ -41,11 +39,13 @@ pub enum SftpChannelError {
#[error("Failed to receive response: {}", .0)]
RecvFailed(#[from] RecvError),
#[cfg(feature = "ssh2")]
#[error("Library-specific error: {}", .0)]
Ssh2(#[source] ssh2::Error),
#[cfg(feature = "libssh-rs")]
#[error("Library-specific error: {}", .0)]
LibSsh(#[source] libssh::Error),
LibSsh(#[source] libssh_rs::Error),
#[error("Not Implemented")]
NotImplemented,
@ -409,8 +409,10 @@ pub(crate) struct Rename {
pub opts: RenameOptions,
}
#[cfg(feature = "ssh2")]
impl From<ssh2::Error> for SftpChannelError {
fn from(err: ssh2::Error) -> Self {
use std::convert::TryFrom;
match SftpError::try_from(err) {
Ok(x) => Self::Sftp(x),
Err(x) => Self::Ssh2(x),
@ -418,8 +420,9 @@ impl From<ssh2::Error> for SftpChannelError {
}
}
impl From<libssh::Error> for SftpChannelError {
fn from(err: libssh::Error) -> Self {
#[cfg(feature = "libssh-rs")]
impl From<libssh_rs::Error> for SftpChannelError {
fn from(err: libssh_rs::Error) -> Self {
Self::LibSsh(err)
}
}

View File

@ -1,6 +1,4 @@
use bitflags::bitflags;
use libssh_rs as libssh;
use std::time::SystemTime;
bitflags! {
struct FileTypeFlags: u32 {
@ -251,6 +249,7 @@ impl Default for RenameOptions {
}
/// Contains libssh2-specific implementations
#[cfg(feature = "ssh2")]
mod ssh2_impl {
use super::*;
use ::ssh2::{
@ -351,65 +350,71 @@ mod ssh2_impl {
}
}
impl From<libssh::FileType> for FileType {
fn from(ft: libssh::FileType) -> Self {
match ft {
libssh::FileType::Directory => Self::Dir,
libssh::FileType::Regular => Self::File,
libssh::FileType::Symlink => Self::Symlink,
_ => Self::Other,
}
}
}
#[cfg(feature = "libssh-rs")]
mod libssh_impl {
use super::*;
use std::time::SystemTime;
fn sys_time_to_unix(t: SystemTime) -> u64 {
t.duration_since(SystemTime::UNIX_EPOCH)
.expect("UNIX_EPOCH < SystemTime")
.as_secs()
}
fn unix_to_sys(u: u64) -> SystemTime {
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(u)
}
impl From<libssh::Metadata> for Metadata {
fn from(stat: libssh::Metadata) -> Self {
Self {
ty: stat
.file_type()
.map(FileType::from)
.unwrap_or(FileType::Other),
permissions: stat.permissions().map(FilePermissions::from_unix_mode),
size: stat.len(),
uid: stat.uid(),
gid: stat.gid(),
accessed: stat.accessed().map(sys_time_to_unix),
modified: stat.modified().map(sys_time_to_unix),
}
}
}
impl Into<libssh::SetAttributes> for Metadata {
fn into(self) -> libssh::SetAttributes {
let size = self.size;
let uid_gid = match (self.uid, self.gid) {
(Some(uid), Some(gid)) => Some((uid, gid)),
_ => None,
};
let permissions = self.permissions.map(FilePermissions::to_unix_mode);
let atime_mtime = match (self.accessed, self.modified) {
(Some(a), Some(m)) => {
let a = unix_to_sys(a);
let m = unix_to_sys(m);
Some((a, m))
impl From<libssh_rs::FileType> for FileType {
fn from(ft: libssh_rs::FileType) -> Self {
match ft {
libssh_rs::FileType::Directory => Self::Dir,
libssh_rs::FileType::Regular => Self::File,
libssh_rs::FileType::Symlink => Self::Symlink,
_ => Self::Other,
}
}
}
fn sys_time_to_unix(t: SystemTime) -> u64 {
t.duration_since(SystemTime::UNIX_EPOCH)
.expect("UNIX_EPOCH < SystemTime")
.as_secs()
}
fn unix_to_sys(u: u64) -> SystemTime {
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(u)
}
impl From<libssh_rs::Metadata> for Metadata {
fn from(stat: libssh_rs::Metadata) -> Self {
Self {
ty: stat
.file_type()
.map(FileType::from)
.unwrap_or(FileType::Other),
permissions: stat.permissions().map(FilePermissions::from_unix_mode),
size: stat.len(),
uid: stat.uid(),
gid: stat.gid(),
accessed: stat.accessed().map(sys_time_to_unix),
modified: stat.modified().map(sys_time_to_unix),
}
}
}
impl Into<libssh_rs::SetAttributes> for Metadata {
fn into(self) -> libssh_rs::SetAttributes {
let size = self.size;
let uid_gid = match (self.uid, self.gid) {
(Some(uid), Some(gid)) => Some((uid, gid)),
_ => None,
};
let permissions = self.permissions.map(FilePermissions::to_unix_mode);
let atime_mtime = match (self.accessed, self.modified) {
(Some(a), Some(m)) => {
let a = unix_to_sys(a);
let m = unix_to_sys(m);
Some((a, m))
}
_ => None,
};
libssh_rs::SetAttributes {
size,
uid_gid,
permissions,
atime_mtime,
}
_ => None,
};
libssh::SetAttributes {
size,
uid_gid,
permissions,
atime_mtime,
}
}
}

View File

@ -1,18 +1,21 @@
use crate::dirwrap::DirWrap;
use crate::filewrap::FileWrap;
use crate::sftp::types::{Metadata, OpenOptions, RenameOptions, WriteMode};
use crate::sftp::{SftpChannelError, SftpChannelResult};
use crate::sftp::types::{Metadata, OpenOptions, RenameOptions};
use crate::sftp::SftpChannelResult;
use camino::{Utf8Path, Utf8PathBuf};
use libc::{O_APPEND, O_RDONLY, O_RDWR, O_WRONLY};
use libssh_rs as libssh;
use std::convert::{TryFrom, TryInto};
pub(crate) enum SftpWrap {
#[cfg(feature = "ssh2")]
Ssh2(ssh2::Sftp),
LibSsh(libssh::Sftp),
#[cfg(feature = "libssh-rs")]
LibSsh(libssh_rs::Sftp),
}
#[cfg(feature = "ssh2")]
fn pathconv(path: std::path::PathBuf) -> SftpChannelResult<Utf8PathBuf> {
use crate::sftp::SftpChannelError;
use std::convert::TryFrom;
Ok(Utf8PathBuf::try_from(path).map_err(|x| {
SftpChannelError::from(std::io::Error::new(std::io::ErrorKind::InvalidData, x))
})?)
@ -21,6 +24,7 @@ fn pathconv(path: std::path::PathBuf) -> SftpChannelResult<Utf8PathBuf> {
impl SftpWrap {
pub fn open(&self, filename: &Utf8Path, opts: OpenOptions) -> SftpChannelResult<FileWrap> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => {
let flags: ssh2::OpenFlags = opts.into();
let mode = opts.mode;
@ -29,7 +33,12 @@ impl SftpWrap {
let file = sftp.open_mode(filename.as_std_path(), flags, mode, open_type)?;
Ok(FileWrap::Ssh2(file))
}
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => {
use crate::sftp::types::WriteMode;
use libc::{O_APPEND, O_RDONLY, O_RDWR, O_WRONLY};
use std::convert::TryInto;
let accesstype = match (opts.write, opts.read) {
(Some(WriteMode::Append), true) => O_RDWR | O_APPEND,
(Some(WriteMode::Append), false) => O_WRONLY | O_APPEND,
@ -47,43 +56,64 @@ impl SftpWrap {
pub fn symlink(&self, path: &Utf8Path, target: &Utf8Path) -> SftpChannelResult<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(sftp.symlink(path.as_std_path(), target.as_std_path())?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => Ok(sftp.symlink(path.as_str(), target.as_str())?),
}
}
pub fn read_link(&self, filename: &Utf8Path) -> SftpChannelResult<Utf8PathBuf> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(pathconv(sftp.readlink(filename.as_std_path())?)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => Ok(sftp.read_link(filename.as_str())?.into()),
}
}
pub fn canonicalize(&self, filename: &Utf8Path) -> SftpChannelResult<Utf8PathBuf> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(pathconv(sftp.realpath(filename.as_std_path())?)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => Ok(sftp.canonicalize(filename.as_str())?.into()),
}
}
pub fn unlink(&self, filename: &Utf8Path) -> SftpChannelResult<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(sftp.unlink(filename.as_std_path())?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => Ok(sftp.remove_file(filename.as_str())?),
}
}
pub fn remove_dir(&self, filename: &Utf8Path) -> SftpChannelResult<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(sftp.rmdir(filename.as_std_path())?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => Ok(sftp.remove_dir(filename.as_str())?),
}
}
pub fn create_dir(&self, filename: &Utf8Path, mode: i32) -> SftpChannelResult<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(sftp.mkdir(filename.as_std_path(), mode)?),
Self::LibSsh(sftp) => Ok(sftp.create_dir(filename.as_str(), mode.try_into().unwrap())?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => {
use std::convert::TryInto;
Ok(sftp.create_dir(filename.as_str(), mode.try_into().unwrap())?)
}
}
}
@ -91,19 +121,25 @@ impl SftpWrap {
&self,
src: &Utf8Path,
dest: &Utf8Path,
opts: RenameOptions,
#[cfg_attr(not(feature = "ssh2"), allow(unused_variables))] opts: RenameOptions,
) -> SftpChannelResult<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => {
Ok(sftp.rename(src.as_std_path(), dest.as_std_path(), Some(opts.into()))?)
}
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => Ok(sftp.rename(src.as_str(), dest.as_str())?),
}
}
pub fn symlink_metadata(&self, filename: &Utf8Path) -> SftpChannelResult<Metadata> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(sftp.lstat(filename.as_std_path()).map(Metadata::from)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => Ok(sftp
.symlink_metadata(filename.as_str())
.map(Metadata::from)?),
@ -112,16 +148,22 @@ impl SftpWrap {
pub fn metadata(&self, filename: &Utf8Path) -> SftpChannelResult<Metadata> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(sftp.stat(filename.as_std_path()).map(Metadata::from)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => Ok(sftp.metadata(filename.as_str()).map(Metadata::from)?),
}
}
pub fn set_metadata(&self, filename: &Utf8Path, metadata: Metadata) -> SftpChannelResult<()> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(sftp.setstat(filename.as_std_path(), metadata.into())?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => {
let attr: libssh::SetAttributes = metadata.into();
let attr: libssh_rs::SetAttributes = metadata.into();
Ok(sftp.set_metadata(filename.as_str(), &attr)?)
}
}
@ -129,13 +171,17 @@ impl SftpWrap {
pub fn open_dir(&self, filename: &Utf8Path) -> SftpChannelResult<DirWrap> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => Ok(sftp.opendir(filename.as_std_path()).map(DirWrap::Ssh2)?),
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => Ok(sftp.open_dir(filename.as_str()).map(DirWrap::LibSsh)?),
}
}
pub fn read_dir(&self, filename: &Utf8Path) -> SftpChannelResult<Vec<(Utf8PathBuf, Metadata)>> {
match self {
#[cfg(feature = "ssh2")]
Self::Ssh2(sftp) => {
let entries = sftp.readdir(filename.as_std_path())?;
let mut mapped_entries = vec![];
@ -146,6 +192,8 @@ impl SftpWrap {
Ok(mapped_entries)
}
#[cfg(feature = "libssh-rs")]
Self::LibSsh(sftp) => {
let entries = sftp.read_dir(filename.as_str())?;
let mut mapped_entries = vec![];

View File

@ -611,8 +611,9 @@ async fn canonicalize_should_either_return_resolved_path_or_error_if_missing(
.await;
match result {
Ok(_) => {}
Err(SftpChannelError::Sftp(SftpError::NoSuchFile))
| Err(SftpChannelError::LibSsh(libssh_rs::Error::Sftp(_))) => {}
Err(SftpChannelError::Sftp(SftpError::NoSuchFile)) => {}
#[cfg(feature = "libssh-rs")]
Err(SftpChannelError::LibSsh(libssh_rs::Error::Sftp(_))) => {}
x => panic!(
"Unexpected result from canonicalize({}: {:?}",
missing.path().display(),

View File

@ -424,6 +424,11 @@ pub async fn session(sshd: &'_ Sshd) -> Session {
let mut config = config.for_host("localhost");
config.insert("port".to_string(), port.to_string());
config.insert("wezterm_ssh_verbose".to_string(), "true".to_string());
// If libssh-rs is not loaded (but ssh2 is), then we use ssh2 as the backend
#[cfg(not(feature = "libssh-rs"))]
config.insert("wezterm_ssh_backend".to_string(), "ssh2".to_string());
config.insert("user".to_string(), USERNAME.to_string());
config.insert("identitiesonly".to_string(), "yes".to_string());
config.insert(