ssh2-0.9.4/.cargo_vcs_info.json0000644000000001360000000000100117250ustar { "git": { "sha1": "346c27a25890e54aca0eb9fcbf9113cbd4692bab" }, "path_in_vcs": "" }ssh2-0.9.4/.github/workflows/linux.yml000064400000000000000000000013050072674642500160230ustar 00000000000000name: linux on: push: branches: - master pull_request: branches: - master jobs: build: strategy: fail-fast: false matrix: os: [ubuntu-20.04] rust_toolchain: [stable, beta] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 - name: Install Rust uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust_toolchain }} - name: Build and test run: | rustc -V cargo -V cargo build tests/run_integration_tests.sh rustdoc --test README.md -L target cargo run --manifest-path systest/Cargo.toml ssh2-0.9.4/.github/workflows/macos.yml000064400000000000000000000012450072674642500157710ustar 00000000000000name: macOS on: push: branches: - master pull_request: branches: - master jobs: build: strategy: fail-fast: false matrix: os: [macos-12, macos-11] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 - name: Install Rust uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Build and test run: | rustc -V cargo -V cargo build tests/run_integration_tests.sh rustdoc --test README.md -L target cargo run --manifest-path systest/Cargo.toml ssh2-0.9.4/.github/workflows/windows.yml000064400000000000000000000016650072674642500163670ustar 00000000000000name: Windows on: push: branches: - master pull_request: branches: - master jobs: build: strategy: fail-fast: false matrix: os: [windows-2019, windows-2022] env: - TARGET: x86_64-pc-windows-msvc - TARGET: i686-pc-windows-msvc runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 - name: Install Rust uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true target: ${{ matrix.env.TARGET }} - name: Build and test env: TARGET: ${{ matrix.env.TARGET }} run: | rustc -V cargo -V cargo test --no-run --target %TARGET% cargo run --manifest-path systest/Cargo.toml --target %TARGET% cargo test --no-run --target %TARGET% --features openssl-on-win32,vendored-openssl shell: cmd ssh2-0.9.4/.gitignore000064400000000000000000000000740072674642500125360ustar 00000000000000.*.sw* /target/ libssh2-sys/target/ /Cargo.lock /tests/sshd ssh2-0.9.4/.gitmodules000064400000000000000000000001750072674642500127250ustar 00000000000000[submodule "libssh2-sys/libssh2"] path = libssh2-sys/libssh2 url = https://github.com/libssh2/libssh2 branch = master ssh2-0.9.4/.travis.yml000064400000000000000000000012620072674642500126570ustar 00000000000000language: rust sudo: required matrix: include: - rust: 1.36.0 - rust: stable - rust: beta - rust: nightly # - name: "master doc to gh-pages" # rust: nightly # script: # - cargo doc --no-deps # - cargo doc --no-deps --manifest-path libssh2-sys/Cargo.toml # deploy: # provider: script # script: curl -LsSf https://git.io/fhJ8n | rustc - && (cd target/doc && ../../rust_out) # skip_cleanup: true # on: # branch: master script: - cargo build - tests/run_integration_tests.sh - rustdoc --test README.md -L target - cargo run --manifest-path systest/Cargo.toml notifications: email: on_success: never ssh2-0.9.4/Cargo.toml0000644000000024560000000000100077320ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] name = "ssh2" version = "0.9.4" authors = [ "Alex Crichton ", "Wez Furlong ", "Matteo Bigoi ", ] description = """ Bindings to libssh2 for interacting with SSH servers and executing remote commands, forwarding local ports, etc. """ homepage = "https://github.com/alexcrichton/ssh2-rs" documentation = "https://docs.rs/ssh2" readme = "README.md" keywords = ["ssh"] license = "MIT/Apache-2.0" repository = "https://github.com/alexcrichton/ssh2-rs" [dependencies.bitflags] version = "1.2" [dependencies.libc] version = "0.2" [dependencies.libssh2-sys] version = "0.3.0" [dependencies.parking_lot] version = "0.11" [dev-dependencies.tempdir] version = "0.3" [features] openssl-on-win32 = ["libssh2-sys/openssl-on-win32"] vendored-openssl = ["libssh2-sys/vendored-openssl"] ssh2-0.9.4/Cargo.toml.orig000064400000000000000000000015310072674642500134340ustar 00000000000000[package] name = "ssh2" version = "0.9.4" authors = ["Alex Crichton ", "Wez Furlong ", "Matteo Bigoi "] license = "MIT/Apache-2.0" keywords = ["ssh"] readme = "README.md" repository = "https://github.com/alexcrichton/ssh2-rs" homepage = "https://github.com/alexcrichton/ssh2-rs" documentation = "https://docs.rs/ssh2" description = """ Bindings to libssh2 for interacting with SSH servers and executing remote commands, forwarding local ports, etc. """ [features] vendored-openssl = ["libssh2-sys/vendored-openssl"] openssl-on-win32 = ["libssh2-sys/openssl-on-win32"] [dependencies] bitflags = "1.2" libc = "0.2" libssh2-sys = { path = "libssh2-sys", version = "0.3.0" } parking_lot = "0.11" [dev-dependencies] tempdir = "0.3" [workspace] members = ['systest'] ssh2-0.9.4/LICENSE-APACHE000064400000000000000000000251370072674642500125010ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ssh2-0.9.4/LICENSE-MIT000064400000000000000000000020410072674642500121760ustar 00000000000000Copyright (c) 2014 Alex Crichton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ssh2-0.9.4/README.md000064400000000000000000000030070072674642500120240ustar 00000000000000# ssh2-rs [![Build Status](https://github.com/alexcrichton/ssh2-rs/workflows/linux/badge.svg)](https://github.com/alexcrichton/ssh2-rs/actions?workflow=linux) [![Build Status](https://github.com/alexcrichton/ssh2-rs/workflows/Windows/badge.svg)](https://github.com/alexcrichton/ssh2-rs/actions?workflow=Windows) [![Build Status](https://github.com/alexcrichton/ssh2-rs/workflows/macOS/badge.svg)](https://github.com/alexcrichton/ssh2-rs/actions?workflow=macOS) [Documentation](https://docs.rs/ssh2) Rust bindings to libssh2, an ssh client library. ## Usage ```toml # Cargo.toml [dependencies] ssh2 = "0.9" ``` ## Building on OSX 10.10+ This library depends on OpenSSL. To get OpenSSL working follow the [`openssl` crate's instructions](https://github.com/sfackler/rust-openssl#macos). Starting with version `0.4` of `ssh2`, you can enable the `vendored-openssl` feature to have `libssh2` built against a statically built version of openssl as [described here](https://docs.rs/openssl/0.10.24/openssl/#vendored) # License This project is licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. ssh2-0.9.4/src/agent.rs000064400000000000000000000116570072674642500130120ustar 00000000000000use parking_lot::{Mutex, MutexGuard}; use std::ffi::{CStr, CString}; use std::ptr::null_mut; use std::slice; use std::str; use std::sync::Arc; use {raw, Error, ErrorCode, SessionInner}; /// A structure representing a connection to an SSH agent. /// /// Agents can be used to authenticate a session. pub struct Agent { raw: *mut raw::LIBSSH2_AGENT, sess: Arc>, } // Agent is both Send and Sync; the compiler can't see it because it // is pessimistic about the raw pointer. We use Arc/Mutex to guard accessing // the raw pointer so we are safe for both. unsafe impl Send for Agent {} unsafe impl Sync for Agent {} /// A public key which is extracted from an SSH agent. #[derive(Debug, PartialEq, Eq)] pub struct PublicKey { blob: Vec, comment: String, } impl Agent { pub(crate) fn from_raw_opt( raw: *mut raw::LIBSSH2_AGENT, err: Option, sess: &Arc>, ) -> Result { if raw.is_null() { Err(err.unwrap_or_else(Error::unknown)) } else { Ok(Self { raw, sess: Arc::clone(sess), }) } } /// Connect to an ssh-agent running on the system. pub fn connect(&mut self) -> Result<(), Error> { let sess = self.sess.lock(); unsafe { sess.rc(raw::libssh2_agent_connect(self.raw)) } } /// Close a connection to an ssh-agent. pub fn disconnect(&mut self) -> Result<(), Error> { let sess = self.sess.lock(); unsafe { sess.rc(raw::libssh2_agent_disconnect(self.raw)) } } /// Request an ssh-agent to list of public keys, and stores them in the /// internal collection of the handle. /// /// Call `identities` to get the public keys. pub fn list_identities(&mut self) -> Result<(), Error> { let sess = self.sess.lock(); unsafe { sess.rc(raw::libssh2_agent_list_identities(self.raw)) } } /// Get list of the identities of this agent. pub fn identities(&self) -> Result, Error> { let sess = self.sess.lock(); let mut res = vec![]; let mut prev = null_mut(); let mut next = null_mut(); loop { match unsafe { raw::libssh2_agent_get_identity(self.raw, &mut next, prev) } { 0 => { prev = next; res.push(unsafe { PublicKey::from_raw(next) }); } 1 => break, rc => return Err(Error::from_session_error_raw(sess.raw, rc)), } } Ok(res) } fn resolve_raw_identity( &self, sess: &MutexGuard, identity: &PublicKey, ) -> Result, Error> { let mut prev = null_mut(); let mut next = null_mut(); loop { match unsafe { raw::libssh2_agent_get_identity(self.raw, &mut next, prev) } { 0 => { prev = next; let this_ident = unsafe { PublicKey::from_raw(next) }; if this_ident == *identity { return Ok(Some(next)); } } 1 => break, rc => return Err(Error::from_session_error_raw(sess.raw, rc)), } } Ok(None) } /// Attempt public key authentication with the help of ssh-agent. pub fn userauth(&self, username: &str, identity: &PublicKey) -> Result<(), Error> { let username = CString::new(username)?; let sess = self.sess.lock(); let raw_ident = self.resolve_raw_identity(&sess, identity)?.ok_or_else(|| { Error::new( ErrorCode::Session(raw::LIBSSH2_ERROR_BAD_USE), "Identity not found in agent", ) })?; unsafe { sess.rc(raw::libssh2_agent_userauth( self.raw, username.as_ptr(), raw_ident, )) } } } impl Drop for Agent { fn drop(&mut self) { unsafe { raw::libssh2_agent_free(self.raw) } } } impl PublicKey { unsafe fn from_raw(raw: *mut raw::libssh2_agent_publickey) -> Self { let blob = slice::from_raw_parts_mut((*raw).blob, (*raw).blob_len as usize); let comment = (*raw).comment; let comment = if comment.is_null() { String::new() } else { CStr::from_ptr(comment).to_string_lossy().into_owned() }; Self { blob: blob.to_vec(), comment, } } /// Return the data of this public key. pub fn blob(&self) -> &[u8] { &self.blob } /// Returns the comment in a printable format pub fn comment(&self) -> &str { &self.comment } } ssh2-0.9.4/src/channel.rs000064400000000000000000000473650072674642500133310ustar 00000000000000use libc::{c_char, c_int, c_uchar, c_uint, c_ulong, c_void, size_t}; use parking_lot::{Mutex, MutexGuard}; use std::cmp; use std::ffi::CString; use std::ptr::{null, null_mut}; use std::io; use std::io::prelude::*; use std::slice; use std::sync::Arc; use {raw, Error, ExtendedData, PtyModes, SessionInner}; struct ChannelInner { unsafe_raw: *mut raw::LIBSSH2_CHANNEL, sess: Arc>, read_limit: Mutex>, } // ChannelInner is both Send and Sync; the compiler can't see it because it // is pessimistic about the raw pointer. We use Arc/Mutex to guard accessing // the raw pointer so we are safe for both. unsafe impl Send for ChannelInner {} unsafe impl Sync for ChannelInner {} struct LockedChannel<'a> { raw: *mut raw::LIBSSH2_CHANNEL, sess: MutexGuard<'a, SessionInner>, } /// A channel represents a portion of an SSH connection on which data can be /// read and written. /// /// Channels denote all of SCP uploads and downloads, shell sessions, remote /// process executions, and other general-purpose sessions. Each channel /// implements the `Reader` and `Writer` traits to send and receive data. /// Whether or not I/O operations are blocking is mandated by the `blocking` /// flag on a channel's corresponding `Session`. pub struct Channel { channel_inner: Arc, } impl Channel { pub(crate) fn from_raw_opt( raw: *mut raw::LIBSSH2_CHANNEL, err: Option, sess: &Arc>, ) -> Result { if raw.is_null() { Err(err.unwrap_or_else(Error::unknown)) } else { Ok(Self { channel_inner: Arc::new(ChannelInner { unsafe_raw: raw, sess: Arc::clone(sess), read_limit: Mutex::new(None), }), }) } } fn lock(&self) -> LockedChannel { let sess = self.channel_inner.sess.lock(); LockedChannel { sess, raw: self.channel_inner.unsafe_raw, } } } /// A channel can have a number of streams, each identified by an id, each of /// which implements the `Read` and `Write` traits. pub struct Stream { channel_inner: Arc, id: i32, } struct LockedStream<'a> { raw: *mut raw::LIBSSH2_CHANNEL, sess: MutexGuard<'a, SessionInner>, id: i32, read_limit: MutexGuard<'a, Option>, } impl<'a> LockedStream<'a> { pub fn eof(&self) -> bool { *self.read_limit == Some(0) || unsafe { raw::libssh2_channel_eof(self.raw) != 0 } } } /// Data received from when a program exits with a signal. pub struct ExitSignal { /// The exit signal received, if the program did not exit cleanly. Does not /// contain a SIG prefix pub exit_signal: Option, /// Error message provided by the remote server (if any) pub error_message: Option, /// Language tag provided by the remote server (if any) pub lang_tag: Option, } /// Description of the read window as returned by `Channel::read_window` #[derive(Copy, Clone)] pub struct ReadWindow { /// The number of bytes which the remote end may send without overflowing /// the window limit. pub remaining: u32, /// The number of bytes actually available to be read. pub available: u32, /// The window_size_initial as defined by the channel open request pub window_size_initial: u32, } /// Description of the write window as returned by `Channel::write_window` #[derive(Copy, Clone)] pub struct WriteWindow { /// The number of bytes which may be safely written on the channel without /// blocking. pub remaining: u32, /// The window_size_initial as defined by the channel open request pub window_size_initial: u32, } impl Channel { /// Set an environment variable in the remote channel's process space. /// /// Note that this does not make sense for all channel types and may be /// ignored by the server despite returning success. pub fn setenv(&mut self, var: &str, val: &str) -> Result<(), Error> { let var = CString::new(var)?; let var = var.as_bytes(); let val = CString::new(val)?; let val = val.as_bytes(); let locked = self.lock(); unsafe { locked.sess.rc(raw::libssh2_channel_setenv_ex( locked.raw, var.as_ptr() as *const _, var.len() as c_uint, val.as_ptr() as *const _, val.len() as c_uint, )) } } /// Request a PTY on an established channel. /// /// Note that this does not make sense for all channel types and may be /// ignored by the server despite returning success. /// /// The dimensions argument is a tuple of (width, height, width_px, /// height_px) /// /// The mode parameter is optional and specifies modes to apply to /// the pty. Use the `PtyModes` type construct these modes. /// A contrived example of this is below: /// /// ``` /// let mut mode = ssh2::PtyModes::new(); /// // Set the interrupt character to CTRL-C (ASCII 3: ETX). /// // This is typically the default, but we're showing how to /// // set a relatable option for the sake of example! /// mode.set_character(ssh2::PtyModeOpcode::VINTR, Some(3 as char)); /// ``` pub fn request_pty( &mut self, term: &str, mode: Option, dim: Option<(u32, u32, u32, u32)>, ) -> Result<(), Error> { let term = CString::new(term)?; let term = term.as_bytes(); let locked = self.lock(); let mode = mode.map(PtyModes::finish); let mode = mode.as_ref().map(Vec::as_slice).unwrap_or(&[]); locked.sess.rc(unsafe { let (width, height, width_px, height_px) = dim.unwrap_or((80, 24, 0, 0)); raw::libssh2_channel_request_pty_ex( locked.raw, term.as_ptr() as *const _, term.len() as c_uint, mode.as_ptr() as *const _, mode.len() as c_uint, width as c_int, height as c_int, width_px as c_int, height_px as c_int, ) }) } /// Request that the PTY size be changed to the specified size. /// width and height are the number of character cells, and you /// may optionally include the size specified in pixels. pub fn request_pty_size( &mut self, width: u32, height: u32, width_px: Option, height_px: Option, ) -> Result<(), Error> { let locked = self.lock(); let width_px = width_px.unwrap_or(0); let height_px = height_px.unwrap_or(0); locked.sess.rc(unsafe { raw::libssh2_channel_request_pty_size_ex( locked.raw, width as c_int, height as c_int, width_px as c_int, height_px as c_int, ) }) } /// Requests that the remote host start an authentication agent; /// if successful requests to that agent will be forwarded from /// the server back to the local authentication agent on the client side. /// /// Note that some hosts are configured to disallow agent forwarding, /// and that even if enabled, there is a possibility that starting /// the agent on the remote system can fail. pub fn request_auth_agent_forwarding(&mut self) -> Result<(), Error> { let locked = self.lock(); locked .sess .rc(unsafe { raw::libssh2_channel_request_auth_agent(locked.raw) }) } /// Execute a command /// /// An execution is one of the standard process services defined by the SSH2 /// protocol. /// /// # Example /// /// ```no_run /// # use std::io::prelude::*; /// # use ssh2::Session; /// # let session: Session = panic!(); /// let mut channel = session.channel_session().unwrap(); /// channel.exec("ls").unwrap(); /// let mut s = String::new(); /// channel.read_to_string(&mut s).unwrap(); /// println!("{}", s); /// ``` pub fn exec(&mut self, command: &str) -> Result<(), Error> { self.process_startup("exec", Some(command)) } /// Start a shell /// /// A shell is one of the standard process services defined by the SSH2 /// protocol. pub fn shell(&mut self) -> Result<(), Error> { self.process_startup("shell", None) } /// Request a subsystem be started. /// /// A subsystem is one of the standard process services defined by the SSH2 /// protocol. pub fn subsystem(&mut self, system: &str) -> Result<(), Error> { self.process_startup("subsystem", Some(system)) } /// Initiate a request on a session type channel. /// /// The SSH2 protocol currently defines shell, exec, and subsystem as /// standard process services. pub fn process_startup(&mut self, request: &str, message: Option<&str>) -> Result<(), Error> { let message = message.map(|s| CString::new(s)).transpose()?; let (message, message_len) = message .as_ref() .map(|s| (s.as_ptr(), s.as_bytes().len())) .unwrap_or((null(), 0)); let locked = self.lock(); unsafe { let rc = raw::libssh2_channel_process_startup( locked.raw, request.as_ptr() as *const _, request.len() as c_uint, message, message_len as c_uint, ); locked.sess.rc(rc) } } /// Get a handle to the stderr stream of this channel. /// /// The returned handle implements the `Read` and `Write` traits. pub fn stderr(&self) -> Stream { self.stream(::EXTENDED_DATA_STDERR) } /// Get a handle to a particular stream for this channel. /// /// The returned handle implements the `Read` and `Write` traits. /// /// Groups of substreams may be flushed by passing one of the following /// constants and then calling `flush()`. /// /// * FLUSH_EXTENDED_DATA - Flush all extended data substreams /// * FLUSH_ALL - Flush all substreams pub fn stream(&self, stream_id: i32) -> Stream { Stream { channel_inner: Arc::clone(&self.channel_inner), id: stream_id, } } /// Change how extended data (such as stderr) is handled pub fn handle_extended_data(&mut self, mode: ExtendedData) -> Result<(), Error> { let locked = self.lock(); unsafe { let rc = raw::libssh2_channel_handle_extended_data2(locked.raw, mode as c_int); locked.sess.rc(rc) } } /// Returns the exit code raised by the process running on the remote host /// at the other end of the named channel. /// /// Note that the exit status may not be available if the remote end has not /// yet set its status to closed. pub fn exit_status(&self) -> Result { let locked = self.lock(); // Should really store existing error, call function, check for error // after and restore previous error if no new one...but the only error // condition right now is a NULL pointer check on self.raw, so let's // assume that's not the case. Ok(unsafe { raw::libssh2_channel_get_exit_status(locked.raw) }) } /// Get the remote exit signal. pub fn exit_signal(&self) -> Result { let locked = self.lock(); unsafe { let mut sig = null_mut(); let mut siglen = 0; let mut msg = null_mut(); let mut msglen = 0; let mut lang = null_mut(); let mut langlen = 0; let rc = raw::libssh2_channel_get_exit_signal( locked.raw, &mut sig, &mut siglen, &mut msg, &mut msglen, &mut lang, &mut langlen, ); locked.sess.rc(rc)?; return Ok(ExitSignal { exit_signal: convert(&locked, sig, siglen), error_message: convert(&locked, msg, msglen), lang_tag: convert(&locked, lang, langlen), }); } unsafe fn convert(locked: &LockedChannel, ptr: *mut c_char, len: size_t) -> Option { if ptr.is_null() { return None; } let slice = slice::from_raw_parts(ptr as *const u8, len as usize); let ret = slice.to_vec(); raw::libssh2_free(locked.sess.raw, ptr as *mut c_void); String::from_utf8(ret).ok() } } /// Check the status of the read window. pub fn read_window(&self) -> ReadWindow { let locked = self.lock(); unsafe { let mut avail = 0; let mut init = 0; let remaining = raw::libssh2_channel_window_read_ex(locked.raw, &mut avail, &mut init); ReadWindow { remaining: remaining as u32, available: avail as u32, window_size_initial: init as u32, } } } /// Check the status of the write window. pub fn write_window(&self) -> WriteWindow { let locked = self.lock(); unsafe { let mut init = 0; let remaining = raw::libssh2_channel_window_write_ex(locked.raw, &mut init); WriteWindow { remaining: remaining as u32, window_size_initial: init as u32, } } } /// Adjust the receive window for a channel by adjustment bytes. /// /// If the amount to be adjusted is less than the minimum adjustment and /// force is false, the adjustment amount will be queued for a later packet. /// /// This function returns the new size of the receive window (as understood /// by remote end) on success. pub fn adjust_receive_window(&mut self, adjust: u64, force: bool) -> Result { let locked = self.lock(); let mut ret = 0; let rc = unsafe { raw::libssh2_channel_receive_window_adjust2( locked.raw, adjust as c_ulong, force as c_uchar, &mut ret, ) }; locked.sess.rc(rc)?; Ok(ret as u64) } /// Artificially limit the number of bytes that will be read from this /// channel. Hack intended for use by scp_recv only. #[doc(hidden)] pub(crate) fn limit_read(&mut self, limit: u64) { *self.channel_inner.read_limit.lock() = Some(limit); } /// Check if the remote host has sent an EOF status for the channel. /// Take care: the EOF status is for the entire channel which can be confusing /// because the reading from the channel reads only the stdout stream. /// unread, buffered, stderr data will cause eof() to return false. pub fn eof(&self) -> bool { let locked = self.lock(); *self.channel_inner.read_limit.lock() == Some(0) || unsafe { raw::libssh2_channel_eof(locked.raw) != 0 } } /// Tell the remote host that no further data will be sent on the specified /// channel. /// /// Processes typically interpret this as a closed stdin descriptor. pub fn send_eof(&mut self) -> Result<(), Error> { let locked = self.lock(); unsafe { locked.sess.rc(raw::libssh2_channel_send_eof(locked.raw)) } } /// Wait for the remote end to send EOF. /// Note that unread buffered stdout and stderr will cause this function /// to return `Ok(())` without waiting. /// You should call the eof() function after calling this to check the /// status of the channel. pub fn wait_eof(&mut self) -> Result<(), Error> { let locked = self.lock(); unsafe { locked.sess.rc(raw::libssh2_channel_wait_eof(locked.raw)) } } /// Close an active data channel. /// /// In practice this means sending an SSH_MSG_CLOSE packet to the remote /// host which serves as instruction that no further data will be sent to /// it. The remote host may still send data back until it sends its own /// close message in response. /// /// To wait for the remote end to close its connection as well, follow this /// command with `wait_closed` pub fn close(&mut self) -> Result<(), Error> { let locked = self.lock(); unsafe { locked.sess.rc(raw::libssh2_channel_close(locked.raw)) } } /// Enter a temporary blocking state until the remote host closes the named /// channel. /// /// Typically sent after `close` in order to examine the exit status. pub fn wait_close(&mut self) -> Result<(), Error> { let locked = self.lock(); unsafe { locked.sess.rc(raw::libssh2_channel_wait_closed(locked.raw)) } } } impl Write for Channel { fn write(&mut self, buf: &[u8]) -> io::Result { self.stream(0).write(buf) } fn flush(&mut self) -> io::Result<()> { self.stream(0).flush() } } impl Read for Channel { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.stream(0).read(buf) } } impl Drop for ChannelInner { fn drop(&mut self) { unsafe { let _ = raw::libssh2_channel_free(self.unsafe_raw); } } } impl Stream { fn lock(&self) -> LockedStream { let sess = self.channel_inner.sess.lock(); LockedStream { sess, raw: self.channel_inner.unsafe_raw, id: self.id, read_limit: self.channel_inner.read_limit.lock(), } } } impl Read for Stream { fn read(&mut self, data: &mut [u8]) -> io::Result { let mut locked = self.lock(); if locked.eof() { return Ok(0); } let data = match locked.read_limit.as_mut() { Some(amt) => { let len = data.len(); &mut data[..cmp::min(*amt as usize, len)] } None => data, }; let ret = unsafe { let rc = raw::libssh2_channel_read_ex( locked.raw, locked.id as c_int, data.as_mut_ptr() as *mut _, data.len() as size_t, ); locked.sess.rc(rc as c_int).map(|()| rc as usize) }; match ret { Ok(n) => { if let Some(ref mut amt) = locked.read_limit.as_mut() { **amt -= n as u64; } Ok(n) } Err(e) => Err(e.into()), } } } impl Write for Stream { fn write(&mut self, data: &[u8]) -> io::Result { let locked = self.lock(); unsafe { let rc = raw::libssh2_channel_write_ex( locked.raw, locked.id as c_int, data.as_ptr() as *mut _, data.len() as size_t, ); locked.sess.rc(rc as c_int).map(|()| rc as usize) } .map_err(Into::into) } fn flush(&mut self) -> io::Result<()> { let locked = self.lock(); unsafe { let rc = raw::libssh2_channel_flush_ex(locked.raw, locked.id as c_int); locked.sess.rc(rc) } .map_err(Into::into) } } ssh2-0.9.4/src/error.rs000064400000000000000000000257320072674642500130440ustar 00000000000000use libc; use std::borrow::Cow; use std::error; use std::ffi::NulError; use std::fmt; use std::io; use std::ptr::null_mut; use std::str; use {raw, Session}; /// An error code originating from a particular source. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ErrorCode { /// Codes for errors that originate in libssh2. /// Can be one of `LIBSSH2_ERROR_*` constants. Session(libc::c_int), /// Codes for errors that originate in the SFTP subsystem. /// Can be one of `LIBSSH2_FX_*` constants. // // TODO: This should be `c_ulong` instead of `c_int` because these constants // are only returned by `libssh2_sftp_last_error()` which returns `c_ulong`. SFTP(libc::c_int), } impl fmt::Display for ErrorCode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } } /// Representation of an error that can occur within libssh2 #[derive(Debug)] #[allow(missing_copy_implementations)] pub struct Error { code: ErrorCode, msg: Cow<'static, str>, } impl Error { #[doc(hidden)] pub fn last_session_error_raw(raw: *mut raw::LIBSSH2_SESSION) -> Option { unsafe { let mut msg = null_mut(); let rc = raw::libssh2_session_last_error(raw, &mut msg, null_mut(), 0); if rc == 0 { return None; } // The pointer stored in `msg` points to the internal buffer of // LIBSSH2_SESSION, so the error message should be copied before // it is overwritten by the next API call. Some(Self { code: ErrorCode::Session(rc), msg: make_error_message(msg), }) } } /// Given a libssh2 error return code, generate an Error object that /// encapsulates that error code and the error reason. /// The error reason is extracted from the Session and is used if the /// session contains the same error code as that provided. /// If the error code doesn't match then an approximation of the error /// reason is used instead of the error message stored in the Session. pub fn from_session_error(sess: &Session, rc: libc::c_int) -> Error { Self::from_session_error_raw(&mut *sess.raw(), rc) } #[doc(hidden)] pub fn from_session_error_raw(raw: *mut raw::LIBSSH2_SESSION, rc: libc::c_int) -> Error { unsafe { let mut msg = null_mut(); let res = raw::libssh2_session_last_error(raw, &mut msg, null_mut(), 0); if res != rc { return Self::from_errno(ErrorCode::Session(rc)); } // The pointer stored in `msg` points to the internal buffer of // LIBSSH2_SESSION, so the error message should be copied before // it is overwritten by the next API call. Self { code: ErrorCode::Session(rc), msg: make_error_message(msg), } } } /// Generate the last error that occurred for a `Session`. /// /// Returns `None` if there was no last error. pub fn last_session_error(sess: &Session) -> Option { Self::last_session_error_raw(&mut *sess.raw()) } /// Create a new error for the given code and message pub fn new(code: ErrorCode, msg: &'static str) -> Error { Error { code, msg: Cow::Borrowed(msg), } } /// Generate an error that represents EOF pub fn eof() -> Error { Error::new( ErrorCode::Session(raw::LIBSSH2_ERROR_CHANNEL_EOF_SENT), "end of file", ) } /// Generate an error for unknown failure pub fn unknown() -> Error { Error::new( ErrorCode::Session(libc::c_int::min_value()), "no other error listed", ) } /// Construct an error from an error code from libssh2 pub fn from_errno(code: ErrorCode) -> Error { let msg = match code { ErrorCode::Session(code) => match code { raw::LIBSSH2_ERROR_BANNER_RECV => "banner recv failure", raw::LIBSSH2_ERROR_BANNER_SEND => "banner send failure", raw::LIBSSH2_ERROR_INVALID_MAC => "invalid mac", raw::LIBSSH2_ERROR_KEX_FAILURE => "kex failure", raw::LIBSSH2_ERROR_ALLOC => "alloc failure", raw::LIBSSH2_ERROR_SOCKET_SEND => "socket send failure", raw::LIBSSH2_ERROR_KEY_EXCHANGE_FAILURE => "key exchange failure", raw::LIBSSH2_ERROR_TIMEOUT => "timed out", raw::LIBSSH2_ERROR_HOSTKEY_INIT => "hostkey init error", raw::LIBSSH2_ERROR_HOSTKEY_SIGN => "hostkey sign error", raw::LIBSSH2_ERROR_DECRYPT => "decrypt error", raw::LIBSSH2_ERROR_SOCKET_DISCONNECT => "socket disconnected", raw::LIBSSH2_ERROR_PROTO => "protocol error", raw::LIBSSH2_ERROR_PASSWORD_EXPIRED => "password expired", raw::LIBSSH2_ERROR_FILE => "file error", raw::LIBSSH2_ERROR_METHOD_NONE => "bad method name", raw::LIBSSH2_ERROR_AUTHENTICATION_FAILED => "authentication failed", raw::LIBSSH2_ERROR_PUBLICKEY_UNVERIFIED => "public key unverified", raw::LIBSSH2_ERROR_CHANNEL_OUTOFORDER => "channel out of order", raw::LIBSSH2_ERROR_CHANNEL_FAILURE => "channel failure", raw::LIBSSH2_ERROR_CHANNEL_REQUEST_DENIED => "request denied", raw::LIBSSH2_ERROR_CHANNEL_UNKNOWN => "unknown channel error", raw::LIBSSH2_ERROR_CHANNEL_WINDOW_EXCEEDED => "window exceeded", raw::LIBSSH2_ERROR_CHANNEL_PACKET_EXCEEDED => "packet exceeded", raw::LIBSSH2_ERROR_CHANNEL_CLOSED => "closed channel", raw::LIBSSH2_ERROR_CHANNEL_EOF_SENT => "eof sent", raw::LIBSSH2_ERROR_SCP_PROTOCOL => "scp protocol error", raw::LIBSSH2_ERROR_ZLIB => "zlib error", raw::LIBSSH2_ERROR_SOCKET_TIMEOUT => "socket timeout", raw::LIBSSH2_ERROR_SFTP_PROTOCOL => "sftp protocol error", raw::LIBSSH2_ERROR_REQUEST_DENIED => "request denied", raw::LIBSSH2_ERROR_METHOD_NOT_SUPPORTED => "method not supported", raw::LIBSSH2_ERROR_INVAL => "invalid", raw::LIBSSH2_ERROR_INVALID_POLL_TYPE => "invalid poll type", raw::LIBSSH2_ERROR_PUBLICKEY_PROTOCOL => "public key protocol error", raw::LIBSSH2_ERROR_EAGAIN => "operation would block", raw::LIBSSH2_ERROR_BUFFER_TOO_SMALL => "buffer too small", raw::LIBSSH2_ERROR_BAD_USE => "bad use error", raw::LIBSSH2_ERROR_COMPRESS => "compression error", raw::LIBSSH2_ERROR_OUT_OF_BOUNDARY => "out of bounds", raw::LIBSSH2_ERROR_AGENT_PROTOCOL => "invalid agent protocol", raw::LIBSSH2_ERROR_SOCKET_RECV => "error receiving on socket", raw::LIBSSH2_ERROR_ENCRYPT => "bad encrypt", raw::LIBSSH2_ERROR_BAD_SOCKET => "bad socket", raw::LIBSSH2_ERROR_KNOWN_HOSTS => "known hosts error", raw::LIBSSH2_ERROR_CHANNEL_WINDOW_FULL => "channel window full", raw::LIBSSH2_ERROR_KEYFILE_AUTH_FAILED => "keyfile auth failed", raw::LIBSSH2_ERROR_RANDGEN => "unable to get random bytes", raw::LIBSSH2_ERROR_MISSING_USERAUTH_BANNER => "missing userauth banner", raw::LIBSSH2_ERROR_ALGO_UNSUPPORTED => "algorithm unsupported", _ => "unknown error", }, ErrorCode::SFTP(code) => match code { raw::LIBSSH2_FX_EOF => "end of file", raw::LIBSSH2_FX_NO_SUCH_FILE => "no such file", raw::LIBSSH2_FX_PERMISSION_DENIED => "permission denied", raw::LIBSSH2_FX_FAILURE => "failure", raw::LIBSSH2_FX_BAD_MESSAGE => "bad message", raw::LIBSSH2_FX_NO_CONNECTION => "no connection", raw::LIBSSH2_FX_CONNECTION_LOST => "connection lost", raw::LIBSSH2_FX_OP_UNSUPPORTED => "operation unsupported", raw::LIBSSH2_FX_INVALID_HANDLE => "invalid handle", raw::LIBSSH2_FX_NO_SUCH_PATH => "no such path", raw::LIBSSH2_FX_FILE_ALREADY_EXISTS => "file already exists", raw::LIBSSH2_FX_WRITE_PROTECT => "file is write protected", raw::LIBSSH2_FX_NO_MEDIA => "no media available", raw::LIBSSH2_FX_NO_SPACE_ON_FILESYSTEM => "no space on filesystem", raw::LIBSSH2_FX_QUOTA_EXCEEDED => "quota exceeded", raw::LIBSSH2_FX_UNKNOWN_PRINCIPAL => "unknown principal", raw::LIBSSH2_FX_LOCK_CONFLICT => "lock conflict", raw::LIBSSH2_FX_DIR_NOT_EMPTY => "directory not empty", raw::LIBSSH2_FX_NOT_A_DIRECTORY => "not a directory", raw::LIBSSH2_FX_INVALID_FILENAME => "invalid filename", raw::LIBSSH2_FX_LINK_LOOP => "link loop", _ => "unknown error", }, }; Error::new(code, msg) } /// Get the message corresponding to this error pub fn message(&self) -> &str { &*self.msg } /// Return the code for this error pub fn code(&self) -> ErrorCode { self.code } } impl From for io::Error { fn from(err: Error) -> io::Error { let kind = match err.code { ErrorCode::Session(raw::LIBSSH2_ERROR_EAGAIN) => io::ErrorKind::WouldBlock, ErrorCode::Session(raw::LIBSSH2_ERROR_TIMEOUT) => io::ErrorKind::TimedOut, ErrorCode::SFTP(raw::LIBSSH2_FX_NO_SUCH_FILE) | ErrorCode::SFTP(raw::LIBSSH2_FX_NO_SUCH_PATH) => io::ErrorKind::NotFound, _ => io::ErrorKind::Other, }; io::Error::new(kind, err.msg) } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}] {}", self.code, self.msg) } } impl error::Error for Error { fn description(&self) -> &str { self.message() } } impl From for Error { fn from(_: NulError) -> Error { Error::new( ErrorCode::Session(raw::LIBSSH2_ERROR_INVAL), "provided data contained a nul byte and could not be used \ as as string", ) } } unsafe fn make_error_message(msg: *mut libc::c_char) -> Cow<'static, str> { const FALLBACK: Cow<'_, str> = Cow::Borrowed(""); ::opt_bytes(&(), msg) .and_then(|msg| { str::from_utf8(msg) .map(|msg| Cow::Owned(msg.to_owned())) .ok() }) .unwrap_or_else(|| FALLBACK) } ssh2-0.9.4/src/knownhosts.rs000064400000000000000000000253010072674642500141200ustar 00000000000000use libc::{c_int, size_t}; use parking_lot::{Mutex, MutexGuard}; use std::ffi::CString; use std::path::Path; use std::ptr::null_mut; use std::str; use std::sync::Arc; use util; use {raw, CheckResult, Error, ErrorCode, KnownHostFileKind, SessionInner}; /// A set of known hosts which can be used to verify the identity of a remote /// server. /// /// # Example /// /// ```no_run /// use std::env; /// use std::path::Path; /// use ssh2::{self, CheckResult, HostKeyType, KnownHostKeyFormat}; /// use ssh2::KnownHostFileKind; /// /// fn check_known_host(session: &ssh2::Session, host: &str) { /// let mut known_hosts = session.known_hosts().unwrap(); /// /// // Initialize the known hosts with a global known hosts file /// let file = Path::new(&env::var("HOME").unwrap()).join(".ssh/known_hosts"); /// known_hosts.read_file(&file, KnownHostFileKind::OpenSSH).unwrap(); /// /// // Now check to see if the seesion's host key is anywhere in the known /// // hosts file /// let (key, key_type) = session.host_key().unwrap(); /// match known_hosts.check(host, key) { /// CheckResult::Match => return, // all good! /// CheckResult::NotFound => {} // ok, we'll add it /// CheckResult::Mismatch => { /// panic!("host mismatch, man in the middle attack?!") /// } /// CheckResult::Failure => panic!("failed to check the known hosts"), /// } /// /// println!("adding {} to the known hosts", host); /// /// known_hosts.add(host, key, host, key_type.into()).unwrap(); /// known_hosts.write_file(&file, KnownHostFileKind::OpenSSH).unwrap(); /// } /// ``` pub struct KnownHosts { raw: *mut raw::LIBSSH2_KNOWNHOSTS, sess: Arc>, } /// Structure representing a known host as part of a `KnownHosts` structure. #[derive(Debug, PartialEq, Eq)] pub struct Host { name: Option, key: String, } impl KnownHosts { pub(crate) fn from_raw_opt( raw: *mut raw::LIBSSH2_KNOWNHOSTS, err: Option, sess: &Arc>, ) -> Result { if raw.is_null() { Err(err.unwrap_or_else(Error::unknown)) } else { Ok(Self { raw, sess: Arc::clone(sess), }) } } /// Reads a collection of known hosts from a specified file and adds them to /// the collection of known hosts. pub fn read_file(&mut self, file: &Path, kind: KnownHostFileKind) -> Result { let file = CString::new(util::path2bytes(file)?)?; let sess = self.sess.lock(); let n = unsafe { raw::libssh2_knownhost_readfile(self.raw, file.as_ptr(), kind as c_int) }; if n < 0 { sess.rc(n)? } Ok(n as u32) } /// Read a line as if it were from a known hosts file. pub fn read_str(&mut self, s: &str, kind: KnownHostFileKind) -> Result<(), Error> { let s = CString::new(s)?; let s = s.as_bytes(); let sess = self.sess.lock(); sess.rc(unsafe { raw::libssh2_knownhost_readline( self.raw, s.as_ptr() as *const _, s.len() as size_t, kind as c_int, ) }) } /// Writes all the known hosts to the specified file using the specified /// file format. pub fn write_file(&self, file: &Path, kind: KnownHostFileKind) -> Result<(), Error> { let file = CString::new(util::path2bytes(file)?)?; let sess = self.sess.lock(); let n = unsafe { raw::libssh2_knownhost_writefile(self.raw, file.as_ptr(), kind as c_int) }; sess.rc(n) } /// Converts a single known host to a single line of output for storage, /// using the 'type' output format. pub fn write_string(&self, host: &Host, kind: KnownHostFileKind) -> Result { let mut v = Vec::with_capacity(128); let sess = self.sess.lock(); let raw_host = self.resolve_to_raw_host(&sess, host)?.ok_or_else(|| { Error::new( ErrorCode::Session(raw::LIBSSH2_ERROR_BAD_USE), "Host is not in the set of known hosts", ) })?; loop { let mut outlen = 0; unsafe { let rc = raw::libssh2_knownhost_writeline( self.raw, raw_host, v.as_mut_ptr() as *mut _, v.capacity() as size_t, &mut outlen, kind as c_int, ); if rc == raw::LIBSSH2_ERROR_BUFFER_TOO_SMALL { // + 1 for the trailing zero v.reserve(outlen as usize + 1); } else { sess.rc(rc)?; v.set_len(outlen as usize); break; } } } Ok(String::from_utf8(v).unwrap()) } /// Create an iterator over all of the known hosts in this structure. pub fn iter(&self) -> Result, Error> { self.hosts() } /// Retrieves the list of known hosts pub fn hosts(&self) -> Result, Error> { let mut next = null_mut(); let mut prev = null_mut(); let sess = self.sess.lock(); let mut hosts = vec![]; loop { match unsafe { raw::libssh2_knownhost_get(self.raw, &mut next, prev) } { 0 => { prev = next; hosts.push(unsafe { Host::from_raw(next) }); } 1 => break, rc => return Err(Error::from_session_error_raw(sess.raw, rc)), } } Ok(hosts) } /// Given a Host object, find the matching raw node in the internal list. /// The returned value is only valid while the session is locked. fn resolve_to_raw_host( &self, sess: &MutexGuard, host: &Host, ) -> Result, Error> { let mut next = null_mut(); let mut prev = null_mut(); loop { match unsafe { raw::libssh2_knownhost_get(self.raw, &mut next, prev) } { 0 => { prev = next; let current = unsafe { Host::from_raw(next) }; if current == *host { return Ok(Some(next)); } } 1 => break, rc => return Err(Error::from_session_error_raw(sess.raw, rc)), } } Ok(None) } /// Delete a known host entry from the collection of known hosts. pub fn remove(&self, host: &Host) -> Result<(), Error> { let sess = self.sess.lock(); if let Some(raw_host) = self.resolve_to_raw_host(&sess, host)? { return sess.rc(unsafe { raw::libssh2_knownhost_del(self.raw, raw_host) }); } else { Ok(()) } } /// Checks a host and its associated key against the collection of known /// hosts, and returns info back about the (partially) matched entry. /// /// The host name can be the IP numerical address of the host or the full /// name. The key must be the raw data of the key. pub fn check(&self, host: &str, key: &[u8]) -> CheckResult { self.check_port_(host, -1, key) } /// Same as `check`, but takes a port as well. pub fn check_port(&self, host: &str, port: u16, key: &[u8]) -> CheckResult { self.check_port_(host, port as i32, key) } fn check_port_(&self, host: &str, port: i32, key: &[u8]) -> CheckResult { let host = CString::new(host).unwrap(); let flags = raw::LIBSSH2_KNOWNHOST_TYPE_PLAIN | raw::LIBSSH2_KNOWNHOST_KEYENC_RAW; unsafe { let rc = raw::libssh2_knownhost_checkp( self.raw, host.as_ptr(), port as c_int, key.as_ptr() as *const _, key.len() as size_t, flags, null_mut(), ); match rc { raw::LIBSSH2_KNOWNHOST_CHECK_MATCH => CheckResult::Match, raw::LIBSSH2_KNOWNHOST_CHECK_MISMATCH => CheckResult::Mismatch, raw::LIBSSH2_KNOWNHOST_CHECK_NOTFOUND => CheckResult::NotFound, _ => CheckResult::Failure, } } } /// Adds a known host to the collection of known hosts. /// /// The host is the host name in plain text. The host name can be the IP /// numerical address of the host or the full name. If you want to add a key /// for a specific port number for the given host, you must provide the host /// name like `"[host]:port"` with the actual characters `[` and `]` enclosing /// the host name and a colon separating the host part from the port number. /// For example: `"[host.example.com]:222"`. /// /// The key provided must be the raw key for the host. pub fn add( &mut self, host: &str, key: &[u8], comment: &str, fmt: ::KnownHostKeyFormat, ) -> Result<(), Error> { let host = CString::new(host)?; let flags = raw::LIBSSH2_KNOWNHOST_TYPE_PLAIN | raw::LIBSSH2_KNOWNHOST_KEYENC_RAW | (fmt as c_int); let sess = self.sess.lock(); unsafe { let rc = raw::libssh2_knownhost_addc( self.raw, host.as_ptr() as *mut _, null_mut(), key.as_ptr() as *mut _, key.len() as size_t, comment.as_ptr() as *const _, comment.len() as size_t, flags, null_mut(), ); sess.rc(rc) } } } impl Drop for KnownHosts { fn drop(&mut self) { let _sess = self.sess.lock(); unsafe { raw::libssh2_knownhost_free(self.raw) } } } impl Host { /// This is `None` if no plain text host name exists. pub fn name(&self) -> Option<&str> { self.name.as_ref().map(String::as_str) } /// Returns the key in base64/printable format pub fn key(&self) -> &str { &self.key } unsafe fn from_raw(raw: *mut raw::libssh2_knownhost) -> Self { let name = ::opt_bytes(&raw, (*raw).name).and_then(|s| String::from_utf8(s.to_vec()).ok()); let key = ::opt_bytes(&raw, (*raw).key).unwrap(); let key = String::from_utf8(key.to_vec()).unwrap(); Self { name, key } } } ssh2-0.9.4/src/lib.rs000064400000000000000000000477640072674642500124720ustar 00000000000000//! Rust bindings to libssh2, an SSH client library. //! //! This library intends to provide a safe interface to the libssh2 library. It //! will build the library if it's not available on the local system, and //! otherwise link to an installed copy. //! //! Note that libssh2 only supports SSH *clients*, not SSH *servers*. //! Additionally it only supports protocol v2, not protocol v1. //! //! # Examples //! //! ## Inspecting ssh-agent //! //! ```no_run //! use ssh2::Session; //! //! // Almost all APIs require a `Session` to be available //! let sess = Session::new().unwrap(); //! let mut agent = sess.agent().unwrap(); //! //! // Connect the agent and request a list of identities //! agent.connect().unwrap(); //! agent.list_identities().unwrap(); //! //! for identity in agent.identities().unwrap() { //! println!("{}", identity.comment()); //! let pubkey = identity.blob(); //! } //! ``` //! //! ## Authenticating with ssh-agent //! //! ```no_run //! use std::net::TcpStream; //! use ssh2::Session; //! //! // Connect to the local SSH server //! let tcp = TcpStream::connect("127.0.0.1:22").unwrap(); //! let mut sess = Session::new().unwrap(); //! sess.set_tcp_stream(tcp); //! sess.handshake().unwrap(); //! //! // Try to authenticate with the first identity in the agent. //! sess.userauth_agent("username").unwrap(); //! //! // Make sure we succeeded //! assert!(sess.authenticated()); //! ``` //! //! ## Authenticating with a password //! //! ```no_run //! use std::net::TcpStream; //! use ssh2::Session; //! //! // Connect to the local SSH server //! let tcp = TcpStream::connect("127.0.0.1:22").unwrap(); //! let mut sess = Session::new().unwrap(); //! sess.set_tcp_stream(tcp); //! sess.handshake().unwrap(); //! //! sess.userauth_password("username", "password").unwrap(); //! assert!(sess.authenticated()); //! ``` //! //! ## Run a command //! //! ```no_run //! use std::io::prelude::*; //! use std::net::{TcpStream}; //! use ssh2::Session; //! //! // Connect to the local SSH server //! let tcp = TcpStream::connect("127.0.0.1:22").unwrap(); //! let mut sess = Session::new().unwrap(); //! sess.set_tcp_stream(tcp); //! sess.handshake().unwrap(); //! sess.userauth_agent("username").unwrap(); //! //! let mut channel = sess.channel_session().unwrap(); //! channel.exec("ls").unwrap(); //! let mut s = String::new(); //! channel.read_to_string(&mut s).unwrap(); //! println!("{}", s); //! channel.wait_close(); //! println!("{}", channel.exit_status().unwrap()); //! ``` //! //! ## Upload a file //! //! ```no_run //! use std::io::prelude::*; //! use std::net::TcpStream; //! use std::path::Path; //! use ssh2::Session; //! //! // Connect to the local SSH server //! let tcp = TcpStream::connect("127.0.0.1:22").unwrap(); //! let mut sess = Session::new().unwrap(); //! sess.set_tcp_stream(tcp); //! sess.handshake().unwrap(); //! sess.userauth_agent("username").unwrap(); //! //! // Write the file //! let mut remote_file = sess.scp_send(Path::new("remote"), //! 0o644, 10, None).unwrap(); //! remote_file.write(b"1234567890").unwrap(); //! // Close the channel and wait for the whole content to be transferred //! remote_file.send_eof().unwrap(); //! remote_file.wait_eof().unwrap(); //! remote_file.close().unwrap(); //! remote_file.wait_close().unwrap(); //! ``` //! //! ## Download a file //! //! ```no_run //! use std::io::prelude::*; //! use std::net::TcpStream; //! use std::path::Path; //! use ssh2::Session; //! //! // Connect to the local SSH server //! let tcp = TcpStream::connect("127.0.0.1:22").unwrap(); //! let mut sess = Session::new().unwrap(); //! sess.set_tcp_stream(tcp); //! sess.handshake().unwrap(); //! sess.userauth_agent("username").unwrap(); //! //! let (mut remote_file, stat) = sess.scp_recv(Path::new("remote")).unwrap(); //! println!("remote file size: {}", stat.size()); //! let mut contents = Vec::new(); //! remote_file.read_to_end(&mut contents).unwrap(); //! //! // Close the channel and wait for the whole content to be tranferred //! remote_file.send_eof().unwrap(); //! remote_file.wait_eof().unwrap(); //! remote_file.close().unwrap(); //! remote_file.wait_close().unwrap(); //! ``` //! //! ## Execute a Netconf XML payload //! //! ```no_run //! use ssh2::{Channel, Session}; //! use std::error::Error; //! use std::io::prelude::*; //! use std::net::TcpStream; //! //! const HELLO: &str = " //! //! urn:ietf:params:netconf:base:1.1 //! //! //! ]]>]]>"; //! //! const PAYLOAD: &str = " //! //! EXECshow version //! "; //! //! fn read(channel: &mut Channel) -> Result> { //! let mut result = String::new(); //! loop { //! // If you plan to use this, be aware that reading 1 byte at a time is terribly //! // inefficient and should be optimized for your usecase. This is just an example. //! let mut buffer = [1u8; 1]; //! let bytes_read = channel.read(&mut buffer[..])?; //! let s = String::from_utf8_lossy(&buffer[..bytes_read]); //! result.push_str(&s); //! if result.ends_with("]]>]]>") { //! println!("Found netconf 1.0 terminator, breaking read loop"); //! break; //! } //! if result.ends_with("##") { //! println!("Found netconf 1.1 terminator, breaking read loop"); //! break; //! } //! if bytes_read == 0 || channel.eof() { //! println!("Buffer is empty, SSH channel read terminated"); //! break; //! } //! } //! Ok(result) //! } //! //! fn main() -> Result<(), Box> { //! let tcp = TcpStream::connect("127.0.0.1:830")?; //! let mut sess = Session::new()?; //! sess.set_tcp_stream(tcp); //! sess.handshake().unwrap(); //! sess.userauth_password("user", "pass")?; //! //! let mut channel = sess.channel_session()?; //! channel.subsystem("netconf")?; //! let result = read(&mut channel)?; //! println!("Result from connection:\n{}", result); //! //! let payload = format!("{}\n#{}\n{}\n##\n", HELLO, PAYLOAD.len(), PAYLOAD); //! let a = channel.write(payload.as_bytes())?; //! println!("Written {} bytes payload", a); //! let result = read(&mut channel)?; //! println!("Result from payload execution:\n{}", result); //! //! channel.send_eof()?; //! channel.wait_eof()?; //! channel.close()?; //! channel.wait_close()?; //! Ok(()) //! } //! ``` #![doc(html_root_url = "https://docs.rs/ssh2")] #![allow(trivial_numeric_casts)] #![deny(missing_docs, unused_results)] #![cfg_attr(test, deny(warnings))] extern crate libc; extern crate libssh2_sys as raw; #[macro_use] extern crate bitflags; extern crate parking_lot; use std::ffi::CStr; pub use agent::{Agent, PublicKey}; pub use channel::{Channel, ExitSignal, ReadWindow, Stream, WriteWindow}; pub use error::{Error, ErrorCode}; pub use knownhosts::{Host, KnownHosts}; pub use listener::Listener; use session::SessionInner; pub use session::{BlockDirections, KeyboardInteractivePrompt, Prompt, ScpFileStat, Session, TraceFlags}; pub use sftp::{File, FileStat, FileType, OpenType}; pub use sftp::{OpenFlags, RenameFlags, Sftp}; pub use DisconnectCode::{AuthCancelledByUser, TooManyConnections}; pub use DisconnectCode::{ByApplication, ConnectionLost, HostKeyNotVerifiable}; pub use DisconnectCode::{CompressionError, KeyExchangeFailed, MacError, Reserved}; pub use DisconnectCode::{HostNotAllowedToConnect, ProtocolError}; pub use DisconnectCode::{IllegalUserName, NoMoreAuthMethodsAvailable}; pub use DisconnectCode::{ProtocolVersionNotSupported, ServiceNotAvailable}; mod agent; mod channel; mod error; mod knownhosts; mod listener; mod session; mod sftp; mod util; /// Initialize the libssh2 library. /// /// This is optional, it is lazily invoked. pub fn init() { raw::init(); } unsafe fn opt_bytes<'a, T>(_: &'a T, c: *const libc::c_char) -> Option<&'a [u8]> { if c.is_null() { None } else { Some(CStr::from_ptr(c).to_bytes()) } } #[allow(missing_docs)] #[derive(Copy, Clone)] pub enum DisconnectCode { HostNotAllowedToConnect = raw::SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT as isize, ProtocolError = raw::SSH_DISCONNECT_PROTOCOL_ERROR as isize, KeyExchangeFailed = raw::SSH_DISCONNECT_KEY_EXCHANGE_FAILED as isize, Reserved = raw::SSH_DISCONNECT_RESERVED as isize, MacError = raw::SSH_DISCONNECT_MAC_ERROR as isize, CompressionError = raw::SSH_DISCONNECT_COMPRESSION_ERROR as isize, ServiceNotAvailable = raw::SSH_DISCONNECT_SERVICE_NOT_AVAILABLE as isize, ProtocolVersionNotSupported = raw::SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED as isize, HostKeyNotVerifiable = raw::SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE as isize, ConnectionLost = raw::SSH_DISCONNECT_CONNECTION_LOST as isize, ByApplication = raw::SSH_DISCONNECT_BY_APPLICATION as isize, TooManyConnections = raw::SSH_DISCONNECT_TOO_MANY_CONNECTIONS as isize, AuthCancelledByUser = raw::SSH_DISCONNECT_AUTH_CANCELLED_BY_USER as isize, NoMoreAuthMethodsAvailable = raw::SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE as isize, IllegalUserName = raw::SSH_DISCONNECT_ILLEGAL_USER_NAME as isize, } #[allow(missing_docs)] #[derive(Copy, Clone, Debug)] pub enum HostKeyType { Unknown = raw::LIBSSH2_HOSTKEY_TYPE_UNKNOWN as isize, Rsa = raw::LIBSSH2_HOSTKEY_TYPE_RSA as isize, Dss = raw::LIBSSH2_HOSTKEY_TYPE_DSS as isize, Ecdsa256 = raw::LIBSSH2_HOSTKEY_TYPE_ECDSA_256 as isize, Ecdsa384 = raw::LIBSSH2_HOSTKEY_TYPE_ECDSA_384 as isize, Ecdsa521 = raw::LIBSSH2_HOSTKEY_TYPE_ECDSA_521 as isize, Ed255219 = raw::LIBSSH2_HOSTKEY_TYPE_ED25519 as isize, } #[allow(missing_docs)] #[derive(Copy, Clone)] pub enum MethodType { Kex = raw::LIBSSH2_METHOD_KEX as isize, HostKey = raw::LIBSSH2_METHOD_HOSTKEY as isize, CryptCs = raw::LIBSSH2_METHOD_CRYPT_CS as isize, CryptSc = raw::LIBSSH2_METHOD_CRYPT_SC as isize, MacCs = raw::LIBSSH2_METHOD_MAC_CS as isize, MacSc = raw::LIBSSH2_METHOD_MAC_SC as isize, CompCs = raw::LIBSSH2_METHOD_COMP_CS as isize, CompSc = raw::LIBSSH2_METHOD_COMP_SC as isize, LangCs = raw::LIBSSH2_METHOD_LANG_CS as isize, LangSc = raw::LIBSSH2_METHOD_LANG_SC as isize, SignAlgo = raw::LIBSSH2_METHOD_SIGN_ALGO as isize, } /// When passed to `Channel::flush_stream`, flushes all extended data /// substreams. pub static FLUSH_EXTENDED_DATA: i32 = -1; /// When passed to `Channel::flush_stream`, flushes all substream. pub static FLUSH_ALL: i32 = -2; /// Stream ID of the stderr channel for stream-related methods on `Channel` pub static EXTENDED_DATA_STDERR: i32 = 1; #[allow(missing_docs)] #[derive(Copy, Clone, Debug)] pub enum HashType { Md5 = raw::LIBSSH2_HOSTKEY_HASH_MD5 as isize, Sha1 = raw::LIBSSH2_HOSTKEY_HASH_SHA1 as isize, Sha256 = raw::LIBSSH2_HOSTKEY_HASH_SHA256 as isize, } #[allow(missing_docs)] #[derive(Copy, Clone, Debug)] pub enum KnownHostFileKind { OpenSSH = raw::LIBSSH2_KNOWNHOST_FILE_OPENSSH as isize, } /// Possible results of a call to `KnownHosts::check` #[derive(Copy, Clone, Debug)] pub enum CheckResult { /// Hosts and keys match Match = raw::LIBSSH2_KNOWNHOST_CHECK_MATCH as isize, /// Host was found, but the keys didn't match! Mismatch = raw::LIBSSH2_KNOWNHOST_CHECK_MISMATCH as isize, /// No host match was found NotFound = raw::LIBSSH2_KNOWNHOST_CHECK_NOTFOUND as isize, /// Something prevented the check to be made Failure = raw::LIBSSH2_KNOWNHOST_CHECK_FAILURE as isize, } #[allow(missing_docs)] #[derive(Copy, Clone, Debug)] pub enum KnownHostKeyFormat { Unknown = raw::LIBSSH2_KNOWNHOST_KEY_UNKNOWN as isize, Rsa1 = raw::LIBSSH2_KNOWNHOST_KEY_RSA1 as isize, SshRsa = raw::LIBSSH2_KNOWNHOST_KEY_SSHRSA as isize, SshDss = raw::LIBSSH2_KNOWNHOST_KEY_SSHDSS as isize, Ecdsa256 = raw::LIBSSH2_KNOWNHOST_KEY_ECDSA_256 as isize, Ecdsa384 = raw::LIBSSH2_KNOWNHOST_KEY_ECDSA_384 as isize, Ecdsa521 = raw::LIBSSH2_KNOWNHOST_KEY_ECDSA_521 as isize, Ed255219 = raw::LIBSSH2_KNOWNHOST_KEY_ED25519 as isize, } impl From for KnownHostKeyFormat { fn from(host_type: HostKeyType) -> KnownHostKeyFormat { match host_type { HostKeyType::Unknown => KnownHostKeyFormat::Unknown, HostKeyType::Rsa => KnownHostKeyFormat::SshRsa, HostKeyType::Dss => KnownHostKeyFormat::SshDss, HostKeyType::Ecdsa256 => KnownHostKeyFormat::Ecdsa256, HostKeyType::Ecdsa384 => KnownHostKeyFormat::Ecdsa384, HostKeyType::Ecdsa521 => KnownHostKeyFormat::Ecdsa521, HostKeyType::Ed255219 => KnownHostKeyFormat::Ed255219, } } } /// How to handle extended data streams, such as stderr #[derive(Copy, Clone, Debug)] pub enum ExtendedData { /// Queue extended data for eventual reading Normal = raw::LIBSSH2_CHANNEL_EXTENDED_DATA_NORMAL as isize, /// Treat extended data and ordinary data the same. Merge all substreams such that calls to /// read will pull from all substreams on a first-in/first-out basis. Merge = raw::LIBSSH2_CHANNEL_EXTENDED_DATA_MERGE as isize, /// Discard all extended data as it arrives. Ignore = raw::LIBSSH2_CHANNEL_EXTENDED_DATA_IGNORE as isize, } /// The modes described in #[allow(non_camel_case_types)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum PtyModeOpcode { /// Indicates end of options. TTY_OP_END = 0, /// Interrupt character; 255 if none. Similarly for the other characters. Not all of these characters are supported on all systems. VINTR = 1, /// The quit character (sends SIGQUIT signal on POSIX systems). VQUIT = 2, /// Erase the character to left of the cursor. VERASE = 3, /// Kill the current input line. VKILL = 4, /// End-of-file character (sends EOF from the terminal). VEOF = 5, /// End-of-line character in addition to carriage return and/or linefeed. VEOL = 6, /// Additional end-of-line character. VEOL2 = 7, /// Continues paused output (normally control-Q). VSTART = 8, /// Pauses output (normally control-S). VSTOP = 9, /// Suspends the current program. VSUSP = 10, /// Another suspend character. VDSUSP = 11, /// Reprints the current input line. VREPRINT = 12, /// Erases a word left of cursor. VWERASE = 13, /// Enter the next character typed literally, even if it is a special character VLNEXT = 14, /// Character to flush output. VFLUSH = 15, /// Switch to a different shell layer. VSWTCH = 16, /// Prints system status line (load, command, pid, etc). VSTATUS = 17, /// Toggles the flushing of terminal output. VDISCARD = 18, /// The ignore parity flag. The parameter SHOULD be 0 if this flag is FALSE, and 1 if it is TRUE. IGNPAR = 30, /// Mark parity and framing errors. PARMRK = 31, /// Enable checking of parity errors. INPCK = 32, /// Strip 8th bit off characters. ISTRIP = 33, /// Map NL into CR on input. INLCR = 34, /// Ignore CR on input. IGNCR = 35, /// Map CR to NL on input. ICRNL = 36, /// Translate uppercase characters to lowercase. IUCLC = 37, /// Enable output flow control. IXON = 38, /// Any char will restart after stop. IXANY = 39, /// Enable input flow control. IXOFF = 49, /// Ring bell on input queue full. IMAXBEL = 41, /// Enable signals INTR, QUIT, [D]SUSP. ISIG = 50, /// Canonicalize input lines. ICANON = 51, /// Enable input and output of uppercase characters by preceding their lowercase equivalents with "\". XCASE = 52, /// Enable echoing. ECHO = 53, /// Visually erase chars. ECHOE = 54, /// Kill character discards current line. ECHOK = 55, /// Echo NL even if ECHO is off. ECHONL = 56, /// Don't flush after interrupt. NOFLSH = 57, /// Stop background jobs from output. TOSTOP = 58, /// Enable extensions. IEXTEN = 59, /// Echo control characters as ^(Char). ECHOCTL = 60, /// Visual erase for line kill. ECHOKE = 61, /// Retype pending input. PENDIN = 62, /// Enable output processing. OPOST = 70, /// Convert lowercase to uppercase. OLCUC = 71, /// Map NL to CR-NL. ONLCR = 72, /// Translate carriage return to newline (output). OCRNL = 73, /// Translate newline to carriage return-newline (output). ONOCR = 74, /// Newline performs a carriage return (output). ONLRET = 75, /// 7 bit mode. CS7 = 90, /// 8 bit mode. CS8 = 91, /// Parity enable. PARENB = 92, /// Odd parity, else even. PARODD = 93, /// Specifies the input baud rate in bits per second. TTY_OP_ISPEED = 128, /// Specifies the output baud rate in bits per second. TTY_OP_OSPEED = 129, } /// An opcode for setting a Pty terminal mode #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ExtensiblePtyModeOpcode { /// Use one of the modes specified by RFC 4250 Mode(PtyModeOpcode), /// Use a mode not reflected by RFC 4250 Extended(u8), } impl From for ExtensiblePtyModeOpcode { fn from(op: PtyModeOpcode) -> ExtensiblePtyModeOpcode { ExtensiblePtyModeOpcode::Mode(op) } } impl From for ExtensiblePtyModeOpcode { fn from(op: u8) -> ExtensiblePtyModeOpcode { ExtensiblePtyModeOpcode::Extended(op) } } impl ExtensiblePtyModeOpcode { fn as_opcode(&self) -> u8 { match self { ExtensiblePtyModeOpcode::Mode(m) => *m as u8, ExtensiblePtyModeOpcode::Extended(op) => *op, } } } /// Encodes modes for Pty allocation requests. /// The modes documented in /// are supported. #[derive(Debug, Clone)] pub struct PtyModes { data: Vec, } impl PtyModes { /// Construct a PtyModes instance so that you can specify values for /// various modes pub fn new() -> Self { Self { data: vec![] } } /// Set a mode to an arbitrary u32 value pub fn set_u32>(&mut self, option: O, value: u32) { let data = [ option.into().as_opcode(), ((value >> 24) & 0xff) as u8, ((value >> 16) & 0xff) as u8, ((value >> 8) & 0xff) as u8, (value & 0xff) as u8, ]; self.data.extend_from_slice(&data); } /// Set a mode to a boolean value pub fn set_boolean>(&mut self, option: O, value: bool) { self.set_u32(option, if value { 1 } else { 0 }) } /// Set a mode to a character value. /// If the character is None it is set to 255 to indicate that it /// is disabled. /// While this interface and the protocol accept unicode characters /// of up to 32 bits in width, these options likely only work for /// characters in the 7-bit ascii range. pub fn set_character>(&mut self, option: O, c: Option) { self.set_u32(option, c.map(|c| c as u32).unwrap_or(255)) } /// Finish accumulating modes and return the encoded /// byte stream suitable for use in the ssh2 protocol pub fn finish(mut self) -> Vec { self.data.push(PtyModeOpcode::TTY_OP_END as u8); self.data } } ssh2-0.9.4/src/listener.rs000064400000000000000000000030420072674642500135260ustar 00000000000000use parking_lot::Mutex; use std::sync::Arc; use {raw, Channel, Error, SessionInner}; /// A listener represents a forwarding port from the remote server. /// /// New channels can be accepted from a listener which represent connections on /// the remote server's port. pub struct Listener { raw: *mut raw::LIBSSH2_LISTENER, sess: Arc>, } // Listener is both Send and Sync; the compiler can't see it because it // is pessimistic about the raw pointer. We use Arc/Mutex to guard accessing // the raw pointer so we are safe for both. unsafe impl Send for Listener {} unsafe impl Sync for Listener {} impl Listener { /// Accept a queued connection from this listener. pub fn accept(&mut self) -> Result { let sess = self.sess.lock(); unsafe { let chan = raw::libssh2_channel_forward_accept(self.raw); let err = sess.last_error(); Channel::from_raw_opt(chan, err, &self.sess) } } pub(crate) fn from_raw_opt( raw: *mut raw::LIBSSH2_LISTENER, err: Option, sess: &Arc>, ) -> Result { if raw.is_null() { Err(err.unwrap_or_else(Error::unknown)) } else { Ok(Self { raw, sess: Arc::clone(sess), }) } } } impl Drop for Listener { fn drop(&mut self) { let _sess = self.sess.lock(); unsafe { let _ = raw::libssh2_channel_forward_cancel(self.raw); } } } ssh2-0.9.4/src/session.rs000064400000000000000000001255270072674642500134010ustar 00000000000000// Usings for openssl function userauth_pubkey_memory() #[cfg(any(unix, feature = "vendored-openssl", feature = "openssl-on-win32"))] use libc::size_t; use libc::{self, c_char, c_int, c_long, c_uint, c_void}; use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; use std::borrow::Cow; use std::ffi::CString; use std::ptr::{null, null_mut}; use std::mem; #[cfg(unix)] use std::os::unix::io::{AsRawFd, RawFd}; #[cfg(windows)] use std::os::windows::io::{AsRawSocket, RawSocket}; use std::path::Path; use std::slice; use std::str; use std::sync::Arc; use util; use {raw, ByApplication, DisconnectCode, Error, ErrorCode, HostKeyType}; use {Agent, Channel, HashType, KnownHosts, Listener, MethodType, Sftp}; bitflags! { /// Flags which can be used with the session trace method to set /// the trace level. pub struct TraceFlags: c_int { /// Authentication debugging const AUTH = raw::LIBSSH2_TRACE_AUTH; /// Connection layer debugging const CONN = raw::LIBSSH2_TRACE_CONN; /// Error debugging const ERROR = raw::LIBSSH2_TRACE_ERROR; /// Key exchange debugging const KEX = raw::LIBSSH2_TRACE_KEX; /// Public Key Debugging const PUBLICKEY = raw::LIBSSH2_TRACE_PUBLICKEY; /// SCP debugging const SCP = raw::LIBSSH2_TRACE_SCP; /// SFTP debugging const SFTP = raw::LIBSSH2_TRACE_SFTP; /// Socket low-level debugging const SOCKET = raw::LIBSSH2_TRACE_SOCKET; /// Transport layer debugging const TRANS = raw::LIBSSH2_TRACE_TRANS; } } /// Called by libssh2 to respond to some number of challenges as part of /// keyboard interactive authentication. pub trait KeyboardInteractivePrompt { /// `username` is the user name to be authenticated. It may not be the /// same as the username passed to `Session::userauth_keyboard_interactive`, /// and may be empty. /// `instructions` is some informational text to be displayed to the user. /// `prompts` is a series of prompts (or challenges) that must be responded /// to. /// The return value should be a Vec that holds one response for each prompt. fn prompt<'a>( &mut self, username: &str, instructions: &str, prompts: &[Prompt<'a>], ) -> Vec; } /// A prompt/challenge returned as part of keyboard-interactive authentication #[derive(Debug)] pub struct Prompt<'a> { /// The label to show when prompting the user pub text: Cow<'a, str>, /// If true, the response that the user inputs should be displayed /// as they type. If false then treat it as a password entry and /// do not display what is typed in response to this prompt. pub echo: bool, } /// This is a little helper function that is perhaps slightly overkill for the /// current needs. /// It saves the current sess->abstract pointer and replaces it with a /// different values for the duration of the call to the supplied lambda. /// When the lambda returns, the original abstract value is restored /// and the result of the lambda is returned. unsafe fn with_abstract R>( sess: *mut raw::LIBSSH2_SESSION, new_value: *mut c_void, f: F, ) -> R { let abstrakt = raw::libssh2_session_abstract(sess); let old_value = *abstrakt; *abstrakt = new_value; let res = f(); *abstrakt = old_value; res } pub(crate) struct SessionInner { pub(crate) raw: *mut raw::LIBSSH2_SESSION, #[cfg(unix)] tcp: Option>, #[cfg(windows)] tcp: Option>, } // The compiler doesn't know that it is Send safe because of the raw // pointer inside. We know that the way that it is used by libssh2 // and this crate is Send safe. unsafe impl Send for SessionInner {} /// An SSH session, typically representing one TCP connection. /// /// All other structures are based on an SSH session and cannot outlive a /// session. Sessions are created and then have the TCP socket handed to them /// (via the `set_tcp_stream` method). /// /// `Session`, and any objects its methods return, hold a reference to the underlying /// SSH session. You may clone `Session` to obtain another handle referencing /// the same session, and create multiple `Channel` and `Stream` objects /// from that same underlying session, which can all be passed across thread /// boundaries (they are `Send` and `Sync`). These are all related objects and /// are internally synchronized via a `Mutex` to make it safe to pass them /// around in this way. /// /// This means that a blocking read from a `Channel` or `Stream` will block /// all other calls on objects created from the same underlying `Session`. /// If you need the ability to perform concurrent operations then you will /// need to create separate `Session` instances, or employ non-blocking mode. #[derive(Clone)] pub struct Session { inner: Arc>, } /// Metadata returned about a remote file when received via `scp`. pub struct ScpFileStat { stat: libc::stat, } /// The io direction an application has to wait for in order not to block. #[derive(Debug, PartialEq)] pub enum BlockDirections { /// No direction blocked. None, /// Inbound direction blocked. Inbound, /// Outbound direction blockd. Outbound, /// Inbound and Outbound direction blocked. Both, } impl Session { /// Initializes an SSH session object. /// /// This function does not associate the session with a remote connection /// just yet. Various configuration options can be set such as the blocking /// mode, compression, sigpipe, the banner, etc. To associate this session /// with a TCP connection, use the `set_tcp_stream` method pass in an /// already-established TCP socket, and then follow up with a call to /// `handshake` to perform the ssh protocol handshake. pub fn new() -> Result { ::init(); unsafe { let ret = raw::libssh2_session_init_ex(None, None, None, null_mut()); if ret.is_null() { Err(Error::unknown()) } else { Ok(Session { inner: Arc::new(Mutex::new(SessionInner { raw: ret, tcp: None, })), }) } } } #[doc(hidden)] pub fn raw(&self) -> MappedMutexGuard { let inner = self.inner(); MutexGuard::map(inner, |inner| unsafe { &mut *inner.raw }) } /// Set the SSH protocol banner for the local client /// /// Set the banner that will be sent to the remote host when the SSH session /// is started with handshake(). This is optional; a banner /// corresponding to the protocol and libssh2 version will be sent by /// default. pub fn set_banner(&self, banner: &str) -> Result<(), Error> { let banner = CString::new(banner)?; let inner = self.inner(); unsafe { inner.rc(raw::libssh2_session_banner_set(inner.raw, banner.as_ptr())) } } /// Flag indicating whether SIGPIPE signals will be allowed or blocked. /// /// By default (on relevant platforms) this library will attempt to block /// and catch SIGPIPE signals. Setting this flag to `true` will cause /// the library to not attempt to block SIGPIPE from the underlying socket /// layer. pub fn set_allow_sigpipe(&self, block: bool) { let inner = self.inner(); let res = unsafe { inner.rc(raw::libssh2_session_flag( inner.raw, raw::LIBSSH2_FLAG_SIGPIPE as c_int, block as c_int, )) }; res.unwrap(); } /// Flag indicating whether this library will attempt to negotiate /// compression. /// /// If set - before the connection negotiation is performed - libssh2 will /// try to negotiate compression enabling for this connection. By default /// libssh2 will not attempt to use compression. pub fn set_compress(&self, compress: bool) { let inner = self.inner(); let res = unsafe { inner.rc(raw::libssh2_session_flag( inner.raw, raw::LIBSSH2_FLAG_COMPRESS as c_int, compress as c_int, )) }; res.unwrap(); } /// Set or clear blocking mode on session /// /// This will instantly affect any channels associated with this session. If /// a read is performed on a session with no data currently available, a /// blocking session will wait for data to arrive and return what it /// receives. A non-blocking session will return immediately with an empty /// buffer. If a write is performed on a session with no room for more data, /// a blocking session will wait for room. A non-blocking session will /// return immediately without writing anything. pub fn set_blocking(&self, blocking: bool) { self.inner().set_blocking(blocking); } /// Returns whether the session was previously set to nonblocking. pub fn is_blocking(&self) -> bool { self.inner().is_blocking() } /// Set timeout for blocking functions. /// /// Set the timeout in milliseconds for how long a blocking the libssh2 /// function calls may wait until they consider the situation an error and /// return an error. /// /// By default or if you set the timeout to zero, libssh2 has no timeout /// for blocking functions. pub fn set_timeout(&self, timeout_ms: u32) { let timeout_ms = timeout_ms as c_long; let inner = self.inner(); unsafe { raw::libssh2_session_set_timeout(inner.raw, timeout_ms) } } /// Returns the timeout, in milliseconds, for how long blocking calls may /// wait until they time out. /// /// A timeout of 0 signifies no timeout. pub fn timeout(&self) -> u32 { let inner = self.inner(); unsafe { raw::libssh2_session_get_timeout(inner.raw) as u32 } } /// Begin transport layer protocol negotiation with the connected host. /// /// You must call this after associating the session with a tcp stream /// via the `set_tcp_stream` function. pub fn handshake(&mut self) -> Result<(), Error> { #[cfg(windows)] unsafe fn handshake( raw: *mut raw::LIBSSH2_SESSION, stream: &dyn AsRawSocket, ) -> libc::c_int { raw::libssh2_session_handshake(raw, stream.as_raw_socket()) } #[cfg(unix)] unsafe fn handshake(raw: *mut raw::LIBSSH2_SESSION, stream: &dyn AsRawFd) -> libc::c_int { raw::libssh2_session_handshake(raw, stream.as_raw_fd()) } let inner = self.inner(); unsafe { let stream = inner.tcp.as_ref().ok_or_else(|| { Error::new( ErrorCode::Session(raw::LIBSSH2_ERROR_BAD_SOCKET), "use set_tcp_stream() to associate with a TcpStream", ) })?; inner.rc(handshake(inner.raw, stream.as_ref())) } } /// The session takes ownership of the stream provided. /// You may use the `AsRawFd` (unix) or `AsRawSocket` (windows) traits /// to obtain the raw fd later if required. /// /// It is also highly recommended that the stream provided is not used /// concurrently elsewhere for the duration of this session as it may /// interfere with the protocol. #[cfg(unix)] pub fn set_tcp_stream(&mut self, stream: S) { let mut inner = self.inner(); let _ = inner.tcp.replace(Box::new(stream)); } /// The session takes ownership of the stream provided. /// You may use the tcp_stream() method to obtain the raw socket later. /// /// It is also highly recommended that the stream provided is not used /// concurrently elsewhere for the duration of this session as it may /// interfere with the protocol. #[cfg(windows)] pub fn set_tcp_stream(&mut self, stream: S) { let mut inner = self.inner(); let _ = inner.tcp.replace(Box::new(stream)); } /// Attempt basic password authentication. /// /// Note that many SSH servers which appear to support ordinary password /// authentication actually have it disabled and use Keyboard Interactive /// authentication (routed via PAM or another authentication backed) /// instead. pub fn userauth_password(&self, username: &str, password: &str) -> Result<(), Error> { let username = CString::new(username)?; let username = username.as_bytes(); let password = CString::new(password)?; let password = password.as_bytes(); let inner = self.inner(); inner.rc(unsafe { raw::libssh2_userauth_password_ex( inner.raw, username.as_ptr() as *const _, username.len() as c_uint, password.as_ptr() as *const _, password.len() as c_uint, None, ) }) } /// Attempt keyboard interactive authentication. /// /// You must supply a callback function to pub fn userauth_keyboard_interactive( &self, username: &str, prompter: &mut P, ) -> Result<(), Error> { // hold on to your hats, this is a bit involved. // The keyboard interactive callback is a bit tricksy, and we want to wrap the // raw C types with something a bit safer and more ergonomic. // Since the interface is defined in terms of a simple function pointer, wrapping // is a bit awkward. // // The session struct has an abstrakt pointer reserved for // the user of the embedding application, and that pointer is passed to the // prompt callback. We can use this to store a pointer to some state so that // we can manage the conversion. // // The prompts and responses are defined to be UTF-8, but we use from_utf8_lossy // to avoid panics in case the server isn't conformant for whatever reason. extern "C" fn prompt( username: *const c_char, username_len: c_int, instruction: *const c_char, instruction_len: c_int, num_prompts: c_int, prompts: *const raw::LIBSSH2_USERAUTH_KBDINT_PROMPT, responses: *mut raw::LIBSSH2_USERAUTH_KBDINT_RESPONSE, abstrakt: *mut *mut c_void, ) { use std::panic::{catch_unwind, AssertUnwindSafe}; // Catch panics; we can't let them unwind to C code. // There's not much to be done with them though because the // signature of the callback doesn't allow reporting an error. let _ = catch_unwind(AssertUnwindSafe(|| { let prompter = unsafe { &mut **(abstrakt as *mut *mut P) }; let username = unsafe { slice::from_raw_parts(username as *const u8, username_len as usize) }; let username = String::from_utf8_lossy(username); let instruction = unsafe { slice::from_raw_parts(instruction as *const u8, instruction_len as usize) }; let instruction = String::from_utf8_lossy(instruction); let prompts = unsafe { slice::from_raw_parts(prompts, num_prompts as usize) }; let responses = unsafe { slice::from_raw_parts_mut(responses, num_prompts as usize) }; let prompts: Vec = prompts .iter() .map(|item| { let data = unsafe { slice::from_raw_parts(item.text as *const u8, item.length as usize) }; Prompt { text: String::from_utf8_lossy(data), echo: item.echo != 0, } }) .collect(); // libssh2 wants to be able to free(3) the response strings, so allocate // storage and copy the responses into appropriately owned memory. // We can't simply call strdup(3) here because the rust string types // are not NUL terminated. fn strdup_string(s: &str) -> *mut c_char { let len = s.len(); let ptr = unsafe { libc::malloc(len + 1) as *mut c_char }; if !ptr.is_null() { unsafe { ::std::ptr::copy_nonoverlapping( s.as_bytes().as_ptr() as *const c_char, ptr, len, ); *ptr.offset(len as isize) = 0; } } ptr } for (i, response) in (*prompter) .prompt(&username, &instruction, &prompts) .into_iter() .take(prompts.len()) .enumerate() { let ptr = strdup_string(&response); if !ptr.is_null() { responses[i].length = response.len() as c_uint; } else { responses[i].length = 0; } responses[i].text = ptr; } })); } let username = CString::new(username)?; let username = username.as_bytes(); let inner = self.inner(); unsafe { with_abstract(inner.raw, prompter as *mut P as *mut c_void, || { inner.rc(raw::libssh2_userauth_keyboard_interactive_ex( inner.raw, username.as_ptr() as *const _, username.len() as c_uint, Some(prompt::

), )) }) } } /// Attempt to perform SSH agent authentication. /// /// This is a helper method for attempting to authenticate the current /// connection with the first public key found in an SSH agent. If more /// control is needed than this method offers, it is recommended to use /// `agent` directly to control how the identity is found. pub fn userauth_agent(&self, username: &str) -> Result<(), Error> { let mut agent = self.agent()?; agent.connect()?; agent.list_identities()?; let identities = agent.identities()?; let identity = match identities.get(0) { Some(identity) => identity, None => { return Err(Error::new( ErrorCode::Session(raw::LIBSSH2_ERROR_INVAL), "no identities found in the ssh agent", )) } }; agent.userauth(username, &identity) } /// Attempt public key authentication using a PEM encoded private key file /// stored on disk. pub fn userauth_pubkey_file( &self, username: &str, pubkey: Option<&Path>, privatekey: &Path, passphrase: Option<&str>, ) -> Result<(), Error> { let username = CString::new(username)?; let username = username.as_bytes(); let pubkey = match pubkey { Some(s) => Some(CString::new(util::path2bytes(s)?)?), None => None, }; let privatekey = CString::new(util::path2bytes(privatekey)?)?; let passphrase = match passphrase { Some(s) => Some(CString::new(s)?), None => None, }; let inner = self.inner(); inner.rc(unsafe { raw::libssh2_userauth_publickey_fromfile_ex( inner.raw, username.as_ptr() as *const _, username.len() as c_uint, pubkey.as_ref().map(|s| s.as_ptr()).unwrap_or(null()), privatekey.as_ptr(), passphrase .as_ref() .map(|s| s.as_ptr()) .unwrap_or(null()), ) }) } /// Attempt public key authentication using a PEM encoded private key from /// memory. Public key is computed from private key if none passed. /// This is available with openssl enabled (Required for Unix, or with vendored-openssl or openssl-on-win32 features). #[cfg(any(unix, feature = "vendored-openssl", feature = "openssl-on-win32"))] pub fn userauth_pubkey_memory( &self, username: &str, pubkeydata: Option<&str>, privatekeydata: &str, passphrase: Option<&str>, ) -> Result<(), Error> { let username = CString::new(username)?; let username = username.as_bytes(); let (pubkeydata, pubkeydata_len) = match pubkeydata { Some(s) => (Some(CString::new(s)?), s.len()), None => (None, 0), }; let privatekeydata_len = privatekeydata.len(); let privatekeydata = CString::new(privatekeydata)?; let passphrase = match passphrase { Some(s) => Some(CString::new(s)?), None => None, }; let inner = self.inner(); inner.rc(unsafe { raw::libssh2_userauth_publickey_frommemory( inner.raw, username.as_ptr() as *const _, username.len() as size_t, pubkeydata .as_ref() .map(|s| s.as_ptr()) .unwrap_or(null()), pubkeydata_len as size_t, privatekeydata.as_ptr(), privatekeydata_len as size_t, passphrase .as_ref() .map(|s| s.as_ptr()) .unwrap_or(null()), ) }) } // Umm... I wish this were documented in libssh2? #[allow(missing_docs)] pub fn userauth_hostbased_file( &self, username: &str, publickey: &Path, privatekey: &Path, passphrase: Option<&str>, hostname: &str, local_username: Option<&str>, ) -> Result<(), Error> { let publickey = CString::new(util::path2bytes(publickey)?)?; let privatekey = CString::new(util::path2bytes(privatekey)?)?; let passphrase = match passphrase { Some(s) => Some(CString::new(s)?), None => None, }; let local_username = match local_username { Some(local) => local, None => username, }; let username = CString::new(username)?; let username = username.as_bytes(); let local_username = CString::new(local_username)?; let local_username = local_username.as_bytes(); let inner = self.inner(); inner.rc(unsafe { raw::libssh2_userauth_hostbased_fromfile_ex( inner.raw, username.as_ptr() as *const _, username.len() as c_uint, publickey.as_ptr(), privatekey.as_ptr(), passphrase .as_ref() .map(|s| s.as_ptr()) .unwrap_or(null()), hostname.as_ptr() as *const _, hostname.len() as c_uint, local_username.as_ptr() as *const _, local_username.len() as c_uint, ) }) } /// Indicates whether or not the named session has been successfully /// authenticated. pub fn authenticated(&self) -> bool { let inner = self.inner(); unsafe { raw::libssh2_userauth_authenticated(inner.raw) != 0 } } /// Send a SSH_USERAUTH_NONE request to the remote host. /// /// Unless the remote host is configured to accept none as a viable /// authentication scheme (unlikely), it will return SSH_USERAUTH_FAILURE /// along with a listing of what authentication schemes it does support. In /// the unlikely event that none authentication succeeds, this method with /// return an error. This case may be distinguished from a failing case by /// examining the return value of the `authenticated` method. /// /// The return value is a comma-separated string of supported auth schemes, /// and may be an empty string. pub fn auth_methods(&self, username: &str) -> Result<&str, Error> { let len = username.len(); let username = CString::new(username)?; let inner = self.inner(); unsafe { let ret = raw::libssh2_userauth_list(inner.raw, username.as_ptr(), len as c_uint); if ret.is_null() { match inner.last_error() { Some(err) => Err(err), None => Ok(""), } } else { Ok(str::from_utf8(::opt_bytes(self, ret).unwrap()).unwrap()) } } } /// Set preferred key exchange method /// /// The preferences provided are a comma delimited list of preferred methods /// to use with the most preferred listed first and the least preferred /// listed last. If a method is listed which is not supported by libssh2 it /// will be ignored and not sent to the remote host during protocol /// negotiation. pub fn method_pref(&self, method_type: MethodType, prefs: &str) -> Result<(), Error> { let prefs = CString::new(prefs)?; let inner = self.inner(); unsafe { inner.rc(raw::libssh2_session_method_pref( inner.raw, method_type as c_int, prefs.as_ptr(), )) } } /// Return the currently active algorithms. /// /// Returns the actual method negotiated for a particular transport /// parameter. May return `None` if the session has not yet been started. pub fn methods(&self, method_type: MethodType) -> Option<&str> { let inner = self.inner(); unsafe { let ptr = raw::libssh2_session_methods(inner.raw, method_type as c_int); ::opt_bytes(self, ptr).and_then(|s| str::from_utf8(s).ok()) } } /// Get list of supported algorithms. pub fn supported_algs(&self, method_type: MethodType) -> Result, Error> { static STATIC: () = (); let method_type = method_type as c_int; let mut ret = Vec::new(); let inner = self.inner(); unsafe { let mut ptr = null_mut(); let rc = raw::libssh2_session_supported_algs(inner.raw, method_type, &mut ptr); if rc <= 0 { inner.rc(rc)?; } for i in 0..(rc as isize) { let s = ::opt_bytes(&STATIC, *ptr.offset(i)).unwrap(); let s = str::from_utf8(s).unwrap(); ret.push(s); } raw::libssh2_free(inner.raw, ptr as *mut c_void); } Ok(ret) } /// Init an ssh-agent handle. /// /// The returned agent will still need to be connected manually before use. pub fn agent(&self) -> Result { let inner = self.inner(); unsafe { let agent = raw::libssh2_agent_init(inner.raw); let err = inner.last_error(); Agent::from_raw_opt(agent, err, &self.inner) } } /// Init a collection of known hosts for this session. /// /// Returns the handle to an internal representation of a known host /// collection. pub fn known_hosts(&self) -> Result { let inner = self.inner(); unsafe { let ptr = raw::libssh2_knownhost_init(inner.raw); let err = inner.last_error(); KnownHosts::from_raw_opt(ptr, err, &self.inner) } } /// Establish a new session-based channel. /// /// This method is commonly used to create a channel to execute commands /// over or create a new login shell. pub fn channel_session(&self) -> Result { self.channel_open( "session", raw::LIBSSH2_CHANNEL_WINDOW_DEFAULT as u32, raw::LIBSSH2_CHANNEL_PACKET_DEFAULT as u32, None, ) } /// Tunnel a TCP connection through an SSH session. /// /// Tunnel a TCP/IP connection through the SSH transport via the remote host /// to a third party. Communication from the client to the SSH server /// remains encrypted, communication from the server to the 3rd party host /// travels in cleartext. /// /// The optional `src` argument is the host/port to tell the SSH server /// where the connection originated from. /// /// The `Channel` returned represents a connection between this host and the /// specified remote host. pub fn channel_direct_tcpip( &self, host: &str, port: u16, src: Option<(&str, u16)>, ) -> Result { let (shost, sport) = src.unwrap_or(("127.0.0.1", 22)); let host = CString::new(host)?; let shost = CString::new(shost)?; let inner = self.inner(); unsafe { let ret = raw::libssh2_channel_direct_tcpip_ex( inner.raw, host.as_ptr(), port as c_int, shost.as_ptr(), sport as c_int, ); let err = inner.last_error(); Channel::from_raw_opt(ret, err, &self.inner) } } /// Instruct the remote SSH server to begin listening for inbound TCP/IP /// connections. /// /// New connections will be queued by the library until accepted by the /// `accept` method on the returned `Listener`. pub fn channel_forward_listen( &self, remote_port: u16, host: Option<&str>, queue_maxsize: Option, ) -> Result<(Listener, u16), Error> { let mut bound_port = 0; let host = host.map(|s| CString::new(s)).transpose()?; let inner = self.inner(); unsafe { let ret = raw::libssh2_channel_forward_listen_ex( inner.raw, host.map(|s| s.as_ptr()).unwrap_or(null()), remote_port as c_int, &mut bound_port, queue_maxsize.unwrap_or(0) as c_int, ); let err = inner.last_error(); Listener::from_raw_opt(ret, err, &self.inner).map(|l| (l, bound_port as u16)) } } /// Request a file from the remote host via SCP. /// /// The path specified is a path on the remote host which will attempt to be /// sent over the returned channel. Some stat information is also returned /// about the remote file to prepare for receiving the file. pub fn scp_recv(&self, path: &Path) -> Result<(Channel, ScpFileStat), Error> { let path = CString::new(util::path2bytes(path)?)?; let inner = self.inner(); unsafe { let mut sb: raw::libssh2_struct_stat = mem::zeroed(); let ret = raw::libssh2_scp_recv2(inner.raw, path.as_ptr(), &mut sb); let err = inner.last_error(); let mut c = Channel::from_raw_opt(ret, err, &self.inner)?; // Hm, apparently when we scp_recv() a file the actual channel // itself does not respond well to read_to_end(), and it also sends // an extra 0 byte (or so it seems). To work around this we // artificially limit the channel to a certain amount of bytes that // can be read. c.limit_read(sb.st_size as u64); Ok((c, ScpFileStat { stat: *sb })) } } /// Send a file to the remote host via SCP. /// /// The `remote_path` provided will the remote file name. The `times` /// argument is a tuple of (mtime, atime), and will default to the remote /// host's current time if not specified. /// /// The size of the file, `size`, must be known ahead of time before /// transmission. pub fn scp_send( &self, remote_path: &Path, mode: i32, size: u64, times: Option<(u64, u64)>, ) -> Result { let path = CString::new(util::path2bytes(remote_path)?)?; let (mtime, atime) = times.unwrap_or((0, 0)); let inner = self.inner(); unsafe { let ret = raw::libssh2_scp_send64( inner.raw, path.as_ptr(), mode as c_int, size as i64, mtime as libc::time_t, atime as libc::time_t, ); let err = inner.last_error(); Channel::from_raw_opt(ret, err, &self.inner) } } /// Open a channel and initialize the SFTP subsystem. /// /// Although the SFTP subsystem operates over the same type of channel as /// those exported by the Channel API, the protocol itself implements its /// own unique binary packet protocol which must be managed with the /// methods on `Sftp`. pub fn sftp(&self) -> Result { let inner = self.inner(); unsafe { let ret = raw::libssh2_sftp_init(inner.raw); let err = inner.last_error(); Sftp::from_raw_opt(ret, err, &self.inner) } } /// Allocate a new channel for exchanging data with the server. /// /// This is typically not called directly but rather through /// `channel_session`, `channel_direct_tcpip`, or `channel_forward_listen`. pub fn channel_open( &self, channel_type: &str, window_size: u32, packet_size: u32, message: Option<&str>, ) -> Result { let channel_type = CString::new(channel_type)?; let channel_type = channel_type.as_bytes(); let message = message.map(|s| CString::new(s)).transpose()?; let (message, message_len) = message .as_ref() .map(|s| (s.as_ptr(), s.as_bytes().len())) .unwrap_or((null(), 0)); let inner = self.inner(); unsafe { let ret = raw::libssh2_channel_open_ex( inner.raw, channel_type.as_ptr() as *const _, channel_type.len() as c_uint, window_size as c_uint, packet_size as c_uint, message, message_len as c_uint, ); let err = inner.last_error(); Channel::from_raw_opt(ret, err, &self.inner) } } /// Get the remote banner /// /// Once the session has been setup and handshake() has completed /// successfully, this function can be used to get the server id from the /// banner each server presents. /// /// May return `None` on invalid utf-8 or if an error has occurred. pub fn banner(&self) -> Option<&str> { self.banner_bytes().and_then(|s| str::from_utf8(s).ok()) } /// See `banner`. /// /// Will only return `None` if an error has occurred. pub fn banner_bytes(&self) -> Option<&[u8]> { let inner = self.inner(); unsafe { ::opt_bytes(self, raw::libssh2_session_banner_get(inner.raw)) } } /// Get the remote key. /// /// Returns `None` if something went wrong. pub fn host_key(&self) -> Option<(&[u8], HostKeyType)> { let mut len = 0; let mut kind = 0; let inner = self.inner(); unsafe { let ret = raw::libssh2_session_hostkey(inner.raw, &mut len, &mut kind); if ret.is_null() { return None; } let data = slice::from_raw_parts(ret as *const u8, len as usize); let kind = match kind { raw::LIBSSH2_HOSTKEY_TYPE_RSA => HostKeyType::Rsa, raw::LIBSSH2_HOSTKEY_TYPE_DSS => HostKeyType::Dss, raw::LIBSSH2_HOSTKEY_TYPE_ECDSA_256 => HostKeyType::Ecdsa256, raw::LIBSSH2_HOSTKEY_TYPE_ECDSA_384 => HostKeyType::Ecdsa384, raw::LIBSSH2_HOSTKEY_TYPE_ECDSA_521 => HostKeyType::Ecdsa521, raw::LIBSSH2_HOSTKEY_TYPE_ED25519 => HostKeyType::Ed255219, raw::LIBSSH2_HOSTKEY_TYPE_UNKNOWN => HostKeyType::Unknown, _ => HostKeyType::Unknown, }; Some((data, kind)) } } /// Returns the computed digest of the remote system's hostkey. /// /// The bytes returned are the raw hash, and are not printable. If the hash /// is not yet available `None` is returned. pub fn host_key_hash(&self, hash: HashType) -> Option<&[u8]> { let len = match hash { HashType::Md5 => 16, HashType::Sha1 => 20, HashType::Sha256 => 32, }; let inner = self.inner(); unsafe { let ret = raw::libssh2_hostkey_hash(inner.raw, hash as c_int); if ret.is_null() { None } else { let ret = ret as *const u8; Some(slice::from_raw_parts(ret, len)) } } } /// Set how often keepalive messages should be sent. /// /// The want_reply argument indicates whether the keepalive messages should /// request a response from the server. /// /// The interval argument is number of seconds that can pass without any /// I/O, use 0 (the default) to disable keepalives. To avoid some busy-loop /// corner-cases, if you specify an interval of 1 it will be treated as 2. pub fn set_keepalive(&self, want_reply: bool, interval: u32) { let inner = self.inner(); unsafe { raw::libssh2_keepalive_config(inner.raw, want_reply as c_int, interval as c_uint) } } /// Send a keepalive message if needed. /// /// Returns how many seconds you can sleep after this call before you need /// to call it again. pub fn keepalive_send(&self) -> Result { let mut ret = 0; let inner = self.inner(); let rc = unsafe { raw::libssh2_keepalive_send(inner.raw, &mut ret) }; inner.rc(rc)?; Ok(ret as u32) } /// Terminate the transport layer. /// /// Send a disconnect message to the remote host associated with session, /// along with a reason symbol and a verbose description. /// /// Note that this does *not* close the underlying socket. pub fn disconnect( &self, reason: Option, description: &str, lang: Option<&str>, ) -> Result<(), Error> { let reason = reason.unwrap_or(ByApplication) as c_int; let description = CString::new(description)?; let lang = CString::new(lang.unwrap_or(""))?; let inner = self.inner(); unsafe { inner.rc(raw::libssh2_session_disconnect_ex( inner.raw, reason, description.as_ptr(), lang.as_ptr(), )) } } /// Returns the blocked io directions that the application needs to wait for. /// /// This function should be used after an error of type `WouldBlock` is returned to /// find out the socket events the application has to wait for. pub fn block_directions(&self) -> BlockDirections { let inner = self.inner(); let dir = unsafe { raw::libssh2_session_block_directions(inner.raw) }; match dir { raw::LIBSSH2_SESSION_BLOCK_INBOUND => BlockDirections::Inbound, raw::LIBSSH2_SESSION_BLOCK_OUTBOUND => BlockDirections::Outbound, x if x == raw::LIBSSH2_SESSION_BLOCK_INBOUND | raw::LIBSSH2_SESSION_BLOCK_OUTBOUND => { BlockDirections::Both } _ => BlockDirections::None, } } fn inner(&self) -> MutexGuard { self.inner.lock() } /// Sets the trace level for the session. /// pub fn trace(&self, bitmask: TraceFlags) { let inner = self.inner(); unsafe { let _ = raw::libssh2_trace(inner.raw, bitmask.bits() as c_int); } } } #[cfg(unix)] impl AsRawFd for Session { fn as_raw_fd(&self) -> RawFd { let inner = self.inner(); match inner.tcp.as_ref() { Some(tcp) => tcp.as_raw_fd(), None => panic!("tried to obtain raw fd without tcp stream set"), } } } #[cfg(windows)] impl AsRawSocket for Session { fn as_raw_socket(&self) -> RawSocket { let inner = self.inner(); match inner.tcp.as_ref() { Some(tcp) => tcp.as_raw_socket(), None => panic!("tried to obtain raw socket without tcp stream set"), } } } impl SessionInner { /// Translate a return code into a Rust-`Result`. pub fn rc(&self, rc: c_int) -> Result<(), Error> { if rc >= 0 { Ok(()) } else { Err(Error::from_session_error_raw(self.raw, rc)) } } pub fn last_error(&self) -> Option { Error::last_session_error_raw(self.raw) } /// Set or clear blocking mode on session pub fn set_blocking(&self, blocking: bool) { unsafe { raw::libssh2_session_set_blocking(self.raw, blocking as c_int) } } /// Returns whether the session was previously set to nonblocking. pub fn is_blocking(&self) -> bool { unsafe { raw::libssh2_session_get_blocking(self.raw) != 0 } } } impl Drop for SessionInner { fn drop(&mut self) { unsafe { let _rc = raw::libssh2_session_free(self.raw); } } } impl ScpFileStat { /// Returns the size of the remote file. pub fn size(&self) -> u64 { self.stat.st_size as u64 } /// Returns the listed mode of the remote file. pub fn mode(&self) -> i32 { self.stat.st_mode as i32 } /// Returns whether the remote file is a directory. pub fn is_dir(&self) -> bool { self.mode() & (libc::S_IFMT as i32) == (libc::S_IFDIR as i32) } /// Returns whether the remote file is a regular file. pub fn is_file(&self) -> bool { self.mode() & (libc::S_IFMT as i32) == (libc::S_IFREG as i32) } } ssh2-0.9.4/src/sftp.rs000064400000000000000000001023270072674642500126630ustar 00000000000000use libc::{c_int, c_long, c_uint, c_ulong, size_t}; use parking_lot::{Mutex, MutexGuard}; use std::convert::TryFrom; use std::ffi::CString; use std::io::prelude::*; use std::io::{self, ErrorKind, SeekFrom}; use std::mem; use std::path::{Path, PathBuf}; use std::ptr::null_mut; use std::sync::Arc; use util; use {raw, Error, ErrorCode, SessionInner}; /// A handle to a remote filesystem over SFTP. /// /// Instances are created through the `sftp` method on a `Session`. pub struct Sftp { inner: Option>, } /// This contains an Option so that we're able to disable the Drop hook when dropping manually, /// while still dropping all the fields of SftpInner (which we couldn't do with `mem::forget`) struct SftpInnerDropWrapper(Option); struct SftpInner { raw: *mut raw::LIBSSH2_SFTP, sess: Arc>, } // Sftp is both Send and Sync; the compiler can't see it because it // is pessimistic about the raw pointer. We use Arc/Mutex to guard accessing // the raw pointer so we are safe for both. unsafe impl Send for Sftp {} unsafe impl Sync for Sftp {} struct LockedSftp<'sftp> { raw: *mut raw::LIBSSH2_SFTP, sess: MutexGuard<'sftp, SessionInner>, } /// A file handle to an SFTP connection. /// /// Files behave similarly to `std::old_io::File` in that they are readable and /// writable and support operations like stat and seek. /// /// Files are created through `open`, `create`, and `open_mode` on an instance /// of `Sftp`. pub struct File { inner: Option, } struct FileInner { raw: *mut raw::LIBSSH2_SFTP_HANDLE, sftp: Arc, } // File is both Send and Sync; the compiler can't see it because it // is pessimistic about the raw pointer. We use Arc/Mutex to guard accessing // the raw pointer so we are safe for both. unsafe impl Send for File {} unsafe impl Sync for File {} struct LockedFile<'file> { raw: *mut raw::LIBSSH2_SFTP_HANDLE, sess: MutexGuard<'file, SessionInner>, } /// Metadata information about a remote file. /// /// Fields are not necessarily all provided #[derive(Debug, Clone, Eq, PartialEq)] #[allow(missing_copy_implementations)] pub struct FileStat { /// File size, in bytes of the file. pub size: Option, /// Owner ID of the file pub uid: Option, /// Owning group of the file pub gid: Option, /// Permissions (mode) of the file pub perm: Option, /// Last access time of the file pub atime: Option, /// Last modification time of the file pub mtime: Option, } /// An enum representing a type of file. #[derive(PartialEq)] pub enum FileType { /// Named pipe (S_IFIFO) NamedPipe, /// Character device (S_IFCHR) CharDevice, /// Block device (S_IFBLK) BlockDevice, /// Directory (S_IFDIR) Directory, /// Regular file (S_IFREG) RegularFile, /// Symbolic link (S_IFLNK) Symlink, /// Unix domain socket (S_IFSOCK) Socket, /// Other filetype (does not correspond to any of the other ones) Other(c_ulong), } bitflags! { /// Options that can be used to configure how a file is opened pub struct OpenFlags: c_ulong { /// Open the file for reading. const READ = raw::LIBSSH2_FXF_READ; /// Open the file for writing. If both this and `Read` are specified, /// the file is opened for both reading and writing. const WRITE = raw::LIBSSH2_FXF_WRITE; /// Force all writes to append data at the end of the file. const APPEND = raw::LIBSSH2_FXF_APPEND; /// If this flag is specified, then a new file will be created if one /// does not already exist (if `Truncate` is specified, the new file /// will be truncated to zero length if it previously exists). const CREATE = raw::LIBSSH2_FXF_CREAT; /// Forces an existing file with the same name to be truncated to zero /// length when creating a file by specifying `Create`. Using this flag /// implies the `Create` flag. const TRUNCATE = raw::LIBSSH2_FXF_TRUNC | Self::CREATE.bits; /// Causes the request to fail if the named file already exists. Using /// this flag implies the `Create` flag. const EXCLUSIVE = raw::LIBSSH2_FXF_EXCL | Self::CREATE.bits; } } bitflags! { /// Options to `Sftp::rename`. pub struct RenameFlags: c_long { /// In a rename operation, overwrite the destination if it already /// exists. If this flag is not present then it is an error if the /// destination already exists. const OVERWRITE = raw::LIBSSH2_SFTP_RENAME_OVERWRITE; /// Inform the remote that an atomic rename operation is desired if /// available. const ATOMIC = raw::LIBSSH2_SFTP_RENAME_ATOMIC; /// Inform the remote end that the native system calls for renaming /// should be used. const NATIVE = raw::LIBSSH2_SFTP_RENAME_NATIVE; } } /// How to open a file handle with libssh2. #[derive(Copy, Clone)] pub enum OpenType { /// Specify that a file shoud be opened. File = raw::LIBSSH2_SFTP_OPENFILE as isize, /// Specify that a directory should be opened. Dir = raw::LIBSSH2_SFTP_OPENDIR as isize, } impl Sftp { pub(crate) fn from_raw_opt( raw: *mut raw::LIBSSH2_SFTP, err: Option, sess: &Arc>, ) -> Result { if raw.is_null() { Err(err.unwrap_or_else(Error::unknown)) } else { Ok(Self { inner: Some(Arc::new(SftpInnerDropWrapper(Some(SftpInner { raw, sess: Arc::clone(sess), })))), }) } } /// Open a handle to a file. pub fn open_mode( &self, filename: &Path, flags: OpenFlags, mode: i32, open_type: OpenType, ) -> Result { let filename = CString::new(util::path2bytes(filename)?)?; let locked = self.lock()?; unsafe { let ret = raw::libssh2_sftp_open_ex( locked.raw, filename.as_ptr() as *const _, filename.as_bytes().len() as c_uint, flags.bits() as c_ulong, mode as c_long, open_type as c_int, ); if ret.is_null() { let rc = raw::libssh2_session_last_errno(locked.sess.raw); Err(Self::error_code_into_error(locked.sess.raw, locked.raw, rc)) } else { Ok(File::from_raw(self, ret)) } } } /// Helper to open a file in the `Read` mode. pub fn open(&self, filename: &Path) -> Result { self.open_mode(filename, OpenFlags::READ, 0o644, OpenType::File) } /// Helper to create a file in write-only mode with truncation. pub fn create(&self, filename: &Path) -> Result { self.open_mode( filename, OpenFlags::WRITE | OpenFlags::TRUNCATE, 0o644, OpenType::File, ) } /// Helper to open a directory for reading its contents. pub fn opendir(&self, dirname: &Path) -> Result { self.open_mode(dirname, OpenFlags::READ, 0, OpenType::Dir) } /// Convenience function to read the files in a directory. /// /// The returned paths are all joined with `dirname` when returned, and the /// paths `.` and `..` are filtered out of the returned list. pub fn readdir(&self, dirname: &Path) -> Result, Error> { let mut dir = self.opendir(dirname)?; let mut ret = Vec::new(); loop { match dir.readdir() { Ok((filename, stat)) => { if &*filename == Path::new(".") || &*filename == Path::new("..") { continue; } ret.push((dirname.join(&filename), stat)) } Err(ref e) if e.code() == ErrorCode::Session(raw::LIBSSH2_ERROR_FILE) => break, Err(e) => { if e.code() != ErrorCode::Session(raw::LIBSSH2_ERROR_EAGAIN) { return Err(e); } } } } Ok(ret) } /// Create a directory on the remote file system. pub fn mkdir(&self, filename: &Path, mode: i32) -> Result<(), Error> { let filename = CString::new(util::path2bytes(filename)?)?; let locked = self.lock()?; Self::rc(&locked, unsafe { raw::libssh2_sftp_mkdir_ex( locked.raw, filename.as_ptr() as *const _, filename.as_bytes().len() as c_uint, mode as c_long, ) }) } /// Remove a directory from the remote file system. pub fn rmdir(&self, filename: &Path) -> Result<(), Error> { let filename = CString::new(util::path2bytes(filename)?)?; let locked = self.lock()?; locked.sess.rc(unsafe { raw::libssh2_sftp_rmdir_ex( locked.raw, filename.as_ptr() as *const _, filename.as_bytes().len() as c_uint, ) }) } /// Get the metadata for a file, performed by stat(2) pub fn stat(&self, filename: &Path) -> Result { let filename = CString::new(util::path2bytes(filename)?)?; let locked = self.lock()?; unsafe { let mut ret = mem::zeroed(); Self::rc( &locked, raw::libssh2_sftp_stat_ex( locked.raw, filename.as_ptr() as *const _, filename.as_bytes().len() as c_uint, raw::LIBSSH2_SFTP_STAT, &mut ret, ), ) .map(|_| FileStat::from_raw(&ret)) } } /// Get the metadata for a file, performed by lstat(2) pub fn lstat(&self, filename: &Path) -> Result { let filename = CString::new(util::path2bytes(filename)?)?; let locked = self.lock()?; unsafe { let mut ret = mem::zeroed(); Self::rc( &locked, raw::libssh2_sftp_stat_ex( locked.raw, filename.as_ptr() as *const _, filename.as_bytes().len() as c_uint, raw::LIBSSH2_SFTP_LSTAT, &mut ret, ), ) .map(|_| FileStat::from_raw(&ret)) } } /// Set the metadata for a file. pub fn setstat(&self, filename: &Path, stat: FileStat) -> Result<(), Error> { let filename = CString::new(util::path2bytes(filename)?)?; let locked = self.lock()?; Self::rc(&locked, unsafe { let mut raw = stat.raw(); raw::libssh2_sftp_stat_ex( locked.raw, filename.as_ptr() as *const _, filename.as_bytes().len() as c_uint, raw::LIBSSH2_SFTP_SETSTAT, &mut raw, ) }) } /// Create a symlink at `target` pointing at `path`. pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), Error> { let path = CString::new(util::path2bytes(path)?)?; let target = CString::new(util::path2bytes(target)?)?; let locked = self.lock()?; locked.sess.rc(unsafe { raw::libssh2_sftp_symlink_ex( locked.raw, path.as_ptr() as *const _, path.as_bytes().len() as c_uint, target.as_ptr() as *mut _, target.as_bytes().len() as c_uint, raw::LIBSSH2_SFTP_SYMLINK, ) }) } /// Read a symlink at `path`. pub fn readlink(&self, path: &Path) -> Result { self.readlink_op(path, raw::LIBSSH2_SFTP_READLINK) } /// Resolve the real path for `path`. pub fn realpath(&self, path: &Path) -> Result { self.readlink_op(path, raw::LIBSSH2_SFTP_REALPATH) } fn readlink_op(&self, path: &Path, op: c_int) -> Result { let path = CString::new(util::path2bytes(path)?)?; let mut ret = Vec::::with_capacity(128); let mut rc; let locked = self.lock()?; loop { rc = unsafe { raw::libssh2_sftp_symlink_ex( locked.raw, path.as_ptr() as *const _, path.as_bytes().len() as c_uint, ret.as_ptr() as *mut _, ret.capacity() as c_uint, op, ) }; if rc == raw::LIBSSH2_ERROR_BUFFER_TOO_SMALL { let cap = ret.capacity(); ret.reserve(cap * 2); } else { break; } } Self::rc(&locked, rc).map(move |_| { unsafe { ret.set_len(rc as usize) } mkpath(ret) }) } /// Rename a filesystem object on the remote filesystem. /// /// The semantics of this command typically include the ability to move a /// filesystem object between folders and/or filesystem mounts. If the /// `Overwrite` flag is not set and the destfile entry already exists, the /// operation will fail. /// /// Use of the other flags (Native or Atomic) indicate a preference (but /// not a requirement) for the remote end to perform an atomic rename /// operation and/or using native system calls when possible. /// /// If no flags are specified then all flags are used. pub fn rename(&self, src: &Path, dst: &Path, flags: Option) -> Result<(), Error> { let flags = flags.unwrap_or(RenameFlags::ATOMIC | RenameFlags::OVERWRITE | RenameFlags::NATIVE); let src = CString::new(util::path2bytes(src)?)?; let dst = CString::new(util::path2bytes(dst)?)?; let locked = self.lock()?; Self::rc(&locked, unsafe { raw::libssh2_sftp_rename_ex( locked.raw, src.as_ptr() as *const _, src.as_bytes().len() as c_uint, dst.as_ptr() as *const _, dst.as_bytes().len() as c_uint, flags.bits(), ) }) } /// Remove a file on the remote filesystem pub fn unlink(&self, file: &Path) -> Result<(), Error> { let file = CString::new(util::path2bytes(file)?)?; let locked = self.lock()?; Self::rc(&locked, unsafe { raw::libssh2_sftp_unlink_ex( locked.raw, file.as_ptr() as *const _, file.as_bytes().len() as c_uint, ) }) } fn lock(&self) -> Result { match self.inner.as_ref() { Some(sftp_inner_drop_wrapper) => { let sftp_inner = sftp_inner_drop_wrapper .0 .as_ref() .expect("Never unset until shutdown, in which case inner is also unset"); let sess = sftp_inner.sess.lock(); Ok(LockedSftp { sess, raw: sftp_inner.raw, }) } None => Err(Error::from_errno(ErrorCode::Session( raw::LIBSSH2_ERROR_BAD_USE, ))), } } // This method is used by the async ssh crate #[doc(hidden)] pub fn shutdown(&mut self) -> Result<(), Error> { // We cannot shutdown the SFTP if files are still open, etc, as these store a ref to the sftp in libssh2. // We have to make sure we are the last reference to it. match self.inner.take() { Some(sftp_inner_arc) => { // We were not already un-initialized match Arc::try_unwrap(sftp_inner_arc) { Ok(mut sftp_inner_wrapper) => { // Early drop let sftp_inner = sftp_inner_wrapper.0.take().expect( "We were holding an Arc, \ so nobody could unset this (set on creation)", ); sftp_inner .sess .lock() .rc(unsafe { raw::libssh2_sftp_shutdown(sftp_inner.raw) })?; Ok(()) } Err(sftp_inner_arc) => { // We are failing shutdown as there are files left open, keep this object usable self.inner = Some(sftp_inner_arc); Err(Error::from_errno(ErrorCode::Session( raw::LIBSSH2_ERROR_BAD_USE, ))) } } } None => { // We have already shut this down. Shutting down twice is a mistake from the caller code Err(Error::from_errno(ErrorCode::Session( raw::LIBSSH2_ERROR_BAD_USE, ))) } } } fn error_code_into_error( session_raw: *mut raw::LIBSSH2_SESSION, sftp_raw: *mut raw::LIBSSH2_SFTP, rc: libc::c_int, ) -> Error { if rc >= 0 { Error::unknown() } else if rc == raw::LIBSSH2_ERROR_SFTP_PROTOCOL { let actual_rc = unsafe { raw::libssh2_sftp_last_error(sftp_raw) }; // TODO: This conversion from `c_ulong` to `c_int` should not be // necessary if the constants `LIBSSH2_FX_*` in the `-sys` crate // are typed as `c_ulong`, as they should be. if let Ok(actual_rc) = libc::c_int::try_from(actual_rc) { Error::from_errno(ErrorCode::SFTP(actual_rc)) } else { Error::unknown() } } else { Error::from_session_error_raw(session_raw, rc) } } fn error_code_into_result( session_raw: *mut raw::LIBSSH2_SESSION, sftp_raw: *mut raw::LIBSSH2_SFTP, rc: libc::c_int, ) -> Result<(), Error> { if rc >= 0 { Ok(()) } else { Err(Self::error_code_into_error(session_raw, sftp_raw, rc)) } } fn rc(locked: &LockedSftp, rc: libc::c_int) -> Result<(), Error> { Self::error_code_into_result(locked.sess.raw, locked.raw, rc) } } impl Drop for SftpInnerDropWrapper { fn drop(&mut self) { // Check we were not early-dropped if let Some(inner) = self.0.take() { let sess = inner.sess.lock(); // Set ssh2 to blocking during the drop let was_blocking = sess.is_blocking(); sess.set_blocking(true); // The shutdown statement can go wrong and return an error code, but we are too late // in the execution to recover it. let _shutdown_result = unsafe { raw::libssh2_sftp_shutdown(inner.raw) }; sess.set_blocking(was_blocking); } } } impl File { /// Wraps a raw pointer in a new File structure tied to the lifetime of the /// given session. /// /// This consumes ownership of `raw`. unsafe fn from_raw(sftp: &Sftp, raw: *mut raw::LIBSSH2_SFTP_HANDLE) -> File { File { inner: Some(FileInner { raw, sftp: Arc::clone( &sftp .inner .as_ref() .expect("Cannot open file after sftp shutdown"), ), }), } } /// Set the metadata for this handle. pub fn setstat(&mut self, stat: FileStat) -> Result<(), Error> { let locked = self.lock()?; self.rc(&locked, unsafe { let mut raw = stat.raw(); raw::libssh2_sftp_fstat_ex(locked.raw, &mut raw, 1) }) } /// Get the metadata for this handle. pub fn stat(&mut self) -> Result { let locked = self.lock()?; unsafe { let mut ret = mem::zeroed(); self.rc(&locked, raw::libssh2_sftp_fstat_ex(locked.raw, &mut ret, 0)) .map(|_| FileStat::from_raw(&ret)) } } #[allow(missing_docs)] // sure wish I knew what this did... pub fn statvfs(&mut self) -> Result { let locked = self.lock()?; unsafe { let mut ret = mem::zeroed(); self.rc(&locked, raw::libssh2_sftp_fstatvfs(locked.raw, &mut ret)) .map(move |_| ret) } } /// Reads a block of data from a handle and returns file entry information /// for the next entry, if any. /// /// Note that this provides raw access to the `readdir` function from /// libssh2. This will return an error when there are no more files to /// read, and files such as `.` and `..` will be included in the return /// values. /// /// Also note that the return paths will not be absolute paths, they are /// the filenames of the files in this directory. pub fn readdir(&mut self) -> Result<(PathBuf, FileStat), Error> { let locked = self.lock()?; // libssh2 through 1.10.0 skips entries if the buffer // is not large enough: it's not enough to resize and try again // on getting an error. So, we make it quite large here. // See . let mut buf = Vec::::with_capacity(4 * 1024); let mut stat = unsafe { mem::zeroed() }; let mut rc; loop { rc = unsafe { raw::libssh2_sftp_readdir_ex( locked.raw, buf.as_mut_ptr() as *mut _, buf.capacity() as size_t, null_mut(), 0, &mut stat, ) }; if rc == raw::LIBSSH2_ERROR_BUFFER_TOO_SMALL { let cap = buf.capacity(); buf.reserve(cap * 2); } else { break; } } if rc == 0 { Err(Error::new( ErrorCode::Session(raw::LIBSSH2_ERROR_FILE), "no more files", )) } else { self.rc(&locked, rc).map(move |_| { unsafe { buf.set_len(rc as usize); } (mkpath(buf), FileStat::from_raw(&stat)) }) } } /// This function causes the remote server to synchronize the file data and /// metadata to disk (like fsync(2)). /// /// For this to work requires fsync@openssh.com support on the server. pub fn fsync(&mut self) -> Result<(), Error> { let locked = self.lock()?; self.rc(&locked, unsafe { raw::libssh2_sftp_fsync(locked.raw) }) } fn lock(&self) -> Result { match self.inner.as_ref() { Some(file_inner) => { let sftp_inner = file_inner.sftp.0.as_ref().expect( "We are holding an Arc, \ so nobody could unset this (set on creation)", ); let sess = sftp_inner.sess.lock(); Ok(LockedFile { sess, raw: file_inner.raw, }) } None => Err(Error::from_errno(ErrorCode::Session( raw::LIBSSH2_ERROR_BAD_USE, ))), } } #[doc(hidden)] pub fn close(&mut self) -> Result<(), Error> { let rc = { let locked = self.lock()?; self.rc(&locked, unsafe { raw::libssh2_sftp_close_handle(locked.raw) }) }; // If EGAIN was returned, we'll need to call this again to complete the operation. // If any other error was returned, or if it completed OK, we must not use the // handle again. match rc { Err(e) if e.code() == ErrorCode::Session(raw::LIBSSH2_ERROR_EAGAIN) => { Err(e) }, rc => { self.inner = None; rc } } } fn rc(&self, locked: &LockedFile, rc: libc::c_int) -> Result<(), Error> { if let Some(file_inner) = self.inner.as_ref() { let sftp_inner = file_inner.sftp.0.as_ref().expect( "We are holding an Arc, \ so nobody could unset this (set on creation)", ); Sftp::error_code_into_result(locked.sess.raw, sftp_inner.raw, rc) } else if rc < 0 { Err(Error::from_errno(ErrorCode::Session(rc))) } else { Ok(()) } } } impl Read for File { fn read(&mut self, buf: &mut [u8]) -> io::Result { let locked = self.lock()?; let rc = unsafe { raw::libssh2_sftp_read(locked.raw, buf.as_mut_ptr() as *mut _, buf.len() as size_t) }; if rc < 0 { let rc = rc as libc::c_int; if let Some(file_inner) = self.inner.as_ref() { let sftp_inner = file_inner.sftp.0.as_ref().expect( "We are holding an Arc, \ so nobody could unset this (set on creation)", ); Err(Sftp::error_code_into_error(locked.sess.raw, sftp_inner.raw, rc).into()) } else { Err(Error::from_errno(ErrorCode::Session(rc)).into()) } } else { Ok(rc as usize) } } } impl Write for File { fn write(&mut self, buf: &[u8]) -> io::Result { let locked = self.lock()?; let rc = unsafe { raw::libssh2_sftp_write(locked.raw, buf.as_ptr() as *const _, buf.len() as size_t) }; if rc < 0 { let rc = rc as libc::c_int; if let Some(file_inner) = self.inner.as_ref() { let sftp_inner = file_inner.sftp.0.as_ref().expect( "We are holding an Arc, \ so nobody could unset this (set on creation)", ); Err(Sftp::error_code_into_error(locked.sess.raw, sftp_inner.raw, rc).into()) } else { Err(Error::from_errno(ErrorCode::Session(rc)).into()) } } else { Ok(rc as usize) } } fn flush(&mut self) -> io::Result<()> { Ok(()) } } impl Seek for File { /// Move the file handle's internal pointer to an arbitrary location. /// /// libssh2 implements file pointers as a localized concept to make file /// access appear more POSIX like. No packets are exchanged with the server /// during a seek operation. The localized file pointer is simply used as a /// convenience offset during read/write operations. /// /// You MUST NOT seek during writing or reading a file with SFTP, as the /// internals use outstanding packets and changing the "file position" /// during transit will results in badness. fn seek(&mut self, how: SeekFrom) -> io::Result { let next = match how { SeekFrom::Start(pos) => pos, SeekFrom::Current(offset) => { let locked = self.lock()?; let cur = unsafe { raw::libssh2_sftp_tell64(locked.raw) }; (cur as i64 + offset) as u64 } SeekFrom::End(offset) => match self.stat() { Ok(s) => match s.size { Some(size) => (size as i64 + offset) as u64, None => return Err(io::Error::new(ErrorKind::Other, "no file size available")), }, Err(e) => return Err(io::Error::new(ErrorKind::Other, e)), }, }; let locked = self.lock()?; unsafe { raw::libssh2_sftp_seek64(locked.raw, next) } Ok(next) } } impl Drop for File { fn drop(&mut self) { // Set ssh2 to blocking if the file was not closed yet (by .close()). if let Some(file_inner) = self.inner.take() { let sftp_inner = file_inner.sftp.0.as_ref().expect( "We are holding an Arc, \ so nobody could unset this (set on creation)", ); let sess_inner = sftp_inner.sess.lock(); let was_blocking = sess_inner.is_blocking(); sess_inner.set_blocking(true); // The close statement can go wrong and return an error code, but we are too late // in the execution to recover it. let _close_handle_result = unsafe { raw::libssh2_sftp_close_handle(file_inner.raw) }; sess_inner.set_blocking(was_blocking); } } } impl FileStat { /// Returns the file type for this filestat. pub fn file_type(&self) -> FileType { FileType::from_perm(self.perm.unwrap_or(0) as c_ulong) } /// Returns whether this metadata is for a directory. pub fn is_dir(&self) -> bool { self.file_type().is_dir() } /// Returns whether this metadata is for a regular file. pub fn is_file(&self) -> bool { self.file_type().is_file() } /// Creates a new instance of a stat from a raw instance. pub fn from_raw(raw: &raw::LIBSSH2_SFTP_ATTRIBUTES) -> FileStat { fn val(raw: &raw::LIBSSH2_SFTP_ATTRIBUTES, t: &T, flag: c_ulong) -> Option { if raw.flags & flag != 0 { Some(*t) } else { None } } FileStat { size: val(raw, &raw.filesize, raw::LIBSSH2_SFTP_ATTR_SIZE), uid: val(raw, &raw.uid, raw::LIBSSH2_SFTP_ATTR_UIDGID).map(|s| s as u32), gid: val(raw, &raw.gid, raw::LIBSSH2_SFTP_ATTR_UIDGID).map(|s| s as u32), perm: val(raw, &raw.permissions, raw::LIBSSH2_SFTP_ATTR_PERMISSIONS).map(|s| s as u32), mtime: val(raw, &raw.mtime, raw::LIBSSH2_SFTP_ATTR_ACMODTIME).map(|s| s as u64), atime: val(raw, &raw.atime, raw::LIBSSH2_SFTP_ATTR_ACMODTIME).map(|s| s as u64), } } /// Convert this stat structure to its raw representation. pub fn raw(&self) -> raw::LIBSSH2_SFTP_ATTRIBUTES { fn flag(o: &Option, flag: c_ulong) -> c_ulong { if o.is_some() { flag } else { 0 } } raw::LIBSSH2_SFTP_ATTRIBUTES { flags: flag(&self.size, raw::LIBSSH2_SFTP_ATTR_SIZE) | flag(&self.uid, raw::LIBSSH2_SFTP_ATTR_UIDGID) | flag(&self.gid, raw::LIBSSH2_SFTP_ATTR_UIDGID) | flag(&self.perm, raw::LIBSSH2_SFTP_ATTR_PERMISSIONS) | flag(&self.atime, raw::LIBSSH2_SFTP_ATTR_ACMODTIME) | flag(&self.mtime, raw::LIBSSH2_SFTP_ATTR_ACMODTIME), filesize: self.size.unwrap_or(0), uid: self.uid.unwrap_or(0) as c_ulong, gid: self.gid.unwrap_or(0) as c_ulong, permissions: self.perm.unwrap_or(0) as c_ulong, atime: self.atime.unwrap_or(0) as c_ulong, mtime: self.mtime.unwrap_or(0) as c_ulong, } } } impl FileType { /// Test whether this file type represents a directory. pub fn is_dir(&self) -> bool { self == &FileType::Directory } /// Test whether this file type represents a regular file. pub fn is_file(&self) -> bool { self == &FileType::RegularFile } /// Test whether this file type represents a symbolic link. pub fn is_symlink(&self) -> bool { self == &FileType::Symlink } fn from_perm(perm: c_ulong) -> Self { match perm & raw::LIBSSH2_SFTP_S_IFMT { raw::LIBSSH2_SFTP_S_IFIFO => FileType::NamedPipe, raw::LIBSSH2_SFTP_S_IFCHR => FileType::CharDevice, raw::LIBSSH2_SFTP_S_IFDIR => FileType::Directory, raw::LIBSSH2_SFTP_S_IFBLK => FileType::BlockDevice, raw::LIBSSH2_SFTP_S_IFREG => FileType::RegularFile, raw::LIBSSH2_SFTP_S_IFLNK => FileType::Symlink, raw::LIBSSH2_SFTP_S_IFSOCK => FileType::Socket, other => FileType::Other(other), } } } #[cfg(unix)] fn mkpath(v: Vec) -> PathBuf { use std::ffi::OsStr; use std::os::unix::prelude::*; PathBuf::from(OsStr::from_bytes(&v)) } #[cfg(windows)] fn mkpath(v: Vec) -> PathBuf { use std::str; PathBuf::from(str::from_utf8(&v).unwrap()) } ssh2-0.9.4/src/util.rs000064400000000000000000000024550072674642500126650ustar 00000000000000use std::borrow::Cow; use std::path::Path; use {raw, Error, ErrorCode}; #[cfg(unix)] pub fn path2bytes(p: &Path) -> Result, Error> { use std::ffi::OsStr; use std::os::unix::prelude::*; let s: &OsStr = p.as_ref(); check(Cow::Borrowed(s.as_bytes())) } #[cfg(windows)] pub fn path2bytes(p: &Path) -> Result, Error> { p.to_str() .map(|s| s.as_bytes()) .ok_or_else(|| { Error::new( ErrorCode::Session(raw::LIBSSH2_ERROR_INVAL), "only unicode paths on windows may be used", ) }) .map(|bytes| { if bytes.contains(&b'\\') { // Normalize to Unix-style path separators let mut bytes = bytes.to_owned(); for b in &mut bytes { if *b == b'\\' { *b = b'/'; } } Cow::Owned(bytes) } else { Cow::Borrowed(bytes) } }) .and_then(check) } fn check(b: Cow<[u8]>) -> Result, Error> { if b.iter().any(|b| *b == 0) { Err(Error::new( ErrorCode::Session(raw::LIBSSH2_ERROR_INVAL), "path provided contains a 0 byte", )) } else { Ok(b) } } ssh2-0.9.4/tests/all/agent.rs000064400000000000000000000005520072674642500141250ustar 00000000000000use ssh2::Session; #[test] fn smoke() { let sess = Session::new().unwrap(); let mut agent = sess.agent().unwrap(); agent.connect().unwrap(); agent.list_identities().unwrap(); { let a = agent.identities().unwrap(); let i1 = &a[0]; assert!(agent.userauth("foo", &i1).is_err()); } agent.disconnect().unwrap(); } ssh2-0.9.4/tests/all/channel.rs000064400000000000000000000170470072674642500144460ustar 00000000000000use ssh2::Channel; use std::io::prelude::*; use std::net::{TcpListener, TcpStream}; use std::thread; /// Consume all available stdout and stderr data. /// It is important to read both if you are using /// channel.eof() to make assertions that the stream /// is complete fn consume_stdio(channel: &mut Channel) -> (String, String) { let mut stdout = String::new(); channel.read_to_string(&mut stdout).unwrap(); let mut stderr = String::new(); channel.stderr().read_to_string(&mut stderr).unwrap(); eprintln!("stdout: {}", stdout); eprintln!("stderr: {}", stderr); (stdout, stderr) } #[test] fn smoke() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); fn must_be_send(_: &T) -> bool { true } assert!(must_be_send(&channel)); assert!(must_be_send(&channel.stream(0))); channel.flush().unwrap(); channel.exec("true").unwrap(); consume_stdio(&mut channel); channel.wait_eof().unwrap(); assert!(channel.eof()); channel.close().unwrap(); channel.wait_close().unwrap(); assert_eq!(channel.exit_status().unwrap(), 0); assert!(channel.eof()); } #[test] fn agent_forward() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); channel.request_auth_agent_forwarding().unwrap(); channel.exec("echo $SSH_AUTH_SOCK").unwrap(); let (output, _) = consume_stdio(&mut channel); let output = output.trim(); // make sure that the sock is set assert_ne!(output, ""); // and that it isn't just inherited the one we set for this // test environment assert_ne!(output, std::env::var("SSH_AUTH_SOCK").unwrap()); } #[test] fn bad_smoke() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); channel.flush().unwrap(); channel.exec("false").unwrap(); consume_stdio(&mut channel); channel.wait_eof().unwrap(); assert!(channel.eof()); channel.close().unwrap(); channel.wait_close().unwrap(); assert_eq!(channel.exit_status().unwrap(), 1); assert!(channel.eof()); } #[test] fn reading_data() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); channel.exec("echo foo").unwrap(); let (output, _) = consume_stdio(&mut channel); assert_eq!(output, "foo\n"); } #[test] fn handle_extended_data() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); channel .handle_extended_data(ssh2::ExtendedData::Merge) .unwrap(); channel.exec("echo foo >&2").unwrap(); let (output, _) = consume_stdio(&mut channel); // This is an ends_with test because stderr may have several // lines of misc output on travis macos hosts; it appears as // though the local shell configuration on travis macos is // broken and contributes to this :-/ assert!(output.ends_with("foo\n")); } #[test] fn writing_data() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); channel.exec("read foo && echo $foo").unwrap(); channel.write_all(b"foo\n").unwrap(); let (output, _) = consume_stdio(&mut channel); assert_eq!(output, "foo\n"); } #[test] fn eof() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); channel.adjust_receive_window(10, false).unwrap(); channel.exec("read foo").unwrap(); channel.send_eof().unwrap(); let mut output = String::new(); channel.read_to_string(&mut output).unwrap(); assert_eq!(output, ""); } #[test] fn shell() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); eprintln!("requesting pty"); channel.request_pty("xterm", None, None).unwrap(); eprintln!("shell"); channel.shell().unwrap(); eprintln!("close"); channel.close().unwrap(); eprintln!("done"); consume_stdio(&mut channel); } #[test] fn setenv() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); let _ = channel.setenv("FOO", "BAR"); channel.close().unwrap(); } #[test] fn direct() { let a = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = a.local_addr().unwrap(); let t = thread::spawn(move || { let mut s = a.accept().unwrap().0; let mut b = [0, 0, 0]; s.read(&mut b).unwrap(); assert_eq!(b, [1, 2, 3]); s.write_all(&[4, 5, 6]).unwrap(); }); let sess = ::authed_session(); let mut channel = sess .channel_direct_tcpip("127.0.0.1", addr.port(), None) .unwrap(); channel.write_all(&[1, 2, 3]).unwrap(); let mut r = [0, 0, 0]; channel.read(&mut r).unwrap(); assert_eq!(r, [4, 5, 6]); t.join().ok().unwrap(); } #[test] fn forward() { let sess = ::authed_session(); let (mut listen, port) = sess.channel_forward_listen(39249, None, None).unwrap(); let t = thread::spawn(move || { let mut s = TcpStream::connect(&("127.0.0.1", port)).unwrap(); let mut b = [0, 0, 0]; s.read(&mut b).unwrap(); assert_eq!(b, [1, 2, 3]); s.write_all(&[4, 5, 6]).unwrap(); }); let mut channel = listen.accept().unwrap(); channel.write_all(&[1, 2, 3]).unwrap(); let mut r = [0, 0, 0]; channel.read(&mut r).unwrap(); assert_eq!(r, [4, 5, 6]); t.join().ok().unwrap(); } #[test] fn drop_nonblocking() { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); let sess = ::authed_session(); sess.set_blocking(false); thread::spawn(move || { let _s = listener.accept().unwrap(); }); let _ = sess.channel_direct_tcpip("127.0.0.1", addr.port(), None); drop(sess); } #[test] fn nonblocking_before_exit_code() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); channel.send_eof().unwrap(); let mut output = String::new(); channel.exec("sleep 1; echo foo").unwrap(); sess.set_blocking(false); assert!(channel.read_to_string(&mut output).is_err()); { use std::thread; use std::time::Duration; thread::sleep(Duration::from_millis(1500)); } sess.set_blocking(true); assert!(channel.read_to_string(&mut output).is_ok()); channel.wait_eof().unwrap(); channel.close().unwrap(); channel.wait_close().unwrap(); assert_eq!(output, "foo\n"); assert!(channel.exit_status().unwrap() == 0); } #[test] fn exit_code_ignores_other_errors() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); channel.exec("true").unwrap(); channel.wait_eof().unwrap(); channel.close().unwrap(); channel.wait_close().unwrap(); let longdescription: String = ::std::iter::repeat('a').take(300).collect(); assert!(sess.disconnect(None, &longdescription, None).is_err()); // max len == 256 assert!(channel.exit_status().unwrap() == 0); } #[test] fn pty_modes_are_propagated() { let sess = ::authed_session(); let mut channel = sess.channel_session().unwrap(); eprintln!("requesting pty"); let mut mode = ssh2::PtyModes::new(); // intr is typically CTRL-C; setting it to unmodified `y` // should be very high signal that it took effect mode.set_character(ssh2::PtyModeOpcode::VINTR, Some('y')); channel.request_pty("xterm", Some(mode), None).unwrap(); channel.exec("stty -a").unwrap(); let (out, _err) = consume_stdio(&mut channel); channel.close().unwrap(); // This may well be linux specific assert!(out.contains("intr = y"), "mode was propagated"); } ssh2-0.9.4/tests/all/knownhosts.rs000064400000000000000000000033250072674642500152450ustar 00000000000000use ssh2::{KnownHostFileKind, Session}; #[test] fn smoke() { let sess = Session::new().unwrap(); let known_hosts = sess.known_hosts().unwrap(); let hosts = known_hosts.hosts().unwrap(); assert_eq!(hosts.len(), 0); } #[test] fn reading() { let encoded = "\ |1|VXwDpq2cv4j3QtmrGiY+HntJc+Q=|80E+wqnFDhkxBDxRBOIPJPAVE6Y= \ ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9I\ DSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVD\ BfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eF\ zLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKS\ CZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2R\ PW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi\ /w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== "; let sess = Session::new().unwrap(); let mut known_hosts = sess.known_hosts().unwrap(); known_hosts .read_str(encoded, KnownHostFileKind::OpenSSH) .unwrap(); let hosts = known_hosts.hosts().unwrap(); assert_eq!(hosts.len(), 1); let host = &hosts[0]; assert_eq!(host.name(), None); assert_eq!( host.key(), "\ AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9I\ DSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVD\ BfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eF\ zLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKS\ CZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2R\ PW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi\ /w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==" ); assert_eq!( known_hosts .write_string(host, KnownHostFileKind::OpenSSH) .unwrap(), encoded ); known_hosts.remove(host).unwrap(); } ssh2-0.9.4/tests/all/main.rs000064400000000000000000000020120072674642500137440ustar 00000000000000#![deny(warnings)] extern crate ssh2; extern crate tempdir; use std::env; use std::net::TcpStream; mod agent; mod channel; mod knownhosts; mod session; mod sftp; pub fn test_addr() -> String { let port = env::var("RUST_SSH2_FIXTURE_PORT") .map(|s| s.parse().unwrap()) .unwrap_or(22); let addr = format!("127.0.0.1:{}", port); addr } pub fn socket() -> TcpStream { TcpStream::connect(&test_addr()).unwrap() } pub fn authed_session() -> ssh2::Session { let user = env::var("USER").unwrap(); let socket = socket(); let mut sess = ssh2::Session::new().unwrap(); sess.set_tcp_stream(socket); sess.handshake().unwrap(); assert!(!sess.authenticated()); { let mut agent = sess.agent().unwrap(); agent.connect().unwrap(); agent.list_identities().unwrap(); let identities = agent.identities().unwrap(); let identity = &identities[0]; agent.userauth(&user, &identity).unwrap(); } assert!(sess.authenticated()); sess } ssh2-0.9.4/tests/all/session.rs000064400000000000000000000134260072674642500145160ustar 00000000000000use std::env; use std::fs::File; use std::io::{self, prelude::*}; use std::path::Path; use tempdir::TempDir; use ssh2::{BlockDirections, HashType, KeyboardInteractivePrompt, MethodType, Prompt, Session}; #[test] fn session_is_send() { fn must_be_send(_: &T) -> bool { true } let sess = Session::new().unwrap(); assert!(must_be_send(&sess)); } #[test] fn smoke() { let sess = Session::new().unwrap(); assert!(sess.banner_bytes().is_none()); sess.set_banner("foo").unwrap(); assert!(sess.is_blocking()); assert_eq!(sess.timeout(), 0); sess.set_compress(true); assert!(sess.host_key().is_none()); sess.method_pref(MethodType::Kex, "diffie-hellman-group14-sha1") .unwrap(); assert!(sess.methods(MethodType::Kex).is_none()); sess.set_blocking(true); sess.set_timeout(0); sess.supported_algs(MethodType::Kex).unwrap(); sess.supported_algs(MethodType::HostKey).unwrap(); sess.channel_session().err().unwrap(); } #[test] fn smoke_handshake() { let user = env::var("USER").unwrap(); let socket = ::socket(); let mut sess = Session::new().unwrap(); sess.set_tcp_stream(socket); sess.handshake().unwrap(); sess.host_key().unwrap(); let methods = sess.auth_methods(&user).unwrap(); assert!(methods.contains("publickey"), "{}", methods); assert!(!sess.authenticated()); let mut agent = sess.agent().unwrap(); agent.connect().unwrap(); agent.list_identities().unwrap(); { let identity = &agent.identities().unwrap()[0]; agent.userauth(&user, &identity).unwrap(); } assert!(sess.authenticated()); sess.host_key_hash(HashType::Md5).unwrap(); } #[test] fn keyboard_interactive() { let user = env::var("USER").unwrap(); let socket = ::socket(); let mut sess = Session::new().unwrap(); sess.set_tcp_stream(socket); sess.handshake().unwrap(); sess.host_key().unwrap(); let methods = sess.auth_methods(&user).unwrap(); assert!( methods.contains("keyboard-interactive"), "test server ({}) must support `ChallengeResponseAuthentication yes`, not just {}", ::test_addr(), methods ); assert!(!sess.authenticated()); // We don't know the correct response for whatever challenges // will be returned to us, but that's ok; the purpose of this // test is to check that we have some basically sane interaction // with the library. struct Prompter { some_data: usize, } impl KeyboardInteractivePrompt for Prompter { fn prompt<'a>( &mut self, username: &str, instructions: &str, prompts: &[Prompt<'a>], ) -> Vec { // Sanity check that the pointer manipulation resolves and // we read back our member data ok assert_eq!(self.some_data, 42); eprintln!("username: {}", username); eprintln!("instructions: {}", instructions); eprintln!("prompts: {:?}", prompts); // Unfortunately, we can't make any assertions about username // or instructions, as they can be empty (on my linux system) // or may have arbitrary contents // assert_eq!(username, env::var("USER").unwrap()); // assert!(!instructions.is_empty()); // Hopefully this isn't too brittle an assertion if prompts.len() == 1 { assert_eq!(prompts.len(), 1); // Might be "Password: " or "Password:" or other variations assert!(prompts[0].text.contains("sword")); assert_eq!(prompts[0].echo, false); } else { // maybe there's some PAM configuration that results // in multiple prompts. We can't make any real assertions // in this case, other than that there has to be at least // one prompt. assert!(!prompts.is_empty()); } prompts.iter().map(|_| "bogus".to_string()).collect() } } let mut p = Prompter { some_data: 42 }; match sess.userauth_keyboard_interactive(&user, &mut p) { Ok(_) => eprintln!("auth succeeded somehow(!)"), Err(err) => eprintln!("auth failed as expected: {}", err), }; // The only way this assertion will be false is if the person // running these tests has "bogus" as their password assert!(!sess.authenticated()); } #[test] fn keepalive() { let sess = ::authed_session(); sess.set_keepalive(false, 10); sess.keepalive_send().unwrap(); } #[test] fn scp_recv() { let sess = ::authed_session(); // Download our own source file; it's the only path that // we know for sure exists on this system. let p = Path::new(file!()).canonicalize().unwrap(); let (mut ch, _) = sess.scp_recv(&p).unwrap(); let mut data = String::new(); ch.read_to_string(&mut data).unwrap(); let mut expected = String::new(); File::open(&p) .unwrap() .read_to_string(&mut expected) .unwrap(); assert!(data == expected); } #[test] fn scp_send() { let td = TempDir::new("test").unwrap(); let sess = ::authed_session(); let mut ch = sess .scp_send(&td.path().join("foo"), 0o644, 6, None) .unwrap(); ch.write_all(b"foobar").unwrap(); drop(ch); let mut actual = Vec::new(); File::open(&td.path().join("foo")) .unwrap() .read_to_end(&mut actual) .unwrap(); assert_eq!(actual, b"foobar"); } #[test] fn block_directions() { let mut sess = ::authed_session(); sess.set_blocking(false); let actual = sess.handshake().map_err(|e| io::Error::from(e).kind()); assert_eq!(actual, Err(io::ErrorKind::WouldBlock)); assert_eq!(sess.block_directions(), BlockDirections::Inbound); } ssh2-0.9.4/tests/all/sftp.rs000064400000000000000000000047670072674642500140170ustar 00000000000000use std::fs::{self, File}; use std::io::prelude::*; use tempdir::TempDir; #[test] fn smoke() { let sess = ::authed_session(); sess.sftp().unwrap(); } #[test] fn ops() { let td = TempDir::new("foo").unwrap(); File::create(&td.path().join("foo")).unwrap(); fs::create_dir(&td.path().join("bar")).unwrap(); let sess = ::authed_session(); let sftp = sess.sftp().unwrap(); sftp.opendir(&td.path().join("bar")).unwrap(); let mut foo = sftp.open(&td.path().join("foo")).unwrap(); sftp.mkdir(&td.path().join("bar2"), 0o755).unwrap(); assert!(fs::metadata(&td.path().join("bar2")) .map(|m| m.is_dir()) .unwrap_or(false)); sftp.rmdir(&td.path().join("bar2")).unwrap(); sftp.create(&td.path().join("foo5")) .unwrap() .write_all(b"foo") .unwrap(); let mut v = Vec::new(); File::open(&td.path().join("foo5")) .unwrap() .read_to_end(&mut v) .unwrap(); assert_eq!(v, b"foo"); assert_eq!(sftp.stat(&td.path().join("foo")).unwrap().size, Some(0)); v.truncate(0); foo.read_to_end(&mut v).unwrap(); assert_eq!(v, Vec::new()); sftp.symlink(&td.path().join("foo"), &td.path().join("foo2")) .unwrap(); let readlink = sftp.readlink(&td.path().join("foo2")).unwrap(); assert!(readlink == td.path().join("foo")); let realpath = sftp.realpath(&td.path().join("foo2")).unwrap(); assert_eq!(realpath, td.path().join("foo").canonicalize().unwrap()); let files = sftp.readdir(td.path()).unwrap(); assert_eq!(files.len(), 4); } #[test] fn not_found() { let td = TempDir::new("foo").unwrap(); let sess = ::authed_session(); let sftp = sess.sftp().unwrap(); // Can't use unwrap_err here since File does not impl Debug. let err = sftp .opendir(&td.path().join("nonexistent")) .err() .expect("open nonexistent dir"); assert_eq!(err.to_string(), "[SFTP(2)] no such file"); let io_err: std::io::Error = err.into(); assert_eq!(io_err.kind(), std::io::ErrorKind::NotFound); assert_eq!(io_err.to_string(), "no such file"); let err = sftp .stat(&td.path().join("nonexistent")) .err() .expect("stat nonexistent"); assert_eq!(err.to_string(), "[SFTP(2)] no such file"); let io_err: std::io::Error = err.into(); assert_eq!(io_err.kind(), std::io::ErrorKind::NotFound); assert_eq!(io_err.to_string(), "no such file"); } ssh2-0.9.4/tests/run_integration_tests.sh000064400000000000000000000026510072674642500167000ustar 00000000000000#!/bin/bash set -e set -x # This script spawns an ssh daemon with a known configuration so that we can # test various functionality against it. # Tell the tests to use the port number we're using to spawn this server export RUST_SSH2_FIXTURE_PORT=8022 cleanup() { # Stop the ssh server and local ssh agent kill $(< $SSHDIR/sshd.pid) $SSH_AGENT_PID || true test -f $SSHDIR/sshd.log && cat $SSHDIR/sshd.log } trap cleanup EXIT # Blow away any prior state and re-configure our test server SSHDIR=$(pwd)/tests/sshd rm -rf $SSHDIR mkdir -p $SSHDIR eval $(ssh-agent -s) ssh-keygen -t rsa -f $SSHDIR/id_rsa -N "" -q chmod 0600 $SSHDIR/id_rsa* ssh-add $SSHDIR/id_rsa cp $SSHDIR/id_rsa.pub $SSHDIR/authorized_keys ssh-keygen -f $SSHDIR/ssh_host_rsa_key -N '' -t rsa cat > $SSHDIR/sshd_config <<-EOT AuthorizedKeysFile=$SSHDIR/authorized_keys HostKey=$SSHDIR/ssh_host_rsa_key PidFile=$SSHDIR/sshd.pid Subsystem sftp internal-sftp UsePAM yes X11Forwarding yes UsePrivilegeSeparation no PrintMotd yes PermitTunnel yes KbdInteractiveAuthentication yes AllowTcpForwarding yes MaxStartups 500 # Relax modes when the repo is under eg: /var/tmp StrictModes no EOT cat $SSHDIR/sshd_config # Start an ssh server /usr/sbin/sshd -p $RUST_SSH2_FIXTURE_PORT -f $SSHDIR/sshd_config -E $SSHDIR/sshd.log # Give it a moment to start up sleep 2 # Run the tests against it cargo test --all -- --nocapture cargo test --features vendored-openssl -- --nocapture