├── .github └── workflows │ ├── android.yml │ ├── fmt.yml │ ├── linux.yml │ ├── macos.yml │ └── windows.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── libssh-rs-sys ├── Cargo.toml ├── README.md ├── binding.h ├── build.rs ├── examples │ └── link.rs ├── src │ └── lib.rs └── update-ffi.sh └── libssh-rs ├── Cargo.toml ├── README.md ├── examples └── whoami.rs └── src ├── channel.rs ├── error.rs ├── lib.rs └── sftp.rs /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | name: build (android-${{ matrix.android-api }} on ${{ matrix.os }}) 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ ubuntu-latest, windows-latest, macos-latest ] 18 | android-api: [ 21, 28 ] 19 | include: 20 | - os: windows-latest 21 | clang-suffix: .cmd 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: Checkout repo 25 | uses: actions/checkout@v4 26 | with: 27 | submodules: "recursive" 28 | - name: Install Android NDK 29 | id: setup-ndk 30 | uses: nttld/setup-ndk@v1 31 | with: 32 | ndk-version: 'r26d' 33 | - name: Adding NDK to PATH (*nix) 34 | if: ${{ !startsWith(matrix.os, 'windows') }} 35 | run: echo ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/*/bin >> $GITHUB_PATH 36 | env: 37 | ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} 38 | - name: Adding NDK to PATH (Windows) 39 | shell: pwsh 40 | if: ${{ startsWith(matrix.os, 'windows') }} 41 | run: echo "${env:ANDROID_NDK_HOME}\toolchains\llvm\prebuilt\windows-x86_64\bin" >> $env:GITHUB_PATH 42 | env: 43 | ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} 44 | - name: Install Rust 45 | uses: actions-rust-lang/setup-rust-toolchain@v1 46 | with: 47 | target: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android 48 | - name: Download prebuilt OpenSSL 49 | uses: actions/checkout@v4 50 | with: 51 | repository: KDAB/android_openssl 52 | path: android_openssl 53 | - name: Build and test 54 | run: | 55 | rustc -V 56 | cargo -V 57 | cargo build --target aarch64-linux-android 58 | cargo build --target armv7-linux-androideabi 59 | cargo build --target i686-linux-android 60 | cargo build --target x86_64-linux-android 61 | env: 62 | OPENSSL_INCLUDE_DIR: "${{ github.workspace }}/android_openssl/ssl_1.1/include" 63 | AARCH64_LINUX_ANDROID_OPENSSL_LIB_DIR: "${{ github.workspace }}/android_openssl/ssl_1.1/arm64-v8a" 64 | ARMV7_LINUX_ANDROIDEABI_OPENSSL_LIB_DIR: "${{ github.workspace }}/android_openssl/ssl_1.1/armeabi-v7a" 65 | I686_LINUX_ANDROID_OPENSSL_LIB_DIR: "${{ github.workspace }}/android_openssl/ssl_1.1/x86" 66 | X86_64_LINUX_ANDROID_OPENSSL_LIB_DIR: "${{ github.workspace }}/android_openssl/ssl_1.1/x86_64" 67 | CC_aarch64_linux_android: "aarch64-linux-android${{ matrix.android-api }}-clang${{ matrix.clang-suffix }}" 68 | CC_armv7-linux-androideabi: "armv7a-linux-androideabi${{ matrix.android-api }}-clang${{ matrix.clang-suffix }}" 69 | CC_i686_linux_android: "i686-linux-android${{ matrix.android-api }}-clang${{ matrix.clang-suffix }}" 70 | CC_x86_64_linux_android: "x86_64-linux-android${{ matrix.android-api }}-clang${{ matrix.clang-suffix }}" -------------------------------------------------------------------------------- /.github/workflows/fmt.yml: -------------------------------------------------------------------------------- 1 | name: Check code formatting 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | paths: 7 | - "**/*.rs" 8 | - ".rustfmt.toml" 9 | pull_request: 10 | branches: [ "main" ] 11 | paths: 12 | - "**/*.rs" 13 | - ".rustfmt.toml" 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | check-code-formatting: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: "Install Rust" 24 | uses: dtolnay/rust-toolchain@stable 25 | with: 26 | components: rustfmt 27 | - name: Rust formatting 28 | run: cargo fmt --all -- --check 29 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-22.04] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: "checkout repo" 20 | uses: actions/checkout@v2 21 | with: 22 | submodules: "recursive" 23 | - name: Install Rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: stable 28 | - name: Install libssh 29 | run: sudo apt install libssh-dev 30 | - name: Build and test 31 | run: | 32 | rustc -V 33 | cargo -V 34 | cargo test 35 | cargo run --example link --features vendored 36 | cargo run --example link --features vendored,vendored-openssl 37 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [macOS-latest] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: "checkout repo" 20 | uses: actions/checkout@v2 21 | with: 22 | submodules: "recursive" 23 | - name: Install Rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: stable 28 | - name: Build and test 29 | run: | 30 | rustc -V 31 | cargo -V 32 | cargo run --example link --features vendored,vendored-openssl 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [windows-2019] 17 | env: 18 | - TARGET: x86_64-pc-windows-msvc 19 | - TARGET: i686-pc-windows-msvc 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - name: "checkout repo" 23 | uses: actions/checkout@v2 24 | with: 25 | submodules: "recursive" 26 | - name: Install Rust 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: stable 31 | override: true 32 | target: ${{ matrix.env.TARGET }} 33 | - name: Build and test 34 | env: 35 | TARGET: ${{ matrix.env.TARGET }} 36 | run: | 37 | rustc -V 38 | cargo -V 39 | cargo run --target %TARGET% --example link --features vendored,vendored-openssl 40 | shell: cmd 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp* 2 | /src/binding.h 3 | /target 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libssh-rs-sys/vendored"] 2 | path = libssh-rs-sys/vendored 3 | url = https://gitlab.com/libssh/libssh-mirror.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "libssh-rs-sys", 4 | "libssh-rs" 5 | ] 6 | resolver = "2" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wez Furlong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libssh-rs 2 | 3 | [![Build Status](https://github.com/wez/libssh-rs/workflows/Linux/badge.svg)](https://github.com/wez/libssh-rs/actions?workflow=Linux) 4 | [![Build Status](https://github.com/wez/libssh-rs/workflows/Windows/badge.svg)](https://github.com/wez/libssh-rs/actions?workflow=Windows) 5 | [![Build Status](https://github.com/wez/libssh-rs/workflows/macOS/badge.svg)](https://github.com/wez/libssh-rs/actions?workflow=macOS) 6 | 7 | Bindings to [libssh](https://www.libssh.org/). 8 | 9 | This repo is home to the `libssh-rs-sys` crate, which provides unsafe FFI bindings to `libssh`, 10 | and the `libssh-rs` crate which provides safe bindings based on `libssh-rs-sys`. 11 | 12 | ## Features 13 | 14 | The `vendored` feature causes a static version of libssh to be compiled and linked into your program. 15 | If no system `libssh` is detected at build time, or that system library is too old, then the vendored 16 | `libssh` implementation will be used automatically. Note that the `libssh-rs` bindings make use of 17 | a couple of new interfaces that have not made it into a released version of `libssh` at the time 18 | of writing this note, so all users will be effectively running with `vendored` enabled until libssh 19 | releases version `0.9.7`. 20 | 21 | The `vendored-openssl` feature causes a vendored copy of `openssl` to be compiled and linked into your program. 22 | 23 | On macOS and Windows systems, you most likely want to enable both `vendored` and `vendored-openssl`. 24 | 25 | ## License 26 | 27 | This crate is licensed under the MIT license, and is: 28 | Copyright (c) 2021-Present Wez Furlong. 29 | 30 | Note that the `vendored` directory is a submodule that references `libssh`; 31 | `libssh` is itself [GNU Library (or: Lesser) General Public License 32 | (LGPL)](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) which has a 33 | viral clause in the case where you modify `libssh`. The license is explained 34 | [on the libssh features page](https://www.libssh.org/features/); the summary is 35 | that simply using an unmodified `libssh-rs-sys` crate will not trigger that 36 | viral clause, and you are thus able to use this crate under the terms of the 37 | MIT license. 38 | 39 | -------------------------------------------------------------------------------- /libssh-rs-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libssh-rs-sys" 3 | version = "0.2.6" 4 | authors = ["Wez Furlong"] 5 | edition = "2018" 6 | links = "libssh" 7 | repository = "https://github.com/wez/libssh-rs" 8 | description = "Native bindings to the libssh library" 9 | license = "MIT" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [build-dependencies] 14 | pkg-config = "0.3" 15 | cc = "1.0" 16 | 17 | [dependencies] 18 | libz-sys = { optional=true, version = "1.1", default-features = false, features = ["libc"] } 19 | openssl-sys = "0.9.105" 20 | 21 | [features] 22 | vendored = ["libz-sys"] 23 | vendored-openssl = ["openssl-sys/vendored"] 24 | 25 | -------------------------------------------------------------------------------- /libssh-rs-sys/README.md: -------------------------------------------------------------------------------- 1 | # libssh-rs-sys 2 | 3 | [![Build Status](https://github.com/wez/libssh-rs/workflows/Linux/badge.svg)](https://github.com/wez/libssh-rs/actions?workflow=Linux) 4 | [![Build Status](https://github.com/wez/libssh-rs/workflows/Windows/badge.svg)](https://github.com/wez/libssh-rs/actions?workflow=Windows) 5 | [![Build Status](https://github.com/wez/libssh-rs/workflows/macOS/badge.svg)](https://github.com/wez/libssh-rs/actions?workflow=macOS) 6 | 7 | Native bindings to [libssh](https://www.libssh.org/). 8 | 9 | ## Features 10 | 11 | The `vendored` feature causes a static version of libssh to be compiled and linked into your program. 12 | If no system `libssh` is detected at build time, or that system library is too old, then the vendored 13 | `libssh` implementation will be used automatically. Note that the `libssh-rs` bindings make use of 14 | a couple of new interfaces that have not made it into a released version of `libssh` at the time 15 | of writing this note, so all users will be effectively running with `vendored` enabled until libssh 16 | releases version `0.9.7`. 17 | 18 | The `vendored-openssl` feature causes a vendored copy of `openssl` to be compiled and linked into your program. 19 | 20 | On macOS and Windows systems, you most likely want to enable both `vendored` and `vendored-openssl`. 21 | 22 | ## License 23 | 24 | This crate is licensed under the MIT license, and is: 25 | Copyright (c) 2021-Present Wez Furlong. 26 | 27 | Note that the `vendored` directory is a submodule that references `libssh`; 28 | `libssh` is itself [GNU Library (or: Lesser) General Public License 29 | (LGPL)](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) which has a 30 | viral clause in the case where you modify `libssh`. The license is explained 31 | [on the libssh features page](https://www.libssh.org/features/); the summary is 32 | that simply using an unmodified `libssh-rs-sys` crate will not trigger that 33 | viral clause, and you are thus able to use this crate under the terms of the 34 | MIT license. 35 | 36 | -------------------------------------------------------------------------------- /libssh-rs-sys/binding.h: -------------------------------------------------------------------------------- 1 | typedef unsigned long size_t; 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | -------------------------------------------------------------------------------- /libssh-rs-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | fn main() { 4 | if std::env::var_os("CARGO_FEATURE_VENDORED").is_none() 5 | && pkg_config::Config::new() 6 | // ssh_userauth_publickey_auto_get_current_identity 7 | // is not yet in a released version of libssh 8 | .atleast_version("0.9.7") 9 | .probe("libssh") 10 | .is_ok() 11 | { 12 | return; 13 | } 14 | 15 | let mut cfg = cc::Build::new(); 16 | cfg.define("LIBSSH_STATIC", None); 17 | cfg.include("vendored/include"); 18 | cfg.flag_if_supported("-Wno-deprecated-declarations"); 19 | 20 | let dst = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); 21 | let include = dst.join("include"); 22 | std::fs::create_dir_all(&include).unwrap(); 23 | cfg.include(&include); 24 | println!("cargo:include={}", include.display()); 25 | println!("cargo:root={}", dst.display()); 26 | 27 | let openssl_version = std::env::var("DEP_OPENSSL_VERSION_NUMBER").unwrap(); 28 | let openssl_version = u64::from_str_radix(&openssl_version, 16).unwrap(); 29 | 30 | let target = std::env::var("TARGET").unwrap(); 31 | 32 | let target_family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap(); 33 | cfg.define("GLOBAL_CLIENT_CONFIG", Some("\"/etc/ssh/ssh_config\"")); 34 | cfg.define( 35 | "GLOBAL_BIND_CONFIG", 36 | Some("\"/etc/ssh/libssh_server_config\""), 37 | ); 38 | cfg.define("HAVE_GETADDRINFO", Some("1")); 39 | cfg.define("HAVE_LIBCRYPTO", Some("1")); 40 | cfg.define("HAVE_OPENSSL_AES_H", Some("1")); 41 | cfg.define("HAVE_OPENSSL_BLOWFISH_H", Some("1")); 42 | cfg.define("HAVE_OPENSSL_DES_H", Some("1")); 43 | cfg.define("HAVE_OPENSSL_ECC", Some("1")); 44 | cfg.define("HAVE_OPENSSL_ECDH_H", Some("1")); 45 | cfg.define("HAVE_OPENSSL_ECDSA_H", Some("1")); 46 | cfg.define("HAVE_ECC", Some("1")); 47 | cfg.define("HAVE_DSA", Some("1")); 48 | cfg.define("HAVE_OPENSSL_EC_H", Some("1")); 49 | 50 | if openssl_version >= 0x1_01_01_00_0 { 51 | cfg.define("HAVE_OPENSSL_EVP_CHACHA20", Some("1")); 52 | } 53 | 54 | /* Don't bother setting these: libssh has a fallback in any case, 55 | * and the documentation doesn't specify when they were introduced, 56 | cfg.define("HAVE_OPENSSL_EVP_DIGESTSIGN", Some("1")); 57 | cfg.define("HAVE_OPENSSL_EVP_DIGESTVERIFY", Some("1")); 58 | */ 59 | 60 | if openssl_version < 0x1_01_00_00_0 { 61 | cfg.file("vendored/src/libcrypto-compat.c"); 62 | } 63 | 64 | if false && openssl_version >= 0x3_00_00_00_0 { 65 | cfg.define("HAVE_OPENSSL_EVP_KDF_CTX_NEW_ID", Some("1")); 66 | } 67 | // cfg.define("HAVE_OPENSSL_FIPS_MODE", Some("1")); 68 | 69 | cfg.define("HAVE_STDINT_H", Some("1")); 70 | cfg.define("WITH_ZLIB", Some("1")); 71 | cfg.define("WITH_GEX", Some("1")); 72 | cfg.define("WITH_SFTP", Some("1")); 73 | cfg.define("WITH_SERVER", Some("1")); 74 | 75 | if target.contains("windows") { 76 | cfg.define("HAVE_IO_H", Some("1")); 77 | // cfg.define("HAVE_MEMSET_S", Some("1")); 78 | cfg.define("HAVE_SECURE_ZERO_MEMORY", Some("1")); 79 | cfg.define("HAVE__SNPRINTF", Some("1")); 80 | cfg.define("HAVE__SNPRINTF_S", Some("1")); 81 | cfg.define("HAVE__STRTOUI64", Some("1")); 82 | cfg.define("HAVE__VSNPRINTF", Some("1")); 83 | cfg.define("HAVE__VSNPRINTF_S", Some("1")); 84 | cfg.define("HAVE_ISBLANK", Some("1")); 85 | } else { 86 | cfg.define("HAVE_ARPA_INET_H", Some("1")); 87 | cfg.define("HAVE_CLOCK_GETTIME", Some("1")); 88 | cfg.define("HAVE_PTHREAD_H", Some("1")); 89 | cfg.define("HAVE_PTHREAD", Some("1")); 90 | cfg.define("HAVE_SELECT", Some("1")); 91 | cfg.define("HAVE_SNPRINTF", Some("1")); 92 | cfg.define("HAVE_STRTOULL", Some("1")); 93 | cfg.define("HAVE_SYS_TIME_H", Some("1")); 94 | cfg.define("HAVE_TERMIOS_H", Some("1")); 95 | cfg.define("HAVE_UNISTD_H", Some("1")); 96 | cfg.define("HAVE_VSNPRINTF", Some("1")); 97 | 98 | if !target.contains("darwin") { 99 | cfg.define("HAVE_POLL", Some("1")); 100 | } 101 | } 102 | if target.contains("linux") { 103 | cfg.define("HAVE_STRNDUP", Some("1")); 104 | } 105 | if target.contains("darwin") { 106 | cfg.define("HAVE_NTOHLL", Some("1")); 107 | cfg.define("HAVE_HTONLL", Some("1")); 108 | } 109 | 110 | if target.contains("android") { 111 | cfg.define("_BSD_SOURCE", None); 112 | } else { 113 | cfg.define("_GNU_SOURCE", None); 114 | } 115 | 116 | let compiler = cfg.get_compiler(); 117 | if compiler.is_like_gnu() || compiler.is_like_clang() { 118 | cfg.define("HAVE_COMPILER__FUNCTION__", Some("1")); 119 | cfg.define("HAVE_COMPILER__FUNC__", Some("1")); 120 | cfg.define("HAVE_GCC_THREAD_LOCAL_STORAGE", Some("1")); 121 | } 122 | 123 | if compiler.is_like_msvc() { 124 | cfg.define("HAVE_COMPILER__FUNC__", Some("1")); 125 | cfg.define("HAVE_MSC_THREAD_LOCAL_STORAGE", Some("1")); 126 | } 127 | 128 | std::fs::write(include.join("config.h"), "// nothing").unwrap(); 129 | 130 | let version = std::fs::read_to_string("vendored/include/libssh/libssh_version.h.cmake") 131 | .unwrap() 132 | .replace("@libssh_VERSION_MAJOR@", "0") 133 | .replace("@libssh_VERSION_MINOR@", "8") 134 | .replace("@libssh_VERSION_PATCH@", "90"); 135 | 136 | std::fs::create_dir_all(include.join("libssh")).unwrap(); 137 | std::fs::write(include.join("libssh/libssh_version.h"), version).unwrap(); 138 | 139 | println!("cargo:rerun-if-env-changed=DEP_Z_INCLUDE"); 140 | if let Some(path) = std::env::var_os("DEP_Z_INCLUDE") { 141 | cfg.include(path); 142 | } 143 | if let Some(zlib_root) = std::env::var_os("DEP_Z_ROOT") { 144 | println!( 145 | "cargo:rustc-link-search=native={}", 146 | PathBuf::from(zlib_root).join("lib").to_str().unwrap() 147 | ); 148 | } 149 | 150 | println!("cargo:rerun-if-env-changed=DEP_OPENSSL_INCLUDE"); 151 | if let Some(path) = std::env::var_os("DEP_OPENSSL_INCLUDE") { 152 | if let Some(path) = std::env::split_paths(&path).next() { 153 | if let Some(path) = path.to_str() { 154 | if !path.is_empty() { 155 | cfg.include(path); 156 | } 157 | } 158 | } 159 | } 160 | if let Some(zlib_root) = std::env::var_os("DEP_OPENSSL_ROOT") { 161 | println!( 162 | "cargo:rustc-link-search=native={}", 163 | PathBuf::from(zlib_root).join("lib").to_str().unwrap() 164 | ); 165 | } 166 | 167 | if false { 168 | for (k, v) in std::env::vars() { 169 | if k.starts_with("CARGO") || k.starts_with("DEP") { 170 | eprintln!("{}={}", k, v); 171 | } 172 | } 173 | panic!("boo"); 174 | } 175 | 176 | cfg.warnings(false); 177 | for f in &[ 178 | "agent.c", 179 | "auth.c", 180 | "base64.c", 181 | "bignum.c", 182 | "bind_config.c", 183 | "buffer.c", 184 | "callbacks.c", 185 | "chachapoly.c", 186 | "channels.c", 187 | "client.c", 188 | "config.c", 189 | "config_parser.c", 190 | "connect.c", 191 | "connector.c", 192 | "crypto_common.c", 193 | "curve25519.c", 194 | "dh-gex.c", 195 | "dh.c", 196 | "dh_crypto.c", 197 | "ecdh.c", 198 | "ecdh_crypto.c", 199 | "error.c", 200 | "external/bcrypt_pbkdf.c", 201 | "external/blowfish.c", 202 | "external/chacha.c", 203 | "external/curve25519_ref.c", 204 | "external/ed25519.c", 205 | "external/fe25519.c", 206 | "external/ge25519.c", 207 | "external/poly1305.c", 208 | "external/sc25519.c", 209 | "getpass.c", 210 | "getrandom_crypto.c", 211 | "gzip.c", 212 | "init.c", 213 | "kdf.c", 214 | "kex.c", 215 | "known_hosts.c", 216 | "knownhosts.c", 217 | "legacy.c", 218 | "libcrypto.c", 219 | "log.c", 220 | "match.c", 221 | "md_crypto.c", 222 | "messages.c", 223 | "misc.c", 224 | "options.c", 225 | "packet.c", 226 | "packet_cb.c", 227 | "packet_crypt.c", 228 | "pcap.c", 229 | "pki.c", 230 | "pki_container_openssh.c", 231 | "pki_crypto.c", 232 | // "pki_ed25519.c", gcrypt only 233 | "pki_ed25519_common.c", 234 | "poll.c", 235 | "scp.c", 236 | "server.c", 237 | "session.c", 238 | "sftp.c", 239 | "sftpserver.c", 240 | "sftp_common.c", 241 | "sftp_aio.c", 242 | "socket.c", 243 | "string.c", 244 | "threads.c", 245 | "threads/libcrypto.c", 246 | "threads/noop.c", 247 | "token.c", 248 | "ttyopts.c", 249 | "wrapper.c", 250 | ] { 251 | cfg.file(&format!("vendored/src/{}", f)); 252 | } 253 | 254 | if target_family == "unix" { 255 | cfg.file("vendored/src/threads/pthread.c"); 256 | } 257 | if target_family == "windows" { 258 | cfg.file("vendored/src/threads/winlocks.c"); 259 | } 260 | cfg.compile("libssh"); 261 | 262 | if target.contains("windows") { 263 | println!("cargo:rustc-link-lib=libcrypto"); 264 | println!("cargo:rustc-link-lib=libssl"); 265 | println!("cargo:rustc-link-lib=crypt32"); 266 | println!("cargo:rustc-link-lib=user32"); 267 | println!("cargo:rustc-link-lib=shell32"); 268 | println!("cargo:rustc-link-lib=ntdll"); 269 | println!("cargo:rustc-link-lib=iphlpapi"); 270 | println!("cargo:rustc-link-lib=ws2_32"); 271 | } else { 272 | println!("cargo:rustc-link-lib=ssl"); 273 | println!("cargo:rustc-link-lib=crypto"); 274 | } 275 | println!("cargo:rustc-link-lib=z"); 276 | } 277 | -------------------------------------------------------------------------------- /libssh-rs-sys/examples/link.rs: -------------------------------------------------------------------------------- 1 | use libssh_rs_sys::*; 2 | 3 | fn main() -> Result<(), String> { 4 | let session = unsafe { ssh_new() }; 5 | assert!(!session.is_null(), "failed to allocate session"); 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /libssh-rs-sys/update-ffi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Regenerate the ffi bindings 3 | cat >binding.h <<-EOT 4 | typedef unsigned long size_t; 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | EOT 11 | 12 | touch vendored/include/libssh/libssh_version.h 13 | 14 | bindgen \ 15 | binding.h \ 16 | -o src/lib.rs \ 17 | --no-layout-tests \ 18 | --no-doc-comments \ 19 | --raw-line "#![allow(non_snake_case)]" \ 20 | --raw-line "#![allow(non_camel_case_types)]" \ 21 | --raw-line "#![allow(non_upper_case_globals)]" \ 22 | --raw-line "#![allow(clippy::unreadable_literal)]" \ 23 | --raw-line "#![allow(clippy::upper_case_acronyms)]" \ 24 | --default-enum-style rust \ 25 | --constified-enum ssh_error_types_e \ 26 | --constified-enum ssh_known_hosts_e \ 27 | --constified-enum ssh_auth_e \ 28 | --constified-enum ssh_keytypes_e \ 29 | --allowlist-type '(sftp|ssh).*' \ 30 | --allowlist-function '(sftp|ssh).*' \ 31 | --allowlist-var 'SSH.*' \ 32 | --verbose \ 33 | -- \ 34 | -Ivendored/include 35 | 36 | rm vendored/include/libssh/libssh_version.h 37 | -------------------------------------------------------------------------------- /libssh-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libssh-rs" 3 | version = "0.3.6" 4 | edition = "2018" 5 | repository = "https://github.com/wez/libssh-rs" 6 | description = "Bindings to the libssh library" 7 | license = "MIT" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | bitflags = "1.3" 13 | libc = "0.2" 14 | libssh-rs-sys = { version = "0.2.6", path = "../libssh-rs-sys" } 15 | thiserror = "1.0" 16 | openssl-sys = "0.9.105" 17 | 18 | [features] 19 | vendored = ["libssh-rs-sys/vendored"] 20 | vendored-openssl = ["libssh-rs-sys/vendored-openssl"] 21 | -------------------------------------------------------------------------------- /libssh-rs/README.md: -------------------------------------------------------------------------------- 1 | # libssh-rs 2 | 3 | [![Build Status](https://github.com/wez/libssh-rs/workflows/Linux/badge.svg)](https://github.com/wez/libssh-rs/actions?workflow=Linux) 4 | [![Build Status](https://github.com/wez/libssh-rs/workflows/Windows/badge.svg)](https://github.com/wez/libssh-rs/actions?workflow=Windows) 5 | [![Build Status](https://github.com/wez/libssh-rs/workflows/macOS/badge.svg)](https://github.com/wez/libssh-rs/actions?workflow=macOS) 6 | 7 | Bindings to [libssh](https://www.libssh.org/). 8 | 9 | ## Features 10 | 11 | The `vendored` feature causes a static version of libssh to be compiled and linked into your program. 12 | If no system `libssh` is detected at build time, or that system library is too old, then the vendored 13 | `libssh` implementation will be used automatically. Note that the `libssh-rs` bindings make use of 14 | a couple of new interfaces that have not made it into a released version of `libssh` at the time 15 | of writing this note, so all users will be effectively running with `vendored` enabled until libssh 16 | releases version `0.9.7`. 17 | 18 | The `vendored-openssl` feature causes a vendored copy of `openssl` to be compiled and linked into your program. 19 | 20 | On macOS and Windows systems, you most likely want to enable both `vendored` and `vendored-openssl`. 21 | 22 | ## License 23 | 24 | This crate is licensed under the MIT license, and is: 25 | 26 | Copyright (c) 2021-Present Wez Furlong. 27 | -------------------------------------------------------------------------------- /libssh-rs/examples/whoami.rs: -------------------------------------------------------------------------------- 1 | use libssh_rs::*; 2 | use std::io::Read; 3 | 4 | fn verify_known_hosts(sess: &Session) -> SshResult<()> { 5 | let key = sess 6 | .get_server_public_key()? 7 | .get_public_key_hash_hexa(PublicKeyHashType::Sha256)?; 8 | 9 | match sess.is_known_server()? { 10 | KnownHosts::Ok => Ok(()), 11 | KnownHosts::NotFound | KnownHosts::Unknown => { 12 | eprintln!("The server is not a known host. Do you trust the host key?"); 13 | eprintln!("Public key hash: {}", key); 14 | 15 | let input = prompt_stdin("Enter yes to trust the key: ")?; 16 | if input == "yes" { 17 | sess.update_known_hosts_file() 18 | } else { 19 | Err(Error::Fatal("untrusted server".to_string())) 20 | } 21 | } 22 | KnownHosts::Changed => { 23 | eprintln!("The key for the server has changed. It is now:"); 24 | eprintln!("{}", key); 25 | Err(Error::Fatal("host key changed".to_string())) 26 | } 27 | KnownHosts::Other => { 28 | eprintln!("The host key for this server was not found, but another"); 29 | eprintln!("type of key exists. An attacker might change the default"); 30 | eprintln!("server key to confuse your client into thinking the key"); 31 | eprintln!("does not exist"); 32 | Err(Error::Fatal("host key has wrong type".to_string())) 33 | } 34 | } 35 | } 36 | 37 | fn prompt(prompt: &str, echo: bool) -> SshResult { 38 | get_input(prompt, None, echo, false).ok_or_else(|| Error::Fatal("reading password".to_string())) 39 | } 40 | 41 | fn prompt_stdin(prompt: &str) -> SshResult { 42 | eprintln!("{}", prompt); 43 | let mut input = String::new(); 44 | let _ = std::io::stdin().read_line(&mut input)?; 45 | Ok(input.trim().to_string()) 46 | } 47 | 48 | fn authenticate(sess: &Session, user_name: Option<&str>) -> SshResult<()> { 49 | match sess.userauth_none(user_name)? { 50 | AuthStatus::Success => return Ok(()), 51 | _ => {} 52 | } 53 | 54 | loop { 55 | let auth_methods = sess.userauth_list(user_name)?; 56 | 57 | if auth_methods.contains(AuthMethods::PUBLIC_KEY) { 58 | match sess.userauth_public_key_auto(None, None)? { 59 | AuthStatus::Success => return Ok(()), 60 | _ => {} 61 | } 62 | } 63 | 64 | if auth_methods.contains(AuthMethods::INTERACTIVE) { 65 | loop { 66 | match sess.userauth_keyboard_interactive(None, None)? { 67 | AuthStatus::Success => return Ok(()), 68 | AuthStatus::Info => { 69 | let info = sess.userauth_keyboard_interactive_info()?; 70 | if !info.instruction.is_empty() { 71 | eprintln!("{}", info.instruction); 72 | } 73 | let mut answers = vec![]; 74 | for p in &info.prompts { 75 | answers.push(prompt(&p.prompt, p.echo)?); 76 | } 77 | sess.userauth_keyboard_interactive_set_answers(&answers)?; 78 | 79 | continue; 80 | } 81 | AuthStatus::Denied => { 82 | break; 83 | } 84 | status => { 85 | return Err(Error::Fatal(format!( 86 | "interactive auth status: {:?}", 87 | status 88 | ))) 89 | } 90 | } 91 | } 92 | } 93 | 94 | if auth_methods.contains(AuthMethods::PASSWORD) { 95 | let pw = prompt("Password: ", false)?; 96 | 97 | match sess.userauth_password(user_name, Some(&pw))? { 98 | AuthStatus::Success => return Ok(()), 99 | status => return Err(Error::Fatal(format!("password auth status: {:?}", status))), 100 | } 101 | } 102 | 103 | return Err(Error::Fatal("unhandled auth case".to_string())); 104 | } 105 | } 106 | 107 | fn main() -> SshResult<()> { 108 | let sess = Session::new()?; 109 | sess.set_auth_callback(|prompt, echo, verify, identity| { 110 | let prompt = match identity { 111 | Some(ident) => format!("{} ({}): ", prompt, ident), 112 | None => prompt.to_string(), 113 | }; 114 | get_input(&prompt, None, echo, verify) 115 | .ok_or_else(|| Error::Fatal("reading password".to_string())) 116 | }); 117 | 118 | sess.set_option(SshOption::Hostname("localhost".to_string()))?; 119 | // sess.set_option(SshOption::LogLevel(LogLevel::Packet))?; 120 | sess.options_parse_config(None)?; 121 | sess.connect()?; 122 | eprintln!( 123 | "using {} as user name for authentication", 124 | sess.get_user_name()? 125 | ); 126 | verify_known_hosts(&sess)?; 127 | 128 | authenticate(&sess, None)?; 129 | 130 | let channel = sess.new_channel()?; 131 | channel.open_session()?; 132 | channel.request_exec("whoami")?; 133 | channel.send_eof()?; 134 | 135 | let mut stdout = String::new(); 136 | channel.stdout().read_to_string(&mut stdout)?; 137 | 138 | eprintln!("whoami -> {}", stdout); 139 | 140 | let res = channel.get_exit_status(); 141 | eprintln!("exit status: {:?}", res); 142 | 143 | Ok(()) 144 | } 145 | -------------------------------------------------------------------------------- /libssh-rs/src/channel.rs: -------------------------------------------------------------------------------- 1 | use crate::{opt_cstring_to_cstr, opt_str_to_cstring, Error, SessionHolder, SshResult}; 2 | use libssh_rs_sys as sys; 3 | use std::convert::TryInto; 4 | use std::ffi::{CStr, CString}; 5 | use std::os::raw::c_int; 6 | use std::sync::{Arc, Mutex, MutexGuard}; 7 | use std::time::Duration; 8 | 9 | /// Represents a channel in a `Session`. 10 | /// 11 | /// A `Session` can have multiple channels; there is typically one 12 | /// for the shell/program being run, but additional channels can 13 | /// be opened to forward TCP or other connections. 14 | /// 15 | /// [open_session](#method.open_session) is often the first 16 | /// thing you will call on the `Channel` after creating it; this establishes 17 | /// the channel for executing commands. 18 | /// 19 | /// Then you will typically use either [request_exec](#method.request_exec) 20 | /// to run a non-interactive command, or [request_pty](#method.request_pty) 21 | /// followed [request_shell](#method.request_shell) to set up an interactive 22 | /// remote shell. 23 | /// 24 | /// # Thread Safety 25 | /// 26 | /// `Channel` is strongly associated with the `Session` to which it belongs. 27 | /// libssh doesn't allow using anything associated with a given `Session` 28 | /// from multiple threads concurrently. These Rust bindings encapsulate 29 | /// the underlying `Session` in an internal mutex, which allows you to 30 | /// safely operate on the various elements of the session and even move 31 | /// them to other threads, but you need to be aware that calling methods 32 | /// on any of those structs will attempt to lock the underlying session, 33 | /// and this can lead to blocking in surprising situations. 34 | pub struct Channel { 35 | pub(crate) sess: Arc>, 36 | pub(crate) chan_inner: sys::ssh_channel, 37 | _callbacks: Box, 38 | callback_state: Box, 39 | } 40 | 41 | unsafe impl Send for Channel {} 42 | 43 | impl Drop for Channel { 44 | fn drop(&mut self) { 45 | unsafe { 46 | // Prevent any callbacks firing as part the remainder of this drop operation 47 | sys::ssh_remove_channel_callbacks(self.chan_inner, self._callbacks.as_mut()); 48 | } 49 | let (_sess, chan) = self.lock_session(); 50 | unsafe { 51 | sys::ssh_channel_free(chan); 52 | } 53 | } 54 | } 55 | 56 | /// State visible to the callbacks 57 | struct CallbackState { 58 | signal_state: Mutex>, 59 | } 60 | 61 | #[derive(Clone, Debug)] 62 | pub struct SignalState { 63 | pub signal_name: Option, 64 | pub core_dumped: bool, 65 | pub error_message: Option, 66 | pub language: Option, 67 | } 68 | 69 | fn cstr_to_opt_string(cstr: *const ::std::os::raw::c_char) -> Option { 70 | if cstr.is_null() { 71 | return None; 72 | } 73 | 74 | Some( 75 | unsafe { CStr::from_ptr(cstr) } 76 | .to_string_lossy() 77 | .to_string(), 78 | ) 79 | } 80 | 81 | unsafe extern "C" fn handle_exit_signal( 82 | _session: sys::ssh_session, 83 | _channel: sys::ssh_channel, 84 | signal: *const ::std::os::raw::c_char, 85 | core_dumped: ::std::os::raw::c_int, 86 | errmsg: *const ::std::os::raw::c_char, 87 | lang: *const ::std::os::raw::c_char, 88 | userdata: *mut ::std::os::raw::c_void, 89 | ) { 90 | let callback_state: &CallbackState = &*(userdata as *const CallbackState); 91 | 92 | let signal_name = cstr_to_opt_string(signal); 93 | let error_message = cstr_to_opt_string(errmsg); 94 | let language = cstr_to_opt_string(lang); 95 | 96 | callback_state 97 | .signal_state 98 | .lock() 99 | .unwrap() 100 | .replace(SignalState { 101 | signal_name, 102 | core_dumped: if core_dumped == 0 { false } else { true }, 103 | error_message, 104 | language, 105 | }); 106 | } 107 | 108 | impl Channel { 109 | /// Accept an X11 forwarding channel. 110 | /// Returns a newly created `Channel`, or `None` if no X11 request from the server. 111 | pub fn accept_x11(&self, timeout: std::time::Duration) -> Option { 112 | let (_sess, chan) = self.lock_session(); 113 | let timeout = timeout.as_millis(); 114 | let chan = unsafe { sys::ssh_channel_accept_x11(chan, timeout.try_into().unwrap()) }; 115 | if chan.is_null() { 116 | None 117 | } else { 118 | Some(Self::new(&self.sess, chan)) 119 | } 120 | } 121 | 122 | pub(crate) fn new(sess: &Arc>, chan: sys::ssh_channel) -> Self { 123 | let callback_state = Box::new(CallbackState { 124 | signal_state: Mutex::new(None), 125 | }); 126 | 127 | let callbacks = Box::new(sys::ssh_channel_callbacks_struct { 128 | size: std::mem::size_of::(), 129 | userdata: callback_state.as_ref() as *const CallbackState as *mut _, 130 | channel_data_function: None, 131 | channel_eof_function: None, 132 | channel_close_function: None, 133 | channel_signal_function: None, 134 | channel_exit_status_function: None, 135 | channel_exit_signal_function: Some(handle_exit_signal), 136 | channel_pty_request_function: None, 137 | channel_shell_request_function: None, 138 | channel_auth_agent_req_function: None, 139 | channel_x11_req_function: None, 140 | channel_pty_window_change_function: None, 141 | channel_exec_request_function: None, 142 | channel_env_request_function: None, 143 | channel_subsystem_request_function: None, 144 | channel_write_wontblock_function: None, 145 | channel_open_response_function: None, 146 | channel_request_response_function: None, 147 | }); 148 | 149 | unsafe { sys::ssh_set_channel_callbacks(chan, callbacks.as_ref() as *const _ as *mut _) }; 150 | 151 | Self { 152 | sess: Arc::clone(&sess), 153 | chan_inner: chan, 154 | callback_state, 155 | _callbacks: callbacks, 156 | } 157 | } 158 | 159 | fn lock_session(&self) -> (MutexGuard, sys::ssh_channel) { 160 | (self.sess.lock().unwrap(), self.chan_inner) 161 | } 162 | 163 | /// Close a channel. 164 | /// This sends an end of file and then closes the channel. 165 | /// You won't be able to recover any data the server was going 166 | /// to send or was in buffers. 167 | pub fn close(&self) -> SshResult<()> { 168 | let (sess, chan) = self.lock_session(); 169 | let res = unsafe { sys::ssh_channel_close(chan) }; 170 | sess.basic_status(res, "error closing channel") 171 | } 172 | 173 | /// Get the exit status of the channel 174 | /// (error code from the executed instruction). 175 | /// This function may block until a timeout (or never) if the other 176 | /// side is not willing to close the channel. 177 | pub fn get_exit_status(&self) -> Option { 178 | let (_sess, chan) = self.lock_session(); 179 | let res = unsafe { sys::ssh_channel_get_exit_status(chan) }; 180 | if res == -1 { 181 | None 182 | } else { 183 | Some(res) 184 | } 185 | } 186 | 187 | /// Get the exit signal status of the channel. 188 | /// If the channel was closed/terminated due to a signal, and the 189 | /// remote system supports the signal concept, the signal state 190 | /// will be set and reported here. 191 | pub fn get_exit_signal(&self) -> Option { 192 | self.callback_state.signal_state.lock().unwrap().clone() 193 | } 194 | 195 | /// Check if the channel is closed or not. 196 | pub fn is_closed(&self) -> bool { 197 | let (_sess, chan) = self.lock_session(); 198 | unsafe { sys::ssh_channel_is_closed(chan) != 0 } 199 | } 200 | 201 | /// Check if remote has sent an EOF. 202 | pub fn is_eof(&self) -> bool { 203 | let (_sess, chan) = self.lock_session(); 204 | unsafe { sys::ssh_channel_is_eof(chan) != 0 } 205 | } 206 | 207 | /// Send an end of file on the channel. 208 | /// 209 | /// You should call this when you have no additional data to send 210 | /// to the channel to signal that information to the remote host. 211 | /// 212 | /// This doesn't close the channel. 213 | /// You may still read from it but not write. 214 | pub fn send_eof(&self) -> SshResult<()> { 215 | let (sess, chan) = self.lock_session(); 216 | let res = unsafe { sys::ssh_channel_send_eof(chan) }; 217 | sess.basic_status(res, "ssh_channel_send_eof failed") 218 | } 219 | 220 | /// Check if the channel is open or not. 221 | pub fn is_open(&self) -> bool { 222 | let (_sess, chan) = self.lock_session(); 223 | unsafe { sys::ssh_channel_is_open(chan) != 0 } 224 | } 225 | 226 | /// Open an agent authentication forwarding channel. 227 | /// This type of channel can be opened by a *server* towards a 228 | /// client in order to provide SSH-Agent services to the server-side 229 | /// process. This channel can only be opened if the client claimed 230 | /// support by sending a channel request beforehand. 231 | pub fn open_auth_agent(&self) -> SshResult<()> { 232 | let (sess, chan) = self.lock_session(); 233 | let res = unsafe { sys::ssh_channel_open_auth_agent(chan) }; 234 | sess.basic_status(res, "ssh_channel_open_auth_agent failed") 235 | } 236 | 237 | /// Send an `"auth-agent-req"` channel request over an existing session channel. 238 | /// 239 | /// This client-side request will enable forwarding the agent 240 | /// over a secure tunnel. When the server is ready to open one 241 | /// authentication agent channel, an 242 | /// ssh_channel_open_request_auth_agent_callback event will be generated. 243 | pub fn request_auth_agent(&self) -> SshResult<()> { 244 | let (sess, chan) = self.lock_session(); 245 | let res = unsafe { sys::ssh_channel_request_auth_agent(chan) }; 246 | sess.basic_status(res, "ssh_channel_request_auth_agent failed") 247 | } 248 | 249 | /// Set environment variable. 250 | /// Some environment variables may be refused by security reasons. 251 | pub fn request_env(&self, name: &str, value: &str) -> SshResult<()> { 252 | let (sess, chan) = self.lock_session(); 253 | let name = CString::new(name)?; 254 | let value = CString::new(value)?; 255 | let res = unsafe { sys::ssh_channel_request_env(chan, name.as_ptr(), value.as_ptr()) }; 256 | sess.basic_status(res, "ssh_channel_request_env failed") 257 | } 258 | 259 | /// Requests a shell; asks the server to spawn the user's shell, 260 | /// rather than directly executing a command specified by the client. 261 | /// 262 | /// The channel must be a session channel; you need to have called 263 | /// [open_session](#method.open_session) before this will succeed. 264 | pub fn request_shell(&self) -> SshResult<()> { 265 | let (sess, chan) = self.lock_session(); 266 | let res = unsafe { sys::ssh_channel_request_shell(chan) }; 267 | sess.basic_status(res, "ssh_channel_request_shell failed") 268 | } 269 | 270 | /// Run a shell command without an interactive shell. 271 | /// This is similar to 'sh -c command'. 272 | /// 273 | /// The channel must be a session channel; you need to have called 274 | /// [open_session](#method.open_session) before this will succeed. 275 | pub fn request_exec(&self, command: &str) -> SshResult<()> { 276 | let (sess, chan) = self.lock_session(); 277 | let command = CString::new(command)?; 278 | let res = unsafe { sys::ssh_channel_request_exec(chan, command.as_ptr()) }; 279 | sess.basic_status(res, "ssh_channel_request_exec failed") 280 | } 281 | 282 | /// Request a subsystem. 283 | /// 284 | /// You probably don't need this unless you know what you are doing! 285 | pub fn request_subsystem(&self, subsys: &str) -> SshResult<()> { 286 | let (sess, chan) = self.lock_session(); 287 | let subsys = CString::new(subsys)?; 288 | let res = unsafe { sys::ssh_channel_request_subsystem(chan, subsys.as_ptr()) }; 289 | sess.basic_status(res, "ssh_channel_request_subsystem failed") 290 | } 291 | 292 | /// Request a PTY with a specific type and size. 293 | /// A PTY is useful when you want to run an interactive program on 294 | /// the remote host. 295 | /// 296 | /// `term` is the initial value for the `TERM` environment variable. 297 | /// If you're not sure what to fill for the values, 298 | /// `term = "xterm"`, `columns = 80` and `rows = 24` are reasonable 299 | /// defaults. 300 | pub fn request_pty(&self, term: &str, columns: u32, rows: u32) -> SshResult<()> { 301 | let (sess, chan) = self.lock_session(); 302 | let term = CString::new(term)?; 303 | let res = unsafe { 304 | sys::ssh_channel_request_pty_size( 305 | chan, 306 | term.as_ptr(), 307 | columns.try_into().unwrap(), 308 | rows.try_into().unwrap(), 309 | ) 310 | }; 311 | sess.basic_status(res, "ssh_channel_request_pty_size failed") 312 | } 313 | 314 | /// Informs the server that the local size of the PTY has changed 315 | pub fn change_pty_size(&self, columns: u32, rows: u32) -> SshResult<()> { 316 | let (sess, chan) = self.lock_session(); 317 | let res = unsafe { 318 | sys::ssh_channel_change_pty_size( 319 | chan, 320 | columns.try_into().unwrap(), 321 | rows.try_into().unwrap(), 322 | ) 323 | }; 324 | sess.basic_status(res, "ssh_channel_change_pty_size failed") 325 | } 326 | 327 | /// Send a break signal to the server (as described in RFC 4335). 328 | /// Sends a break signal to the remote process. Note, that remote 329 | /// system may not support breaks. In such a case this request will 330 | /// be silently ignored. 331 | pub fn request_send_break(&self, length: Duration) -> SshResult<()> { 332 | let (sess, chan) = self.lock_session(); 333 | let res = unsafe { sys::ssh_channel_request_send_break(chan, length.as_millis() as _) }; 334 | sess.basic_status(res, "ssh_channel_request_send_break failed") 335 | } 336 | 337 | /// Send a signal to remote process (as described in RFC 4254, section 6.9). 338 | /// Sends a signal to the remote process. 339 | /// Note, that remote system may not support signals concept. 340 | /// In such a case this request will be silently ignored. 341 | /// 342 | /// `signal` is the name of the signal, without the `"SIG"` prefix. 343 | /// For example, `"ABRT"`, `"INT"`, `"KILL"` and so on. 344 | /// 345 | /// The OpenSSH server has only supported signals since OpenSSH version 8.1, 346 | /// released in 2019. 347 | /// 348 | pub fn request_send_signal(&self, signal: &str) -> SshResult<()> { 349 | let (sess, chan) = self.lock_session(); 350 | let signal = CString::new(signal)?; 351 | let res = unsafe { sys::ssh_channel_request_send_signal(chan, signal.as_ptr()) }; 352 | sess.basic_status(res, "ssh_channel_request_send_signal failed") 353 | } 354 | 355 | /// Open a TCP/IP forwarding channel. 356 | /// `remote_host`, `remote_port` identify the destination for the 357 | /// connection. 358 | /// `source_host`, `source_port` identify the origin of the connection 359 | /// on the client side; these are used primarily for logging purposes. 360 | /// 361 | /// This function does not bind the source port and does not 362 | /// automatically forward the content of a socket to the channel. 363 | /// You still have to read/write this channel object to achieve that. 364 | pub fn open_forward( 365 | &self, 366 | remote_host: &str, 367 | remote_port: u16, 368 | source_host: &str, 369 | source_port: u16, 370 | ) -> SshResult<()> { 371 | let (sess, chan) = self.lock_session(); 372 | let remote_host = CString::new(remote_host)?; 373 | let source_host = CString::new(source_host)?; 374 | let res = unsafe { 375 | sys::ssh_channel_open_forward( 376 | chan, 377 | remote_host.as_ptr(), 378 | remote_port as i32, 379 | source_host.as_ptr(), 380 | source_port as i32, 381 | ) 382 | }; 383 | sess.basic_status(res, "ssh_channel_open_forward failed") 384 | } 385 | 386 | /// Open a UNIX domain socket forwarding channel. 387 | /// `remote_path` is the path to the unix socket to open on the remote 388 | /// machine. 389 | /// `source_host` and `source_port` identify the originating connection 390 | /// from the client machine and are used for logging purposes. 391 | /// 392 | /// This function does not bind the source and does not 393 | /// automatically forward the content of a socket to the channel. 394 | /// You still have to read/write this channel object to achieve that. 395 | pub fn open_forward_unix( 396 | &self, 397 | remote_path: &str, 398 | source_host: &str, 399 | source_port: u16, 400 | ) -> SshResult<()> { 401 | let (sess, chan) = self.lock_session(); 402 | let remote_path = CString::new(remote_path)?; 403 | let source_host = CString::new(source_host)?; 404 | let res = unsafe { 405 | sys::ssh_channel_open_forward_unix( 406 | chan, 407 | remote_path.as_ptr(), 408 | source_host.as_ptr(), 409 | source_port as i32, 410 | ) 411 | }; 412 | sess.basic_status(res, "ssh_channel_open_forward_unix failed") 413 | } 414 | 415 | /// Sends the `"x11-req"` channel request over an existing session channel. 416 | /// This will enable redirecting the display of the remote X11 applications 417 | /// to local X server over an secure tunnel. 418 | pub fn request_x11( 419 | &self, 420 | single_connection: bool, 421 | protocol: Option<&str>, 422 | cookie: Option<&str>, 423 | screen_number: c_int, 424 | ) -> SshResult<()> { 425 | let (sess, chan) = self.lock_session(); 426 | let protocol = opt_str_to_cstring(protocol); 427 | let cookie = opt_str_to_cstring(cookie); 428 | let res = unsafe { 429 | sys::ssh_channel_request_x11( 430 | chan, 431 | if single_connection { 1 } else { 0 }, 432 | opt_cstring_to_cstr(&protocol), 433 | opt_cstring_to_cstr(&cookie), 434 | screen_number, 435 | ) 436 | }; 437 | 438 | sess.basic_status(res, "ssh_channel_open_forward failed") 439 | } 440 | 441 | /// Open a session channel (suited for a shell, not TCP forwarding). 442 | pub fn open_session(&self) -> SshResult<()> { 443 | let (sess, chan) = self.lock_session(); 444 | let res = unsafe { sys::ssh_channel_open_session(chan) }; 445 | sess.basic_status(res, "ssh_channel_open_session failed") 446 | } 447 | 448 | /// Polls a channel for data to read. 449 | /// Returns the number of bytes available for reading. 450 | /// If `timeout` is None, then blocks until data is available. 451 | pub fn poll_timeout( 452 | &self, 453 | is_stderr: bool, 454 | timeout: Option, 455 | ) -> SshResult { 456 | let (sess, chan) = self.lock_session(); 457 | let timeout = match timeout { 458 | Some(t) => t.as_millis() as c_int, 459 | None => -1, 460 | }; 461 | let res = 462 | unsafe { sys::ssh_channel_poll_timeout(chan, if is_stderr { 1 } else { 0 }, timeout) }; 463 | match res { 464 | sys::SSH_ERROR => { 465 | if let Some(err) = sess.last_error() { 466 | Err(err) 467 | } else { 468 | Err(Error::fatal("ssh_channel_poll failed")) 469 | } 470 | } 471 | sys::SSH_EOF => Ok(PollStatus::EndOfFile), 472 | n if n >= 0 => Ok(PollStatus::AvailableBytes(n as u32)), 473 | n => Err(Error::Fatal(format!( 474 | "ssh_channel_poll returned unexpected {} value", 475 | n 476 | ))), 477 | } 478 | } 479 | 480 | /// Reads data from a channel. 481 | /// This function may fewer bytes than the buf size. 482 | pub fn read_timeout( 483 | &self, 484 | buf: &mut [u8], 485 | is_stderr: bool, 486 | timeout: Option, 487 | ) -> SshResult { 488 | let (sess, chan) = self.lock_session(); 489 | 490 | let timeout = match timeout { 491 | Some(t) => t.as_millis() as c_int, 492 | None => -1, 493 | }; 494 | let res = unsafe { 495 | sys::ssh_channel_read_timeout( 496 | chan, 497 | buf.as_mut_ptr() as _, 498 | buf.len() as u32, 499 | if is_stderr { 1 } else { 0 }, 500 | timeout, 501 | ) 502 | }; 503 | match res { 504 | sys::SSH_ERROR => { 505 | if let Some(err) = sess.last_error() { 506 | Err(err) 507 | } else { 508 | Err(Error::fatal("ssh_channel_read_timeout failed")) 509 | } 510 | } 511 | sys::SSH_AGAIN => Err(Error::TryAgain), 512 | n if n < 0 => Err(Error::Fatal(format!( 513 | "ssh_channel_read_timeout returned unexpected {} value", 514 | n 515 | ))), 516 | 0 if !sess.is_blocking() => Err(Error::TryAgain), 517 | n => Ok(n as usize), 518 | } 519 | } 520 | 521 | /// Do a nonblocking read on the channel. 522 | /// A nonblocking read on the specified channel. it will return <= count bytes of data read atomically. 523 | pub fn read_nonblocking(&self, buf: &mut [u8], is_stderr: bool) -> SshResult { 524 | let (sess, chan) = self.lock_session(); 525 | 526 | let res = unsafe { 527 | sys::ssh_channel_read_nonblocking( 528 | chan, 529 | buf.as_mut_ptr() as _, 530 | buf.len() as u32, 531 | if is_stderr { 1 } else { 0 }, 532 | ) 533 | }; 534 | match res { 535 | sys::SSH_ERROR => { 536 | if let Some(err) = sess.last_error() { 537 | Err(err) 538 | } else { 539 | Err(Error::fatal("ssh_channel_read_timeout failed")) 540 | } 541 | } 542 | sys::SSH_EOF => Ok(0 as usize), 543 | n if n < 0 => Err(Error::Fatal(format!( 544 | "ssh_channel_read_timeout returned unexpected value: {n}" 545 | ))), 546 | n => Ok(n as usize), 547 | } 548 | } 549 | 550 | /// Get the remote window size. 551 | /// This is the maximum amounts of bytes the remote side expects us to send 552 | /// before growing the window again. 553 | /// A nonzero return value does not guarantee the socket is ready to send that much data. 554 | /// Buffering may happen in the local SSH packet buffer, so beware of really big window sizes. 555 | /// A zero return value means that a write will block (if the session is in blocking mode) 556 | /// until the window grows back. 557 | pub fn window_size(&self) -> usize { 558 | let (_sess, chan) = self.lock_session(); 559 | unsafe { sys::ssh_channel_window_size(chan).try_into().unwrap() } 560 | } 561 | 562 | fn read_impl(&self, buf: &mut [u8], is_stderr: bool) -> std::io::Result { 563 | Ok(self.read_timeout(buf, is_stderr, None)?) 564 | } 565 | 566 | fn write_impl(&self, buf: &[u8], is_stderr: bool) -> SshResult { 567 | let (sess, chan) = self.lock_session(); 568 | 569 | let res = unsafe { 570 | (if is_stderr { 571 | sys::ssh_channel_write_stderr 572 | } else { 573 | sys::ssh_channel_write 574 | })(chan, buf.as_ptr() as _, buf.len() as _) 575 | }; 576 | 577 | match res { 578 | sys::SSH_ERROR => { 579 | if let Some(err) = sess.last_error() { 580 | Err(err) 581 | } else { 582 | Err(Error::fatal("ssh_channel_read_timeout failed")) 583 | } 584 | } 585 | sys::SSH_AGAIN => Err(Error::TryAgain), 586 | n if n < 0 => Err(Error::Fatal(format!( 587 | "ssh_channel_read_timeout returned unexpected {} value", 588 | n 589 | ))), 590 | n => Ok(n as usize), 591 | } 592 | } 593 | 594 | /// Returns a struct that implements `std::io::Read` 595 | /// and that will read data from the stdout channel. 596 | pub fn stdout(&self) -> impl std::io::Read + '_ { 597 | ChannelStdout { chan: self } 598 | } 599 | 600 | /// Returns a struct that implements `std::io::Read` 601 | /// and that will read data from the stderr channel. 602 | pub fn stderr(&self) -> impl std::io::Read + '_ { 603 | ChannelStderr { chan: self } 604 | } 605 | 606 | /// Returns a struct that implements `std::io::Write` 607 | /// and that will write data to the stdin channel 608 | pub fn stdin(&self) -> impl std::io::Write + '_ { 609 | ChannelStdin { chan: self } 610 | } 611 | } 612 | 613 | /// Represents the stdin stream for the channel. 614 | /// Implements std::io::Write; writing to this struct 615 | /// will write to the stdin of the channel. 616 | struct ChannelStdin<'a> { 617 | chan: &'a Channel, 618 | } 619 | 620 | impl<'a> std::io::Write for ChannelStdin<'a> { 621 | fn flush(&mut self) -> std::io::Result<()> { 622 | Ok(self.chan.sess.lock().unwrap().blocking_flush(None)?) 623 | } 624 | 625 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 626 | Ok(self.chan.write_impl(buf, false)?) 627 | } 628 | } 629 | 630 | /// Represents the stdout stream for the channel. 631 | /// Implements std::io::Read; reading from this struct 632 | /// will read from the stdout of the channel. 633 | struct ChannelStdout<'a> { 634 | chan: &'a Channel, 635 | } 636 | 637 | impl<'a> std::io::Read for ChannelStdout<'a> { 638 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 639 | self.chan.read_impl(buf, false) 640 | } 641 | } 642 | 643 | /// Represents the stderr stream for the channel. 644 | /// Implements std::io::Read; reading from this struct 645 | /// will read from the stderr of the channel. 646 | struct ChannelStderr<'a> { 647 | chan: &'a Channel, 648 | } 649 | 650 | impl<'a> std::io::Read for ChannelStderr<'a> { 651 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 652 | self.chan.read_impl(buf, true) 653 | } 654 | } 655 | 656 | /// Indicates available data for the stdout or stderr on a `Channel`. 657 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 658 | pub enum PollStatus { 659 | /// The available bytes to read; may be 0 660 | AvailableBytes(u32), 661 | /// The channel is in the EOF state 662 | EndOfFile, 663 | } 664 | -------------------------------------------------------------------------------- /libssh-rs/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Represents an error condition 4 | #[derive(Error, Debug, PartialEq, Eq)] 5 | pub enum Error { 6 | /// The last request was denied but situation is recoverable 7 | #[error("RequestDenied: {}", .0)] 8 | RequestDenied(String), 9 | /// A fatal error occurred. This could be an unexpected disconnection 10 | #[error("Fatal: {}", .0)] 11 | Fatal(String), 12 | /// The session is in non-blocking mode and the call must be tried again 13 | #[error("TryAgain")] 14 | TryAgain, 15 | 16 | #[error("SftpError: {}", .0)] 17 | Sftp(crate::sftp::SftpError), 18 | } 19 | 20 | /// Represents the result of a fallible operation 21 | pub type SshResult = Result; 22 | 23 | impl Error { 24 | pub fn is_try_again(&self) -> bool { 25 | matches!(self, Self::TryAgain) 26 | } 27 | 28 | pub fn fatal>(s: S) -> Self { 29 | Self::Fatal(s.into()) 30 | } 31 | } 32 | 33 | impl From for Error { 34 | fn from(err: std::io::Error) -> Error { 35 | Error::fatal(err.to_string()) 36 | } 37 | } 38 | 39 | impl From for std::io::Error { 40 | fn from(err: Error) -> std::io::Error { 41 | match err { 42 | Error::TryAgain => std::io::Error::new(std::io::ErrorKind::WouldBlock, "TryAgain"), 43 | Error::RequestDenied(msg) | Error::Fatal(msg) => { 44 | std::io::Error::new(std::io::ErrorKind::Other, msg) 45 | } 46 | Error::Sftp(err) => std::io::Error::new(std::io::ErrorKind::Other, err.to_string()), 47 | } 48 | } 49 | } 50 | 51 | impl From for Error { 52 | fn from(err: std::ffi::NulError) -> Error { 53 | Error::Fatal(err.to_string()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /libssh-rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides ergonomic bindings to the functions 2 | //! provided by [libssh](https://libssh.org), a library that provides 3 | //! an implementation of the SSH 2 protocol. It is distinct from the 4 | //! `ssh2` rust crate which uses [libssh2](https://www.libssh2.org), 5 | //! which is an unrelated project that implements similar functionality! 6 | 7 | // This is a bad lint 8 | #![allow(clippy::wildcard_in_or_patterns)] 9 | 10 | /// Re-exporting the underlying unsafe API, should you need it 11 | pub use libssh_rs_sys as sys; 12 | 13 | use std::ffi::{CStr, CString}; 14 | use std::os::raw::{c_int, c_uint, c_ulong}; 15 | #[cfg(unix)] 16 | use std::os::unix::io::RawFd as RawSocket; 17 | #[cfg(windows)] 18 | use std::os::windows::io::RawSocket; 19 | use std::ptr::null_mut; 20 | use std::sync::LazyLock; 21 | use std::sync::{Arc, Mutex, MutexGuard}; 22 | use std::time::Duration; 23 | 24 | mod channel; 25 | mod error; 26 | mod sftp; 27 | 28 | pub use crate::channel::*; 29 | pub use crate::error::*; 30 | pub use crate::sftp::*; 31 | 32 | struct LibraryState {} 33 | impl LibraryState { 34 | pub fn new() -> Option { 35 | // Force openssl to initialize. 36 | // In theory, we don't need this, but in practice we do because of 37 | // this bug: 38 | // 39 | // which weirdly requires that *all* openssl threads be joined before 40 | // the process exits, which is an unrealistic expectation on behalf 41 | // of that library. 42 | // That was worked around in openssl_sys: 43 | // 44 | // which tells openssl to skip the process-wide shutdown. 45 | openssl_sys::init(); 46 | let res = unsafe { sys::ssh_init() }; 47 | if res != sys::SSH_OK as i32 { 48 | None 49 | } else { 50 | Some(Self {}) 51 | } 52 | } 53 | } 54 | impl Drop for LibraryState { 55 | fn drop(&mut self) { 56 | unsafe { sys::ssh_finalize() }; 57 | } 58 | } 59 | 60 | static LIB: LazyLock> = LazyLock::new(|| LibraryState::new()); 61 | 62 | fn initialize() -> SshResult<()> { 63 | if LIB.is_none() { 64 | Err(Error::fatal("ssh_init failed")) 65 | } else { 66 | Ok(()) 67 | } 68 | } 69 | 70 | pub(crate) struct SessionHolder { 71 | sess: sys::ssh_session, 72 | callbacks: sys::ssh_callbacks_struct, 73 | auth_callback: Option) -> SshResult>>, 74 | pending_agent_forward_channels: Vec, 75 | } 76 | unsafe impl Send for SessionHolder {} 77 | 78 | impl std::ops::Deref for SessionHolder { 79 | type Target = sys::ssh_session; 80 | fn deref(&self) -> &sys::ssh_session { 81 | &self.sess 82 | } 83 | } 84 | 85 | impl Drop for SessionHolder { 86 | fn drop(&mut self) { 87 | self.clear_pending_agent_forward_channels(); 88 | unsafe { 89 | sys::ssh_free(self.sess); 90 | } 91 | } 92 | } 93 | 94 | impl SessionHolder { 95 | pub fn is_blocking(&self) -> bool { 96 | unsafe { sys::ssh_is_blocking(self.sess) != 0 } 97 | } 98 | 99 | fn last_error(&self) -> Option { 100 | let code = unsafe { sys::ssh_get_error_code(self.sess as _) } as sys::ssh_error_types_e; 101 | if code == sys::ssh_error_types_e_SSH_NO_ERROR { 102 | return None; 103 | } 104 | 105 | let reason = unsafe { sys::ssh_get_error(self.sess as _) }; 106 | let reason = if reason.is_null() { 107 | String::new() 108 | } else { 109 | unsafe { CStr::from_ptr(reason) } 110 | .to_string_lossy() 111 | .to_string() 112 | }; 113 | 114 | if code == sys::ssh_error_types_e_SSH_REQUEST_DENIED { 115 | Some(Error::RequestDenied(reason)) 116 | } else { 117 | Some(Error::Fatal(reason)) 118 | } 119 | } 120 | 121 | fn basic_status(&self, res: i32, what: &str) -> SshResult<()> { 122 | if res == sys::SSH_OK as i32 { 123 | Ok(()) 124 | } else if res == sys::SSH_AGAIN { 125 | Err(Error::TryAgain) 126 | } else if let Some(err) = self.last_error() { 127 | Err(err) 128 | } else { 129 | Err(Error::fatal(what)) 130 | } 131 | } 132 | 133 | fn blocking_flush(&self, timeout: Option) -> SshResult<()> { 134 | let timeout = match timeout { 135 | Some(t) => t.as_millis() as c_int, 136 | None => -1, 137 | }; 138 | let res = unsafe { sys::ssh_blocking_flush(self.sess, timeout) }; 139 | self.basic_status(res, "blocking_flush") 140 | } 141 | 142 | fn auth_result(&self, res: sys::ssh_auth_e, what: &str) -> SshResult { 143 | match res { 144 | sys::ssh_auth_e_SSH_AUTH_SUCCESS => Ok(AuthStatus::Success), 145 | sys::ssh_auth_e_SSH_AUTH_DENIED => Ok(AuthStatus::Denied), 146 | sys::ssh_auth_e_SSH_AUTH_PARTIAL => Ok(AuthStatus::Partial), 147 | sys::ssh_auth_e_SSH_AUTH_INFO => Ok(AuthStatus::Info), 148 | sys::ssh_auth_e_SSH_AUTH_AGAIN => Ok(AuthStatus::Again), 149 | sys::ssh_auth_e_SSH_AUTH_ERROR | _ => { 150 | if let Some(err) = self.last_error() { 151 | Err(err) 152 | } else { 153 | Err(Error::fatal(what)) 154 | } 155 | } 156 | } 157 | } 158 | 159 | fn clear_pending_agent_forward_channels(&mut self) { 160 | for chan in self.pending_agent_forward_channels.drain(..) { 161 | unsafe { 162 | // We have no callbacks on these channels, no need to cleanup. 163 | sys::ssh_channel_free(chan); 164 | } 165 | } 166 | } 167 | } 168 | 169 | /// A Session represents the state needed to make a connection to 170 | /// a remote host. 171 | /// 172 | /// You need at least one Session per target host. 173 | /// A given session can open multiple `Channel`s to perform multiple actions 174 | /// on a given target host. 175 | /// 176 | /// # Thread Safety 177 | /// 178 | /// libssh doesn't allow using anything associated with a given `Session` 179 | /// from multiple threads concurrently. These Rust bindings encapsulate 180 | /// the underlying `Session` in an internal mutex, which allows you to 181 | /// safely operate on the various elements of the session and even move 182 | /// them to other threads, but you need to be aware that calling methods 183 | /// on any of those structs will attempt to lock the underlying session, 184 | /// and this can lead to blocking in surprising situations. 185 | pub struct Session { 186 | sess: Arc>, 187 | } 188 | 189 | impl Session { 190 | /// Create a new Session. 191 | pub fn new() -> SshResult { 192 | initialize()?; 193 | let sess = unsafe { sys::ssh_new() }; 194 | if sess.is_null() { 195 | Err(Error::fatal("ssh_new failed")) 196 | } else { 197 | let callbacks = sys::ssh_callbacks_struct { 198 | size: std::mem::size_of::(), 199 | userdata: std::ptr::null_mut(), 200 | auth_function: None, 201 | log_function: None, 202 | connect_status_function: None, 203 | global_request_function: None, 204 | channel_open_request_x11_function: None, 205 | channel_open_request_auth_agent_function: None, 206 | channel_open_request_forwarded_tcpip_function: None, 207 | }; 208 | let sess = Arc::new(Mutex::new(SessionHolder { 209 | sess, 210 | callbacks, 211 | auth_callback: None, 212 | pending_agent_forward_channels: Vec::new(), 213 | })); 214 | 215 | { 216 | let mut sess = sess.lock().unwrap(); 217 | let ptr: *mut SessionHolder = &mut *sess; 218 | sess.callbacks.userdata = ptr as _; 219 | 220 | unsafe { 221 | sys::ssh_set_callbacks(**sess, &mut sess.callbacks); 222 | } 223 | } 224 | 225 | Ok(Self { sess }) 226 | } 227 | } 228 | 229 | unsafe extern "C" fn bridge_auth_callback( 230 | prompt: *const ::std::os::raw::c_char, 231 | buf: *mut ::std::os::raw::c_char, 232 | len: usize, 233 | echo: ::std::os::raw::c_int, 234 | verify: ::std::os::raw::c_int, 235 | userdata: *mut ::std::os::raw::c_void, 236 | ) -> ::std::os::raw::c_int { 237 | let prompt = CStr::from_ptr(prompt).to_string_lossy().to_string(); 238 | let echo = if echo == 0 { false } else { true }; 239 | let verify = if verify == 0 { false } else { true }; 240 | 241 | let result = std::panic::catch_unwind(|| { 242 | let sess: &mut SessionHolder = &mut *(userdata as *mut SessionHolder); 243 | 244 | let identity = { 245 | let mut value = std::ptr::null_mut(); 246 | sys::ssh_userauth_publickey_auto_get_current_identity(**sess, &mut value); 247 | if value.is_null() { 248 | None 249 | } else { 250 | let s = CStr::from_ptr(value).to_string_lossy().to_string(); 251 | sys::ssh_string_free_char(value); 252 | Some(s) 253 | } 254 | }; 255 | 256 | let cb = sess.auth_callback.as_mut().unwrap(); 257 | let response = (cb)(&prompt, echo, verify, identity)?; 258 | if response.len() > len { 259 | return Err(Error::Fatal(format!( 260 | "passphrase is larger than buffer allows {} vs available {}", 261 | response.len(), 262 | len 263 | ))); 264 | } 265 | 266 | let len = response.len().min(len); 267 | let buf = std::slice::from_raw_parts_mut(buf as *mut u8, len); 268 | buf.copy_from_slice(response.as_bytes()); 269 | 270 | Ok(()) 271 | }); 272 | 273 | match result { 274 | Err(err) => { 275 | eprintln!("Error in auth callback: {:?}", err); 276 | sys::SSH_ERROR 277 | } 278 | Ok(Err(err)) => { 279 | eprintln!("Error in auth callback: {:#}", err); 280 | sys::SSH_ERROR 281 | } 282 | Ok(Ok(())) => sys::SSH_OK as c_int, 283 | } 284 | } 285 | 286 | unsafe extern "C" fn channel_open_request_auth_agent_callback( 287 | session: sys::ssh_session, 288 | userdata: *mut ::std::os::raw::c_void, 289 | ) -> sys::ssh_channel { 290 | let sess: &mut SessionHolder = &mut *(userdata as *mut SessionHolder); 291 | let chan = sys::ssh_channel_new(session); 292 | if chan.is_null() { 293 | eprintln!("ssh_channel_new failed: {:?}", sess.last_error()); 294 | return std::ptr::null_mut(); 295 | } 296 | // We are guarenteed to be holding a session lock here. 297 | sess.pending_agent_forward_channels.push(chan); 298 | chan 299 | } 300 | 301 | /// Sets a callback that is used by libssh when it needs to prompt 302 | /// for the passphrase during public key authentication. 303 | /// This is NOT used for password or keyboard interactive authentication. 304 | /// The callback has the signature: 305 | /// 306 | /// ```no_run 307 | /// use libssh_rs::SshResult; 308 | /// fn callback(prompt: &str, echo: bool, verify: bool, 309 | /// identity: Option) -> SshResult { 310 | /// unimplemented!() 311 | /// } 312 | /// ``` 313 | /// 314 | /// The `prompt` parameter is the prompt text to show to the user. 315 | /// The `identity` parameter, if not None, will hold the identity that 316 | /// is currently being tried by the `userauth_public_key_auto` method, 317 | /// which is helpful to show to the user so that they can input the 318 | /// correct passphrase. 319 | /// 320 | /// The `echo` parameter, if `true`, means that the input entered by 321 | /// the user should be visible on screen. If `false`, it should not be 322 | /// shown on screen because it is deemed sensitive in some way. 323 | /// 324 | /// The `verify` parameter, if `true`, means that the user should be 325 | /// prompted twice to make sure they entered the same text both times. 326 | /// 327 | /// The function should return the user's input as a string, or an 328 | /// `Error` indicating what went wrong. 329 | /// 330 | /// You can use the `get_input` function to satisfy the auth callback: 331 | /// 332 | /// ``` 333 | /// use libssh_rs::*; 334 | /// let sess = Session::new().unwrap(); 335 | /// sess.set_auth_callback(|prompt, echo, verify, identity| { 336 | /// let prompt = match identity { 337 | /// Some(ident) => format!("{} ({}): ", prompt, ident), 338 | /// None => prompt.to_string(), 339 | /// }; 340 | /// get_input(&prompt, None, echo, verify) 341 | /// .ok_or_else(|| Error::Fatal("reading password".to_string())) 342 | /// }); 343 | /// ``` 344 | pub fn set_auth_callback(&self, callback: F) 345 | where 346 | F: FnMut(&str, bool, bool, Option) -> SshResult + 'static, 347 | { 348 | let mut sess = self.lock_session(); 349 | sess.auth_callback.replace(Box::new(callback)); 350 | sess.callbacks.auth_function = Some(Self::bridge_auth_callback); 351 | } 352 | 353 | /// Enable or disable creating channels when the remote side requests a new channel for SSH 354 | /// agent forwarding. 355 | /// You are supposed to periodically check whether there's pending channels (already bound to 356 | /// remote side's agent client) by using the `accept_agent_forward` function. 357 | pub fn enable_accept_agent_forward(&self, enable: bool) { 358 | let mut sess = self.lock_session(); 359 | sess.callbacks.channel_open_request_auth_agent_function = if enable { 360 | Some(Self::channel_open_request_auth_agent_callback) 361 | } else { 362 | sess.clear_pending_agent_forward_channels(); 363 | // libssh denies auth agent channel requests with no callback set. 364 | None 365 | } 366 | } 367 | 368 | // Accept an auth agent forward channel. 369 | // Returns a `Channel` bound to the remote side SSH agent client, or `None` if no pending 370 | // request from the server. 371 | pub fn accept_agent_forward(&self) -> Option { 372 | let mut sess = self.lock_session(); 373 | let chan = sess.pending_agent_forward_channels.pop()?; 374 | Some(Channel::new(&self.sess, chan)) 375 | } 376 | 377 | /// Create a new channel. 378 | /// Channels are used to handle I/O for commands and forwarded streams. 379 | pub fn new_channel(&self) -> SshResult { 380 | let sess = self.lock_session(); 381 | let chan = unsafe { sys::ssh_channel_new(**sess) }; 382 | if chan.is_null() { 383 | if let Some(err) = sess.last_error() { 384 | Err(err) 385 | } else { 386 | Err(Error::fatal("ssh_channel_new failed")) 387 | } 388 | } else { 389 | Ok(Channel::new(&self.sess, chan)) 390 | } 391 | } 392 | 393 | fn lock_session(&self) -> MutexGuard { 394 | self.sess.lock().unwrap() 395 | } 396 | 397 | /// Blocking flush of the outgoing buffer. 398 | pub fn blocking_flush(&self, timeout: Option) -> SshResult<()> { 399 | let sess = self.lock_session(); 400 | sess.blocking_flush(timeout) 401 | } 402 | 403 | /// Disconnect from a session (client or server). 404 | /// The session can then be reused to open a new session. 405 | pub fn disconnect(&self) { 406 | let sess = self.lock_session(); 407 | unsafe { sys::ssh_disconnect(**sess) }; 408 | } 409 | 410 | /// Connect to the configured remote host 411 | pub fn connect(&self) -> SshResult<()> { 412 | let sess = self.lock_session(); 413 | let res = unsafe { sys::ssh_connect(**sess) }; 414 | sess.basic_status(res, "ssh_connect failed") 415 | } 416 | 417 | /// Check if the servers public key for the connected session is known. 418 | /// This checks if we already know the public key of the server we want 419 | /// to connect to. This allows to detect if there is a MITM attack going 420 | /// on of if there have been changes on the server we don't know about. 421 | pub fn is_known_server(&self) -> SshResult { 422 | let sess = self.lock_session(); 423 | match unsafe { sys::ssh_session_is_known_server(**sess) } { 424 | sys::ssh_known_hosts_e_SSH_KNOWN_HOSTS_NOT_FOUND => Ok(KnownHosts::NotFound), 425 | sys::ssh_known_hosts_e_SSH_KNOWN_HOSTS_UNKNOWN => Ok(KnownHosts::Unknown), 426 | sys::ssh_known_hosts_e_SSH_KNOWN_HOSTS_OK => Ok(KnownHosts::Ok), 427 | sys::ssh_known_hosts_e_SSH_KNOWN_HOSTS_CHANGED => Ok(KnownHosts::Changed), 428 | sys::ssh_known_hosts_e_SSH_KNOWN_HOSTS_OTHER => Ok(KnownHosts::Other), 429 | sys::ssh_known_hosts_e_SSH_KNOWN_HOSTS_ERROR | _ => { 430 | if let Some(err) = sess.last_error() { 431 | Err(err) 432 | } else { 433 | Err(Error::fatal("unknown error in ssh_session_is_known_server")) 434 | } 435 | } 436 | } 437 | } 438 | 439 | /// Add the current connected server to the user known_hosts file. 440 | /// This adds the currently connected server to the known_hosts file 441 | /// by appending a new line at the end. The global known_hosts file 442 | /// is considered read-only so it is not touched by this function. 443 | pub fn update_known_hosts_file(&self) -> SshResult<()> { 444 | let sess = self.lock_session(); 445 | let res = unsafe { sys::ssh_session_update_known_hosts(**sess) }; 446 | 447 | if res == sys::SSH_OK as i32 { 448 | Ok(()) 449 | } else if let Some(err) = sess.last_error() { 450 | Err(err) 451 | } else { 452 | Err(Error::fatal("error updating known hosts file")) 453 | } 454 | } 455 | 456 | /// Parse the ssh config file. 457 | /// This should be the last call of all options, it may overwrite options 458 | /// which are already set. 459 | /// It requires that the `SshOption::Hostname` is already set. 460 | /// if `file_name` is None the default `~/.ssh/config` will be used. 461 | pub fn options_parse_config(&self, file_name: Option<&str>) -> SshResult<()> { 462 | let sess = self.lock_session(); 463 | let file_name = opt_str_to_cstring(file_name); 464 | let res = unsafe { sys::ssh_options_parse_config(**sess, opt_cstring_to_cstr(&file_name)) }; 465 | if res == 0 { 466 | Ok(()) 467 | } else if let Some(err) = sess.last_error() { 468 | Err(err) 469 | } else { 470 | Err(Error::Fatal(format!( 471 | "error parsing config file: {:?}", 472 | file_name 473 | ))) 474 | } 475 | } 476 | 477 | /// Get the issue banner from the server. 478 | /// This is the banner showing a disclaimer to users who log in, 479 | /// typically their right or the fact that they will be monitored. 480 | pub fn get_issue_banner(&self) -> SshResult { 481 | let sess = self.lock_session(); 482 | let banner = unsafe { sys::ssh_get_issue_banner(**sess) }; 483 | if banner.is_null() { 484 | if let Some(err) = sess.last_error() { 485 | Err(err) 486 | } else { 487 | Err(Error::fatal("failed to get issue banner")) 488 | } 489 | } else { 490 | let banner_text = unsafe { CStr::from_ptr(banner) } 491 | .to_string_lossy() 492 | .to_string(); 493 | unsafe { sys::ssh_string_free_char(banner) }; 494 | Ok(banner_text) 495 | } 496 | } 497 | 498 | /// Gets the server banner. 499 | /// This typically holds the server version information 500 | pub fn get_server_banner(&self) -> SshResult { 501 | let sess = self.lock_session(); 502 | let banner = unsafe { sys::ssh_get_serverbanner(**sess) }; 503 | if banner.is_null() { 504 | if let Some(err) = sess.last_error() { 505 | Err(err) 506 | } else { 507 | Err(Error::fatal("failed to get server banner")) 508 | } 509 | } else { 510 | let banner_text = unsafe { CStr::from_ptr(banner) } 511 | .to_string_lossy() 512 | .to_string(); 513 | Ok(banner_text) 514 | } 515 | } 516 | 517 | /// Returns the user name that will be used to authenticate with the remote host 518 | pub fn get_user_name(&self) -> SshResult { 519 | let sess = self.lock_session(); 520 | let mut name = std::ptr::null_mut(); 521 | let res = unsafe { 522 | sys::ssh_options_get(**sess, sys::ssh_options_e::SSH_OPTIONS_USER, &mut name) 523 | }; 524 | if res != sys::SSH_OK as i32 || name.is_null() { 525 | if let Some(err) = sess.last_error() { 526 | Err(err) 527 | } else { 528 | Err(Error::fatal("error getting user name")) 529 | } 530 | } else { 531 | let user_name = unsafe { CStr::from_ptr(name) } 532 | .to_string_lossy() 533 | .to_string(); 534 | unsafe { sys::ssh_string_free_char(name) }; 535 | Ok(user_name) 536 | } 537 | } 538 | 539 | /// Returns the public key as string 540 | pub fn get_pubkey(&self) -> SshResult { 541 | let sess = self.lock_session(); 542 | let sstring = unsafe { sys::ssh_get_pubkey(**sess) }; 543 | if sstring.is_null() { 544 | if let Some(err) = sess.last_error() { 545 | Err(err) 546 | } else { 547 | Err(Error::fatal("failed to get pubkey")) 548 | } 549 | } else { 550 | let key = unsafe { sys::ssh_string_to_char(sstring) }; 551 | let key_text = unsafe { CStr::from_ptr(key) }.to_string_lossy().to_string(); 552 | unsafe { sys::ssh_string_free_char(key) }; 553 | Ok(key_text) 554 | } 555 | } 556 | 557 | /// Configures the session. 558 | /// You will need to set at least `SshOption::Hostname` prior to 559 | /// connecting, in order for libssh to know where to connect. 560 | pub fn set_option(&self, option: SshOption) -> SshResult<()> { 561 | let sess = self.lock_session(); 562 | let res = match option { 563 | SshOption::LogLevel(level) => unsafe { 564 | let level = match level { 565 | LogLevel::NoLogging => sys::SSH_LOG_NOLOG, 566 | LogLevel::Warning => sys::SSH_LOG_WARNING, 567 | LogLevel::Protocol => sys::SSH_LOG_PROTOCOL, 568 | LogLevel::Packet => sys::SSH_LOG_PACKET, 569 | LogLevel::Functions => sys::SSH_LOG_FUNCTIONS, 570 | } as u32 as c_int; 571 | sys::ssh_options_set( 572 | **sess, 573 | sys::ssh_options_e::SSH_OPTIONS_LOG_VERBOSITY, 574 | &level as *const _ as _, 575 | ) 576 | }, 577 | SshOption::Hostname(name) => unsafe { 578 | let name = CString::new(name)?; 579 | sys::ssh_options_set( 580 | **sess, 581 | sys::ssh_options_e::SSH_OPTIONS_HOST, 582 | name.as_ptr() as _, 583 | ) 584 | }, 585 | SshOption::BindAddress(name) => unsafe { 586 | let name = CString::new(name)?; 587 | sys::ssh_options_set( 588 | **sess, 589 | sys::ssh_options_e::SSH_OPTIONS_BINDADDR, 590 | name.as_ptr() as _, 591 | ) 592 | }, 593 | SshOption::KeyExchange(name) => unsafe { 594 | let name = CString::new(name)?; 595 | sys::ssh_options_set( 596 | **sess, 597 | sys::ssh_options_e::SSH_OPTIONS_KEY_EXCHANGE, 598 | name.as_ptr() as _, 599 | ) 600 | }, 601 | SshOption::HostKeys(name) => unsafe { 602 | let name = CString::new(name)?; 603 | sys::ssh_options_set( 604 | **sess, 605 | sys::ssh_options_e::SSH_OPTIONS_HOSTKEYS, 606 | name.as_ptr() as _, 607 | ) 608 | }, 609 | SshOption::PublicKeyAcceptedTypes(name) => unsafe { 610 | let name = CString::new(name)?; 611 | sys::ssh_options_set( 612 | **sess, 613 | sys::ssh_options_e::SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES, 614 | name.as_ptr() as _, 615 | ) 616 | }, 617 | SshOption::AddIdentity(name) => unsafe { 618 | let name = CString::new(name)?; 619 | sys::ssh_options_set( 620 | **sess, 621 | sys::ssh_options_e::SSH_OPTIONS_ADD_IDENTITY, 622 | name.as_ptr() as _, 623 | ) 624 | }, 625 | SshOption::IdentityAgent(name) => unsafe { 626 | let name = opt_string_to_cstring(name); 627 | sys::ssh_options_set( 628 | **sess, 629 | sys::ssh_options_e::SSH_OPTIONS_IDENTITY_AGENT, 630 | opt_cstring_to_cstr(&name) as _, 631 | ) 632 | }, 633 | SshOption::User(name) => unsafe { 634 | let name = opt_string_to_cstring(name); 635 | sys::ssh_options_set( 636 | **sess, 637 | sys::ssh_options_e::SSH_OPTIONS_USER, 638 | opt_cstring_to_cstr(&name) as _, 639 | ) 640 | }, 641 | SshOption::SshDir(name) => unsafe { 642 | let name = opt_string_to_cstring(name); 643 | sys::ssh_options_set( 644 | **sess, 645 | sys::ssh_options_e::SSH_OPTIONS_SSH_DIR, 646 | opt_cstring_to_cstr(&name) as _, 647 | ) 648 | }, 649 | SshOption::KnownHosts(known_hosts) => unsafe { 650 | let known_hosts = opt_string_to_cstring(known_hosts); 651 | sys::ssh_options_set( 652 | **sess, 653 | sys::ssh_options_e::SSH_OPTIONS_KNOWNHOSTS, 654 | opt_cstring_to_cstr(&known_hosts) as _, 655 | ) 656 | }, 657 | SshOption::ProxyCommand(cmd) => unsafe { 658 | let cmd = opt_string_to_cstring(cmd); 659 | sys::ssh_options_set( 660 | **sess, 661 | sys::ssh_options_e::SSH_OPTIONS_PROXYCOMMAND, 662 | opt_cstring_to_cstr(&cmd) as _, 663 | ) 664 | }, 665 | SshOption::Port(port) => { 666 | let port: c_uint = port.into(); 667 | unsafe { 668 | sys::ssh_options_set( 669 | **sess, 670 | sys::ssh_options_e::SSH_OPTIONS_PORT, 671 | &port as *const _ as _, 672 | ) 673 | } 674 | } 675 | SshOption::Socket(socket) => unsafe { 676 | sys::ssh_options_set( 677 | **sess, 678 | sys::ssh_options_e::SSH_OPTIONS_FD, 679 | &socket as *const _ as _, 680 | ) 681 | }, 682 | SshOption::Timeout(duration) => unsafe { 683 | let micros: c_ulong = duration.as_micros() as c_ulong; 684 | sys::ssh_options_set( 685 | **sess, 686 | sys::ssh_options_e::SSH_OPTIONS_TIMEOUT_USEC, 687 | µs as *const _ as _, 688 | ) 689 | }, 690 | SshOption::CiphersCS(name) => unsafe { 691 | let name = CString::new(name)?; 692 | sys::ssh_options_set( 693 | **sess, 694 | sys::ssh_options_e::SSH_OPTIONS_CIPHERS_C_S, 695 | name.as_ptr() as _, 696 | ) 697 | }, 698 | SshOption::CiphersSC(name) => unsafe { 699 | let name = CString::new(name)?; 700 | sys::ssh_options_set( 701 | **sess, 702 | sys::ssh_options_e::SSH_OPTIONS_CIPHERS_S_C, 703 | name.as_ptr() as _, 704 | ) 705 | }, 706 | SshOption::HmacCS(name) => unsafe { 707 | let name = CString::new(name)?; 708 | sys::ssh_options_set( 709 | **sess, 710 | sys::ssh_options_e::SSH_OPTIONS_HMAC_C_S, 711 | name.as_ptr() as _, 712 | ) 713 | }, 714 | SshOption::HmacSC(name) => unsafe { 715 | let name = CString::new(name)?; 716 | sys::ssh_options_set( 717 | **sess, 718 | sys::ssh_options_e::SSH_OPTIONS_HMAC_S_C, 719 | name.as_ptr() as _, 720 | ) 721 | }, 722 | SshOption::ProcessConfig(value) => unsafe { 723 | let value: c_uint = value.into(); 724 | sys::ssh_options_set( 725 | **sess, 726 | sys::ssh_options_e::SSH_OPTIONS_PROCESS_CONFIG, 727 | &value as *const _ as _, 728 | ) 729 | }, 730 | SshOption::GlobalKnownHosts(known_hosts) => unsafe { 731 | let known_hosts = opt_string_to_cstring(known_hosts); 732 | sys::ssh_options_set( 733 | **sess, 734 | sys::ssh_options_e::SSH_OPTIONS_GLOBAL_KNOWNHOSTS, 735 | opt_cstring_to_cstr(&known_hosts) as _, 736 | ) 737 | }, 738 | }; 739 | 740 | if res == 0 { 741 | Ok(()) 742 | } else if let Some(err) = sess.last_error() { 743 | Err(err) 744 | } else { 745 | Err(Error::fatal("failed to set option")) 746 | } 747 | } 748 | 749 | /// This function allows you to get a hash of the public key. 750 | /// You can then print this hash in a human-readable form to the user 751 | /// so that he is able to verify it. 752 | /// It is very important that you verify at some moment that the hash 753 | /// matches a known server. If you don't do it, cryptography wont help 754 | /// you at making things secure. OpenSSH uses SHA1 to print public key digests. 755 | pub fn get_server_public_key(&self) -> SshResult { 756 | let sess = self.lock_session(); 757 | let mut key = std::ptr::null_mut(); 758 | let res = unsafe { sys::ssh_get_server_publickey(**sess, &mut key) }; 759 | if res == sys::SSH_OK as i32 && !key.is_null() { 760 | Ok(SshKey { key }) 761 | } else if let Some(err) = sess.last_error() { 762 | Err(err) 763 | } else { 764 | Err(Error::fatal("failed to get server public key")) 765 | } 766 | } 767 | 768 | /// Try to authenticate with the given public key. 769 | /// 770 | /// To avoid unnecessary processing and user interaction, the following 771 | /// method is provided for querying whether authentication using the 772 | /// 'pubkey' would be possible. 773 | /// On success, you want now to use [userauth_publickey](#method.userauth_publickey). 774 | /// `username` should almost always be `None` to use the username as 775 | /// previously configured via [set_option](#method.set_option) or that 776 | /// was loaded from the ssh configuration prior to calling 777 | /// [connect](#method.connect), as most ssh server implementations 778 | /// do not allow changing the username during authentication. 779 | /// 780 | pub fn userauth_try_publickey( 781 | &self, 782 | username: Option<&str>, 783 | pubkey: &SshKey, 784 | ) -> SshResult { 785 | let sess = self.lock_session(); 786 | 787 | let username = opt_str_to_cstring(username); 788 | 789 | let res = unsafe { 790 | sys::ssh_userauth_try_publickey(**sess, opt_cstring_to_cstr(&username), pubkey.key) 791 | }; 792 | 793 | sess.auth_result(res, "failed authenticating with public key ") 794 | } 795 | 796 | /// Authenticate with public/private key or certificate. 797 | /// 798 | /// `username` should almost always be `None` to use the username as 799 | /// previously configured via [set_option](#method.set_option) or that 800 | /// was loaded from the ssh configuration prior to calling 801 | /// [connect](#method.connect), as most ssh server implementations 802 | /// do not allow changing the username during authentication. 803 | pub fn userauth_publickey( 804 | &self, 805 | username: Option<&str>, 806 | privkey: &SshKey, 807 | ) -> SshResult { 808 | let sess = self.lock_session(); 809 | 810 | let username = opt_str_to_cstring(username); 811 | 812 | let res = unsafe { 813 | sys::ssh_userauth_publickey(**sess, opt_cstring_to_cstr(&username), privkey.key) 814 | }; 815 | 816 | sess.auth_result(res, "authentication error") 817 | } 818 | 819 | /// Try to authenticate using an ssh agent. 820 | /// 821 | /// `username` should almost always be `None` to use the username as 822 | /// previously configured via [set_option](#method.set_option) or that 823 | /// was loaded from the ssh configuration prior to calling 824 | /// [connect](#method.connect), as most ssh server implementations 825 | /// do not allow changing the username during authentication. 826 | pub fn userauth_agent(&self, username: Option<&str>) -> SshResult { 827 | let sess = self.lock_session(); 828 | 829 | let username = opt_str_to_cstring(username); 830 | 831 | let res = unsafe { sys::ssh_userauth_agent(**sess, opt_cstring_to_cstr(&username)) }; 832 | 833 | sess.auth_result(res, "authentication error") 834 | } 835 | 836 | /// Try to automatically authenticate using public key authentication. 837 | /// 838 | /// This will attempt to use an ssh agent if available, and will then 839 | /// attempt to use your keys/identities from your `~/.ssh` dir. 840 | /// 841 | /// `username` should almost always be `None` to use the username as 842 | /// previously configured via [set_option](#method.set_option) or that 843 | /// was loaded from the ssh configuration prior to calling 844 | /// [connect](#method.connect), as most ssh server implementations 845 | /// do not allow changing the username during authentication. 846 | /// 847 | /// The `password` parameter can be used to pre-fill a password to 848 | /// unlock the private key(s). Leaving it set to `None` will cause 849 | /// libssh to prompt for the passphrase if you have previously 850 | /// used [set_auth_callback](#method.set_auth_callback) 851 | /// to configure a callback. If you haven't set the callback and 852 | /// a key is password protected, this authentication method will fail. 853 | pub fn userauth_public_key_auto( 854 | &self, 855 | username: Option<&str>, 856 | password: Option<&str>, 857 | ) -> SshResult { 858 | let sess = self.lock_session(); 859 | 860 | let username = opt_str_to_cstring(username); 861 | let password = opt_str_to_cstring(password); 862 | 863 | let res = unsafe { 864 | sys::ssh_userauth_publickey_auto( 865 | **sess, 866 | opt_cstring_to_cstr(&username), 867 | opt_cstring_to_cstr(&password), 868 | ) 869 | }; 870 | 871 | sess.auth_result(res, "authentication error") 872 | } 873 | 874 | /// Try to perform `"none"` authentication. 875 | /// 876 | /// Typically, the server will not allow `none` auth to succeed, but it has 877 | /// the side effect of informing the client which authentication methods 878 | /// are available, so a full-featured client will call this prior to calling 879 | /// `userauth_list`. 880 | /// 881 | /// `username` should almost always be `None` to use the username as 882 | /// previously configured via [set_option](#method.set_option) or that 883 | /// was loaded from the ssh configuration prior to calling 884 | /// [connect](#method.connect), as most ssh server implementations 885 | /// do not allow changing the username during authentication. 886 | pub fn userauth_none(&self, username: Option<&str>) -> SshResult { 887 | let sess = self.lock_session(); 888 | let username = opt_str_to_cstring(username); 889 | let res = unsafe { sys::ssh_userauth_none(**sess, opt_cstring_to_cstr(&username)) }; 890 | 891 | sess.auth_result(res, "authentication error") 892 | } 893 | 894 | /// Returns the permitted `AuthMethods`. 895 | /// 896 | /// The list is not available until after [userauth_none](#method.userauth_none) 897 | /// has been called at least once. 898 | /// 899 | /// The list can change in response to authentication events; for example, 900 | /// after successfully completing pubkey auth, the server may then require 901 | /// keyboard interactive auth to enter a second authentication factor. 902 | /// 903 | /// `username` should almost always be `None` to use the username as 904 | /// previously configured via [set_option](#method.set_option) or that 905 | /// was loaded from the ssh configuration prior to calling 906 | /// [connect](#method.connect), as most ssh server implementations 907 | /// do not allow changing the username during authentication. 908 | pub fn userauth_list(&self, username: Option<&str>) -> SshResult { 909 | let sess = self.lock_session(); 910 | let username = opt_str_to_cstring(username); 911 | Ok(unsafe { 912 | AuthMethods::from_bits_unchecked(sys::ssh_userauth_list( 913 | **sess, 914 | opt_cstring_to_cstr(&username), 915 | ) as u32) 916 | }) 917 | } 918 | 919 | /// After [userauth_keyboard_interactive](#method.userauth_keyboard_interactive) 920 | /// has been called and returned `AuthStatus::Info`, this method must be called 921 | /// to discover the prompts to questions that the server needs answered in order 922 | /// to authenticate the session. 923 | /// 924 | /// It is then up to your application to obtain those answers and set them via 925 | /// [userauth_keyboard_interactive_set_answers](#method.userauth_keyboard_interactive_set_answers). 926 | pub fn userauth_keyboard_interactive_info(&self) -> SshResult { 927 | let sess = self.lock_session(); 928 | let name = unsafe { sys::ssh_userauth_kbdint_getname(**sess) }; 929 | let name = unsafe { CStr::from_ptr(name) } 930 | .to_string_lossy() 931 | .to_string(); 932 | 933 | let instruction = unsafe { sys::ssh_userauth_kbdint_getinstruction(**sess) }; 934 | let instruction = unsafe { CStr::from_ptr(instruction) } 935 | .to_string_lossy() 936 | .to_string(); 937 | 938 | let n_prompts = unsafe { sys::ssh_userauth_kbdint_getnprompts(**sess) }; 939 | assert!(n_prompts >= 0); 940 | let n_prompts = n_prompts as u32; 941 | let mut prompts = vec![]; 942 | for i in 0..n_prompts { 943 | let mut echo = 0; 944 | let prompt = unsafe { sys::ssh_userauth_kbdint_getprompt(**sess, i, &mut echo) }; 945 | 946 | prompts.push(InteractiveAuthPrompt { 947 | prompt: unsafe { CStr::from_ptr(prompt) } 948 | .to_string_lossy() 949 | .to_string(), 950 | echo: echo != 0, 951 | }); 952 | } 953 | 954 | Ok(InteractiveAuthInfo { 955 | name, 956 | instruction, 957 | prompts, 958 | }) 959 | } 960 | 961 | /// After [userauth_keyboard_interactive_info](#method.userauth_keyboard_interactive_info) 962 | /// has been called, and your application has produced the answers to the prompts, 963 | /// you must call this method to record those answers. 964 | /// 965 | /// You will then need to call 966 | /// [userauth_keyboard_interactive](#method.userauth_keyboard_interactive) to present 967 | /// those answers to the server and discover the next stage of authentication. 968 | pub fn userauth_keyboard_interactive_set_answers(&self, answers: &[String]) -> SshResult<()> { 969 | let sess = self.lock_session(); 970 | for (idx, answer) in answers.iter().enumerate() { 971 | let answer = CString::new(answer.as_bytes())?; 972 | 973 | let res = 974 | unsafe { sys::ssh_userauth_kbdint_setanswer(**sess, idx as u32, answer.as_ptr()) }; 975 | 976 | if res != 0 { 977 | if let Some(err) = sess.last_error() { 978 | return Err(err); 979 | } 980 | return Err(Error::fatal("error setting answer")); 981 | } 982 | } 983 | Ok(()) 984 | } 985 | 986 | /// Initiates keyboard-interactive authentication. 987 | /// 988 | /// This appears similar to, but is not the same as password authentication. 989 | /// You should prefer using keyboard-interactive authentication over password 990 | /// auth. 991 | /// 992 | /// `username` should almost always be `None` to use the username as 993 | /// previously configured via [set_option](#method.set_option) or that 994 | /// was loaded from the ssh configuration prior to calling 995 | /// [connect](#method.connect), as most ssh server implementations 996 | /// do not allow changing the username during authentication. 997 | /// 998 | /// `sub_methods` is not documented in the underlying libssh and 999 | /// should almost always be `None`. 1000 | /// 1001 | /// If the returned `AuthStatus` is `Info`, then your application 1002 | /// should use [userauth_keyboard_interactive_info](#method.userauth_keyboard_interactive_info) 1003 | /// and use the results of that method to prompt the user to answer 1004 | /// the questions sent by the server, then 1005 | /// [userauth_keyboard_interactive_set_answers](#method.userauth_keyboard_interactive_set_answers) 1006 | /// to record the answers, before again calling this method to 1007 | /// present them to the server and determine the next steps. 1008 | pub fn userauth_keyboard_interactive( 1009 | &self, 1010 | username: Option<&str>, 1011 | sub_methods: Option<&str>, 1012 | ) -> SshResult { 1013 | let sess = self.lock_session(); 1014 | 1015 | let username = opt_str_to_cstring(username); 1016 | let sub_methods = opt_str_to_cstring(sub_methods); 1017 | 1018 | let res = unsafe { 1019 | sys::ssh_userauth_kbdint( 1020 | **sess, 1021 | opt_cstring_to_cstr(&username), 1022 | opt_cstring_to_cstr(&sub_methods), 1023 | ) 1024 | }; 1025 | sess.auth_result(res, "authentication error") 1026 | } 1027 | 1028 | /// Initiates password based authentication. 1029 | /// 1030 | /// This appears similar to, but is not the same as keyboard-interactive 1031 | /// authentication. You should prefer using keyboard-interactive 1032 | /// authentication over password auth. 1033 | /// 1034 | /// `username` should almost always be `None` to use the username as 1035 | /// previously configured via [set_option](#method.set_option) or that 1036 | /// was loaded from the ssh configuration prior to calling 1037 | /// [connect](#method.connect), as most ssh server implementations 1038 | /// do not allow changing the username during authentication. 1039 | /// 1040 | /// `password` should be a password entered by the user, or otherwise 1041 | /// securely communicated to your application. 1042 | pub fn userauth_password( 1043 | &self, 1044 | username: Option<&str>, 1045 | password: Option<&str>, 1046 | ) -> SshResult { 1047 | let sess = self.lock_session(); 1048 | let username = opt_str_to_cstring(username); 1049 | let password = opt_str_to_cstring(password); 1050 | let res = unsafe { 1051 | sys::ssh_userauth_password( 1052 | **sess, 1053 | opt_cstring_to_cstr(&username), 1054 | opt_cstring_to_cstr(&password), 1055 | ) 1056 | }; 1057 | sess.auth_result(res, "authentication error") 1058 | } 1059 | 1060 | /// Sends the "tcpip-forward" global request to ask the server 1061 | /// to begin listening for inbound connections; this is for 1062 | /// *remote (or reverse) port forwarding*. 1063 | /// 1064 | /// If `bind_address` is None then bind to all interfaces on 1065 | /// the server side. Otherwise, bind only to the specified address. 1066 | /// If `port` is `0` then the server will pick a port to bind to, 1067 | /// otherwise, will attempt to use the requested port. 1068 | /// Returns the bound port number. 1069 | /// 1070 | /// Later in your program, you will use `Session::accept_forward` to 1071 | /// wait for a forwarded connection from the address you specified. 1072 | pub fn listen_forward(&self, bind_address: Option<&str>, port: u16) -> SshResult { 1073 | let sess = self.lock_session(); 1074 | let bind_address = opt_str_to_cstring(bind_address); 1075 | let mut bound_port = 0; 1076 | let res = unsafe { 1077 | sys::ssh_channel_listen_forward( 1078 | **sess, 1079 | opt_cstring_to_cstr(&bind_address), 1080 | port as i32, 1081 | &mut bound_port, 1082 | ) 1083 | }; 1084 | if res == sys::SSH_OK as i32 { 1085 | Ok(bound_port as u16) 1086 | } else if let Some(err) = sess.last_error() { 1087 | Err(err) 1088 | } else { 1089 | Err(Error::fatal("error in ssh_channel_listen_forward")) 1090 | } 1091 | } 1092 | 1093 | /// Accept a remote forwarded connection. 1094 | /// You must have called `Session::listen_forward` previously to set up 1095 | /// remote port forwarding. 1096 | /// Returns a tuple `(destination_port, Channel)`. 1097 | /// The destination port is so that you can distinguish between multiple 1098 | /// remote forwards and corresponds to the port returned from `listen_forward`. 1099 | pub fn accept_forward(&self, timeout: Duration) -> SshResult<(u16, Channel)> { 1100 | let mut port = 0; 1101 | let sess = self.lock_session(); 1102 | let chan = 1103 | unsafe { sys::ssh_channel_accept_forward(**sess, timeout.as_millis() as _, &mut port) }; 1104 | if chan.is_null() { 1105 | if let Some(err) = sess.last_error() { 1106 | Err(err) 1107 | } else { 1108 | Err(Error::TryAgain) 1109 | } 1110 | } else { 1111 | let channel = Channel::new(&self.sess, chan); 1112 | 1113 | Ok((port as u16, channel)) 1114 | } 1115 | } 1116 | 1117 | /// Returns a tuple of `(read_pending, write_pending)`. 1118 | /// If `read_pending` is true, then your OS polling mechanism 1119 | /// should request a wakeup when the socket is readable. 1120 | /// If `write_pending` is true, then your OS polling mechanism 1121 | /// should request a wakeup when the socket is writable. 1122 | /// 1123 | /// You can use the `AsRawFd` or `AsRawSocket` trait impl 1124 | /// to obtain the socket descriptor for polling purposes. 1125 | pub fn get_poll_state(&self) -> (bool, bool) { 1126 | let state = unsafe { sys::ssh_get_poll_flags(**self.lock_session()) }; 1127 | let read_pending = (state & sys::SSH_READ_PENDING as i32) != 0; 1128 | let write_pending = (state & sys::SSH_WRITE_PENDING as i32) != 0; 1129 | (read_pending, write_pending) 1130 | } 1131 | 1132 | /// Returns `true` if the session is in blocking mode, `false` otherwise. 1133 | pub fn is_blocking(&self) -> bool { 1134 | self.lock_session().is_blocking() 1135 | } 1136 | 1137 | /// If `blocking == true` then set the session to block mode, otherwise 1138 | /// set it to non-blocking mode. 1139 | /// In non-blocking mode, a number of methods in the objects associated 1140 | /// with the session can return `Error::TryAgain`. 1141 | pub fn set_blocking(&self, blocking: bool) { 1142 | unsafe { sys::ssh_set_blocking(**self.lock_session(), if blocking { 1 } else { 0 }) } 1143 | } 1144 | 1145 | /// Returns `true` if this session is in the connected state, `false` 1146 | /// otherwise. 1147 | pub fn is_connected(&self) -> bool { 1148 | unsafe { sys::ssh_is_connected(**self.lock_session()) != 0 } 1149 | } 1150 | 1151 | pub fn sftp(&self) -> SshResult { 1152 | let sftp = { 1153 | let sess = self.lock_session(); 1154 | let sftp = unsafe { sys::sftp_new(**sess) }; 1155 | if sftp.is_null() { 1156 | return if let Some(err) = sess.last_error() { 1157 | Err(err) 1158 | } else { 1159 | Err(Error::fatal("failed to allocate sftp session")) 1160 | }; 1161 | } 1162 | 1163 | Sftp { 1164 | sess: Arc::clone(&self.sess), 1165 | sftp_inner: sftp, 1166 | } 1167 | }; 1168 | 1169 | sftp.init()?; 1170 | Ok(sftp) 1171 | } 1172 | 1173 | /// Send a message that should be ignored by the peer 1174 | pub fn send_ignore(&self, data: &[u8]) -> SshResult<()> { 1175 | let sess = self.lock_session(); 1176 | let status = unsafe { sys::ssh_send_ignore(**sess, data.as_ptr() as _) }; 1177 | if status != sys::SSH_OK as i32 { 1178 | if let Some(err) = sess.last_error() { 1179 | Err(err) 1180 | } else { 1181 | Err(Error::TryAgain) 1182 | } 1183 | } else { 1184 | Ok(()) 1185 | } 1186 | } 1187 | } 1188 | 1189 | #[cfg(unix)] 1190 | impl std::os::unix::io::AsRawFd for Session { 1191 | fn as_raw_fd(&self) -> RawSocket { 1192 | unsafe { sys::ssh_get_fd(**self.lock_session()) } 1193 | } 1194 | } 1195 | 1196 | #[cfg(windows)] 1197 | impl std::os::windows::io::AsRawSocket for Session { 1198 | fn as_raw_socket(&self) -> RawSocket { 1199 | unsafe { sys::ssh_get_fd(**self.lock_session()) as RawSocket } 1200 | } 1201 | } 1202 | 1203 | /// Indicates the disposition of an authentication operation 1204 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 1205 | pub enum AuthStatus { 1206 | /// You have been fully authenticated and can now move on 1207 | /// to opening channels 1208 | Success, 1209 | /// The authentication attempt failed. Perhaps retry, or 1210 | /// try an alternative auth method. 1211 | Denied, 1212 | /// You've been partially authenticated. Check `Session::userauth_list` 1213 | /// to determine which methods you should continue with. 1214 | Partial, 1215 | /// There is additional information about how to proceed 1216 | /// with authentication. For keyboard-interactive auth, 1217 | /// you will need to obtain auth prompts and provide answers 1218 | /// before you can continue. 1219 | Info, 1220 | /// In non-blocking mode, you will need to try again as 1221 | /// the request couldn't be completed without blocking. 1222 | Again, 1223 | } 1224 | 1225 | bitflags::bitflags! { 1226 | /// bitflags that indicates permitted authentication methods 1227 | pub struct AuthMethods : u32 { 1228 | /// The `"none"` authentication method is available. 1229 | const NONE = sys::SSH_AUTH_METHOD_NONE; 1230 | /// The `"password"` authentication method is available. 1231 | const PASSWORD = sys::SSH_AUTH_METHOD_PASSWORD; 1232 | /// The `"public-key"` authentication method is available. 1233 | const PUBLIC_KEY = sys::SSH_AUTH_METHOD_PUBLICKEY; 1234 | /// Host-based authentication is available 1235 | const HOST_BASED = sys::SSH_AUTH_METHOD_HOSTBASED; 1236 | /// keyboard-interactive authentication is available 1237 | const INTERACTIVE = sys::SSH_AUTH_METHOD_INTERACTIVE; 1238 | /// GSSAPI authentication is available 1239 | const GSSAPI_MIC = sys::SSH_AUTH_METHOD_GSSAPI_MIC; 1240 | } 1241 | } 1242 | 1243 | /// Represents the public key provided by the remote host 1244 | pub struct SshKey { 1245 | key: sys::ssh_key, 1246 | } 1247 | 1248 | impl Drop for SshKey { 1249 | fn drop(&mut self) { 1250 | unsafe { sys::ssh_key_free(self.key) } 1251 | } 1252 | } 1253 | 1254 | impl SshKey { 1255 | /// Returns the public key hash in the requested format. 1256 | /// The hash is returned as binary bytes. 1257 | /// Consider using [get_public_key_hash_hexa](#method.get_public_key_hash_hexa) 1258 | /// to return it in a more human readable format. 1259 | pub fn get_public_key_hash(&self, hash_type: PublicKeyHashType) -> SshResult> { 1260 | let mut bytes = std::ptr::null_mut(); 1261 | let mut len = 0; 1262 | let res = unsafe { 1263 | sys::ssh_get_publickey_hash( 1264 | self.key, 1265 | match hash_type { 1266 | PublicKeyHashType::Sha1 => { 1267 | sys::ssh_publickey_hash_type::SSH_PUBLICKEY_HASH_SHA1 1268 | } 1269 | PublicKeyHashType::Md5 => sys::ssh_publickey_hash_type::SSH_PUBLICKEY_HASH_MD5, 1270 | PublicKeyHashType::Sha256 => { 1271 | sys::ssh_publickey_hash_type::SSH_PUBLICKEY_HASH_SHA256 1272 | } 1273 | }, 1274 | &mut bytes, 1275 | &mut len, 1276 | ) 1277 | }; 1278 | 1279 | if res != 0 || bytes.is_null() { 1280 | Err(Error::fatal("failed to get public key hash")) 1281 | } else { 1282 | let data = unsafe { std::slice::from_raw_parts(bytes, len).to_vec() }; 1283 | unsafe { 1284 | sys::ssh_clean_pubkey_hash(&mut bytes); 1285 | } 1286 | Ok(data) 1287 | } 1288 | } 1289 | 1290 | /// Returns the public key hash in a human readable form 1291 | pub fn get_public_key_hash_hexa(&self, hash_type: PublicKeyHashType) -> SshResult { 1292 | let bytes = self.get_public_key_hash(hash_type)?; 1293 | let hexa = unsafe { sys::ssh_get_hexa(bytes.as_ptr(), bytes.len()) }; 1294 | if hexa.is_null() { 1295 | Err(Error::fatal( 1296 | "failed to allocate bytes for hexa representation", 1297 | )) 1298 | } else { 1299 | let res = unsafe { CStr::from_ptr(hexa) } 1300 | .to_string_lossy() 1301 | .to_string(); 1302 | unsafe { sys::ssh_string_free_char(hexa) }; 1303 | Ok(res) 1304 | } 1305 | } 1306 | 1307 | pub fn from_privkey_base64(b64_key: &str, passphrase: Option<&str>) -> SshResult { 1308 | let b64_key = CString::new(b64_key) 1309 | .map_err(|e| Error::Fatal(format!("Failed to process ssh key: {:?}", e)))?; 1310 | let passphrase = opt_str_to_cstring(passphrase); 1311 | unsafe { 1312 | let mut key = sys::ssh_key_new(); 1313 | if sys::ssh_pki_import_privkey_base64( 1314 | b64_key.as_ptr(), 1315 | opt_cstring_to_cstr(&passphrase), 1316 | None, 1317 | null_mut(), 1318 | &mut key, 1319 | ) != sys::SSH_OK as i32 1320 | { 1321 | sys::ssh_key_free(key); 1322 | return Err(Error::Fatal(format!("Failed to parse ssh key"))); 1323 | } 1324 | return Ok(SshKey { key }); 1325 | } 1326 | } 1327 | 1328 | pub fn from_privkey_file(filename: &str, passphrase: Option<&str>) -> SshResult { 1329 | let filename_cstr = CString::new(filename).map_err(|e| { 1330 | Error::Fatal(format!( 1331 | "Could not make CString from filename '{filename}': {e:#}" 1332 | )) 1333 | })?; 1334 | let passphrase = opt_str_to_cstring(passphrase); 1335 | unsafe { 1336 | let mut key = sys::ssh_key_new(); 1337 | if sys::ssh_pki_import_privkey_file( 1338 | filename_cstr.as_ptr(), 1339 | opt_cstring_to_cstr(&passphrase), 1340 | None, 1341 | null_mut(), 1342 | &mut key, 1343 | ) != sys::SSH_OK as i32 1344 | { 1345 | sys::ssh_key_free(key); 1346 | return Err(Error::Fatal(format!( 1347 | "Failed to parse ssh key from file '{filename}'" 1348 | ))); 1349 | } 1350 | return Ok(SshKey { key }); 1351 | } 1352 | } 1353 | } 1354 | 1355 | /// Allows configuring the underlying `libssh` debug logging level 1356 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 1357 | pub enum LogLevel { 1358 | NoLogging, 1359 | Warning, 1360 | Protocol, 1361 | Packet, 1362 | Functions, 1363 | } 1364 | 1365 | /// Allows configuring different aspects of a `Session`. 1366 | /// You always need to set at least `SshOption::Hostname`. 1367 | #[derive(Debug)] 1368 | pub enum SshOption { 1369 | /// The hostname or ip address to connect to 1370 | Hostname(String), 1371 | 1372 | /// The port to connect to 1373 | Port(u16), 1374 | 1375 | LogLevel(LogLevel), 1376 | 1377 | /// The pre-opened socket. 1378 | /// You don't typically need to provide this. 1379 | /// Don't forget to set the hostname as the hostname is used as a 1380 | /// key in the known_host mechanism. 1381 | Socket(RawSocket), 1382 | 1383 | /// The address to bind the client to 1384 | BindAddress(String), 1385 | 1386 | /// The username for authentication 1387 | /// If the value is None, the username is set to the default username. 1388 | User(Option), 1389 | 1390 | /// Set the ssh directory 1391 | /// If the value is None, the directory is set to the default ssh directory. 1392 | /// The ssh directory is used for files like known_hosts and identity (private and public key). It may include "%s" which will be replaced by the user home directory. 1393 | SshDir(Option), 1394 | 1395 | /// Set the known hosts file name 1396 | /// If the value is None, the directory is set to the default known hosts file, normally ~/.ssh/known_hosts. 1397 | /// The known hosts file is used to certify remote hosts are genuine. It may include "%d" which will be replaced by the user home directory. 1398 | KnownHosts(Option), 1399 | 1400 | /// Configures the ProxyCommand ssh option, which is used to establish an 1401 | /// alternative transport to using a direct TCP connection 1402 | ProxyCommand(Option), 1403 | 1404 | /// Add a new identity file (const char *, format string) to the identity list. 1405 | /// By default identity, id_dsa and id_rsa are checked. 1406 | /// The identity used to authenticate with public key will be prepended to the list. It may include "%s" which will be replaced by the user home directory. 1407 | AddIdentity(String), 1408 | 1409 | /// Set a timeout for the connection 1410 | Timeout(Duration), 1411 | 1412 | IdentityAgent(Option), 1413 | /// Set the key exchange method to be used. ex: 1414 | /// ecdh-sha2-nistp256,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1 1415 | KeyExchange(String), 1416 | /// Set the preferred server host key types. ex: 1417 | /// ssh-rsa,rsa-sha2-256,ssh-dss,ecdh-sha2-nistp256 1418 | HostKeys(String), 1419 | /// Set the preferred public key algorithms to be used for 1420 | /// authentication as a comma-separated list. ex: 1421 | /// ssh-rsa,rsa-sha2-256,ssh-dss,ecdh-sha2-nistp256 1422 | PublicKeyAcceptedTypes(String), 1423 | ///Set the symmetric cipher client to server as a comma-separated list. 1424 | CiphersCS(String), 1425 | ///Set the symmetric cipher server to client as a comma-separated list. 1426 | CiphersSC(String), 1427 | /// Set the MAC algorithm client to server as a comma-separated list. 1428 | HmacCS(String), 1429 | /// Set the MAC algorithm server to client as a comma-separated list. 1430 | HmacSC(String), 1431 | /// Set it to false to disable automatic processing of per-user and system-wide OpenSSH configuration files. 1432 | ProcessConfig(bool), 1433 | /// Set the global known hosts file name 1434 | /// If the value is None, the directory is set to the default known hosts file, normally /etc/ssh/ssh_known_hosts. 1435 | /// The known hosts file is used to certify remote hosts are genuine. 1436 | GlobalKnownHosts(Option), 1437 | } 1438 | 1439 | /// Indicates the state of known-host matching, an important set 1440 | /// to detect and avoid MITM attacks. 1441 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 1442 | pub enum KnownHosts { 1443 | /// The known host file does not exist. The host is thus unknown. File will be created if host key is accepted. 1444 | NotFound, 1445 | /// The server is unknown. User should confirm the public key hash is correct. 1446 | Unknown, 1447 | /// The server is known and has not changed. 1448 | Ok, 1449 | /// The server key has changed. Either you are under attack or the administrator changed the key. You HAVE to warn the user about a possible attack. 1450 | Changed, 1451 | /// The server gave use a key of a type while we had an other type recorded. It is a possible attack. 1452 | Other, 1453 | } 1454 | 1455 | /// The type of hash to use when inspecting a public key fingerprint 1456 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 1457 | pub enum PublicKeyHashType { 1458 | Sha1, 1459 | Md5, 1460 | Sha256, 1461 | } 1462 | 1463 | /// Represents a question prompt in keyboard-interactive auth 1464 | #[derive(Debug, Clone, PartialEq, Eq)] 1465 | pub struct InteractiveAuthPrompt { 1466 | /// The prompt to show to the user 1467 | pub prompt: String, 1468 | /// If `true`, echo the user's answer to the screen. 1469 | /// If `false`, conceal it, as it is secret/sensitive. 1470 | pub echo: bool, 1471 | } 1472 | 1473 | /// Represents the overall set of instructions in keyboard-interactive auth 1474 | #[derive(Debug, Clone, PartialEq, Eq)] 1475 | pub struct InteractiveAuthInfo { 1476 | /// An overall set of instructions. 1477 | /// May be empty. 1478 | pub instruction: String, 1479 | /// The session name. 1480 | /// May be empty. 1481 | pub name: String, 1482 | /// The set of prompts for information that need answers before 1483 | /// authentication can succeed. 1484 | pub prompts: Vec, 1485 | } 1486 | 1487 | /// A utility function that will prompt the user for input 1488 | /// via the console/tty. 1489 | /// 1490 | /// `prompt` is the text to show to the user. 1491 | /// `default_value` can be used to pre-set the answer, allowing the 1492 | /// user to simply press enter. 1493 | /// 1494 | /// `echo`, if `true`, means to show the user's answer on the screen 1495 | /// as they type it. If `false`, means to conceal it. 1496 | /// 1497 | /// `verify`, if `true`, will ask the user for their input twice in 1498 | /// order to confirm that they provided the same text both times. 1499 | /// This is useful when creating a password and `echo == false`. 1500 | pub fn get_input( 1501 | prompt: &str, 1502 | default_value: Option<&str>, 1503 | echo: bool, 1504 | verify: bool, 1505 | ) -> Option { 1506 | const BUF_LEN: usize = 128; 1507 | let mut buf = [0u8; BUF_LEN]; 1508 | 1509 | if let Some(def) = default_value { 1510 | let def = def.as_bytes(); 1511 | let len = buf.len().min(def.len()); 1512 | buf[0..len].copy_from_slice(&def[0..len]); 1513 | } 1514 | 1515 | let prompt = CString::new(prompt).ok()?; 1516 | 1517 | let res = unsafe { 1518 | sys::ssh_getpass( 1519 | prompt.as_ptr(), 1520 | buf.as_mut_ptr() as *mut _, 1521 | buf.len(), 1522 | if echo { 1 } else { 0 }, 1523 | if verify { 1 } else { 0 }, 1524 | ) 1525 | }; 1526 | 1527 | if res == 0 { 1528 | Some( 1529 | unsafe { CStr::from_ptr(buf.as_ptr() as *const _) } 1530 | .to_string_lossy() 1531 | .to_string(), 1532 | ) 1533 | } else { 1534 | None 1535 | } 1536 | } 1537 | 1538 | fn opt_str_to_cstring(s: Option<&str>) -> Option { 1539 | s.and_then(|s| CString::new(s).ok()) 1540 | } 1541 | 1542 | fn opt_string_to_cstring(s: Option) -> Option { 1543 | s.and_then(|s| CString::new(s).ok()) 1544 | } 1545 | 1546 | fn opt_cstring_to_cstr(s: &Option) -> *const ::std::os::raw::c_char { 1547 | match s { 1548 | Some(s) => s.as_ptr(), 1549 | None => std::ptr::null(), 1550 | } 1551 | } 1552 | 1553 | #[cfg(test)] 1554 | mod test { 1555 | use super::*; 1556 | 1557 | #[test] 1558 | fn init() { 1559 | let sess = Session::new().unwrap(); 1560 | assert!(!sess.is_connected()); 1561 | assert_eq!(sess.connect(), Err(Error::fatal("Hostname required"))); 1562 | } 1563 | } 1564 | -------------------------------------------------------------------------------- /libssh-rs/src/sftp.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, SessionHolder, SshResult}; 2 | use libssh_rs_sys as sys; 3 | use std::convert::TryInto; 4 | use std::ffi::{CStr, CString}; 5 | use std::os::raw::{c_char, c_int}; 6 | use std::sync::{Arc, Mutex, MutexGuard}; 7 | use std::time::{Duration, SystemTime}; 8 | use thiserror::Error; 9 | 10 | #[derive(Error, Debug, PartialEq, Eq)] 11 | #[error("Sftp error code {}", .0)] 12 | pub struct SftpError(u32); 13 | 14 | impl SftpError { 15 | pub(crate) fn from_session(sftp: sys::sftp_session) -> Self { 16 | let code = unsafe { sys::sftp_get_error(sftp) as u32 }; 17 | Self(code) 18 | } 19 | 20 | pub(crate) fn result(sftp: sys::sftp_session, status: i32, res: T) -> SshResult { 21 | if status == sys::SSH_OK as i32 { 22 | Ok(res) 23 | } else { 24 | Err(Error::Sftp(SftpError::from_session(sftp))) 25 | } 26 | } 27 | } 28 | 29 | pub struct Sftp { 30 | pub(crate) sess: Arc>, 31 | pub(crate) sftp_inner: sys::sftp_session, 32 | } 33 | 34 | unsafe impl Send for Sftp {} 35 | 36 | impl Drop for Sftp { 37 | fn drop(&mut self) { 38 | let (_sess, sftp) = self.lock_session(); 39 | unsafe { 40 | sys::sftp_free(sftp); 41 | } 42 | } 43 | } 44 | 45 | impl Sftp { 46 | fn lock_session(&self) -> (MutexGuard, sys::sftp_session) { 47 | (self.sess.lock().unwrap(), self.sftp_inner) 48 | } 49 | 50 | pub(crate) fn init(&self) -> SshResult<()> { 51 | let (_sess, sftp) = self.lock_session(); 52 | let res = unsafe { sys::sftp_init(sftp) }; 53 | SftpError::result(sftp, res, ()) 54 | } 55 | 56 | /// Create a directory. 57 | /// `mode` specifies the permission bits to use on the directory. 58 | /// They will be modified by the effective umask on the server. 59 | pub fn create_dir(&self, filename: &str, mode: sys::mode_t) -> SshResult<()> { 60 | let filename = CString::new(filename)?; 61 | let (_sess, sftp) = self.lock_session(); 62 | let res = unsafe { sys::sftp_mkdir(sftp, filename.as_ptr(), mode) }; 63 | SftpError::result(sftp, res, ()) 64 | } 65 | 66 | /// Canonicalize `filename`, resolving relative directory references 67 | /// and symlinks. 68 | pub fn canonicalize(&self, filename: &str) -> SshResult { 69 | let filename = CString::new(filename)?; 70 | let (_sess, sftp) = self.lock_session(); 71 | let res = unsafe { sys::sftp_canonicalize_path(sftp, filename.as_ptr()) }; 72 | if res.is_null() { 73 | Err(Error::Sftp(SftpError::from_session(sftp))) 74 | } else { 75 | let result = unsafe { CStr::from_ptr(res) }.to_string_lossy().to_string(); 76 | unsafe { sys::ssh_string_free_char(res) }; 77 | Ok(result) 78 | } 79 | } 80 | 81 | /// Change the permissions of a file 82 | pub fn chmod(&self, filename: &str, mode: sys::mode_t) -> SshResult<()> { 83 | let filename = CString::new(filename)?; 84 | let (_sess, sftp) = self.lock_session(); 85 | let res = unsafe { sys::sftp_chmod(sftp, filename.as_ptr(), mode) }; 86 | SftpError::result(sftp, res, ()) 87 | } 88 | 89 | /// Change the ownership of a file. 90 | pub fn chown(&self, filename: &str, owner: sys::uid_t, group: sys::gid_t) -> SshResult<()> { 91 | let filename = CString::new(filename)?; 92 | let (_sess, sftp) = self.lock_session(); 93 | let res = unsafe { sys::sftp_chown(sftp, filename.as_ptr(), owner, group) }; 94 | SftpError::result(sftp, res, ()) 95 | } 96 | 97 | /// Read the payload of a symlink 98 | pub fn read_link(&self, filename: &str) -> SshResult { 99 | let filename = CString::new(filename)?; 100 | let (_sess, sftp) = self.lock_session(); 101 | let res = unsafe { sys::sftp_readlink(sftp, filename.as_ptr()) }; 102 | if res.is_null() { 103 | Err(Error::Sftp(SftpError::from_session(sftp))) 104 | } else { 105 | let result = unsafe { CStr::from_ptr(res) }.to_string_lossy().to_string(); 106 | unsafe { sys::ssh_string_free_char(res) }; 107 | Ok(result) 108 | } 109 | } 110 | 111 | /// Change certain metadata attributes of the named file. 112 | pub fn set_metadata(&self, filename: &str, metadata: &SetAttributes) -> SshResult<()> { 113 | let filename = CString::new(filename)?; 114 | let (_sess, sftp) = self.lock_session(); 115 | let mut attributes: sys::sftp_attributes_struct = unsafe { std::mem::zeroed() }; 116 | 117 | if let Some(size) = metadata.size { 118 | attributes.size = size; 119 | attributes.flags |= sys::SSH_FILEXFER_ATTR_SIZE; 120 | } 121 | 122 | if let Some((uid, gid)) = metadata.uid_gid { 123 | attributes.uid = uid; 124 | attributes.gid = gid; 125 | attributes.flags |= sys::SSH_FILEXFER_ATTR_UIDGID; 126 | } 127 | 128 | if let Some(perms) = metadata.permissions { 129 | attributes.permissions = perms; 130 | attributes.flags |= sys::SSH_FILEXFER_ATTR_PERMISSIONS; 131 | } 132 | 133 | if let Some((atime, mtime)) = metadata.atime_mtime { 134 | attributes.atime = atime 135 | .duration_since(SystemTime::UNIX_EPOCH) 136 | .expect("SystemTime to always be > UNIX_EPOCH") 137 | .as_secs() 138 | .try_into() 139 | .unwrap(); 140 | attributes.mtime = mtime 141 | .duration_since(SystemTime::UNIX_EPOCH) 142 | .expect("SystemTime to always be > UNIX_EPOCH") 143 | .as_secs() 144 | .try_into() 145 | .unwrap(); 146 | attributes.flags |= sys::SSH_FILEXFER_ATTR_ACMODTIME; 147 | } 148 | 149 | let res = unsafe { sys::sftp_setstat(sftp, filename.as_ptr(), &mut attributes) }; 150 | SftpError::result(sftp, res, ()) 151 | } 152 | 153 | /// Retrieve metadata for a file, traversing symlinks 154 | pub fn metadata(&self, filename: &str) -> SshResult { 155 | let filename = CString::new(filename)?; 156 | let (_sess, sftp) = self.lock_session(); 157 | let attr = unsafe { sys::sftp_stat(sftp, filename.as_ptr()) }; 158 | if attr.is_null() { 159 | Err(Error::Sftp(SftpError::from_session(sftp))) 160 | } else { 161 | Ok(Metadata { attr }) 162 | } 163 | } 164 | 165 | /// Retrieve metadata for a file, without traversing symlinks. 166 | pub fn symlink_metadata(&self, filename: &str) -> SshResult { 167 | let filename = CString::new(filename)?; 168 | let (_sess, sftp) = self.lock_session(); 169 | let attr = unsafe { sys::sftp_lstat(sftp, filename.as_ptr()) }; 170 | if attr.is_null() { 171 | Err(Error::Sftp(SftpError::from_session(sftp))) 172 | } else { 173 | Ok(Metadata { attr }) 174 | } 175 | } 176 | 177 | /// Rename a file from `filename` to `new_name` 178 | pub fn rename(&self, filename: &str, new_name: &str) -> SshResult<()> { 179 | let filename = CString::new(filename)?; 180 | let new_name = CString::new(new_name)?; 181 | let (_sess, sftp) = self.lock_session(); 182 | let res = unsafe { sys::sftp_rename(sftp, filename.as_ptr(), new_name.as_ptr()) }; 183 | SftpError::result(sftp, res, ()) 184 | } 185 | 186 | /// Remove a file or an empty directory 187 | pub fn remove_file(&self, filename: &str) -> SshResult<()> { 188 | let filename = CString::new(filename)?; 189 | let (_sess, sftp) = self.lock_session(); 190 | let res = unsafe { sys::sftp_unlink(sftp, filename.as_ptr()) }; 191 | SftpError::result(sftp, res, ()) 192 | } 193 | 194 | /// Remove an empty directory 195 | pub fn remove_dir(&self, filename: &str) -> SshResult<()> { 196 | let filename = CString::new(filename)?; 197 | let (_sess, sftp) = self.lock_session(); 198 | let res = unsafe { sys::sftp_rmdir(sftp, filename.as_ptr()) }; 199 | SftpError::result(sftp, res, ()) 200 | } 201 | 202 | /// Create a symlink on the server. 203 | /// `target` is the filename of the symlink to be created, 204 | /// and `dest` is the payload of the symlink. 205 | pub fn symlink(&self, target: &str, dest: &str) -> SshResult<()> { 206 | let target = CString::new(target)?; 207 | let dest = CString::new(dest)?; 208 | let (_sess, sftp) = self.lock_session(); 209 | let res = unsafe { sys::sftp_symlink(sftp, target.as_ptr(), dest.as_ptr()) }; 210 | SftpError::result(sftp, res, ()) 211 | } 212 | 213 | /// Open a file on the server. 214 | /// `accesstype` corresponds to the `open(2)` `flags` parameter 215 | /// and controls whether the file is opened for read/write and so on. 216 | /// `mode` specified the permission bits to use when creating a new file; 217 | /// they will be modified by the effective umask on the server side. 218 | pub fn open( 219 | &self, 220 | filename: &str, 221 | accesstype: OpenFlags, 222 | mode: sys::mode_t, 223 | ) -> SshResult { 224 | let filename = CString::new(filename)?; 225 | let (_sess, sftp) = self.lock_session(); 226 | let res = unsafe { sys::sftp_open(sftp, filename.as_ptr(), accesstype.bits(), mode) }; 227 | if res.is_null() { 228 | Err(Error::Sftp(SftpError::from_session(sftp))) 229 | } else { 230 | Ok(SftpFile { 231 | sess: Arc::clone(&self.sess), 232 | file_inner: res, 233 | sftp: sftp, 234 | }) 235 | } 236 | } 237 | 238 | /// Open a directory to obtain directory entries 239 | pub fn open_dir(&self, filename: &str) -> SshResult { 240 | let filename = CString::new(filename)?; 241 | let (_sess, sftp) = self.lock_session(); 242 | let res = unsafe { sys::sftp_opendir(sftp, filename.as_ptr()) }; 243 | if res.is_null() { 244 | Err(Error::Sftp(SftpError::from_session(sftp))) 245 | } else { 246 | Ok(SftpDir { 247 | sess: Arc::clone(&self.sess), 248 | dir_inner: res, 249 | sftp: sftp, 250 | }) 251 | } 252 | } 253 | 254 | /// Convenience function that reads all of the directory entries 255 | /// into a Vec. If you need to deal with very large directories, 256 | /// you may wish to directly use [open_dir](#method.open_dir) 257 | /// and manually iterate the directory contents. 258 | pub fn read_dir(&self, filename: &str) -> SshResult> { 259 | let dir = self.open_dir(filename)?; 260 | let mut res = vec![]; 261 | while let Some(item) = dir.read_dir() { 262 | res.push(item?); 263 | } 264 | Ok(res) 265 | } 266 | } 267 | 268 | pub struct SftpFile { 269 | pub(crate) sess: Arc>, 270 | pub(crate) file_inner: sys::sftp_file, 271 | pub(crate) sftp: sys::sftp_session, 272 | } 273 | 274 | unsafe impl Send for SftpFile {} 275 | 276 | impl Drop for SftpFile { 277 | fn drop(&mut self) { 278 | let (_sess, file) = self.lock_session(); 279 | unsafe { 280 | sys::sftp_close(file); 281 | } 282 | } 283 | } 284 | 285 | impl SftpFile { 286 | fn lock_session(&self) -> (MutexGuard, sys::sftp_file) { 287 | (self.sess.lock().unwrap(), self.file_inner) 288 | } 289 | 290 | pub fn set_blocking(&self, blocking: bool) { 291 | let (_sess, file) = self.lock_session(); 292 | if blocking { 293 | unsafe { sys::sftp_file_set_blocking(file) } 294 | } else { 295 | unsafe { sys::sftp_file_set_nonblocking(file) } 296 | } 297 | } 298 | 299 | /// Retrieve metadata for the file 300 | pub fn metadata(&self) -> SshResult { 301 | let (_sess, file) = self.lock_session(); 302 | let attr = unsafe { sys::sftp_fstat(file) }; 303 | if attr.is_null() { 304 | Err(Error::Sftp(SftpError::from_session(self.sftp))) 305 | } else { 306 | Ok(Metadata { attr }) 307 | } 308 | } 309 | } 310 | 311 | fn io_err_from_sftp(sftp: sys::sftp_session, reason: &str) -> std::io::Error { 312 | use std::io::ErrorKind; 313 | let res = unsafe { sys::sftp_get_error(sftp) }; 314 | let kind = match res as u32 { 315 | sys::SSH_FX_OK => ErrorKind::Other, 316 | sys::SSH_FX_EOF => ErrorKind::UnexpectedEof, 317 | sys::SSH_FX_NO_SUCH_FILE => ErrorKind::NotFound, 318 | sys::SSH_FX_PERMISSION_DENIED => ErrorKind::PermissionDenied, 319 | sys::SSH_FX_FAILURE => ErrorKind::Other, 320 | sys::SSH_FX_BAD_MESSAGE => ErrorKind::Other, 321 | sys::SSH_FX_NO_CONNECTION => ErrorKind::NotConnected, 322 | sys::SSH_FX_CONNECTION_LOST => ErrorKind::ConnectionReset, 323 | sys::SSH_FX_OP_UNSUPPORTED => ErrorKind::Unsupported, 324 | sys::SSH_FX_INVALID_HANDLE => ErrorKind::Other, 325 | sys::SSH_FX_NO_SUCH_PATH => ErrorKind::NotFound, 326 | sys::SSH_FX_FILE_ALREADY_EXISTS => ErrorKind::AlreadyExists, 327 | sys::SSH_FX_WRITE_PROTECT => ErrorKind::Other, 328 | sys::SSH_FX_NO_MEDIA => ErrorKind::Other, 329 | _ => ErrorKind::Other, 330 | }; 331 | std::io::Error::new(kind, format!("{}: sftp error code {}", reason, res)) 332 | } 333 | 334 | impl std::io::Read for SftpFile { 335 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 336 | let (_sess, file) = self.lock_session(); 337 | 338 | let res = unsafe { sys::sftp_read(file, buf.as_mut_ptr() as _, buf.len()) }; 339 | 340 | if res >= 0 { 341 | Ok(res as usize) 342 | } else { 343 | let err = io_err_from_sftp(self.sftp, "read"); 344 | if err.kind() == std::io::ErrorKind::UnexpectedEof { 345 | Ok(0) 346 | } else { 347 | Err(err) 348 | } 349 | } 350 | } 351 | } 352 | 353 | impl std::io::Write for SftpFile { 354 | fn flush(&mut self) -> std::io::Result<()> { 355 | let (_sess, file) = self.lock_session(); 356 | let res = unsafe { sys::sftp_fsync(file) }; 357 | if res == 0 { 358 | Ok(()) 359 | } else { 360 | Err(io_err_from_sftp(self.sftp, "fsync")) 361 | } 362 | } 363 | 364 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 365 | let (_sess, file) = self.lock_session(); 366 | 367 | let res = unsafe { sys::sftp_write(file, buf.as_ptr() as _, buf.len()) }; 368 | 369 | if res >= 0 { 370 | Ok(res as usize) 371 | } else { 372 | let err = io_err_from_sftp(self.sftp, "write"); 373 | if err.kind() == std::io::ErrorKind::UnexpectedEof { 374 | Ok(0) 375 | } else { 376 | Err(err) 377 | } 378 | } 379 | } 380 | } 381 | 382 | impl std::io::Seek for SftpFile { 383 | fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { 384 | let (_sess, file) = self.lock_session(); 385 | match pos { 386 | std::io::SeekFrom::Start(p) => { 387 | let res = unsafe { sys::sftp_seek64(file, p) }; 388 | if res == 0 { 389 | Ok(p) 390 | } else { 391 | Err(io_err_from_sftp(self.sftp, "seek")) 392 | } 393 | } 394 | std::io::SeekFrom::End(p) => { 395 | let end = self.metadata().map_err(|e| e)?.len().ok_or_else(|| { 396 | std::io::Error::new( 397 | std::io::ErrorKind::Other, 398 | "metadata didn't return the length", 399 | ) 400 | })?; 401 | let target = if p < 0 { 402 | end.saturating_sub(p.abs() as u64) 403 | } else { 404 | end.saturating_add(p as u64) 405 | }; 406 | let res = unsafe { sys::sftp_seek64(file, target) }; 407 | if res == 0 { 408 | Ok(target) 409 | } else { 410 | Err(io_err_from_sftp(self.sftp, "seek")) 411 | } 412 | } 413 | std::io::SeekFrom::Current(p) => { 414 | let current = unsafe { sys::sftp_tell64(file) }; 415 | let target = if p < 0 { 416 | current.saturating_sub(p.abs() as u64) 417 | } else { 418 | current.saturating_add(p as u64) 419 | }; 420 | let res = unsafe { sys::sftp_seek64(file, target) }; 421 | if res == 0 { 422 | Ok(target) 423 | } else { 424 | Err(io_err_from_sftp(self.sftp, "seek")) 425 | } 426 | } 427 | } 428 | } 429 | 430 | fn stream_position(&mut self) -> std::io::Result { 431 | let (_sess, file) = self.lock_session(); 432 | let current = unsafe { sys::sftp_tell64(file) }; 433 | Ok(current) 434 | } 435 | } 436 | 437 | /// Change multiple file attributes at once. 438 | /// If a field is_some, then its value will be applied 439 | /// to the file on the server side. If it is_none, then 440 | /// that particular field will be left unmodified. 441 | #[derive(Debug, Clone, PartialEq, Eq)] 442 | pub struct SetAttributes { 443 | /// Change the file length 444 | pub size: Option, 445 | /// Change the ownership (chown) 446 | pub uid_gid: Option<(sys::uid_t, sys::gid_t)>, 447 | /// Change the permissions (chmod) 448 | pub permissions: Option, 449 | /// Note that the protocol/libssh implementation has 450 | /// 1-second granularity for access and mtime 451 | pub atime_mtime: Option<(SystemTime, SystemTime)>, 452 | } 453 | 454 | /// Represents metadata about a file. 455 | /// libssh returns this in a couple of contexts, and not all 456 | /// fields are used in all contexts. 457 | pub struct Metadata { 458 | attr: sys::sftp_attributes, 459 | } 460 | 461 | impl Drop for Metadata { 462 | fn drop(&mut self) { 463 | unsafe { sys::sftp_attributes_free(self.attr) } 464 | } 465 | } 466 | 467 | impl Metadata { 468 | fn attr(&self) -> &sys::sftp_attributes_struct { 469 | unsafe { &*self.attr } 470 | } 471 | 472 | pub fn len(&self) -> Option { 473 | if self.attr().flags & sys::SSH_FILEXFER_ATTR_SIZE != 0 { 474 | Some(self.attr().size) 475 | } else { 476 | None 477 | } 478 | } 479 | 480 | fn name_helper(&self, name: *const c_char) -> Option<&str> { 481 | if name.is_null() { 482 | None 483 | } else { 484 | unsafe { CStr::from_ptr(name) }.to_str().ok() 485 | } 486 | } 487 | 488 | pub fn name(&self) -> Option<&str> { 489 | self.name_helper(self.attr().name) 490 | } 491 | 492 | /// libssh docs say that this is the ls -l output on openssh 493 | /// servers, but is unreliable with other servers 494 | pub fn long_name(&self) -> Option<&str> { 495 | self.name_helper(self.attr().longname) 496 | } 497 | 498 | /// Set in openssh version 4 and up 499 | pub fn owner(&self) -> Option<&str> { 500 | self.name_helper(self.attr().owner) 501 | } 502 | 503 | /// Set in openssh version 4 and up 504 | pub fn group(&self) -> Option<&str> { 505 | self.name_helper(self.attr().group) 506 | } 507 | 508 | /// Flags the indicate which attributes are present. 509 | /// Is a bitmask of `SSH_FILEXFER_ATTR_XXX` constants 510 | pub fn flags(&self) -> u32 { 511 | self.attr().flags 512 | } 513 | 514 | /// The owner uid of the file 515 | pub fn uid(&self) -> Option { 516 | if self.attr().flags & sys::SSH_FILEXFER_ATTR_UIDGID != 0 { 517 | Some(self.attr().uid) 518 | } else { 519 | None 520 | } 521 | } 522 | 523 | /// The owner gid of the file 524 | pub fn gid(&self) -> Option { 525 | if self.attr().flags & sys::SSH_FILEXFER_ATTR_UIDGID != 0 { 526 | Some(self.attr().gid) 527 | } else { 528 | None 529 | } 530 | } 531 | 532 | /// The unix mode_t permission bits 533 | pub fn permissions(&self) -> Option { 534 | if self.attr().flags & sys::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { 535 | Some(self.attr().permissions) 536 | } else { 537 | None 538 | } 539 | } 540 | 541 | /// The type of the file decoded from the permissions 542 | pub fn file_type(&self) -> Option { 543 | if self.attr().flags & sys::SSH_FILEXFER_ATTR_PERMISSIONS != 0 { 544 | Some(match self.attr().type_ as u32 { 545 | sys::SSH_FILEXFER_TYPE_SPECIAL => FileType::Special, 546 | sys::SSH_FILEXFER_TYPE_SYMLINK => FileType::Symlink, 547 | sys::SSH_FILEXFER_TYPE_REGULAR => FileType::Regular, 548 | sys::SSH_FILEXFER_TYPE_DIRECTORY => FileType::Directory, 549 | sys::SSH_FILEXFER_TYPE_UNKNOWN | _ => FileType::Unknown, 550 | }) 551 | } else { 552 | None 553 | } 554 | } 555 | 556 | /// The last-accessed time 557 | pub fn accessed(&self) -> Option { 558 | let duration = if self.attr().flags & sys::SSH_FILEXFER_ATTR_ACCESSTIME != 0 { 559 | Duration::from_secs(self.attr().atime64) 560 | + Duration::from_nanos( 561 | if self.attr().flags & sys::SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 { 562 | self.attr().atime_nseconds.into() 563 | } else { 564 | 0 565 | }, 566 | ) 567 | } else if self.attr().flags & sys::SSH_FILEXFER_ATTR_ACMODTIME != 0 { 568 | Duration::from_secs(self.attr().atime.into()) 569 | } else { 570 | return None; 571 | }; 572 | SystemTime::UNIX_EPOCH.checked_add(duration) 573 | } 574 | 575 | /// The file creation time 576 | pub fn created(&self) -> Option { 577 | let duration = if self.attr().flags & sys::SSH_FILEXFER_ATTR_CREATETIME != 0 { 578 | Duration::from_secs(self.attr().createtime) 579 | + Duration::from_nanos( 580 | if self.attr().flags & sys::SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 { 581 | self.attr().createtime_nseconds.into() 582 | } else { 583 | 0 584 | }, 585 | ) 586 | } else { 587 | return None; 588 | }; 589 | SystemTime::UNIX_EPOCH.checked_add(duration) 590 | } 591 | 592 | /// The file modification time 593 | pub fn modified(&self) -> Option { 594 | let duration = if self.attr().flags & sys::SSH_FILEXFER_ATTR_MODIFYTIME != 0 { 595 | Duration::from_secs(self.attr().mtime64) 596 | + Duration::from_nanos( 597 | if self.attr().flags & sys::SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 { 598 | self.attr().mtime_nseconds.into() 599 | } else { 600 | 0 601 | }, 602 | ) 603 | } else if self.attr().flags & sys::SSH_FILEXFER_ATTR_ACMODTIME != 0 { 604 | Duration::from_secs(self.attr().mtime.into()) 605 | } else { 606 | return None; 607 | }; 608 | SystemTime::UNIX_EPOCH.checked_add(duration) 609 | } 610 | } 611 | 612 | pub struct SftpDir { 613 | pub(crate) sess: Arc>, 614 | pub(crate) dir_inner: sys::sftp_dir, 615 | pub(crate) sftp: sys::sftp_session, 616 | } 617 | 618 | unsafe impl Send for SftpDir {} 619 | 620 | impl Drop for SftpDir { 621 | fn drop(&mut self) { 622 | let (_sess, dir) = self.lock_session(); 623 | unsafe { 624 | sys::sftp_closedir(dir); 625 | } 626 | } 627 | } 628 | 629 | impl SftpDir { 630 | fn lock_session(&self) -> (MutexGuard, sys::sftp_dir) { 631 | (self.sess.lock().unwrap(), self.dir_inner) 632 | } 633 | 634 | /// Read the next entry from the directory. 635 | /// Returns None if there are no more entries. 636 | pub fn read_dir(&self) -> Option> { 637 | let (_sess, dir) = self.lock_session(); 638 | let attr = unsafe { sys::sftp_readdir(self.sftp, dir) }; 639 | if attr.is_null() { 640 | if unsafe { sys::sftp_dir_eof(dir) } == 1 { 641 | None 642 | } else { 643 | Some(Err(Error::Sftp(SftpError::from_session(self.sftp)))) 644 | } 645 | } else { 646 | Some(Ok(Metadata { attr })) 647 | } 648 | } 649 | } 650 | 651 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 652 | pub enum FileType { 653 | Special, 654 | Symlink, 655 | Regular, 656 | Directory, 657 | Unknown, 658 | } 659 | 660 | bitflags::bitflags! { 661 | /// Bitflags that indicate options for opening a sftp file. 662 | pub struct OpenFlags: c_int { 663 | /// The file should be opened as read-only. 664 | const READ_ONLY = libc::O_RDONLY; 665 | /// The file should be opened as write-only. 666 | const WRITE_ONLY = libc::O_WRONLY; 667 | /// The file should be opened as read and write. 668 | /// 669 | /// Note that this is a different value than `READ_ONLY | WRITE_ONLY`, which is a logic error. 670 | const READ_WRITE = libc::O_RDWR; 671 | /// Create the file if it does not exist. 672 | const CREATE = libc::O_CREAT; 673 | /// When used with `CREATE`, this flag ensures that a new file is created. 674 | const EXCLUSIVE = libc::O_EXCL; 675 | /// If the file exists, truncate it. 676 | const TRUNCATE = libc::O_TRUNC; 677 | /// Before each write, the file offset is set to the end of the file. 678 | const APPEND = libc::O_APPEND; 679 | /// Create a new file, failing if it already exists. 680 | /// 681 | /// This is an alias for `CREATE | EXCLUSIVE`. 682 | const CREATE_NEW = libc::O_CREAT | libc::O_EXCL; 683 | } 684 | } 685 | --------------------------------------------------------------------------------