├── .github ├── dependabot.yml └── workflows │ ├── linux.yml │ ├── macos.yml │ └── windows.yml ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── README.md ├── rustfmt.toml ├── src ├── agent.rs ├── channel.rs ├── error.rs ├── lib.rs ├── listener.rs ├── session.rs ├── sftp.rs └── util.rs └── tests ├── .gitignore ├── all ├── agent.rs ├── channel.rs ├── knownhosts.rs ├── main.rs ├── session.rs └── sftp.rs └── run_integration_tests.sh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-16.04, ubuntu-18.04] 17 | rust_toolchain: [stable, nightly, beta] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Install Rust 22 | run: curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${{ matrix.rust_toolchain }} 23 | - name: Build and test 24 | run: | 25 | source $HOME/.cargo/env 26 | rustc -V 27 | cargo -V 28 | cargo build 29 | tests/run_integration_tests.sh 30 | rustdoc --test README.md -L target -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [macos-10.14] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v1 20 | - name: Install Rust 21 | run: curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly 22 | - name: Build and test 23 | run: | 24 | export OPENSSL_INCLUDE_DIR=`brew --prefix openssl`/include 25 | export OPENSSL_LIB_DIR=`brew --prefix openssl`/lib 26 | source $HOME/.cargo/env 27 | rustc -V 28 | cargo -V 29 | cargo build 30 | tests/run_integration_tests.sh 31 | rustdoc --test README.md -L target -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [windows-2016, windows-2019] 17 | env: 18 | - TARGET: x86_64-pc-windows-msvc 19 | - TARGET: i686-pc-windows-msvc 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v1 23 | - name: Download Rust Installer 24 | # GitHub Actions doesn't automatically apply the environment from the matrix, 25 | # so we get to do that for ourselves here 26 | env: 27 | TARGET: ${{ matrix.env.TARGET }} 28 | run: | 29 | $wc = New-Object System.Net.WebClient 30 | $wc.DownloadFile("https://static.rust-lang.org/dist/rust-nightly-${env:TARGET}.exe", "install-rust.exe") 31 | shell: powershell 32 | - name: Install Rust 33 | run: install-rust.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust" 34 | shell: cmd 35 | - name: Build and test 36 | env: 37 | TARGET: ${{ matrix.env.TARGET }} 38 | run: | 39 | SET PATH=C:\Program Files (x86)\Rust\bin;%PATH% 40 | rustc -V 41 | cargo -V 42 | cargo test --no-run --target %TARGET% 43 | shell: cmd 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw* 2 | /target/ 3 | /Cargo.lock 4 | /tests/sshd -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: required 3 | 4 | matrix: 5 | include: 6 | - rust: 1.39.0 7 | - rust: stable 8 | - rust: beta 9 | - rust: nightly 10 | 11 | # - name: "master doc to gh-pages" 12 | # rust: nightly 13 | # script: 14 | # - cargo doc --no-deps 15 | # - cargo doc --no-deps --manifest-path libssh2-sys/Cargo.toml 16 | # deploy: 17 | # provider: script 18 | # script: curl -LsSf https://git.io/fhJ8n | rustc - && (cd target/doc && ../../rust_out) 19 | # skip_cleanup: true 20 | # on: 21 | # branch: master 22 | 23 | script: 24 | - cargo build 25 | - tests/run_integration_tests.sh 26 | - rustdoc --test README.md -L target 27 | 28 | notifications: 29 | email: 30 | on_success: never 31 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-ssh2" 3 | version = "0.3.0" 4 | authors = ["spebern sp33cht@googlemail.com"] 5 | license = "MIT/Apache-2.0" 6 | keywords = ["ssh", "async"] 7 | readme = "README.md" 8 | repository = "https://github.com/spebern/async-ssh2" 9 | homepage = "https://github.com/spebern/async-ssh2" 10 | documentation = "https://docs.rs/async-ssh2" 11 | description = """Async wrapper over ssh2.""" 12 | edition = "2018" 13 | 14 | [features] 15 | vendored-openssl = ["ssh2/vendored-openssl"] 16 | 17 | [dependencies] 18 | ssh2 = "0.9.1" 19 | libssh2-sys = "0.2" 20 | async-io = "^1.3" 21 | futures = "0.3.8" 22 | futures-util = "0.3.8" 23 | 24 | [dev-dependencies] 25 | tempfile = "3.1" 26 | tokio = { version = "1", features = ["full"] } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-ssh2-rs 2 | 3 | [![Build Status](https://travis-ci.com/spebern/async-ssh2.svg?branch=master)](https://travis-ci.com/spebern/async-ssh2) 4 | [![Build Status](https://github.com/spebern/async-ssh2/workflows/linux/badge.svg)](https://github.com/spebern/async-ssh2/actions?workflow=linux) 5 | [![Build Status](https://github.com/spebern/async-ssh2/workflows/Windows/badge.svg)](https://github.com/spebern/async-ssh2/actions?workflow=Windows) 6 | [![Build Status](https://github.com/spebern/async-ssh2/workflows/macOS/badge.svg)](https://github.com/spebern/async-ssh2/actions?workflow=macOS) 7 | 8 | [Documentation](https://docs.rs/async-ssh2) 9 | 10 | Async wrapper over [ssh2-rs](https://github.com/alexcrichton/ssh2-rs). 11 | 12 | ## Usage 13 | 14 | ```toml 15 | # Cargo.toml 16 | [dependencies] 17 | async-ssh2 = { version = "0.1", git = "https://github.com/spebern/async-ssh2.git" } 18 | ``` 19 | 20 | ## Building on OSX 10.10+ 21 | 22 | This library depends on OpenSSL. To get OpenSSL working follow the 23 | [`openssl` crate's instructions](https://github.com/sfackler/rust-openssl#macos). 24 | 25 | You can enable the `vendored-openssl` feature 26 | to have `libssh2` built against a statically built version of openssl as [described 27 | here](https://docs.rs/openssl/0.10.24/openssl/#vendored). 28 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | merge_imports = true 2 | -------------------------------------------------------------------------------- /src/agent.rs: -------------------------------------------------------------------------------- 1 | use crate::{util::run_ssh2_fn, Error}; 2 | use async_io::Async; 3 | use ssh2::{self, PublicKey}; 4 | use std::{convert::From, net::TcpStream, sync::Arc}; 5 | 6 | /// See [`Agent`](ssh2::Agent). 7 | pub struct Agent { 8 | inner: ssh2::Agent, 9 | inner_session: ssh2::Session, 10 | stream: Arc>, 11 | } 12 | 13 | impl Agent { 14 | pub(crate) fn new(agent: ssh2::Agent, session: ssh2::Session, stream: Arc>) -> Agent { 15 | Agent { 16 | inner: agent, 17 | inner_session: session, 18 | stream, 19 | } 20 | } 21 | 22 | /// See [`connect`](ssh2::Agent::connect). 23 | pub async fn connect(&mut self) -> Result<(), Error> { 24 | let inner = &mut self.inner; 25 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.connect()).await 26 | } 27 | 28 | /// See [`disconnect`](ssh2::Agent::disconnect). 29 | pub async fn disconnect(&mut self) -> Result<(), Error> { 30 | let inner = &mut self.inner; 31 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.disconnect()).await 32 | } 33 | 34 | /// See [`list_identities`](ssh2::Agent::list_identities). 35 | pub fn list_identities(&mut self) -> Result<(), Error> { 36 | self.inner.list_identities().map_err(From::from) 37 | } 38 | 39 | /// See [`identities`](ssh2::Agent::identities). 40 | pub fn identities(&self) -> Result, Error> { 41 | self.inner.identities().map_err(From::from) 42 | } 43 | 44 | /// See [`userauth`](ssh2::Agent::userauth). 45 | pub async fn userauth(&self, username: &str, identity: &PublicKey) -> Result<(), Error> { 46 | run_ssh2_fn(&self.stream, &self.inner_session, || { 47 | self.inner.userauth(username, identity) 48 | }) 49 | .await 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/channel.rs: -------------------------------------------------------------------------------- 1 | use crate::{util::{run_ssh2_fn, poll_ssh2_io_op}, Error}; 2 | use futures::prelude::*; 3 | use async_io::Async; 4 | use ssh2::{self, ExitSignal, ExtendedData, PtyModes, ReadWindow, Stream, WriteWindow}; 5 | use std::{ 6 | convert::From, 7 | io, 8 | io::{Read, Write}, 9 | net::TcpStream, 10 | pin::Pin, 11 | sync::Arc, 12 | task::{Context, Poll}, 13 | }; 14 | 15 | /// See [`Channel`](ssh2::Channel). 16 | pub struct Channel { 17 | inner: ssh2::Channel, 18 | inner_session: ssh2::Session, 19 | stream: Arc>, 20 | } 21 | 22 | impl Channel { 23 | pub(crate) fn new(channel: ssh2::Channel, session: ssh2::Session, stream: Arc>) -> Channel { 24 | Channel { 25 | inner: channel, 26 | inner_session: session, 27 | stream, 28 | } 29 | } 30 | 31 | /// See [`setenv`](ssh2::Channel::setenv). 32 | pub async fn setenv(&mut self, var: &str, val: &str) -> Result<(), Error> { 33 | let inner = &mut self.inner; 34 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.setenv(var, val)).await 35 | } 36 | 37 | /// See [`request_pty`](ssh2::Channel::request_pty). 38 | pub async fn request_pty( 39 | &mut self, 40 | term: &str, 41 | mode: Option, 42 | dim: Option<(u32, u32, u32, u32)>, 43 | ) -> Result<(), Error> { 44 | let inner = &mut self.inner; 45 | run_ssh2_fn(&self.stream, &self.inner_session, || { 46 | inner.request_pty(term, mode.clone(), dim) 47 | }) 48 | .await 49 | } 50 | 51 | /// See [`request_pty_size`](ssh2::Channel::request_pty_size). 52 | pub async fn request_pty_size( 53 | &mut self, 54 | width: u32, 55 | height: u32, 56 | width_px: Option, 57 | height_px: Option, 58 | ) -> Result<(), Error> { 59 | let inner = &mut self.inner; 60 | run_ssh2_fn(&self.stream, &self.inner_session, || { 61 | inner 62 | .request_pty_size(width, height, width_px, height_px) 63 | }) 64 | .await 65 | } 66 | 67 | /// See [`exec`](ssh2::Channel::exec). 68 | pub async fn exec(&mut self, command: &str) -> Result<(), Error> { 69 | let inner = &mut self.inner; 70 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.exec(command)).await 71 | } 72 | 73 | /// See [`shell`](ssh2::Channel::shell). 74 | pub async fn shell(&mut self) -> Result<(), Error> { 75 | let inner = &mut self.inner; 76 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.shell()).await 77 | } 78 | 79 | /// See [`subsystem`](ssh2::Channel::subsystem). 80 | pub async fn subsystem(&mut self, system: &str) -> Result<(), Error> { 81 | let inner = &mut self.inner; 82 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.subsystem(system)).await 83 | } 84 | 85 | /// See [`process_startup`](ssh2::Channel::process_startup). 86 | pub async fn process_startup( 87 | &mut self, 88 | request: &str, 89 | message: Option<&str>, 90 | ) -> Result<(), Error> { 91 | let inner = &mut self.inner; 92 | run_ssh2_fn(&self.stream, &self.inner_session, || { 93 | inner.process_startup(request, message) 94 | }) 95 | .await 96 | } 97 | 98 | /// See [`stderr`](ssh2::Channel::stderr). 99 | pub fn stderr(&mut self) -> Stream { 100 | self.inner.stderr() 101 | } 102 | 103 | /// See [`stream`](ssh2::Channel::stream). 104 | pub fn stream(&mut self, stream_id: i32) -> Stream { 105 | self.inner.stream(stream_id) 106 | } 107 | 108 | /// See [`handle_extended_data`](ssh2::Channel::handle_extended_data). 109 | pub async fn handle_extended_data(&mut self, mode: ExtendedData) -> Result<(), Error> { 110 | let inner = &mut self.inner; 111 | run_ssh2_fn(&self.stream, &self.inner_session, || { 112 | inner.handle_extended_data(mode) 113 | }) 114 | .await 115 | } 116 | 117 | /// See [`exit_status`](ssh2::Channel::exit_status). 118 | pub fn exit_status(&self) -> Result { 119 | self.inner.exit_status().map_err(From::from) 120 | } 121 | 122 | /// See [`exit_signal`](ssh2::Channel::exit_signal). 123 | pub fn exit_signal(&self) -> Result { 124 | self.inner.exit_signal().map_err(From::from) 125 | } 126 | 127 | /// See [`read_window`](ssh2::Channel::read_window). 128 | pub fn read_window(&self) -> ReadWindow { 129 | self.inner.read_window() 130 | } 131 | 132 | /// See [`write_window`](ssh2::Channel::write_window). 133 | pub fn write_window(&self) -> WriteWindow { 134 | self.inner.write_window() 135 | } 136 | 137 | /// See [`adjust_receive_window`](ssh2::Channel::adjust_receive_window). 138 | pub async fn adjust_receive_window(&mut self, adjust: u64, force: bool) -> Result { 139 | let inner = &mut self.inner; 140 | run_ssh2_fn(&self.stream, &self.inner_session, || { 141 | inner.adjust_receive_window(adjust, force) 142 | }) 143 | .await 144 | } 145 | 146 | /// See [`eof`](ssh2::Channel::eof). 147 | pub fn eof(&self) -> bool { 148 | self.inner.eof() 149 | } 150 | 151 | /// See [`send_eof`](ssh2::Channel::send_eof). 152 | pub async fn send_eof(&mut self) -> Result<(), Error> { 153 | let inner = &mut self.inner; 154 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.send_eof()).await 155 | } 156 | 157 | /// See [`wait_eof`](ssh2::Channel::wait_eof). 158 | pub async fn wait_eof(&mut self) -> Result<(), Error> { 159 | let inner = &mut self.inner; 160 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.wait_eof()).await 161 | } 162 | 163 | /// See [`close`](ssh2::Channel::close). 164 | pub async fn close(&mut self) -> Result<(), Error> { 165 | let inner = &mut self.inner; 166 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.close()).await 167 | } 168 | 169 | /// See [`wait_close`](ssh2::Channel::wait_close). 170 | pub async fn wait_close(&mut self) -> Result<(), Error> { 171 | let inner = &mut self.inner; 172 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.wait_close()).await 173 | } 174 | } 175 | 176 | impl AsyncRead for Channel { 177 | fn poll_read( 178 | self: Pin<&mut Self>, 179 | cx: &mut Context<'_>, 180 | buf: &mut [u8], 181 | ) -> Poll> { 182 | let this = self.get_mut(); 183 | let inner = &mut this.inner; 184 | poll_ssh2_io_op(cx, &this.stream, &this.inner_session, || inner.read(buf)) 185 | } 186 | } 187 | 188 | impl AsyncWrite for Channel { 189 | fn poll_write( 190 | self: Pin<&mut Self>, 191 | cx: &mut Context<'_>, 192 | buf: &[u8], 193 | ) -> Poll> { 194 | let this = self.get_mut(); 195 | let inner = &mut this.inner; 196 | poll_ssh2_io_op(cx, &this.stream, &this.inner_session, || inner.write(buf)) 197 | } 198 | 199 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 200 | let this = self.get_mut(); 201 | let inner = &mut this.inner; 202 | poll_ssh2_io_op(cx, &this.stream, &this.inner_session, || inner.flush()) 203 | } 204 | 205 | fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 206 | let this = self.get_mut(); 207 | let inner = &mut this.inner; 208 | poll_ssh2_io_op(cx, 209 | &this.stream, 210 | &this.inner_session, 211 | || inner.close().map_err(|e| io::Error::from(ssh2::Error::from_errno(e.code()))) 212 | ) 213 | } 214 | } 215 | 216 | /* 217 | impl<'channel> Read for Stream<'channel> { 218 | fn read(&mut self, data: &mut [u8]) -> io::Result { 219 | if self.channel.eof() { 220 | return Ok(0); 221 | } 222 | 223 | let data = match self.channel.read_limit { 224 | Some(amt) => { 225 | let len = data.len(); 226 | &mut data[..cmp::min(amt as usize, len)] 227 | } 228 | None => data, 229 | }; 230 | let ret = unsafe { 231 | let rc = raw::libssh2_channel_read_ex( 232 | self.channel.raw, 233 | self.id as c_int, 234 | data.as_mut_ptr() as *mut _, 235 | data.len() as size_t, 236 | ); 237 | self.channel.sess.rc(rc as c_int).map(|()| rc as usize) 238 | }; 239 | match ret { 240 | Ok(n) => { 241 | if let Some(ref mut amt) = self.channel.read_limit { 242 | *amt -= n as u64; 243 | } 244 | Ok(n) 245 | } 246 | Err(e) => Err(e.into()), 247 | } 248 | } 249 | } 250 | 251 | impl<'channel> Write for Stream<'channel> { 252 | fn write(&mut self, data: &[u8]) -> io::Result { 253 | unsafe { 254 | let rc = raw::libssh2_channel_write_ex( 255 | self.channel.raw, 256 | self.id as c_int, 257 | data.as_ptr() as *mut _, 258 | data.len() as size_t, 259 | ); 260 | self.channel.sess.rc(rc as c_int).map(|()| rc as usize) 261 | } 262 | .map_err(Into::into) 263 | } 264 | 265 | fn flush(&mut self) -> io::Result<()> { 266 | unsafe { 267 | let rc = raw::libssh2_channel_flush_ex(self.channel.raw, self.id as c_int); 268 | self.channel.sess.rc(rc) 269 | } 270 | .map_err(Into::into) 271 | } 272 | } 273 | */ 274 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::From, error, fmt, io}; 2 | 3 | /// Representation of an error. 4 | #[derive(Debug)] 5 | pub enum Error { 6 | // An error that can occur within libssh2. 7 | SSH2(ssh2::Error), 8 | // An io error. 9 | Io(io::Error), 10 | } 11 | 12 | impl fmt::Display for Error { 13 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | match self { 15 | Error::Io(e) => e.fmt(f), 16 | Error::SSH2(e) => e.fmt(f), 17 | } 18 | } 19 | } 20 | 21 | impl error::Error for Error {} 22 | 23 | impl From for Error { 24 | fn from(e: ssh2::Error) -> Error { 25 | Error::SSH2(e) 26 | } 27 | } 28 | 29 | impl From for Error { 30 | fn from(e: io::Error) -> Error { 31 | Error::Io(e) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | mod agent; 4 | mod channel; 5 | mod error; 6 | mod listener; 7 | mod session; 8 | mod sftp; 9 | 10 | pub use agent::Agent; 11 | pub use channel::Channel; 12 | pub use error::Error; 13 | pub use listener::Listener; 14 | pub use session::Session; 15 | pub use sftp::{File, Sftp}; 16 | 17 | pub use ssh2::{ 18 | BlockDirections, ExitSignal, FileStat, FileType, Host, KnownHostFileKind, KnownHosts, 19 | OpenFlags, Prompt, PtyModes, PublicKey, ReadWindow, RenameFlags, ScpFileStat, WriteWindow, 20 | TraceFlags 21 | }; 22 | -------------------------------------------------------------------------------- /src/listener.rs: -------------------------------------------------------------------------------- 1 | use crate::{channel::Channel, util::run_ssh2_fn, Error}; 2 | use async_io::Async; 3 | use ssh2::{self}; 4 | use std::{net::TcpStream, sync::Arc}; 5 | 6 | /// See [`Listener`](ssh2::Listener). 7 | pub struct Listener { 8 | inner: ssh2::Listener, 9 | inner_session: ssh2::Session, 10 | stream: Arc>, 11 | } 12 | 13 | impl Listener { 14 | pub(crate) fn new(listener: ssh2::Listener, session: ssh2::Session, stream: Arc>) -> Listener { 15 | Listener { 16 | inner: listener, 17 | inner_session: session, 18 | stream, 19 | } 20 | } 21 | 22 | /// See [`accept`](ssh2::Listener::accept). 23 | pub async fn accept(&mut self) -> Result { 24 | let inner = &mut self.inner; 25 | let channel = run_ssh2_fn(&self.stream.clone(), &self.inner_session, || inner.accept()).await?; 26 | Ok(Channel::new(channel, self.inner_session.clone(), self.stream.clone())) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | agent::Agent, channel::Channel, listener::Listener, sftp::Sftp, util::run_ssh2_fn, Error, 3 | }; 4 | use async_io::Async; 5 | use ssh2::{ 6 | self, DisconnectCode, HashType, HostKeyType, KeyboardInteractivePrompt, KnownHosts, MethodType, 7 | ScpFileStat, BlockDirections 8 | }; 9 | #[cfg(unix)] 10 | use std::os::unix::io::{AsRawFd, RawFd}; 11 | #[cfg(windows)] 12 | use std::os::windows::io::{AsRawSocket, RawSocket}; 13 | use std::{ 14 | convert::From, 15 | net::TcpStream, 16 | path::Path, 17 | sync::Arc, 18 | }; 19 | 20 | /// See [`Session`](ssh2::Session). 21 | #[derive(Clone)] 22 | pub struct Session { 23 | inner: ssh2::Session, 24 | stream: Option>>, 25 | } 26 | 27 | #[cfg(unix)] 28 | struct RawFdWrapper(RawFd); 29 | 30 | #[cfg(unix)] 31 | impl AsRawFd for RawFdWrapper { 32 | fn as_raw_fd(&self) -> RawFd { 33 | self.0 34 | } 35 | } 36 | 37 | #[cfg(windows)] 38 | struct RawSocketWrapper(RawSocket); 39 | 40 | #[cfg(windows)] 41 | impl AsRawSocket for RawSocketWrapper { 42 | fn as_raw_socket(&self) -> RawSocket { 43 | self.0 44 | } 45 | } 46 | 47 | impl Session { 48 | /// See [`new`](ssh2::Session::new). 49 | pub fn new() -> Result { 50 | let session = ssh2::Session::new()?; 51 | session.set_blocking(false); 52 | 53 | Ok(Self { 54 | inner: session, 55 | stream: None, 56 | }) 57 | } 58 | 59 | /// See [`set_banner`](ssh2::Session::set_banner). 60 | pub async fn set_banner(&self, banner: &str) -> Result<(), Error> { 61 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 62 | self.inner.set_banner(banner) 63 | }) 64 | .await 65 | } 66 | 67 | /// See [`set_allow_sigpipe`](ssh2::Session::set_allow_sigpipe). 68 | pub fn set_allow_sigpipe(&self, block: bool) { 69 | self.inner.set_allow_sigpipe(block) 70 | } 71 | 72 | /// See [`set_allow_sigpipe`](ssh2::Session::set_compress). 73 | pub fn set_compress(&self, compress: bool) { 74 | self.inner.set_compress(compress) 75 | } 76 | 77 | /// See [`is_blocking`](ssh2::Session::is_blocking). 78 | pub fn is_blocking(&self) -> bool { 79 | self.inner.is_blocking() 80 | } 81 | 82 | /// See [`set_timeout`](ssh2::Session::set_timeout). 83 | pub fn set_timeout(&self, timeout_ms: u32) { 84 | self.inner.set_timeout(timeout_ms) 85 | } 86 | 87 | /// See [`timeout`](ssh2::Session::timeout). 88 | pub fn timeout(&self) -> u32 { 89 | self.inner.timeout() 90 | } 91 | 92 | /// See [`handshake`](ssh2::Session::handshake). 93 | pub async fn handshake(&mut self) -> Result<(), Error> { 94 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 95 | self.inner.clone().handshake() 96 | }) 97 | .await 98 | } 99 | 100 | /// Sets the tcp stream for the underlying `ssh2` lib. 101 | /// 102 | /// ```rust,no_run 103 | /// use async_ssh2::Session; 104 | /// use std::net::{ToSocketAddrs, SocketAddr, TcpStream}; 105 | /// use async_io::Async; 106 | /// 107 | /// #[tokio::main] 108 | /// async fn main() { 109 | /// let mut addr = SocketAddr::from(([127, 0, 0, 1], 22)).to_socket_addrs().unwrap(); 110 | /// let stream = Async::::connect(addr.next().unwrap()).await.unwrap(); 111 | /// let mut sess = async_ssh2::Session::new().unwrap(); 112 | /// sess.set_tcp_stream(stream).unwrap(); 113 | /// } 114 | /// ``` 115 | pub fn set_tcp_stream(&mut self, stream: Async) -> Result<(), Error> { 116 | #[cfg(unix)] 117 | { 118 | let raw_fd = RawFdWrapper(stream.as_raw_fd()); 119 | self.inner.set_tcp_stream(raw_fd); 120 | } 121 | #[cfg(windows)] 122 | { 123 | let raw_socket = RawSocketWrapper(stream.as_raw_socket()); 124 | self.inner.set_tcp_stream(raw_socket); 125 | } 126 | self.stream = Some(Arc::new(stream)); 127 | Ok(()) 128 | } 129 | 130 | /// See [`userauth_password`](ssh2::Session::userauth_password). 131 | pub async fn userauth_password(&self, username: &str, password: &str) -> Result<(), Error> { 132 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 133 | self.inner.userauth_password(username, password) 134 | }) 135 | .await 136 | } 137 | 138 | /// See [`userauth_keyboard_interactive`](ssh2::Session::userauth_keyboard_interactive). 139 | pub fn userauth_keyboard_interactive( 140 | &self, 141 | _username: &str, 142 | _prompter: &mut P, 143 | ) -> Result<(), Error> { 144 | unimplemented!(); 145 | } 146 | 147 | /// See [`userauth_agent`](ssh2::Session::userauth_agent). 148 | pub async fn userauth_agent(&self, username: &str) -> Result<(), Error> { 149 | let mut agent = self.agent()?; 150 | agent.connect().await?; 151 | agent.list_identities()?; 152 | let identities = agent.identities()?; 153 | let identity = match identities.get(0) { 154 | Some(identity) => identity, 155 | None => return Err(Error::from(ssh2::Error::from_errno(ssh2::ErrorCode::Session(-4)))), 156 | }; 157 | agent.userauth(username, &identity).await 158 | } 159 | 160 | /// See [`userauth_pubkey_file`](ssh2::Session::userauth_pubkey_file). 161 | pub async fn userauth_pubkey_file( 162 | &self, 163 | username: &str, 164 | pubkey: Option<&Path>, 165 | privatekey: &Path, 166 | passphrase: Option<&str>, 167 | ) -> Result<(), Error> { 168 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 169 | self.inner 170 | .userauth_pubkey_file(username, pubkey, privatekey, passphrase) 171 | }) 172 | .await 173 | } 174 | 175 | /// See [`userauth_pubkey_memory`](ssh2::Session::userauth_pubkey_memory). 176 | #[cfg(unix)] 177 | pub async fn userauth_pubkey_memory( 178 | &self, 179 | username: &str, 180 | pubkeydata: Option<&str>, 181 | privatekeydata: &str, 182 | passphrase: Option<&str>, 183 | ) -> Result<(), Error> { 184 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 185 | self.inner 186 | .userauth_pubkey_memory(username, pubkeydata, privatekeydata, passphrase) 187 | }) 188 | .await 189 | } 190 | 191 | /// See [`userauth_hostbased_file`](ssh2::Session::userauth_hostbased_file). 192 | #[allow(missing_docs)] 193 | pub async fn userauth_hostbased_file( 194 | &self, 195 | username: &str, 196 | publickey: &Path, 197 | privatekey: &Path, 198 | passphrase: Option<&str>, 199 | hostname: &str, 200 | local_username: Option<&str>, 201 | ) -> Result<(), Error> { 202 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 203 | self.inner.userauth_hostbased_file( 204 | username, 205 | publickey, 206 | privatekey, 207 | passphrase, 208 | hostname, 209 | local_username, 210 | ) 211 | }) 212 | .await 213 | } 214 | 215 | /// See [`authenticated`](ssh2::Session::authenticated). 216 | pub fn authenticated(&self) -> bool { 217 | self.inner.authenticated() 218 | } 219 | 220 | /// See [`auth_methods`](ssh2::Session::auth_methods). 221 | pub async fn auth_methods(&self, username: &str) -> Result<&str, Error> { 222 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 223 | self.inner.auth_methods(username) 224 | }) 225 | .await 226 | } 227 | 228 | /// See [`method_pref`](ssh2::Session::method_pref). 229 | pub fn method_pref(&self, method_type: MethodType, prefs: &str) -> Result<(), Error> { 230 | self.inner.method_pref(method_type, prefs)?; 231 | Ok(()) 232 | } 233 | 234 | /// See [`methods`](ssh2::Session::methods). 235 | pub fn methods(&self, method_type: MethodType) -> Option<&str> { 236 | self.inner.methods(method_type) 237 | } 238 | 239 | /// See [`supported_algs`](ssh2::Session::supported_algs). 240 | pub fn supported_algs(&self, method_type: MethodType) -> Result, Error> { 241 | self.inner.supported_algs(method_type).map_err(From::from) 242 | } 243 | 244 | /// See [`agent`](ssh2::Session::agent). 245 | pub fn agent(&self) -> Result { 246 | let agent = self.inner.agent()?; 247 | Ok(Agent::new(agent, self.inner.clone(), self.stream.as_ref().unwrap().clone())) 248 | } 249 | 250 | /// See [`known_hosts`](ssh2::Session::known_hosts). 251 | pub fn known_hosts(&self) -> Result { 252 | self.inner.known_hosts().map_err(From::from) 253 | } 254 | 255 | /// See [`channel_session`](ssh2::Session::channel_session). 256 | pub async fn channel_session(&self) -> Result { 257 | let channel = run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 258 | self.inner.channel_session() 259 | }) 260 | .await?; 261 | Ok(Channel::new(channel, self.inner.clone(), self.stream.as_ref().unwrap().clone())) 262 | } 263 | 264 | /// See [`channel_direct_tcpip`](ssh2::Session::channel_direct_tcpip). 265 | pub async fn channel_direct_tcpip( 266 | &self, 267 | host: &str, 268 | port: u16, 269 | src: Option<(&str, u16)>, 270 | ) -> Result { 271 | let channel = run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 272 | self.inner.channel_direct_tcpip(host, port, src) 273 | }) 274 | .await?; 275 | Ok(Channel::new(channel, self.inner.clone(), self.stream.as_ref().unwrap().clone())) 276 | } 277 | 278 | /// See [`channel_forward_listen`](ssh2::Session::channel_forward_listen). 279 | pub async fn channel_forward_listen( 280 | &self, 281 | remote_port: u16, 282 | host: Option<&str>, 283 | queue_maxsize: Option, 284 | ) -> Result<(Listener, u16), Error> { 285 | let (listener, port) = run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 286 | self.inner 287 | .channel_forward_listen(remote_port, host, queue_maxsize) 288 | }) 289 | .await?; 290 | Ok(( 291 | Listener::new(listener, self.inner.clone(), self.stream.as_ref().unwrap().clone()), 292 | port, 293 | )) 294 | } 295 | 296 | /// See [`scp_recv`](ssh2::Session::scp_recv). 297 | pub async fn scp_recv(&self, path: &Path) -> Result<(Channel, ScpFileStat), Error> { 298 | let (channel, file_stat) = 299 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || self.inner.scp_recv(path)).await?; 300 | Ok(( 301 | Channel::new(channel, self.inner.clone(), self.stream.as_ref().unwrap().clone()), 302 | file_stat, 303 | )) 304 | } 305 | 306 | /// See [`scp_send`](ssh2::Session::scp_send). 307 | pub async fn scp_send( 308 | &self, 309 | remote_path: &Path, 310 | mode: i32, 311 | size: u64, 312 | times: Option<(u64, u64)>, 313 | ) -> Result { 314 | let channel = run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 315 | self.inner.scp_send(remote_path, mode, size, times) 316 | }) 317 | .await?; 318 | Ok(Channel::new(channel, self.inner.clone(), self.stream.as_ref().unwrap().clone())) 319 | } 320 | 321 | /// See [`sftp`](ssh2::Session::sftp). 322 | pub async fn sftp(& self) -> Result { 323 | let sftp = run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || self.inner.sftp()).await?; 324 | Ok(Sftp::new(sftp, self.inner.clone(), self.stream.as_ref().unwrap().clone())) 325 | } 326 | 327 | /// See [`channel_open`](ssh2::Session::channel_open). 328 | pub async fn channel_open( 329 | &self, 330 | channel_type: &str, 331 | window_size: u32, 332 | packet_size: u32, 333 | message: Option<&str>, 334 | ) -> Result { 335 | let channel = run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 336 | self.inner 337 | .channel_open(channel_type, window_size, packet_size, message) 338 | }) 339 | .await?; 340 | Ok(Channel::new(channel, self.inner.clone(), self.stream.as_ref().unwrap().clone())) 341 | } 342 | 343 | /// See [`banner`](ssh2::Session::banner). 344 | pub fn banner(&self) -> Option<&str> { 345 | self.inner.banner() 346 | } 347 | 348 | /// See [`banner_bytes`](ssh2::Session::banner_bytes). 349 | pub fn banner_bytes(&self) -> Option<&[u8]> { 350 | self.inner.banner_bytes() 351 | } 352 | 353 | /// See [`host_key`](ssh2::Session::host_key). 354 | pub fn host_key(&self) -> Option<(&[u8], HostKeyType)> { 355 | self.inner.host_key() 356 | } 357 | 358 | /// See [`host_key_hash`](ssh2::Session::host_key_hash). 359 | pub fn host_key_hash(&self, hash: HashType) -> Option<&[u8]> { 360 | self.inner.host_key_hash(hash) 361 | } 362 | 363 | /// See [`set_keepalive`](ssh2::Session::set_keepalive). 364 | pub fn set_keepalive(&self, want_reply: bool, interval: u32) { 365 | self.inner.set_keepalive(want_reply, interval) 366 | } 367 | 368 | /// See [`keepalive_send`](ssh2::Session::keepalive_send). 369 | pub async fn keepalive_send(&self) -> Result { 370 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 371 | self.inner.keepalive_send() 372 | }) 373 | .await 374 | } 375 | 376 | /// See [`disconnect`](ssh2::Session::disconnect). 377 | pub async fn disconnect( 378 | &self, 379 | reason: Option, 380 | description: &str, 381 | lang: Option<&str>, 382 | ) -> Result<(), Error> { 383 | run_ssh2_fn(self.stream.as_ref().unwrap(), &self.inner, || { 384 | self.inner.disconnect(reason, description, lang) 385 | }) 386 | .await 387 | } 388 | 389 | /// See [`block_directions`](ssh2::Session::block_directions). 390 | pub fn block_directions(&self) -> BlockDirections { 391 | self.inner.block_directions() 392 | } 393 | 394 | /// See [`trace`](ssh2::Session::trace). 395 | pub fn trace(&self, bitmask: ssh2::TraceFlags) { 396 | self.inner.trace(bitmask); 397 | } 398 | } 399 | 400 | #[cfg(unix)] 401 | impl AsRawFd for Session { 402 | fn as_raw_fd(&self) -> RawFd { 403 | self.inner.as_raw_fd() 404 | } 405 | } 406 | 407 | #[cfg(windows)] 408 | impl AsRawSocket for Session { 409 | fn as_raw_socket(&self) -> RawSocket { 410 | self.inner.as_raw_socket() 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /src/sftp.rs: -------------------------------------------------------------------------------- 1 | use crate::{util::{run_ssh2_fn,poll_ssh2_io_op},Error}; 2 | use futures::prelude::*; 3 | use async_io::Async; 4 | use ssh2::{self, FileStat, OpenFlags, OpenType}; 5 | use std::{ 6 | io::{self, Read, Seek, Write}, 7 | net::TcpStream, 8 | path::{Path, PathBuf}, 9 | pin::Pin, 10 | sync::Arc, 11 | task::{Context, Poll}, 12 | }; 13 | 14 | /// See [`Sftp`](ssh2::Sftp). 15 | pub struct Sftp { 16 | inner: ssh2::Sftp, 17 | inner_session: ssh2::Session, 18 | stream: Arc>, 19 | } 20 | 21 | /// See [`File`](ssh2::File). 22 | pub struct File { 23 | inner: ssh2::File, 24 | inner_session: ssh2::Session, 25 | stream: Arc>, 26 | } 27 | 28 | impl Sftp { 29 | pub(crate) fn new<'b>(sftp: ssh2::Sftp, session: ssh2::Session, stream: Arc>) -> Sftp { 30 | Sftp { 31 | inner: sftp, 32 | inner_session: session, 33 | stream, 34 | } 35 | } 36 | 37 | /// See [`open_mode`](ssh2::Sftp::open_mode). 38 | pub async fn open_mode( 39 | &self, 40 | filename: &Path, 41 | flags: ssh2::OpenFlags, 42 | mode: i32, 43 | open_type: ssh2::OpenType, 44 | ) -> Result { 45 | let file = run_ssh2_fn(&self.stream, &self.inner_session,|| { 46 | self.inner.open_mode(filename, flags, mode, open_type) 47 | }) 48 | .await?; 49 | Ok(File::new(file, self.inner_session.clone(), self.stream.clone())) 50 | } 51 | 52 | /// See [`open`](ssh2::Sftp::open). 53 | pub async fn open(&self, filename: &Path) -> Result { 54 | self.open_mode(filename, OpenFlags::READ, 0o644, OpenType::File) 55 | .await 56 | } 57 | 58 | /// See [`create`](ssh2::Sftp::create). 59 | pub async fn create(&self, filename: &Path) -> Result { 60 | self.open_mode( 61 | filename, 62 | OpenFlags::WRITE | OpenFlags::TRUNCATE, 63 | 0o644, 64 | OpenType::File, 65 | ) 66 | .await 67 | } 68 | 69 | /// See [`opendir`](ssh2::Sftp::opendir). 70 | pub async fn opendir(&self, dirname: &Path) -> Result { 71 | self.open_mode(dirname, OpenFlags::READ, 0, OpenType::Dir) 72 | .await 73 | } 74 | 75 | /// See [`readdir`](ssh2::Sftp::readdir). 76 | pub async fn readdir(&self, dirname: &Path) -> Result, Error> { 77 | let mut dir = self.opendir(dirname).await?; 78 | let mut ret = Vec::new(); 79 | loop { 80 | match dir.readdir().await { 81 | Ok((filename, stat)) => { 82 | if &*filename == Path::new(".") || &*filename == Path::new("..") { 83 | continue; 84 | } 85 | 86 | ret.push((dirname.join(&filename), stat)) 87 | } 88 | Err(Error::SSH2(ref e)) if e.code() == ssh2::ErrorCode::Session(-16) => { 89 | break; 90 | } 91 | Err(e) => { 92 | return Err(e); 93 | } 94 | } 95 | } 96 | Ok(ret) 97 | } 98 | 99 | /// See [`mkdir`](ssh2::Sftp::mkdir). 100 | pub async fn mkdir(&self, filename: &Path, mode: i32) -> Result<(), Error> { 101 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.mkdir(filename, mode)).await 102 | } 103 | 104 | /// See [`rmdir`](ssh2::Sftp::rmdir). 105 | pub async fn rmdir(&self, filename: &Path) -> Result<(), Error> { 106 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.rmdir(filename)).await 107 | } 108 | 109 | /// See [`stat`](ssh2::Sftp::stat). 110 | pub async fn stat(&self, filename: &Path) -> Result { 111 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.stat(filename)).await 112 | } 113 | 114 | /// See [`lstat`](ssh2::Sftp::lstat). 115 | pub async fn lstat(&self, filename: &Path) -> Result { 116 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.lstat(filename)).await 117 | } 118 | 119 | /// See [`setstat`](ssh2::Sftp::setstat). 120 | pub async fn setstat(&self, filename: &Path, stat: ssh2::FileStat) -> Result<(), Error> { 121 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.setstat(filename, stat.clone())).await 122 | } 123 | 124 | /// See [`symlink`](ssh2::Sftp::symlink). 125 | pub async fn symlink(&self, path: &Path, target: &Path) -> Result<(), Error> { 126 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.symlink(path, target)).await 127 | } 128 | 129 | /// See [`readlink`](ssh2::Sftp::readlink). 130 | pub async fn readlink(&self, path: &Path) -> Result { 131 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.readlink(path)).await 132 | } 133 | 134 | /// See [`realpath`](ssh2::Sftp::realpath). 135 | pub async fn realpath(&self, path: &Path) -> Result { 136 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.realpath(path)).await 137 | } 138 | 139 | /// See [`rename`](ssh2::Sftp::rename). 140 | pub async fn rename( 141 | &self, 142 | src: &Path, 143 | dst: &Path, 144 | flags: Option, 145 | ) -> Result<(), Error> { 146 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.rename(src, dst, flags)).await 147 | } 148 | 149 | /// See [`unlink`](ssh2::Sftp::unlink). 150 | pub async fn unlink(&self, file: &Path) -> Result<(), Error> { 151 | run_ssh2_fn(&self.stream, &self.inner_session,|| self.inner.unlink(file)).await 152 | } 153 | 154 | /// See [`unlink`](ssh2::Sftp::shutdown). 155 | /// FIXME: This does not work properly. The inner `shutdown()` method can only be called once. 156 | /// When called it unwraps the sftp handle and calls libssh2_sftp_shutdown, which will likely return EAGAIN, 157 | /// but when we try to call it a second time it fails because the handle is already unwrapped. 158 | pub async fn shutdown(mut self) -> Result<(), Error> { 159 | run_ssh2_fn(&self.stream.clone(), &self.inner_session.clone(), || self.inner.shutdown()).await 160 | } 161 | } 162 | 163 | impl File { 164 | pub(crate) fn new(file: ssh2::File, session: ssh2::Session, stream: Arc>) -> File { 165 | File { 166 | inner: file, 167 | inner_session: session, 168 | stream, 169 | } 170 | } 171 | 172 | /// See [`setstat`](ssh2::File::setstat). 173 | pub async fn setstat(&mut self, stat: FileStat) -> Result<(), Error> { 174 | let inner = &mut self.inner; 175 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.setstat(stat.clone())).await 176 | } 177 | 178 | /// See [`stat`](ssh2::File::stat). 179 | pub async fn stat(&mut self) -> Result { 180 | let inner = &mut self.inner; 181 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.stat()).await 182 | } 183 | 184 | #[allow(missing_docs)] 185 | /// See [`statvfs`](ssh2::File::statvfs). 186 | // TODO 187 | /* 188 | pub async fn statvfs(&mut self) -> Result { 189 | run_ssh2_fn(&self.stream.clone(), self.inner_session, || self.inner.statvfs().await 190 | } 191 | */ 192 | 193 | /// See [`readdir`](ssh2::File::readdir). 194 | pub async fn readdir(&mut self) -> Result<(PathBuf, FileStat), Error> { 195 | let inner = &mut self.inner; 196 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.readdir()).await 197 | } 198 | 199 | /// See [`fsync`](ssh2::File::fsync). 200 | pub async fn fsync(&mut self) -> Result<(), Error> { 201 | let inner = &mut self.inner; 202 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.fsync()).await 203 | } 204 | 205 | /// See [`close`](ssh2::File::close). 206 | pub async fn close(mut self) -> Result<(), Error> { 207 | let inner = &mut self.inner; 208 | run_ssh2_fn(&self.stream, &self.inner_session, || inner.close()).await 209 | } 210 | } 211 | 212 | impl AsyncRead for File { 213 | fn poll_read( 214 | self: Pin<&mut Self>, 215 | cx: &mut Context<'_>, 216 | buf: &mut [u8], 217 | ) -> Poll> { 218 | let this = self.get_mut(); 219 | let inner = &mut this.inner; 220 | poll_ssh2_io_op(cx, &this.stream.clone(), &this.inner_session, || inner.read(buf)) 221 | } 222 | } 223 | 224 | impl AsyncWrite for File { 225 | fn poll_write( 226 | self: Pin<&mut Self>, 227 | cx: &mut Context<'_>, 228 | buf: &[u8], 229 | ) -> Poll> { 230 | let this = self.get_mut(); 231 | let inner = &mut this.inner; 232 | poll_ssh2_io_op(cx, &this.stream, &this.inner_session, || inner.write(buf)) 233 | } 234 | 235 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 236 | let this = self.get_mut(); 237 | let inner = &mut this.inner; 238 | poll_ssh2_io_op(cx, &this.stream, &this.inner_session, || inner.flush()) 239 | } 240 | 241 | fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 242 | let this = self.get_mut(); 243 | let inner = &mut this.inner; 244 | poll_ssh2_io_op(cx, 245 | &this.stream, 246 | &this.inner_session, 247 | || inner.close().map_err(|e| io::Error::from(ssh2::Error::from_errno(e.code()))) 248 | ) 249 | } 250 | } 251 | 252 | impl Seek for File { 253 | fn seek(&mut self, pos: io::SeekFrom) -> Result { 254 | self.inner.seek(pos) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use async_io::Async; 3 | use std::{io, 4 | net::TcpStream, 5 | task::{Context, Poll}, 6 | }; 7 | use futures::{future, ready}; 8 | use futures_util; 9 | use ssh2::{self, BlockDirections, ErrorCode}; 10 | use libssh2_sys; 11 | 12 | fn would_block(e: &ssh2::Error) -> bool { 13 | match e.code() { 14 | ErrorCode::Session(e) if e == libssh2_sys::LIBSSH2_ERROR_EAGAIN => true, 15 | _ => false 16 | } 17 | } 18 | 19 | pub async fn run_ssh2_fn Result>( 20 | stream: &Async, 21 | session: &ssh2::Session, 22 | mut cb: F, 23 | ) -> Result { 24 | 25 | loop { 26 | match cb() { 27 | Ok(v) => return Ok(v), 28 | Err(e) if would_block(&e) => { 29 | match session.block_directions() { 30 | BlockDirections::Inbound => { 31 | stream.readable().await? 32 | }, 33 | BlockDirections::Outbound => { 34 | stream.writable().await? 35 | }, 36 | BlockDirections::Both => { 37 | let readable = stream.readable(); 38 | let writable = stream.writable(); 39 | futures_util::pin_mut!(readable); 40 | futures_util::pin_mut!(writable); 41 | let (ready,_) = future::select(readable, writable).await.factor_first(); 42 | ready? 43 | }, 44 | BlockDirections::None => { 45 | // This should not happen - libssh2 has already reported that it would block 46 | panic!("libssh2 reports EAGAIN but is not blocked"); 47 | }, 48 | } 49 | }, 50 | Err(e) => return Err(Error::from(e)) 51 | } 52 | } 53 | } 54 | 55 | /// Perform libssh2 asynchronous I/O Operation 56 | pub fn poll_ssh2_io_op Result>( 57 | cx: &mut Context<'_>, 58 | stream: &Async, 59 | session: &ssh2::Session, 60 | mut op: F, 61 | ) -> Poll> { 62 | 63 | loop { 64 | match op() { 65 | Ok(result) => return Poll::Ready(Ok(result)), 66 | Err(e) if e.kind() == io::ErrorKind::WouldBlock => { 67 | match session.block_directions() { 68 | BlockDirections::Inbound => { 69 | ready!(stream.poll_readable(cx))?; 70 | }, 71 | BlockDirections::Outbound => { 72 | ready!(stream.poll_writable(cx))?; 73 | }, 74 | BlockDirections::Both => { 75 | match stream.poll_readable(cx) { 76 | Poll::Pending => ready!(stream.poll_writable(cx))?, 77 | Poll::Ready(_) => {} 78 | }; 79 | }, 80 | BlockDirections::None => { 81 | // This should not happen - libssh2 has already reported that it would block 82 | panic!("libssh2 reports EAGAIN but is not blocked"); 83 | }, 84 | } 85 | }, 86 | Err(e) => return Poll::Ready(Err(e)) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | sshd 2 | -------------------------------------------------------------------------------- /tests/all/agent.rs: -------------------------------------------------------------------------------- 1 | use async_ssh2::Session; 2 | 3 | #[tokio::test] 4 | async fn smoke() { 5 | let socket = crate::socket().await; 6 | let mut sess = Session::new().unwrap(); 7 | sess.set_tcp_stream(socket).unwrap(); 8 | let mut agent = sess.agent().unwrap(); 9 | agent.connect().await.unwrap(); 10 | agent.list_identities().unwrap(); 11 | { 12 | let a = agent.identities().unwrap(); 13 | let i1 = &a[0]; 14 | assert!(agent.userauth("foo", &i1).await.is_err()); 15 | } 16 | agent.disconnect().await.unwrap(); 17 | } 18 | -------------------------------------------------------------------------------- /tests/all/channel.rs: -------------------------------------------------------------------------------- 1 | use async_ssh2::Channel; 2 | use futures::io::{AsyncReadExt, AsyncWriteExt}; 3 | use std::{ 4 | io::prelude::*, 5 | net::{TcpListener, TcpStream}, 6 | thread, 7 | }; 8 | 9 | /// Consume all available stdout and stderr data. 10 | /// It is important to read both if you are using 11 | /// channel.eof() to make assertions that the stream 12 | /// is complete 13 | async fn consume_stdio(channel: &mut Channel) -> (String, String) { 14 | let mut stdout = String::new(); 15 | channel.read_to_string(&mut stdout).await.unwrap(); 16 | 17 | let mut stderr = String::new(); 18 | channel.stderr().read_to_string(&mut stderr).unwrap(); 19 | 20 | eprintln!("stdout: {}", stdout); 21 | eprintln!("stderr: {}", stderr); 22 | 23 | (stdout, stderr) 24 | } 25 | 26 | #[tokio::test] 27 | async fn smoke() { 28 | let sess = crate::authed_session().await; 29 | let mut channel = sess.channel_session().await.unwrap(); 30 | 31 | fn must_be_send(_: &T) -> bool { 32 | true 33 | } 34 | assert!(must_be_send(&channel)); 35 | assert!(must_be_send(&channel.stream(0))); 36 | 37 | channel.flush().await.unwrap(); 38 | channel.exec("true").await.unwrap(); 39 | consume_stdio(&mut channel).await; 40 | 41 | channel.wait_eof().await.unwrap(); 42 | assert!(channel.eof()); 43 | 44 | channel.close().await.unwrap(); 45 | channel.wait_close().await.unwrap(); 46 | assert_eq!(channel.exit_status().unwrap(), 0); 47 | assert!(channel.eof()); 48 | } 49 | 50 | #[tokio::test] 51 | async fn bad_smoke() { 52 | let sess = crate::authed_session().await; 53 | let mut channel = sess.channel_session().await.unwrap(); 54 | channel.flush().await.unwrap(); 55 | channel.exec("false").await.unwrap(); 56 | consume_stdio(&mut channel).await; 57 | 58 | channel.wait_eof().await.unwrap(); 59 | assert!(channel.eof()); 60 | 61 | channel.close().await.unwrap(); 62 | channel.wait_close().await.unwrap(); 63 | assert_eq!(channel.exit_status().unwrap(), 1); 64 | assert!(channel.eof()); 65 | } 66 | 67 | #[tokio::test] 68 | async fn reading_data() { 69 | let sess = crate::authed_session().await; 70 | let mut channel = sess.channel_session().await.unwrap(); 71 | channel.exec("echo foo").await.unwrap(); 72 | 73 | let (output, _) = consume_stdio(&mut channel).await; 74 | assert_eq!(output, "foo\n"); 75 | } 76 | 77 | #[tokio::test] 78 | async fn handle_extended_data() { 79 | let sess = crate::authed_session().await; 80 | let mut channel = sess.channel_session().await.unwrap(); 81 | channel 82 | .handle_extended_data(ssh2::ExtendedData::Merge) 83 | .await 84 | .unwrap(); 85 | channel.exec("echo foo >&2").await.unwrap(); 86 | let (output, _) = consume_stdio(&mut channel).await; 87 | // This is an ends_with test because stderr may have several 88 | // lines of misc output on travis macos hosts; it appears as 89 | // though the local shell configuration on travis macos is 90 | // broken and contributes to this :-/ 91 | assert!(output.ends_with("foo\n")); 92 | } 93 | 94 | #[tokio::test] 95 | async fn writing_data() { 96 | let sess = crate::authed_session().await; 97 | let mut channel = sess.channel_session().await.unwrap(); 98 | channel.exec("read foo && echo $foo").await.unwrap(); 99 | channel.write_all(b"foo\n").await.unwrap(); 100 | 101 | let (output, _) = consume_stdio(&mut channel).await; 102 | assert_eq!(output, "foo\n"); 103 | } 104 | 105 | #[tokio::test] 106 | async fn eof() { 107 | let sess = crate::authed_session().await; 108 | let mut channel = sess.channel_session().await.unwrap(); 109 | channel.adjust_receive_window(10, false).await.unwrap(); 110 | channel.exec("read foo").await.unwrap(); 111 | channel.send_eof().await.unwrap(); 112 | let mut output = String::new(); 113 | channel.read_to_string(&mut output).await.unwrap(); 114 | assert_eq!(output, ""); 115 | } 116 | 117 | #[tokio::test] 118 | async fn shell() { 119 | let sess = crate::authed_session().await; 120 | let mut channel = sess.channel_session().await.unwrap(); 121 | eprintln!("requesting pty"); 122 | channel.request_pty("xterm", None, None).await.unwrap(); 123 | eprintln!("shell"); 124 | channel.shell().await.unwrap(); 125 | eprintln!("close"); 126 | channel.close().await.unwrap(); 127 | eprintln!("done"); 128 | consume_stdio(&mut channel).await; 129 | } 130 | 131 | #[tokio::test] 132 | async fn setenv() { 133 | let sess = crate::authed_session().await; 134 | let mut channel = sess.channel_session().await.unwrap(); 135 | let _ = channel.setenv("FOO", "BAR").await; 136 | channel.close().await.unwrap(); 137 | } 138 | 139 | #[tokio::test] 140 | async fn direct() { 141 | let a = TcpListener::bind("127.0.0.1:0").unwrap(); 142 | let addr = a.local_addr().unwrap(); 143 | let t = thread::spawn(move || { 144 | let mut s = a.accept().unwrap().0; 145 | let mut b = [0, 0, 0]; 146 | s.read(&mut b).unwrap(); 147 | assert_eq!(b, [1, 2, 3]); 148 | s.write_all(&[4, 5, 6]).unwrap(); 149 | }); 150 | let sess = crate::authed_session().await; 151 | let mut channel = sess 152 | .channel_direct_tcpip("127.0.0.1", addr.port(), None) 153 | .await 154 | .unwrap(); 155 | channel.write_all(&[1, 2, 3]).await.unwrap(); 156 | let mut r = [0, 0, 0]; 157 | channel.read(&mut r).await.unwrap(); 158 | assert_eq!(r, [4, 5, 6]); 159 | t.join().ok().unwrap(); 160 | } 161 | 162 | #[tokio::test] 163 | async fn forward() { 164 | let sess = crate::authed_session().await; 165 | let (mut listen, port) = sess 166 | .channel_forward_listen(39249, None, None) 167 | .await 168 | .unwrap(); 169 | let t = thread::spawn(move || { 170 | let mut s = TcpStream::connect(&("127.0.0.1", port)).unwrap(); 171 | let mut b = [0, 0, 0]; 172 | s.read(&mut b).unwrap(); 173 | assert_eq!(b, [1, 2, 3]); 174 | s.write_all(&[4, 5, 6]).unwrap(); 175 | }); 176 | 177 | let mut channel = listen.accept().await.unwrap(); 178 | channel.write_all(&[1, 2, 3]).await.unwrap(); 179 | let mut r = [0, 0, 0]; 180 | channel.read(&mut r).await.unwrap(); 181 | assert_eq!(r, [4, 5, 6]); 182 | t.join().ok().unwrap(); 183 | } 184 | 185 | #[tokio::test] 186 | async fn drop_nonblocking() { 187 | let listener = TcpListener::bind("127.0.0.1:0").unwrap(); 188 | let addr = listener.local_addr().unwrap(); 189 | 190 | let sess = crate::authed_session().await; 191 | 192 | thread::spawn(move || { 193 | let _s = listener.accept().unwrap(); 194 | }); 195 | 196 | let _ = sess 197 | .channel_direct_tcpip("127.0.0.1", addr.port(), None) 198 | .await; 199 | drop(sess); 200 | } 201 | 202 | #[tokio::test] 203 | async fn nonblocking_before_exit_code() { 204 | let sess = crate::authed_session().await; 205 | let mut channel = sess.channel_session().await.unwrap(); 206 | channel.send_eof().await.unwrap(); 207 | let mut output = String::new(); 208 | 209 | channel.exec("sleep 1; echo foo").await.unwrap(); 210 | assert!(channel.read_to_string(&mut output).await.is_ok()); 211 | 212 | channel.wait_eof().await.unwrap(); 213 | channel.close().await.unwrap(); 214 | channel.wait_close().await.unwrap(); 215 | assert_eq!(output, "foo\n"); 216 | assert!(channel.exit_status().unwrap() == 0); 217 | } 218 | 219 | #[tokio::test] 220 | async fn exit_code_ignores_other_errors() { 221 | let sess = crate::authed_session().await; 222 | let mut channel = sess.channel_session().await.unwrap(); 223 | channel.exec("true").await.unwrap(); 224 | channel.wait_eof().await.unwrap(); 225 | channel.close().await.unwrap(); 226 | channel.wait_close().await.unwrap(); 227 | let longdescription: String = ::std::iter::repeat('a').take(300).collect(); 228 | assert!(sess.disconnect(None, &longdescription, None).await.is_err()); // max len == 256 229 | assert!(channel.exit_status().unwrap() == 0); 230 | } 231 | 232 | /* 233 | #[test] 234 | fn pty_modes_are_propagated() { 235 | let sess = ::authed_session(); 236 | let mut channel = sess.channel_session().unwrap(); 237 | eprintln!("requesting pty"); 238 | 239 | let mut mode = ssh2::PtyModes::new(); 240 | // intr is typically CTRL-C; setting it to unmodified `y` 241 | // should be very high signal that it took effect 242 | mode.set_character(ssh2::PtyModeOpcode::VINTR, Some('y')); 243 | 244 | channel.request_pty("xterm", Some(mode), None).unwrap(); 245 | channel.exec("stty -a").unwrap(); 246 | 247 | let (out, _err) = consume_stdio(&mut channel); 248 | channel.close().unwrap(); 249 | 250 | // This may well be linux specific 251 | assert!(out.contains("intr = y"), "mode was propagated"); 252 | } 253 | */ 254 | -------------------------------------------------------------------------------- /tests/all/knownhosts.rs: -------------------------------------------------------------------------------- 1 | use async_ssh2::{KnownHostFileKind, Session}; 2 | 3 | #[test] 4 | fn smoke() { 5 | let sess = Session::new().unwrap(); 6 | let known_hosts = sess.known_hosts().unwrap(); 7 | let hosts = known_hosts.hosts().unwrap(); 8 | assert_eq!(hosts.len(), 0); 9 | } 10 | 11 | #[test] 12 | fn reading() { 13 | let encoded = "\ 14 | |1|VXwDpq2cv4j3QtmrGiY+HntJc+Q=|80E+wqnFDhkxBDxRBOIPJPAVE6Y= \ 15 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9I\ 16 | DSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVD\ 17 | BfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eF\ 18 | zLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKS\ 19 | CZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2R\ 20 | PW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi\ 21 | /w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== 22 | "; 23 | let sess = Session::new().unwrap(); 24 | let mut known_hosts = sess.known_hosts().unwrap(); 25 | known_hosts 26 | .read_str(encoded, KnownHostFileKind::OpenSSH) 27 | .unwrap(); 28 | 29 | let hosts = known_hosts.hosts().unwrap(); 30 | assert_eq!(hosts.len(), 1); 31 | let host = &hosts[0]; 32 | 33 | assert_eq!(host.name(), None); 34 | assert_eq!( 35 | host.key(), 36 | "\ 37 | AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9I\ 38 | DSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVD\ 39 | BfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eF\ 40 | zLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKS\ 41 | CZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2R\ 42 | PW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi\ 43 | /w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==" 44 | ); 45 | 46 | assert_eq!( 47 | known_hosts 48 | .write_string(&host, KnownHostFileKind::OpenSSH) 49 | .unwrap(), 50 | encoded 51 | ); 52 | known_hosts.remove(host).unwrap(); 53 | } 54 | -------------------------------------------------------------------------------- /tests/all/main.rs: -------------------------------------------------------------------------------- 1 | //#![deny(warnings)] 2 | 3 | extern crate async_ssh2; 4 | extern crate tempfile; 5 | 6 | use async_io::Async; 7 | use std::{env, net::{TcpStream, ToSocketAddrs}}; 8 | 9 | mod agent; 10 | mod channel; 11 | mod knownhosts; 12 | mod session; 13 | mod sftp; 14 | 15 | pub fn test_addr() -> String { 16 | let port = env::var("RUST_SSH2_FIXTURE_PORT") 17 | .map(|s| s.parse().unwrap()) 18 | .unwrap_or(22); 19 | let addr = format!("127.0.0.1:{}", port); 20 | addr 21 | } 22 | 23 | pub async fn socket() -> Async { 24 | Async::::connect(test_addr().to_socket_addrs().unwrap().next().unwrap()).await.unwrap() 25 | } 26 | 27 | pub async fn authed_session() -> async_ssh2::Session { 28 | let user = env::var("USER").unwrap(); 29 | let socket = socket().await; 30 | let mut sess = async_ssh2::Session::new().unwrap(); 31 | sess.set_tcp_stream(socket).unwrap(); 32 | sess.handshake().await.unwrap(); 33 | assert!(!sess.authenticated()); 34 | 35 | { 36 | let mut agent = sess.agent().unwrap(); 37 | agent.connect().await.unwrap(); 38 | agent.list_identities().unwrap(); 39 | let identity = &agent.identities().unwrap()[0]; 40 | agent.userauth(&user, &identity).await.unwrap(); 41 | } 42 | assert!(sess.authenticated()); 43 | sess 44 | } 45 | -------------------------------------------------------------------------------- /tests/all/session.rs: -------------------------------------------------------------------------------- 1 | use async_ssh2::Session; 2 | use futures::io::{AsyncReadExt, AsyncWriteExt}; 3 | use ssh2::{HashType, MethodType}; 4 | use std::{env, fs::File, io::prelude::*, path::Path}; 5 | use tempfile::tempdir; 6 | 7 | #[test] 8 | fn session_is_send() { 9 | fn must_be_send(_: &T) -> bool { 10 | true 11 | } 12 | 13 | let sess = Session::new().unwrap(); 14 | assert!(must_be_send(&sess)); 15 | } 16 | 17 | #[tokio::test] 18 | async fn smoke() { 19 | let socket = crate::socket().await; 20 | let mut sess = Session::new().unwrap(); 21 | sess.set_tcp_stream(socket).unwrap(); 22 | assert!(sess.banner_bytes().is_none()); 23 | sess.set_banner("foo").await.unwrap(); 24 | assert!(!sess.is_blocking()); 25 | assert_eq!(sess.timeout(), 0); 26 | sess.set_compress(true); 27 | assert!(sess.host_key().is_none()); 28 | sess.method_pref(MethodType::Kex, "diffie-hellman-group14-sha1") 29 | .unwrap(); 30 | assert!(sess.methods(MethodType::Kex).is_none()); 31 | sess.set_timeout(0); 32 | sess.supported_algs(MethodType::Kex).unwrap(); 33 | sess.supported_algs(MethodType::HostKey).unwrap(); 34 | sess.channel_session().await.err().unwrap(); 35 | } 36 | 37 | #[tokio::test] 38 | async fn smoke_handshake() { 39 | let user = env::var("USER").unwrap(); 40 | let socket = crate::socket().await; 41 | let mut sess = Session::new().unwrap(); 42 | sess.set_tcp_stream(socket).unwrap(); 43 | sess.handshake().await.unwrap(); 44 | sess.host_key().unwrap(); 45 | let methods = sess.auth_methods(&user).await.unwrap(); 46 | assert!(methods.contains("publickey"), "{}", methods); 47 | assert!(!sess.authenticated()); 48 | 49 | let mut agent = sess.agent().unwrap(); 50 | agent.connect().await.unwrap(); 51 | agent.list_identities().unwrap(); 52 | { 53 | let identity = &agent.identities().unwrap()[0]; 54 | agent.userauth(&user, &identity).await.unwrap(); 55 | } 56 | assert!(sess.authenticated()); 57 | sess.host_key_hash(HashType::Md5).unwrap(); 58 | } 59 | 60 | /* 61 | #[test] 62 | fn keyboard_interactive() { 63 | let user = env::var("USER").unwrap(); 64 | let socket = ::socket(); 65 | let mut sess = Session::new().unwrap(); 66 | sess.set_tcp_stream(socket); 67 | sess.handshake().unwrap(); 68 | sess.host_key().unwrap(); 69 | let methods = sess.auth_methods(&user).unwrap(); 70 | assert!( 71 | methods.contains("keyboard-interactive"), 72 | "test server ({}) must support `ChallengeResponseAuthentication yes`, not just {}", 73 | ::test_addr(), 74 | methods 75 | ); 76 | assert!(!sess.authenticated()); 77 | 78 | // We don't know the correct response for whatever challenges 79 | // will be returned to us, but that's ok; the purpose of this 80 | // test is to check that we have some basically sane interaction 81 | // with the library. 82 | 83 | struct Prompter { 84 | some_data: usize, 85 | } 86 | 87 | impl KeyboardInteractivePrompt for Prompter { 88 | fn prompt<'a>( 89 | &mut self, 90 | username: &str, 91 | instructions: &str, 92 | prompts: &[Prompt<'a>], 93 | ) -> Vec { 94 | // Sanity check that the pointer manipulation resolves and 95 | // we read back our member data ok 96 | assert_eq!(self.some_data, 42); 97 | 98 | eprintln!("username: {}", username); 99 | eprintln!("instructions: {}", instructions); 100 | eprintln!("prompts: {:?}", prompts); 101 | 102 | // Unfortunately, we can't make any assertions about username 103 | // or instructions, as they can be empty (on my linux system) 104 | // or may have arbitrary contents 105 | // assert_eq!(username, env::var("USER").unwrap()); 106 | // assert!(!instructions.is_empty()); 107 | 108 | // Hopefully this isn't too brittle an assertion 109 | if prompts.len() == 1 { 110 | assert_eq!(prompts.len(), 1); 111 | // Might be "Password: " or "Password:" or other variations 112 | assert!(prompts[0].text.contains("sword")); 113 | assert_eq!(prompts[0].echo, false); 114 | } else { 115 | // maybe there's some PAM configuration that results 116 | // in multiple prompts. We can't make any real assertions 117 | // in this case, other than that there has to be at least 118 | // one prompt. 119 | assert!(!prompts.is_empty()); 120 | } 121 | 122 | prompts.iter().map(|_| "bogus".to_string()).collect() 123 | } 124 | } 125 | 126 | let mut p = Prompter { some_data: 42 }; 127 | 128 | match sess.userauth_keyboard_interactive(&user, &mut p) { 129 | Ok(_) => eprintln!("auth succeeded somehow(!)"), 130 | Err(err) => eprintln!("auth failed as expected: {}", err), 131 | }; 132 | 133 | // The only way this assertion will be false is if the person 134 | // running these tests has "bogus" as their password 135 | assert!(!sess.authenticated()); 136 | } 137 | */ 138 | 139 | #[tokio::test] 140 | async fn keepalive() { 141 | let sess = crate::authed_session().await; 142 | sess.set_keepalive(false, 10); 143 | sess.keepalive_send().await.unwrap(); 144 | } 145 | 146 | #[tokio::test] 147 | async fn scp_recv() { 148 | let sess = crate::authed_session().await; 149 | 150 | // Download our own source file; it's the only path that 151 | // we know for sure exists on this system. 152 | let p = Path::new(file!()).canonicalize().unwrap(); 153 | 154 | let (mut ch, _) = sess.scp_recv(&p).await.unwrap(); 155 | let mut data = String::new(); 156 | ch.read_to_string(&mut data).await.unwrap(); 157 | let mut expected = String::new(); 158 | File::open(&p) 159 | .unwrap() 160 | .read_to_string(&mut expected) 161 | .unwrap(); 162 | assert!(data == expected); 163 | } 164 | 165 | #[tokio::test] 166 | async fn scp_send() { 167 | let td = tempdir().unwrap(); 168 | let sess = crate::authed_session().await; 169 | let mut ch = sess 170 | .scp_send(&td.path().join("foo"), 0o644, 6, None) 171 | .await 172 | .unwrap(); 173 | ch.write_all(b"foobar").await.unwrap(); 174 | drop(ch); 175 | 176 | // TODO why doesn't this work without sleep? 177 | std::thread::sleep(std::time::Duration::from_millis(100)); 178 | 179 | let mut actual = Vec::new(); 180 | File::open(&td.path().join("foo")) 181 | .unwrap() 182 | .read_to_end(&mut actual) 183 | .unwrap(); 184 | assert_eq!(actual, b"foobar"); 185 | } 186 | -------------------------------------------------------------------------------- /tests/all/sftp.rs: -------------------------------------------------------------------------------- 1 | use futures::io::{AsyncReadExt, AsyncWriteExt}; 2 | use std::{ 3 | fs::{self, File}, 4 | io::prelude::*, 5 | }; 6 | use tempfile::tempdir; 7 | use tokio; 8 | 9 | #[tokio::test] 10 | async fn smoke() { 11 | let sess = crate::authed_session().await; 12 | sess.sftp().await.unwrap(); 13 | } 14 | 15 | #[tokio::test] 16 | async fn ops() { 17 | let td = tempdir().unwrap(); 18 | File::create(&td.path().join("foo")).unwrap(); 19 | fs::create_dir(&td.path().join("bar")).unwrap(); 20 | 21 | let sess = crate::authed_session().await; 22 | let sftp = sess.sftp().await.unwrap(); 23 | sftp.opendir(&td.path().join("bar")).await.unwrap(); 24 | let mut foo = sftp.open(&td.path().join("foo")).await.unwrap(); 25 | sftp.mkdir(&td.path().join("bar2"), 0o755).await.unwrap(); 26 | assert!(fs::metadata(&td.path().join("bar2")) 27 | .map(|m| m.is_dir()) 28 | .unwrap_or(false)); 29 | sftp.rmdir(&td.path().join("bar2")).await.unwrap(); 30 | 31 | sftp.create(&td.path().join("foo5")) 32 | .await 33 | .unwrap() 34 | .write_all(b"foo") 35 | .await 36 | .unwrap(); 37 | let mut v = Vec::new(); 38 | File::open(&td.path().join("foo5")) 39 | .unwrap() 40 | .read_to_end(&mut v) 41 | .unwrap(); 42 | assert_eq!(v, b"foo"); 43 | 44 | assert_eq!( 45 | sftp.stat(&td.path().join("foo")).await.unwrap().size, 46 | Some(0) 47 | ); 48 | v.truncate(0); 49 | foo.read_to_end(&mut v).await.unwrap(); 50 | assert_eq!(v, Vec::new()); 51 | 52 | foo.close().await.unwrap(); 53 | 54 | sftp.symlink(&td.path().join("foo"), &td.path().join("foo2")) 55 | .await 56 | .unwrap(); 57 | let readlink = sftp.readlink(&td.path().join("foo2")).await.unwrap(); 58 | assert!(readlink == td.path().join("foo")); 59 | let realpath = sftp.realpath(&td.path().join("foo2")).await.unwrap(); 60 | assert_eq!(realpath, td.path().join("foo").canonicalize().unwrap()); 61 | 62 | let files = sftp.readdir(&td.path()).await.unwrap(); 63 | assert_eq!(files.len(), 4); 64 | 65 | // This test fails, see FIXME in the implementation 66 | //sftp.shutdown().await.unwrap(); 67 | } 68 | -------------------------------------------------------------------------------- /tests/run_integration_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -x 4 | 5 | # This script spawns an ssh daemon with a known configuration so that we can 6 | # test various functionality against it. 7 | 8 | # Tell the tests to use the port number we're using to spawn this server 9 | export RUST_SSH2_FIXTURE_PORT=8022 10 | 11 | cleanup() { 12 | # Stop the ssh server and local ssh agent 13 | sudo kill $(< $SSHDIR/sshd.pid) $SSH_AGENT_PID || true 14 | 15 | test -f $SSHDIR/sshd.log && sudo cat $SSHDIR/sshd.log 16 | } 17 | trap cleanup EXIT 18 | 19 | # Blow away any prior state and re-configure our test server 20 | SSHDIR=$(pwd)/tests/sshd 21 | 22 | sudo rm -rf $SSHDIR 23 | mkdir -p $SSHDIR 24 | 25 | eval $(ssh-agent -s) 26 | 27 | ssh-keygen -t rsa -f $SSHDIR/id_rsa -N "" -q 28 | chmod 0600 $SSHDIR/id_rsa* 29 | ssh-add $SSHDIR/id_rsa 30 | cp $SSHDIR/id_rsa.pub $SSHDIR/authorized_keys 31 | 32 | ssh-keygen -f $SSHDIR/ssh_host_rsa_key -N '' -t rsa 33 | 34 | cat > $SSHDIR/sshd_config <<-EOT 35 | AuthorizedKeysFile=$SSHDIR/authorized_keys 36 | HostKey=$SSHDIR/ssh_host_rsa_key 37 | PidFile=$SSHDIR/sshd.pid 38 | Subsystem sftp internal-sftp 39 | UsePAM yes 40 | X11Forwarding yes 41 | UsePrivilegeSeparation no 42 | PrintMotd yes 43 | PermitTunnel yes 44 | KbdInteractiveAuthentication yes 45 | AllowTcpForwarding yes 46 | MaxStartups 500 47 | # Relax modes when the repo is under eg: /var/tmp 48 | StrictModes no 49 | EOT 50 | 51 | cat $SSHDIR/sshd_config 52 | 53 | # Start an ssh server 54 | sudo /usr/sbin/sshd -p $RUST_SSH2_FIXTURE_PORT -f $SSHDIR/sshd_config -E $SSHDIR/sshd.log 55 | # Give it a moment to start up 56 | sleep 2 57 | 58 | # Run the tests against it 59 | cargo test --all -- --nocapture 60 | cargo test --features vendored-openssl -- --nocapture 61 | --------------------------------------------------------------------------------