├── rustfmt.toml ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── actions-rs │ └── grcov.yml ├── workflows │ ├── build.yml │ └── coverage.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── src ├── default_algorithms.rs ├── host.rs ├── default_algorithms │ └── openssh.rs ├── serializer.rs ├── params │ └── algos.rs ├── params.rs ├── lib.rs ├── parser │ └── field.rs └── parser.rs ├── assets └── ssh.config ├── LICENSE ├── examples ├── print.rs ├── query.rs └── client.rs ├── Cargo.toml ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── CONTRIBUTING.md └── README.md /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Module" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask what you want about the project 4 | title: "[QUESTION] - TITLE" 5 | labels: question 6 | assignees: veeso 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/actions-rs/grcov.yml: -------------------------------------------------------------------------------- 1 | branch: false 2 | ignore-not-existing: true 3 | llvm: true 4 | output-type: lcov 5 | ignore: 6 | - "/*" 7 | - "C:/*" 8 | - "../*" 9 | - src/lib.rs 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .rpm/ 3 | 4 | # Created by https://www.gitignore.io/api/rust 5 | # Edit at https://www.gitignore.io/?templates=rust 6 | 7 | ### Rust ### 8 | # Generated by Cargo 9 | # will have compiled files and executables 10 | /target/ 11 | # for libs 12 | Cargo.lock 13 | 14 | # These are backup files generated by rustfmt 15 | **/*.rs.bk 16 | 17 | # End of https://www.gitignore.io/api/rust 18 | 19 | # Distributions 20 | *.rpm 21 | *.deb 22 | dist/pkgs/arch/*.tar.gz 23 | 24 | # Macos 25 | .DS_Store 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea to improve ssh2-config 4 | title: "[Feature Request] - FEATURE_TITLE" 5 | labels: "new feature" 6 | assignees: veeso 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | Put here a brief introduction to your suggestion. 13 | 14 | ### Changes 15 | 16 | The following changes to the application are expected 17 | 18 | - ... 19 | 20 | ## Implementation 21 | 22 | Provide any kind of suggestion you propose on how to implement the feature. 23 | If you have none, delete this section. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report of the bug you've encountered 4 | title: "[BUG] - ISSUE_TITLE" 5 | labels: bug 6 | assignees: veeso 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## Steps to reproduce 15 | 16 | Steps to reproduce the bug you encountered 17 | 18 | ## Expected behaviour 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## Environment 23 | 24 | - OS: [e.g. GNU/Linux Debian 10] 25 | - Architecture [Arm, x86_64, ...] 26 | - Rust version 27 | - remotefs version 28 | - Protocol used 29 | - Remote server version and name 30 | 31 | ## Additional information 32 | 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: Swatinem/rust-cache@v2 15 | with: 16 | cache-on-failure: true 17 | - name: Set up Rust 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | toolchain: stable 21 | components: clippy, rustfmt 22 | 23 | - name: Set up environment for tests 24 | run: mkdir -p $HOME/.ssh && touch $HOME/.ssh/config 25 | - name: build 26 | run: cargo build --verbose 27 | - name: Style check 28 | run: cargo fmt --all --check 29 | - name: Run tests 30 | run: cargo test 31 | - name: Clippy 32 | run: cargo clippy -- -Dwarnings 33 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: dtolnay/rust-toolchain@stable 15 | - uses: taiki-e/install-action@cargo-llvm-cov 16 | - name: Set up environment for tests 17 | run: mkdir -p $HOME/.ssh && touch $HOME/.ssh/config 18 | - name: Generate code coverage 19 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 20 | - name: Upload coverage artifact 21 | uses: actions/upload-artifact@v4 22 | with: 23 | path: lcov.info 24 | - name: Coveralls 25 | uses: coverallsapp/github-action@v2.3.4 26 | with: 27 | github-token: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /src/default_algorithms.rs: -------------------------------------------------------------------------------- 1 | mod openssh; 2 | 3 | /// Default algorithms for ssh. 4 | #[derive(Debug, Clone, PartialEq, Eq)] 5 | pub struct DefaultAlgorithms { 6 | pub ca_signature_algorithms: Vec, 7 | pub ciphers: Vec, 8 | pub host_key_algorithms: Vec, 9 | pub kex_algorithms: Vec, 10 | pub mac: Vec, 11 | pub pubkey_accepted_algorithms: Vec, 12 | } 13 | 14 | impl Default for DefaultAlgorithms { 15 | fn default() -> Self { 16 | self::openssh::defaults() 17 | } 18 | } 19 | 20 | impl DefaultAlgorithms { 21 | /// Create a new instance of [`DefaultAlgorithms`] with empty fields. 22 | pub fn empty() -> Self { 23 | Self { 24 | ca_signature_algorithms: vec![], 25 | ciphers: vec![], 26 | host_key_algorithms: vec![], 27 | kex_algorithms: vec![], 28 | mac: vec![], 29 | pubkey_accepted_algorithms: vec![], 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /assets/ssh.config: -------------------------------------------------------------------------------- 1 | # ssh config example 2 | 3 | # Command line options, overriding host-specific options 4 | Compression yes 5 | ConnectionAttempts 10 6 | ConnectTimeout 60 7 | ServerAliveInterval 40 8 | TcpKeepAlive yes 9 | 10 | # Host configuration 11 | 12 | Host 192.168.*.* 172.26.*.* !192.168.1.30 13 | User omar 14 | ForwardAgent yes 15 | BindAddress 10.8.0.10 16 | BindInterface tun0 17 | Ciphers +aes128-cbc,aes192-cbc,aes256-cbc 18 | Macs +hmac-sha1-etm@openssh.com 19 | 20 | Host tostapane 21 | User ciro-esposito 22 | HostName 192.168.24.32 23 | RemoteForward 88 24 | Compression no 25 | Port 2222 26 | 27 | Host 192.168.1.30 28 | User nutellaro 29 | RemoteForward 123 30 | 31 | Host * 32 | Ciphers aes128-ctr,aes192-ctr,aes256-ctr 33 | KexAlgorithms diffie-hellman-group-exchange-sha256 34 | MACs hmac-sha2-512,hmac-sha2-256,hmac-ripemd160 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 Christian Visintin 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 | -------------------------------------------------------------------------------- /examples/print.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | use std::fs::File; 3 | use std::io::BufReader; 4 | use std::path::{Path, PathBuf}; 5 | 6 | use dirs::home_dir; 7 | use ssh2_config::{ParseRule, SshConfig}; 8 | 9 | fn main() { 10 | // get args 11 | let args: Vec = args().collect(); 12 | // check path 13 | let config_path = match args.get(1) { 14 | Some(p) => PathBuf::from(p), 15 | None => { 16 | let mut p = home_dir().expect("Failed to get home_dir for guest OS"); 17 | p.extend(Path::new(".ssh/config")); 18 | p 19 | } 20 | }; 21 | // Open config file 22 | let config = read_config(config_path.as_path()); 23 | 24 | println!("{config}"); 25 | } 26 | 27 | fn read_config(p: &Path) -> SshConfig { 28 | let mut reader = match File::open(p) { 29 | Ok(f) => BufReader::new(f), 30 | Err(err) => panic!("Could not open file '{}': {}", p.display(), err), 31 | }; 32 | match SshConfig::default().parse(&mut reader, ParseRule::STRICT) { 33 | Ok(config) => config, 34 | Err(err) => panic!("Failed to parse configuration: {}", err), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Christian Visintin "] 3 | categories = ["network-programming"] 4 | description = "an ssh configuration parser for ssh2-rs" 5 | documentation = "https://docs.rs/ssh2-config" 6 | edition = "2024" 7 | rust-version = "1.88.0" 8 | homepage = "https://veeso.github.io/ssh2-config/" 9 | include = [ 10 | "build/**/*", 11 | "examples/**/*", 12 | "src/**/*", 13 | "LICENSE", 14 | "README.md", 15 | "CHANGELOG.md", 16 | ] 17 | keywords = ["ssh2", "ssh", "ssh-config", "ssh-config-parser"] 18 | license = "MIT" 19 | name = "ssh2-config" 20 | readme = "README.md" 21 | repository = "https://github.com/veeso/ssh2-config" 22 | version = "0.6.2" 23 | build = "build/main.rs" 24 | 25 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 26 | 27 | [dependencies] 28 | bitflags = "^2" 29 | dirs = "^6" 30 | log = "^0.4" 31 | glob = "0.3" 32 | thiserror = "^2" 33 | wildmatch = "^2" 34 | 35 | [dev-dependencies] 36 | env_logger = "^0.11" 37 | pretty_assertions = "^1" 38 | rpassword = "^7" 39 | ssh2 = "^0.9" 40 | tempfile = "^3" 41 | 42 | [build-dependencies] 43 | anyhow = "1" 44 | git2 = "0.20" 45 | 46 | [features] 47 | default = [] 48 | nolog = ["log/max_level_off"] 49 | 50 | [[example]] 51 | name = "client" 52 | path = "examples/client.rs" 53 | 54 | [[example]] 55 | name = "query" 56 | path = "examples/query.rs" 57 | 58 | [[example]] 59 | name = "print" 60 | path = "examples/print.rs" 61 | -------------------------------------------------------------------------------- /examples/query.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | use std::fs::File; 3 | use std::io::BufReader; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::exit; 6 | 7 | use dirs::home_dir; 8 | use ssh2_config::{ParseRule, SshConfig}; 9 | 10 | fn main() { 11 | // get args 12 | let args: Vec = args().collect(); 13 | let address = match args.get(1) { 14 | Some(addr) => addr.to_string(), 15 | None => { 16 | usage(); 17 | exit(255) 18 | } 19 | }; 20 | // check path 21 | let config_path = match args.get(2) { 22 | Some(p) => PathBuf::from(p), 23 | None => { 24 | let mut p = home_dir().expect("Failed to get home_dir for guest OS"); 25 | p.extend(Path::new(".ssh/config")); 26 | p 27 | } 28 | }; 29 | // Open config file 30 | let config = read_config(config_path.as_path()); 31 | let params = config.query(address.as_str()); 32 | println!("Configuration for {}: {:?}", address, params); 33 | } 34 | 35 | fn usage() { 36 | eprintln!("Usage: cargo run --example query --
[config-path]"); 37 | } 38 | 39 | fn read_config(p: &Path) -> SshConfig { 40 | let mut reader = match File::open(p) { 41 | Ok(f) => BufReader::new(f), 42 | Err(err) => panic!("Could not open file '{}': {}", p.display(), err), 43 | }; 44 | match SshConfig::default().parse(&mut reader, ParseRule::STRICT) { 45 | Ok(config) => config, 46 | Err(err) => panic!("Failed to parse configuration: {}", err), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # ISSUE _NUMBER_ - PULL_REQUEST_TITLE 2 | 3 | Fixes # (issue) 4 | 5 | ## Description 6 | 7 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 8 | 9 | List here your changes 10 | 11 | - I made this... 12 | - I made also that... 13 | 14 | ## Type of change 15 | 16 | Please select relevant options. 17 | 18 | - [ ] Bug fix (non-breaking change which fixes an issue) 19 | - [ ] New feature (non-breaking change which adds functionality) 20 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 21 | - [ ] This change requires a documentation update 22 | 23 | ## Checklist 24 | 25 | - [ ] My code follows the contribution guidelines of this project 26 | - [ ] I have performed a self-review of my own code 27 | - [ ] I have commented my code, particularly in hard-to-understand areas 28 | - [ ] My changes generate no new warnings 29 | - [ ] I formatted the code with `cargo fmt` 30 | - [ ] I checked my code using `cargo clippy` and reports no warnings 31 | - [ ] I have added tests that prove my fix is effective or that my feature works 32 | - [ ] The changes I've made are Windows, MacOS, Linux compatible (or I've handled them using `cfg target_os`) 33 | - [ ] I increased or maintained the code coverage for the project, compared to the previous commit 34 | 35 | ## Acceptance tests 36 | 37 | wait for a *project maintainer* to fulfill this section... 38 | 39 | - [ ] regression test: ... 40 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at christian.visintin1997@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | 77 | -------------------------------------------------------------------------------- /src/host.rs: -------------------------------------------------------------------------------- 1 | //! # host 2 | //! 3 | //! Ssh host type 4 | 5 | use std::fmt; 6 | 7 | use wildmatch::WildMatch; 8 | 9 | use super::HostParams; 10 | 11 | /// Describes the rules to be used for a certain host 12 | #[derive(Debug, Clone, PartialEq, Eq)] 13 | pub struct Host { 14 | /// List of hosts for which params are valid. String is string pattern, bool is whether condition is negated 15 | pub pattern: Vec, 16 | pub params: HostParams, 17 | } 18 | 19 | impl Host { 20 | pub fn new(pattern: Vec, params: HostParams) -> Self { 21 | Self { pattern, params } 22 | } 23 | 24 | /// Returns whether `host` argument intersects the host clauses 25 | pub fn intersects(&self, host: &str) -> bool { 26 | let mut has_matched = false; 27 | for entry in self.pattern.iter() { 28 | let matches = entry.intersects(host); 29 | // If the entry is negated and it matches we can stop searching 30 | if matches && entry.negated { 31 | return false; 32 | } 33 | has_matched |= matches; 34 | } 35 | has_matched 36 | } 37 | } 38 | 39 | /// Describes a single clause to match host 40 | #[derive(Debug, Clone, PartialEq, Eq)] 41 | pub struct HostClause { 42 | pub pattern: String, 43 | pub negated: bool, 44 | } 45 | 46 | impl fmt::Display for HostClause { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | if self.negated { 49 | write!(f, "!{}", self.pattern) 50 | } else { 51 | write!(f, "{}", self.pattern) 52 | } 53 | } 54 | } 55 | 56 | impl HostClause { 57 | /// Creates a new `HostClause` from arguments 58 | pub fn new(pattern: String, negated: bool) -> Self { 59 | Self { pattern, negated } 60 | } 61 | 62 | /// Returns whether `host` argument intersects the clause 63 | pub fn intersects(&self, host: &str) -> bool { 64 | WildMatch::new(self.pattern.as_str()).matches(host) 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | 71 | use pretty_assertions::assert_eq; 72 | 73 | use super::*; 74 | use crate::DefaultAlgorithms; 75 | 76 | #[test] 77 | fn should_build_host_clause() { 78 | let clause = HostClause::new("192.168.1.1".to_string(), false); 79 | assert_eq!(clause.pattern.as_str(), "192.168.1.1"); 80 | assert_eq!(clause.negated, false); 81 | } 82 | 83 | #[test] 84 | fn should_intersect_host_clause() { 85 | let clause = HostClause::new("192.168.*.*".to_string(), false); 86 | assert!(clause.intersects("192.168.2.30")); 87 | let clause = HostClause::new("192.168.?0.*".to_string(), false); 88 | assert!(clause.intersects("192.168.40.28")); 89 | } 90 | 91 | #[test] 92 | fn should_not_intersect_host_clause() { 93 | let clause = HostClause::new("192.168.*.*".to_string(), false); 94 | assert_eq!(clause.intersects("172.26.104.4"), false); 95 | } 96 | 97 | #[test] 98 | fn should_init_host() { 99 | let host = Host::new( 100 | vec![HostClause::new("192.168.*.*".to_string(), false)], 101 | HostParams::new(&DefaultAlgorithms::default()), 102 | ); 103 | assert_eq!(host.pattern.len(), 1); 104 | } 105 | 106 | #[test] 107 | fn should_intersect_clause() { 108 | let host = Host::new( 109 | vec![ 110 | HostClause::new("192.168.*.*".to_string(), false), 111 | HostClause::new("172.26.*.*".to_string(), false), 112 | HostClause::new("10.8.*.*".to_string(), false), 113 | HostClause::new("10.8.0.8".to_string(), true), 114 | ], 115 | HostParams::new(&DefaultAlgorithms::default()), 116 | ); 117 | assert!(host.intersects("192.168.1.32")); 118 | assert!(host.intersects("172.26.104.4")); 119 | assert!(host.intersects("10.8.0.10")); 120 | } 121 | 122 | #[test] 123 | fn should_not_intersect_clause() { 124 | let host = Host::new( 125 | vec![ 126 | HostClause::new("192.168.*.*".to_string(), false), 127 | HostClause::new("172.26.*.*".to_string(), false), 128 | HostClause::new("10.8.*.*".to_string(), false), 129 | HostClause::new("10.8.0.8".to_string(), true), 130 | ], 131 | HostParams::new(&DefaultAlgorithms::default()), 132 | ); 133 | assert_eq!(host.intersects("192.169.1.32"), false); 134 | assert_eq!(host.intersects("172.28.104.4"), false); 135 | assert_eq!(host.intersects("10.9.0.8"), false); 136 | assert_eq!(host.intersects("10.8.0.8"), false); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /examples/client.rs: -------------------------------------------------------------------------------- 1 | //! # client 2 | //! 3 | //! Ssh2-config implementation with a ssh2 client 4 | 5 | use std::env::args; 6 | use std::fs::File; 7 | use std::io::BufReader; 8 | use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; 9 | use std::path::{Path, PathBuf}; 10 | use std::process::exit; 11 | use std::time::Duration; 12 | 13 | use dirs::home_dir; 14 | use ssh2::{MethodType, Session}; 15 | use ssh2_config::{HostParams, ParseRule, SshConfig}; 16 | 17 | fn main() { 18 | // get args 19 | let args: Vec = args().collect(); 20 | let address = match args.get(1) { 21 | Some(addr) => addr.to_string(), 22 | None => { 23 | usage(); 24 | exit(255) 25 | } 26 | }; 27 | // check path 28 | let config_path = match args.get(2) { 29 | Some(p) => PathBuf::from(p), 30 | None => { 31 | let mut p = home_dir().expect("Failed to get home_dir for guest OS"); 32 | p.extend(Path::new(".ssh/config")); 33 | p 34 | } 35 | }; 36 | // Open config file 37 | let config = read_config(config_path.as_path()); 38 | let params = config.query(address.as_str()); 39 | connect(address.as_str(), ¶ms); 40 | } 41 | 42 | fn usage() { 43 | eprintln!("Usage: cargo run --example client -- [config-path]"); 44 | } 45 | 46 | fn read_config(p: &Path) -> SshConfig { 47 | let mut reader = match File::open(p) { 48 | Ok(f) => BufReader::new(f), 49 | Err(err) => panic!("Could not open file '{}': {}", p.display(), err), 50 | }; 51 | match SshConfig::default().parse(&mut reader, ParseRule::STRICT) { 52 | Ok(config) => config, 53 | Err(err) => panic!("Failed to parse configuration: {}", err), 54 | } 55 | } 56 | 57 | fn connect(host: &str, params: &HostParams) { 58 | // Resolve host 59 | let host = match params.host_name.as_deref() { 60 | Some(h) => h, 61 | None => host, 62 | }; 63 | let port = params.port.unwrap_or(22); 64 | let host = match host.contains(':') { 65 | true => host.to_string(), 66 | false => format!("{}:{}", host, port), 67 | }; 68 | println!("Connecting to host {}...", host); 69 | let socket_addresses: Vec = match host.to_socket_addrs() { 70 | Ok(s) => s.collect(), 71 | Err(err) => { 72 | panic!("Could not parse host: {}", err); 73 | } 74 | }; 75 | let mut tcp: Option = None; 76 | // Try addresses 77 | for socket_addr in socket_addresses.iter() { 78 | match TcpStream::connect_timeout( 79 | socket_addr, 80 | params.connect_timeout.unwrap_or(Duration::from_secs(30)), 81 | ) { 82 | Ok(stream) => { 83 | println!("Established connection with {}", socket_addr); 84 | tcp = Some(stream); 85 | break; 86 | } 87 | Err(_) => continue, 88 | } 89 | } 90 | // If stream is None, return connection timeout 91 | let stream: TcpStream = match tcp { 92 | Some(t) => t, 93 | None => { 94 | panic!("No suitable socket address found; connection timeout"); 95 | } 96 | }; 97 | let mut session: Session = match Session::new() { 98 | Ok(s) => s, 99 | Err(err) => { 100 | panic!("Could not create session: {}", err); 101 | } 102 | }; 103 | // Configure session 104 | configure_session(&mut session, params); 105 | // Connect 106 | session.set_tcp_stream(stream); 107 | if let Err(err) = session.handshake() { 108 | panic!("Handshake failed: {}", err); 109 | } 110 | // Get username 111 | let username = match params.user.as_ref() { 112 | Some(u) => { 113 | println!("Using username '{}'", u); 114 | u.clone() 115 | } 116 | None => read_secret("Username: "), 117 | }; 118 | let password = read_secret("Password: "); 119 | if let Err(err) = session.userauth_password(username.as_str(), password.as_str()) { 120 | panic!("Authentication failed: {}", err); 121 | } 122 | if let Some(banner) = session.banner() { 123 | println!("{}", banner); 124 | } 125 | println!("Connection OK!"); 126 | if let Err(err) = session.disconnect(None, "mandi mandi!", None) { 127 | panic!("Disconnection failed: {}", err); 128 | } 129 | } 130 | 131 | fn configure_session(session: &mut Session, params: &HostParams) { 132 | println!("Configuring session..."); 133 | if let Some(compress) = params.compression { 134 | println!("compression: {}", compress); 135 | session.set_compress(compress); 136 | } 137 | if params.tcp_keep_alive.unwrap_or(false) && params.server_alive_interval.is_some() { 138 | let interval = params.server_alive_interval.unwrap().as_secs() as u32; 139 | println!("keepalive interval: {} seconds", interval); 140 | session.set_keepalive(true, interval); 141 | } 142 | 143 | // KEX 144 | if let Err(err) = session.method_pref( 145 | MethodType::Kex, 146 | params.kex_algorithms.algorithms().join(",").as_str(), 147 | ) { 148 | panic!("Could not set KEX algorithms: {}", err); 149 | } 150 | 151 | // host key 152 | if let Err(err) = session.method_pref( 153 | MethodType::HostKey, 154 | params.host_key_algorithms.algorithms().join(",").as_str(), 155 | ) { 156 | panic!("Could not set host key algorithms: {}", err); 157 | } 158 | 159 | // ciphers 160 | if let Err(err) = session.method_pref( 161 | MethodType::CryptCs, 162 | params.ciphers.algorithms().join(",").as_str(), 163 | ) { 164 | panic!("Could not set crypt algorithms (client-server): {}", err); 165 | } 166 | if let Err(err) = session.method_pref( 167 | MethodType::CryptSc, 168 | params.ciphers.algorithms().join(",").as_str(), 169 | ) { 170 | panic!("Could not set crypt algorithms (server-client): {}", err); 171 | } 172 | 173 | // mac 174 | if let Err(err) = session.method_pref( 175 | MethodType::MacCs, 176 | params.mac.algorithms().join(",").as_str(), 177 | ) { 178 | panic!("Could not set MAC algorithms (client-server): {}", err); 179 | } 180 | if let Err(err) = session.method_pref( 181 | MethodType::MacSc, 182 | params.mac.algorithms().join(",").as_str(), 183 | ) { 184 | panic!("Could not set MAC algorithms (server-client): {}", err); 185 | } 186 | } 187 | 188 | fn read_secret(prompt: &str) -> String { 189 | rpassword::prompt_password(prompt).expect("Failed to read from stdin") 190 | } 191 | -------------------------------------------------------------------------------- /src/default_algorithms/openssh.rs: -------------------------------------------------------------------------------- 1 | //! This file is autogenerated at build-time when `RELOAD_SSH_ALGO` is set to environment. 2 | 3 | use crate::DefaultAlgorithms; 4 | 5 | /// Default algorithms for ssh. 6 | pub fn defaults() -> DefaultAlgorithms { 7 | DefaultAlgorithms { 8 | ca_signature_algorithms: vec![ 9 | "ssh-ed25519".to_string(), 10 | "ecdsa-sha2-nistp256".to_string(), 11 | "ecdsa-sha2-nistp384".to_string(), 12 | "ecdsa-sha2-nistp521".to_string(), 13 | "sk-ssh-ed25519@openssh.com".to_string(), 14 | "sk-ecdsa-sha2-nistp256@openssh.com".to_string(), 15 | "rsa-sha2-512".to_string(), 16 | "rsa-sha2-256".to_string(), 17 | ], 18 | ciphers: vec![ 19 | "chacha20-poly1305@openssh.com".to_string(), 20 | "aes128-gcm@openssh.com,aes256-gcm@openssh.com".to_string(), 21 | "aes128-ctr,aes192-ctr,aes256-ctr".to_string(), 22 | ], 23 | host_key_algorithms: vec![ 24 | "ssh-ed25519-cert-v01@openssh.com".to_string(), 25 | "ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(), 26 | "ecdsa-sha2-nistp384-cert-v01@openssh.com".to_string(), 27 | "ecdsa-sha2-nistp521-cert-v01@openssh.com".to_string(), 28 | "sk-ssh-ed25519-cert-v01@openssh.com".to_string(), 29 | "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(), 30 | "rsa-sha2-512-cert-v01@openssh.com".to_string(), 31 | "rsa-sha2-256-cert-v01@openssh.com".to_string(), 32 | "ssh-ed25519".to_string(), 33 | "ecdsa-sha2-nistp256".to_string(), 34 | "ecdsa-sha2-nistp384".to_string(), 35 | "ecdsa-sha2-nistp521".to_string(), 36 | "sk-ssh-ed25519@openssh.com".to_string(), 37 | "sk-ecdsa-sha2-nistp256@openssh.com".to_string(), 38 | "rsa-sha2-512".to_string(), 39 | "rsa-sha2-256".to_string(), 40 | ], 41 | kex_algorithms: vec![ 42 | "mlkem768x25519-sha256".to_string(), 43 | "sntrup761x25519-sha512".to_string(), 44 | "sntrup761x25519-sha512@openssh.com".to_string(), 45 | "curve25519-sha256".to_string(), 46 | "curve25519-sha256@libssh.org".to_string(), 47 | "ecdh-sha2-nistp256".to_string(), 48 | "ecdh-sha2-nistp384".to_string(), 49 | "ecdh-sha2-nistp521".to_string(), 50 | "diffie-hellman-group-exchange-sha256".to_string(), 51 | "diffie-hellman-group16-sha512".to_string(), 52 | "diffie-hellman-group18-sha512".to_string(), 53 | "diffie-hellman-group14-sha256".to_string(), 54 | "ssh-ed25519-cert-v01@openssh.com".to_string(), 55 | "ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(), 56 | "ecdsa-sha2-nistp384-cert-v01@openssh.com".to_string(), 57 | "ecdsa-sha2-nistp521-cert-v01@openssh.com".to_string(), 58 | "sk-ssh-ed25519-cert-v01@openssh.com".to_string(), 59 | "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(), 60 | "rsa-sha2-512-cert-v01@openssh.com".to_string(), 61 | "rsa-sha2-256-cert-v01@openssh.com".to_string(), 62 | "ssh-ed25519".to_string(), 63 | "ecdsa-sha2-nistp256".to_string(), 64 | "ecdsa-sha2-nistp384".to_string(), 65 | "ecdsa-sha2-nistp521".to_string(), 66 | "sk-ssh-ed25519@openssh.com".to_string(), 67 | "sk-ecdsa-sha2-nistp256@openssh.com".to_string(), 68 | "rsa-sha2-512".to_string(), 69 | "rsa-sha2-256".to_string(), 70 | "chacha20-poly1305@openssh.com".to_string(), 71 | "aes128-gcm@openssh.com,aes256-gcm@openssh.com".to_string(), 72 | "aes128-ctr,aes192-ctr,aes256-ctr".to_string(), 73 | "chacha20-poly1305@openssh.com".to_string(), 74 | "aes128-gcm@openssh.com,aes256-gcm@openssh.com".to_string(), 75 | "aes128-ctr,aes192-ctr,aes256-ctr".to_string(), 76 | "umac-64-etm@openssh.com".to_string(), 77 | "umac-128-etm@openssh.com".to_string(), 78 | "hmac-sha2-256-etm@openssh.com".to_string(), 79 | "hmac-sha2-512-etm@openssh.com".to_string(), 80 | "hmac-sha1-etm@openssh.com".to_string(), 81 | "umac-64@openssh.com".to_string(), 82 | "umac-128@openssh.com".to_string(), 83 | "hmac-sha2-256".to_string(), 84 | "hmac-sha2-512".to_string(), 85 | "hmac-sha1".to_string(), 86 | "umac-64-etm@openssh.com".to_string(), 87 | "umac-128-etm@openssh.com".to_string(), 88 | "hmac-sha2-256-etm@openssh.com".to_string(), 89 | "hmac-sha2-512-etm@openssh.com".to_string(), 90 | "hmac-sha1-etm@openssh.com".to_string(), 91 | "umac-64@openssh.com".to_string(), 92 | "umac-128@openssh.com".to_string(), 93 | "hmac-sha2-256".to_string(), 94 | "hmac-sha2-512".to_string(), 95 | "hmac-sha1".to_string(), 96 | "none,zlib@openssh.com".to_string(), 97 | "none,zlib@openssh.com".to_string(), 98 | ], 99 | mac: vec![ 100 | "umac-64-etm@openssh.com".to_string(), 101 | "umac-128-etm@openssh.com".to_string(), 102 | "hmac-sha2-256-etm@openssh.com".to_string(), 103 | "hmac-sha2-512-etm@openssh.com".to_string(), 104 | "hmac-sha1-etm@openssh.com".to_string(), 105 | "umac-64@openssh.com".to_string(), 106 | "umac-128@openssh.com".to_string(), 107 | "hmac-sha2-256".to_string(), 108 | "hmac-sha2-512".to_string(), 109 | "hmac-sha1".to_string(), 110 | ], 111 | pubkey_accepted_algorithms: vec![ 112 | "ssh-ed25519-cert-v01@openssh.com".to_string(), 113 | "ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(), 114 | "ecdsa-sha2-nistp384-cert-v01@openssh.com".to_string(), 115 | "ecdsa-sha2-nistp521-cert-v01@openssh.com".to_string(), 116 | "sk-ssh-ed25519-cert-v01@openssh.com".to_string(), 117 | "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com".to_string(), 118 | "rsa-sha2-512-cert-v01@openssh.com".to_string(), 119 | "rsa-sha2-256-cert-v01@openssh.com".to_string(), 120 | "ssh-ed25519".to_string(), 121 | "ecdsa-sha2-nistp256".to_string(), 122 | "ecdsa-sha2-nistp384".to_string(), 123 | "ecdsa-sha2-nistp521".to_string(), 124 | "sk-ssh-ed25519@openssh.com".to_string(), 125 | "sk-ecdsa-sha2-nistp256@openssh.com".to_string(), 126 | "rsa-sha2-512".to_string(), 127 | "rsa-sha2-256".to_string(), 128 | ], 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - [Changelog](#changelog) 4 | - [0.6.2](#062) 5 | - [0.6.1](#061) 6 | - [0.6.0](#060) 7 | - [0.5.4](#054) 8 | - [0.5.1](#051) 9 | - [0.5.0](#050) 10 | - [0.4.0](#040) 11 | - [0.3.0](#030) 12 | - [0.2.3](#023) 13 | - [0.2.2](#022) 14 | - [0.2.1](#021) 15 | - [0.2.0](#020) 16 | - [0.1.6](#016) 17 | - [0.1.5](#015) 18 | - [0.1.4](#014) 19 | - [0.1.3](#013) 20 | - [0.1.2](#012) 21 | - [0.1.1](#011) 22 | - [0.1.0](#010) 23 | 24 | --- 25 | 26 | ## 0.6.2 27 | 28 | Released on 25/09/2025 29 | 30 | - fix: Identify the root/default host when serialising (#27) 31 | - fix: Combine host declarations when serialising (#28) 32 | - fix: Add AddKeysToAgent, ForwardAgent, ProxyJump fields (#29) 33 | 34 | Developed by [@milliams](https://github.com/milliams) 35 | 36 | ## 0.6.1 37 | 38 | Released on 25/09/2025 39 | 40 | - [Issue 30](https://github.com/veeso/ssh2-config/issues/30): Host blocks from included files didn't get registered. Fixed that. 41 | 42 | ## 0.6.0 43 | 44 | Released on 15/08/2025 45 | 46 | - Added a new constructor `SshConfig::from_hosts()` to build a `SshConfig` from a list of `Host`. 47 | - If `Include` directive contains a relative path, it must be resolved to `$HOME/.ssh/${PATH}` 48 | - Updated ssh default algos to `V_10_0_P2` 49 | 50 | ## 0.5.4 51 | 52 | Released on 27/03/2025 53 | 54 | - on docsrs DON'T build algos. It's not allowed by docs.rs 55 | - added `RELOAD_SSH_ALGO` env variable to rebuild algos. 56 | 57 | ## 0.5.1 58 | 59 | Released on 27/03/2025 60 | 61 | - build was not included in the package. Fixed that. 62 | 63 | ## 0.5.0 64 | 65 | Released on 27/03/2025 66 | 67 | - [issue 22](https://github.com/veeso/ssh2-config/issues/22): should parse tokens with `=` and quotes (`"`) 68 | - [issue 21](https://github.com/veeso/ssh2-config/issues/21): Finally fixed how parameters are applied to host patterns 69 | - Replaced algorithms `Vec` with `Algorithms` type. 70 | - The new type is a variant with `Append`, `Head`, `Exclude` and `Set`. 71 | - This allows to **ACTUALLY** handle algorithms correctly. 72 | - To pass to ssh options, use `algorithms()` method 73 | - Beware that when accessing the internal vec, you MUST care of what it means for that variant. 74 | - Replaced `HostParams::merge` with `HostParams::overwrite_if_none` to avoid overwriting existing values. 75 | - Added default Algorithms to the SshConfig structure. See readme for details on how to use it. 76 | 77 | ## 0.4.0 78 | 79 | Released on 15/03/2025 80 | 81 | - Added support for `Include` directive. 82 | - Fixed ordering in appliance of options. **It's always top-bottom**. 83 | - Added logging to parser. You can now disable logging by using `nolog` feature. 84 | - `parse_default_file` is now available to Windows users 85 | - Added `Display` and `ToString` traits for `SshConfig` which serializes the configuration into ssh2 format 86 | 87 | ## 0.3.0 88 | 89 | Released on 19/12/2024 90 | 91 | - thiserror `2.0` 92 | - ‼️ **BREAKING CHANGE**: Added support for unsupported fields: 93 | 94 | `AddressFamily, BatchMode, CanonicalDomains, CanonicalizeFallbackLock, CanonicalizeHostname, CanonicalizeMaxDots, CanonicalizePermittedCNAMEs, CheckHostIP, ClearAllForwardings, ControlMaster, ControlPath, ControlPersist, DynamicForward, EnableSSHKeysign, EscapeChar, ExitOnForwardFailure, FingerprintHash, ForkAfterAuthentication, ForwardAgent, ForwardX11, ForwardX11Timeout, ForwardX11Trusted, GatewayPorts, GlobalKnownHostsFile, GSSAPIAuthentication, GSSAPIDelegateCredentials, HashKnownHosts, HostbasedAcceptedAlgorithms, HostbasedAuthentication, HostKeyAlias, HostbasedKeyTypes, IdentitiesOnly, IdentityAgent, Include, IPQoS, KbdInteractiveAuthentication, KbdInteractiveDevices, KnownHostsCommand, LocalCommand, LocalForward, LogLevel, LogVerbose, NoHostAuthenticationForLocalhost, NumberOfPasswordPrompts, PasswordAuthentication, PermitLocalCommand, PermitRemoteOpen, PKCS11Provider, PreferredAuthentications, ProxyCommand, ProxyJump, ProxyUseFdpass, PubkeyAcceptedKeyTypes, RekeyLimit, RequestTTY, RevokedHostKeys, SecruityKeyProvider, SendEnv, ServerAliveCountMax, SessionType, SetEnv, StdinNull, StreamLocalBindMask, StrictHostKeyChecking, SyslogFacility, UpdateHostKeys, UserKnownHostsFile, VerifyHostKeyDNS, VisualHostKey, XAuthLocation` 95 | 96 | If you want to keep the behaviour as-is, use `ParseRule::STRICT | ParseRule::ALLOW_UNSUPPORTED_FIELDS` when calling `parse()` if you were using `ParseRule::STRICT` before. 97 | 98 | Otherwise you can now access unsupported fields by using the `unsupported_fields` field on the `HostParams` structure like this: 99 | 100 | ```rust 101 | use ssh2_config::{ParseRule, SshConfig}; 102 | use std::fs::File; 103 | use std::io::BufReader; 104 | 105 | let mut reader = BufReader::new(File::open(config_path).expect("Could not open configuration file")); 106 | let config = SshConfig::default().parse(&mut reader, ParseRule::ALLOW_UNSUPPORTED_FIELDS).expect("Failed to parse configuration"); 107 | 108 | // Query attributes for a certain host 109 | let params = config.query("192.168.1.2"); 110 | let forwards = params.unsupported_fields.get("dynamicforward"); 111 | ``` 112 | 113 | ## 0.2.3 114 | 115 | Released on 05/12/2023 116 | 117 | - Fixed the order of appliance of configuration argument when overriding occurred. Thanks @LeoniePhiline 118 | 119 | ## 0.2.2 120 | 121 | Released on 31/07/2023 122 | 123 | - Exposed `ignored_fields` as `Map>` (KeyName => Args) for `HostParams` 124 | 125 | ## 0.2.1 126 | 127 | Released on 28/07/2023 128 | 129 | - Added `parse_default_file` to parse directly the default ssh config file at `$HOME/.ssh/config` 130 | - Added `get_hosts` to retrieve current configuration's hosts 131 | 132 | ## 0.2.0 133 | 134 | Released on 09/05/2023 135 | 136 | - Added `ParseRule` field to `parse()` method to specify some rules for parsing. ❗ To keep the behaviour as-is use `ParseRule::STRICT` 137 | 138 | ## 0.1.6 139 | 140 | Released on 03/03/2023 141 | 142 | - Added legacy field support 143 | - HostbasedKeyTypes 144 | - PubkeyAcceptedKeyTypes 145 | 146 | ## 0.1.5 147 | 148 | Released on 27/02/2023 149 | 150 | - Fixed comments not being properly stripped 151 | 152 | ## 0.1.4 153 | 154 | Released on 02/02/2023 155 | 156 | - Fixed [issue 2](https://github.com/veeso/ssh2-config/issues/2) hosts not being sorted by priority in host query 157 | 158 | ## 0.1.3 159 | 160 | Released on 29/01/2022 161 | 162 | - Added missing `ForwardX11Trusted` field to known fields 163 | 164 | ## 0.1.2 165 | 166 | Released on 11/01/2022 167 | 168 | - Implemented `IgnoreUnknown` parameter 169 | - Added `UseKeychain` support for MacOS 170 | 171 | ## 0.1.1 172 | 173 | Released on 02/01/2022 174 | 175 | - Added `IdentityFile` parameter 176 | 177 | ## 0.1.0 178 | 179 | Released on 04/12/2021 180 | 181 | - First release 182 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Before contributing to this repository, please first discuss the change you wish to make via issue of this repository before making a change. 4 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 5 | 6 | - [Contributing](#contributing) 7 | - [Open an issue](#open-an-issue) 8 | - [Questions](#questions) 9 | - [Bug reports](#bug-reports) 10 | - [Feature requests](#feature-requests) 11 | - [Preferred contributions](#preferred-contributions) 12 | - [Pull Request Process](#pull-request-process) 13 | - [Software guidelines](#software-guidelines) 14 | 15 | --- 16 | 17 | ## Open an issue 18 | 19 | Open an issue when: 20 | 21 | - You have questions or concerns regarding the project or the application itself. 22 | - You have a bug to report. 23 | - You have a feature or a suggestion to improve ssh2-config to submit. 24 | 25 | ### Questions 26 | 27 | If you have a question open an issue using the `Question` template. 28 | By default your question should already be labeled with the `question` label, if you need help with your installation, please also add the `help wanted` label. 29 | Check the issue is always assigned to `veeso`. 30 | 31 | ### Bug reports 32 | 33 | If you want to report an issue or a bug you've encountered while using ssh2-config, open an issue using the `Bug report` template. 34 | The `Bug` label should already be set and the issue should already be assigned to `veeso`. 35 | Don't set other labels to your issue, not even priority. 36 | 37 | When you open a bug try to be the most precise as possible in describing your issue. I'm not saying you should always be that precise, since sometimes it's very easy for maintainers to understand what you're talking about. Just try to be reasonable to understand sometimes we might not know what you're talking about or we just don't have the technical knowledge you might think. 38 | Please always provide the environment you're working on and consider that we don't provide any support for older version of ssh2-config, at least for those not classified as LTS (if we'll ever have them). 39 | If you can, provide the log file or the snippet involving your issue. You can find in the [user manual](docs/man.md) the location of the log file. 40 | Last but not least: the template I've written must be used. Full stop. 41 | 42 | Maintainers will may add additional labels to your issue: 43 | 44 | - **duplicate**: the issue is duplicated; the reference to the related issue will be added to your description. Your issue will be closed. 45 | - **priority**: this must be fixed asap 46 | - **sorcery**: it is not possible to find out what's causing your bug, nor is reproducible on our test environments. 47 | - **wontfix**: your bug has a very high ratio between the difficulty to fix it and the probability to encounter it, or it just isn't a bug, but a feature. 48 | 49 | ### Feature requests 50 | 51 | Whenever you have a good idea which chould improve the project, it is a good idea to submit it to the project owner. 52 | The first thing you should do though, is not starting to write the code, but is to become concern about how ssh2-config works, what kind 53 | of contribution I appreciate and what kind of contribution I won't consider. 54 | Said so, follow these steps: 55 | 56 | - Read the contributing guidelines, entirely 57 | - Think on whether your idea would fit in the project mission and guidelines or not 58 | - Think about the impact your idea would have on the project 59 | - Open an issue using the `feature request` template describing with accuracy your suggestion 60 | - Wait for the maintainer feedback on your idea 61 | 62 | If you want to implement the feature by yourself and your suggestion gets approved, start writing the code. Remember that on [docs.rs](https://docs.rs/ssh2-config) there is the documentation for the project. Open a PR related to your issue. See [Pull request process for more details](#pull-request-process) 63 | 64 | It is very important to follow these steps, since it will prevent you from working on a feature that will be rejected and trust me, none of us wants to deal with this situation. 65 | 66 | Always mind that your suggestion, may be rejected: I'll always provide a feedback on the reasons that brought me to reject your feature, just try not to get mad about that. 67 | 68 | --- 69 | 70 | ## Preferred contributions 71 | 72 | At the moment, these kind of contributions are more appreciated and should be preferred: 73 | 74 | - Fix for issues described in [Known Issues](./README.md#known-issues-) or [issues reported by the community](https://github.com/veeso/ssh2-config/issues) 75 | - New file transfers: for further details see [Implementing File Transfer](#implementing-file-transfers) 76 | - Code optimizations: any optimization to the code is welcome 77 | 78 | For any other kind of contribution, especially for new features, please submit a new issue first. 79 | 80 | ## Pull Request Process 81 | 82 | Let's make it simple and clear: 83 | 84 | 1. Open a PR with an **appropriate label** (e.g. bug, enhancement, ...). 85 | 2. Write a **properly documentation** for your software compliant with **rustdoc** standard. 86 | 3. Write tests for your code. This doesn't apply necessarily for implementation regarding the user-interface module (`ui/activities`) and (if a test server is not available) for file transfers. 87 | 4. Check your code with `cargo clippy`. 88 | 5. Check if the CI for your commits reports three-green. 89 | 6. Report changes to the PR you opened, writing a report of what you changed and what you have introduced. 90 | 7. Update the `CHANGELOG.md` file with details of changes to the application. In changelog report changes under a chapter called `PR{PULL_REQUEST_NUMBER}` (e.g. PR12). 91 | 8. Assign a maintainer to the reviewers. 92 | 9. Wait for a maintainer to fullfil the acceptance tests 93 | 10. Wait for a maintainer to complete the acceptance tests 94 | 11. Request maintainers to merge your changes. 95 | 96 | ### Software guidelines 97 | 98 | In addition to the process described for the PRs, I've also decided to introduce a list of guidelines to follow when writing the code, that should be followed: 99 | 100 | 1. **Let's stop the NPM apocalypse**: personally I'm against the abuse of dependencies we make in software projects and I think that NodeJS has opened the way to this drama (and has already gone too far). Nowadays nobody cares about adding hundreds of dependencies to their projects. Don't misunderstand me: I think that package managers are cool, but I'm totally against the abuse we're making of them. I think when we work on a project, we should try to use the minor quantity of dependencies as possible, especially because it's not hard to see how many libraries are getting abandoned right now, causing compatibility issues after a while. So please, when working on ssh2-config, try not to add useless dependencies. 101 | 2. **Test units matter**: Whenever you implement something new to this project, always implement test units which cover the most cases as possible. 102 | 3. **Comments are useful**: Many people say that the code should be that simple to talk by itself about what it does, and comments should then be useless. I personally don't agree. I'm not saying they're wrong, but I'm just saying that this approach has, in my personal opinion, many aspects which are underrated: 103 | 1. What's obvious for me, might not be for the others. 104 | 2. Our capacity to work on a code depends mostly on **time and experience**, not on complexity: I'm not denying complexity matter, but the most decisive factor when working on code is the experience we've acquired working on it and the time we've spent. As the author of the project, I know the project like the back of my hands, but if I didn't work on it for a year, then I would probably have some problems in working on it again as the same speed as before. And do you know what's really time-saving in these cases? Comments. 105 | 106 | --- 107 | 108 | Thank you for any contribution! 109 | Christian Visintin 110 | -------------------------------------------------------------------------------- /src/serializer.rs: -------------------------------------------------------------------------------- 1 | //! SSH Config serializer 2 | 3 | use std::fmt; 4 | 5 | use crate::{Host, HostClause, HostParams, SshConfig}; 6 | 7 | pub struct SshConfigSerializer<'a>(&'a SshConfig); 8 | 9 | impl SshConfigSerializer<'_> { 10 | pub fn serialize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | if self.0.hosts.is_empty() { 12 | return Ok(()); 13 | } 14 | 15 | // serialize first host 16 | let root = self.0.hosts.first().unwrap(); 17 | // check if first host is the default host 18 | if root.pattern == vec![HostClause::new(String::from("*"), false)] { 19 | Self::serialize_host_params(f, &root.params, false)?; 20 | } else { 21 | Self::serialize_host(f, root)?; 22 | } 23 | 24 | // serialize other hosts 25 | for host in self.0.hosts.iter().skip(1) { 26 | Self::serialize_host(f, host)?; 27 | } 28 | 29 | Ok(()) 30 | } 31 | 32 | fn serialize_host(f: &mut fmt::Formatter<'_>, host: &Host) -> fmt::Result { 33 | let patterns = &host 34 | .pattern 35 | .iter() 36 | .map(|p| p.to_string()) 37 | .collect::>() 38 | .join(" "); 39 | writeln!(f, "Host {patterns}",)?; 40 | Self::serialize_host_params(f, &host.params, true)?; 41 | writeln!(f,)?; 42 | 43 | Ok(()) 44 | } 45 | 46 | fn serialize_host_params( 47 | f: &mut fmt::Formatter<'_>, 48 | params: &HostParams, 49 | nested: bool, 50 | ) -> fmt::Result { 51 | let padding = if nested { " " } else { "" }; 52 | 53 | if let Some(value) = params.bind_address.as_ref() { 54 | writeln!(f, "{padding}Hostname {value}",)?; 55 | } 56 | if let Some(value) = params.bind_interface.as_ref() { 57 | writeln!(f, "{padding}BindAddress {value}",)?; 58 | } 59 | if !params.ca_signature_algorithms.is_default() { 60 | writeln!( 61 | f, 62 | "{padding}CASignatureAlgorithms {ca_signature_algorithms}", 63 | padding = padding, 64 | ca_signature_algorithms = params.ca_signature_algorithms 65 | )?; 66 | } 67 | if let Some(certificate_file) = params.certificate_file.as_ref() { 68 | writeln!(f, "{padding}CertificateFile {}", certificate_file.display())?; 69 | } 70 | if !params.ciphers.is_default() { 71 | writeln!( 72 | f, 73 | "{padding}Ciphers {ciphers}", 74 | padding = padding, 75 | ciphers = params.ciphers 76 | )?; 77 | } 78 | if let Some(value) = params.compression.as_ref() { 79 | writeln!( 80 | f, 81 | "{padding}Compression {}", 82 | if *value { "yes" } else { "no" } 83 | )?; 84 | } 85 | if let Some(connection_attempts) = params.connection_attempts { 86 | writeln!(f, "{padding}ConnectionAttempts {connection_attempts}",)?; 87 | } 88 | if let Some(connect_timeout) = params.connect_timeout { 89 | writeln!(f, "{padding}ConnectTimeout {}", connect_timeout.as_secs())?; 90 | } 91 | if !params.host_key_algorithms.is_default() { 92 | writeln!( 93 | f, 94 | "{padding}HostKeyAlgorithms {host_key_algorithms}", 95 | padding = padding, 96 | host_key_algorithms = params.host_key_algorithms 97 | )?; 98 | } 99 | if let Some(host_name) = params.host_name.as_ref() { 100 | writeln!(f, "{padding}HostName {host_name}",)?; 101 | } 102 | if let Some(identity_file) = params.identity_file.as_ref() { 103 | writeln!( 104 | f, 105 | "{padding}IdentityFile {}", 106 | identity_file 107 | .iter() 108 | .map(|p| p.display().to_string()) 109 | .collect::>() 110 | .join(",") 111 | )?; 112 | } 113 | if let Some(ignore_unknown) = params.ignore_unknown.as_ref() { 114 | writeln!( 115 | f, 116 | "{padding}IgnoreUnknown {}", 117 | ignore_unknown 118 | .iter() 119 | .map(|p| p.to_string()) 120 | .collect::>() 121 | .join(",") 122 | )?; 123 | } 124 | if !params.kex_algorithms.is_default() { 125 | writeln!( 126 | f, 127 | "{padding}KexAlgorithms {kex_algorithms}", 128 | padding = padding, 129 | kex_algorithms = params.kex_algorithms 130 | )?; 131 | } 132 | if !params.mac.is_default() { 133 | writeln!( 134 | f, 135 | "{padding}MACs {mac}", 136 | padding = padding, 137 | mac = params.mac 138 | )?; 139 | } 140 | if let Some(port) = params.port { 141 | writeln!(f, "{padding}Port {port}", port = port)?; 142 | } 143 | if !params.pubkey_accepted_algorithms.is_default() { 144 | writeln!( 145 | f, 146 | "{padding}PubkeyAcceptedAlgorithms {pubkey_accepted_algorithms}", 147 | padding = padding, 148 | pubkey_accepted_algorithms = params.pubkey_accepted_algorithms 149 | )?; 150 | } 151 | if let Some(pubkey_authentication) = params.pubkey_authentication.as_ref() { 152 | writeln!( 153 | f, 154 | "{padding}PubkeyAuthentication {}", 155 | if *pubkey_authentication { "yes" } else { "no" } 156 | )?; 157 | } 158 | if let Some(remote_forward) = params.remote_forward.as_ref() { 159 | writeln!(f, "{padding}RemoteForward {remote_forward}",)?; 160 | } 161 | if let Some(server_alive_interval) = params.server_alive_interval { 162 | writeln!( 163 | f, 164 | "{padding}ServerAliveInterval {}", 165 | server_alive_interval.as_secs() 166 | )?; 167 | } 168 | if let Some(tcp_keep_alive) = params.tcp_keep_alive.as_ref() { 169 | writeln!( 170 | f, 171 | "{padding}TCPKeepAlive {}", 172 | if *tcp_keep_alive { "yes" } else { "no" } 173 | )?; 174 | } 175 | #[cfg(target_os = "macos")] 176 | if let Some(use_keychain) = params.use_keychain.as_ref() { 177 | writeln!( 178 | f, 179 | "{padding}UseKeychain {}", 180 | if *use_keychain { "yes" } else { "no" } 181 | )?; 182 | } 183 | if let Some(user) = params.user.as_ref() { 184 | writeln!(f, "{padding}User {user}",)?; 185 | } 186 | for (field, value) in ¶ms.ignored_fields { 187 | writeln!( 188 | f, 189 | "{padding}{field} {value}", 190 | field = field, 191 | value = value 192 | .iter() 193 | .map(|v| v.to_string()) 194 | .collect::>() 195 | .join(" ") 196 | )?; 197 | } 198 | for (field, value) in ¶ms.unsupported_fields { 199 | writeln!( 200 | f, 201 | "{padding}{field} {value}", 202 | field = field, 203 | value = value 204 | .iter() 205 | .map(|v| v.to_string()) 206 | .collect::>() 207 | .join(" ") 208 | )?; 209 | } 210 | 211 | Ok(()) 212 | } 213 | } 214 | 215 | impl<'a> From<&'a SshConfig> for SshConfigSerializer<'a> { 216 | fn from(config: &'a SshConfig) -> Self { 217 | SshConfigSerializer(config) 218 | } 219 | } 220 | 221 | #[cfg(test)] 222 | mod tests { 223 | use std::time::Duration; 224 | 225 | use super::*; 226 | use crate::{DefaultAlgorithms, HostClause}; 227 | 228 | #[test] 229 | fn are_host_patterns_combined() { 230 | let mut host_params = HostParams::new(&DefaultAlgorithms::empty()); 231 | host_params.host_name = Some("bastion.example.com".to_string()); 232 | 233 | let host = Host::new( 234 | vec![ 235 | HostClause::new(String::from("*.example.com"), false), 236 | HostClause::new(String::from("foo.example.com"), true), 237 | ], 238 | host_params, 239 | ); 240 | 241 | let output = SshConfig::from_hosts(vec![/*default_host,*/ host]).to_string(); 242 | assert!(&output.contains("Host *.example.com !foo.example.com")); 243 | } 244 | 245 | #[test] 246 | fn is_default_host_serialized_without_host() { 247 | let mut root_params = HostParams::new(&DefaultAlgorithms::empty()); 248 | root_params.server_alive_interval = Some(Duration::from_secs(60)); 249 | let root = Host::new(vec![HostClause::new(String::from("*"), false)], root_params); 250 | 251 | let mut host_params = HostParams::new(&DefaultAlgorithms::empty()); 252 | host_params.user = Some("example".to_string()); 253 | let host = Host::new( 254 | vec![HostClause::new(String::from("*.example.com"), false)], 255 | host_params, 256 | ); 257 | 258 | let output = SshConfig::from_hosts(vec![root, host]).to_string(); 259 | assert!(&output.starts_with("ServerAliveInterval 60")); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/params/algos.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::str::FromStr; 3 | 4 | use crate::SshParserError; 5 | 6 | const ID_APPEND: char = '+'; 7 | const ID_HEAD: char = '^'; 8 | const ID_EXCLUDE: char = '-'; 9 | 10 | /// List of algorithms to be used. 11 | /// The algorithms can be appended to the default set, placed at the head of the list, 12 | /// excluded from the default set, or set as the default set. 13 | /// 14 | /// # Configuring SSH Algorithms 15 | /// 16 | /// In order to configure ssh you should use the `to_string()` method to get the string representation 17 | /// with the correct format for ssh2. 18 | #[derive(Debug, Clone, PartialEq, Eq)] 19 | pub struct Algorithms { 20 | /// Algorithms to be used. 21 | algos: Vec, 22 | /// whether the default algorithms have been overridden 23 | overridden: bool, 24 | /// applied rule 25 | rule: Option, 26 | } 27 | 28 | impl Algorithms { 29 | /// Create a new instance of [`Algorithms`] with the given default algorithms. 30 | /// 31 | /// ## Example 32 | /// 33 | /// ```rust 34 | /// use ssh2_config::Algorithms; 35 | /// 36 | /// let algos = Algorithms::new(&["aes128-ctr", "aes192-ctr"]); 37 | /// ``` 38 | pub fn new(default: I) -> Self 39 | where 40 | I: IntoIterator, 41 | S: AsRef, 42 | { 43 | Self { 44 | algos: default 45 | .into_iter() 46 | .map(|s| s.as_ref().to_string()) 47 | .collect(), 48 | overridden: false, 49 | rule: None, 50 | } 51 | } 52 | } 53 | 54 | /// List of algorithms to be used. 55 | /// The algorithms can be appended to the default set, placed at the head of the list, 56 | /// excluded from the default set, or set as the default set. 57 | /// 58 | /// # Configuring SSH Algorithms 59 | /// 60 | /// In order to configure ssh you should use the `to_string()` method to get the string representation 61 | /// with the correct format for ssh2. 62 | /// 63 | /// # Algorithms vector 64 | /// 65 | /// Otherwise you can access the inner [`Vec`] of algorithms with the [`Algorithms::algos`] method. 66 | /// 67 | /// Beware though, that you must **TAKE CARE of the current variant**. 68 | /// 69 | /// For instance in case the variant is [`Algorithms::Exclude`] the algos contained in the vec are the ones **to be excluded**. 70 | /// 71 | /// While in case of [`Algorithms::Append`] the algos contained in the vec are the ones to be appended to the default ones. 72 | #[derive(Clone, Debug, PartialEq, Eq)] 73 | pub enum AlgorithmsRule { 74 | /// Append the given algorithms to the default set. 75 | Append(Vec), 76 | /// Place the given algorithms at the head of the list. 77 | Head(Vec), 78 | /// Exclude the given algorithms from the default set. 79 | Exclude(Vec), 80 | /// Set the given algorithms as the default set. 81 | Set(Vec), 82 | } 83 | 84 | /// Rule applied; used to format algorithms 85 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 86 | enum AlgorithmsOp { 87 | Append, 88 | Head, 89 | Exclude, 90 | Set, 91 | } 92 | 93 | impl Algorithms { 94 | /// Returns whether the default algorithms are being used. 95 | pub fn is_default(&self) -> bool { 96 | !self.overridden 97 | } 98 | 99 | /// Returns algorithms to be used. 100 | pub fn algorithms(&self) -> &[String] { 101 | &self.algos 102 | } 103 | 104 | /// Apply an [`AlgorithmsRule`] to the [`Algorithms`] instance. 105 | /// 106 | /// If defaults haven't been overridden, apply changes from incoming rule; 107 | /// otherwise keep as-is. 108 | pub fn apply(&mut self, rule: AlgorithmsRule) { 109 | if self.overridden { 110 | // don't apply changes if defaults have been overridden 111 | return; 112 | } 113 | 114 | let mut current_algos = self.algos.clone(); 115 | 116 | match rule.clone() { 117 | AlgorithmsRule::Append(algos) => { 118 | // append but exclude duplicates 119 | for algo in algos { 120 | if !current_algos.iter().any(|s| s == &algo) { 121 | current_algos.push(algo); 122 | } 123 | } 124 | } 125 | AlgorithmsRule::Head(algos) => { 126 | current_algos = algos; 127 | current_algos.extend(self.algorithms().iter().map(|s| s.to_string())); 128 | } 129 | AlgorithmsRule::Exclude(exclude) => { 130 | current_algos = current_algos 131 | .iter() 132 | .filter(|algo| !exclude.contains(algo)) 133 | .map(|s| s.to_string()) 134 | .collect(); 135 | } 136 | AlgorithmsRule::Set(algos) => { 137 | // override default with new set 138 | current_algos = algos; 139 | } 140 | } 141 | 142 | // apply changes 143 | self.rule = Some(rule); 144 | self.algos = current_algos; 145 | self.overridden = true; 146 | } 147 | } 148 | 149 | impl AlgorithmsRule { 150 | fn op(&self) -> AlgorithmsOp { 151 | match self { 152 | Self::Append(_) => AlgorithmsOp::Append, 153 | Self::Head(_) => AlgorithmsOp::Head, 154 | Self::Exclude(_) => AlgorithmsOp::Exclude, 155 | Self::Set(_) => AlgorithmsOp::Set, 156 | } 157 | } 158 | } 159 | 160 | impl FromStr for AlgorithmsRule { 161 | type Err = SshParserError; 162 | 163 | fn from_str(s: &str) -> Result { 164 | if s.is_empty() { 165 | return Err(SshParserError::ExpectedAlgorithms); 166 | } 167 | 168 | // get first char 169 | let (op, start) = match s.chars().next().expect("can't be empty") { 170 | ID_APPEND => (AlgorithmsOp::Append, 1), 171 | ID_HEAD => (AlgorithmsOp::Head, 1), 172 | ID_EXCLUDE => (AlgorithmsOp::Exclude, 1), 173 | _ => (AlgorithmsOp::Set, 0), 174 | }; 175 | 176 | let algos = s[start..] 177 | .split(',') 178 | .map(|s| s.trim().to_string()) 179 | .collect::>(); 180 | 181 | match op { 182 | AlgorithmsOp::Append => Ok(Self::Append(algos)), 183 | AlgorithmsOp::Head => Ok(Self::Head(algos)), 184 | AlgorithmsOp::Exclude => Ok(Self::Exclude(algos)), 185 | AlgorithmsOp::Set => Ok(Self::Set(algos)), 186 | } 187 | } 188 | } 189 | 190 | impl fmt::Display for AlgorithmsRule { 191 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 192 | let op = self.op(); 193 | write!(f, "{op}") 194 | } 195 | } 196 | 197 | impl fmt::Display for AlgorithmsOp { 198 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 199 | match &self { 200 | Self::Append => write!(f, "{ID_APPEND}"), 201 | Self::Head => write!(f, "{ID_HEAD}"), 202 | Self::Exclude => write!(f, "{ID_EXCLUDE}"), 203 | Self::Set => write!(f, ""), 204 | } 205 | } 206 | } 207 | 208 | impl fmt::Display for Algorithms { 209 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 210 | if let Some(rule) = self.rule.as_ref() { 211 | write!(f, "{rule}",) 212 | } else { 213 | write!(f, "{}", self.algos.join(",")) 214 | } 215 | } 216 | } 217 | 218 | #[cfg(test)] 219 | mod tests { 220 | 221 | use pretty_assertions::assert_eq; 222 | 223 | use super::*; 224 | 225 | #[test] 226 | fn test_should_parse_algos_set() { 227 | let algo = 228 | AlgorithmsRule::from_str("aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse"); 229 | assert_eq!( 230 | algo, 231 | AlgorithmsRule::Set(vec![ 232 | "aes128-ctr".to_string(), 233 | "aes192-ctr".to_string(), 234 | "aes256-ctr".to_string() 235 | ]) 236 | ); 237 | } 238 | 239 | #[test] 240 | fn test_should_parse_algos_append() { 241 | let algo = 242 | AlgorithmsRule::from_str("+aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse"); 243 | assert_eq!( 244 | algo, 245 | AlgorithmsRule::Append(vec![ 246 | "aes128-ctr".to_string(), 247 | "aes192-ctr".to_string(), 248 | "aes256-ctr".to_string() 249 | ]) 250 | ); 251 | } 252 | 253 | #[test] 254 | fn test_should_parse_algos_head() { 255 | let algo = 256 | AlgorithmsRule::from_str("^aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse"); 257 | assert_eq!( 258 | algo, 259 | AlgorithmsRule::Head(vec![ 260 | "aes128-ctr".to_string(), 261 | "aes192-ctr".to_string(), 262 | "aes256-ctr".to_string() 263 | ]) 264 | ); 265 | } 266 | 267 | #[test] 268 | fn test_should_parse_algos_exclude() { 269 | let algo = 270 | AlgorithmsRule::from_str("-aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse"); 271 | assert_eq!( 272 | algo, 273 | AlgorithmsRule::Exclude(vec![ 274 | "aes128-ctr".to_string(), 275 | "aes192-ctr".to_string(), 276 | "aes256-ctr".to_string() 277 | ]) 278 | ); 279 | } 280 | 281 | #[test] 282 | fn test_should_apply_append() { 283 | let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]); 284 | let algo2 = AlgorithmsRule::from_str("+aes256-ctr").expect("failed to parse"); 285 | algo1.apply(algo2); 286 | assert_eq!( 287 | algo1.algorithms(), 288 | vec![ 289 | "aes128-ctr".to_string(), 290 | "aes192-ctr".to_string(), 291 | "aes256-ctr".to_string() 292 | ] 293 | ); 294 | } 295 | 296 | #[test] 297 | fn test_should_merge_append_if_undefined() { 298 | let algos: Vec = vec![]; 299 | let mut algo1 = Algorithms::new(algos); 300 | let algo2 = AlgorithmsRule::from_str("+aes256-ctr").expect("failed to parse"); 301 | algo1.apply(algo2); 302 | assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string()]); 303 | } 304 | 305 | #[test] 306 | fn test_should_merge_head() { 307 | let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]); 308 | let algo2 = AlgorithmsRule::from_str("^aes256-ctr").expect("failed to parse"); 309 | algo1.apply(algo2); 310 | assert_eq!( 311 | algo1.algorithms(), 312 | vec![ 313 | "aes256-ctr".to_string(), 314 | "aes128-ctr".to_string(), 315 | "aes192-ctr".to_string() 316 | ] 317 | ); 318 | } 319 | 320 | #[test] 321 | fn test_should_apply_head() { 322 | let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]); 323 | let algo2 = AlgorithmsRule::from_str("^aes256-ctr").expect("failed to parse"); 324 | algo1.apply(algo2); 325 | assert_eq!( 326 | algo1.algorithms(), 327 | vec![ 328 | "aes256-ctr".to_string(), 329 | "aes128-ctr".to_string(), 330 | "aes192-ctr".to_string() 331 | ] 332 | ); 333 | } 334 | 335 | #[test] 336 | fn test_should_merge_exclude() { 337 | let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr", "aes256-ctr"]); 338 | let algo2 = AlgorithmsRule::from_str("-aes192-ctr").expect("failed to parse"); 339 | algo1.apply(algo2); 340 | assert_eq!( 341 | algo1.algorithms(), 342 | vec!["aes128-ctr".to_string(), "aes256-ctr".to_string()] 343 | ); 344 | } 345 | 346 | #[test] 347 | fn test_should_merge_set() { 348 | let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]); 349 | let algo2 = AlgorithmsRule::from_str("aes256-ctr").expect("failed to parse"); 350 | algo1.apply(algo2); 351 | assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string()]); 352 | } 353 | 354 | #[test] 355 | fn test_should_not_apply_twice() { 356 | let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]); 357 | let algo2 = AlgorithmsRule::from_str("aes256-ctr").expect("failed to parse"); 358 | algo1.apply(algo2); 359 | assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string(),]); 360 | 361 | let algo3 = AlgorithmsRule::from_str("aes128-ctr").expect("failed to parse"); 362 | algo1.apply(algo3); 363 | assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string()]); 364 | assert_eq!(algo1.overridden, true); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/params.rs: -------------------------------------------------------------------------------- 1 | //! # params 2 | //! 3 | //! Ssh config params for host rule 4 | 5 | mod algos; 6 | 7 | use std::collections::HashMap; 8 | 9 | pub use self::algos::Algorithms; 10 | pub(crate) use self::algos::AlgorithmsRule; 11 | use super::{Duration, PathBuf}; 12 | use crate::DefaultAlgorithms; 13 | 14 | /// Describes the ssh configuration. 15 | /// Configuration is describes in this document: 16 | /// Only arguments supported by libssh2 are implemented 17 | #[derive(Debug, Clone, PartialEq, Eq)] 18 | pub struct HostParams { 19 | /// Specifies whether keys should be automatically added to a running ssh-agent(1) 20 | pub add_keys_to_agent: Option, 21 | /// Specifies to use the specified address on the local machine as the source address of the connection 22 | pub bind_address: Option, 23 | /// Use the specified address on the local machine as the source address of the connection 24 | pub bind_interface: Option, 25 | /// Specifies which algorithms are allowed for signing of certificates by certificate authorities 26 | pub ca_signature_algorithms: Algorithms, 27 | /// Specifies a file from which the user's certificate is read 28 | pub certificate_file: Option, 29 | /// Specifies the ciphers allowed for protocol version 2 in order of preference 30 | pub ciphers: Algorithms, 31 | /// Specifies whether to use compression 32 | pub compression: Option, 33 | /// Specifies the number of attempts to make before exiting 34 | pub connection_attempts: Option, 35 | /// Specifies the timeout used when connecting to the SSH server 36 | pub connect_timeout: Option, 37 | /// Specifies whether the connection to the authentication agent (if any) will be forwarded to the remote machine 38 | pub forward_agent: Option, 39 | /// Specifies the host key signature algorithms that the client wants to use in order of preference 40 | pub host_key_algorithms: Algorithms, 41 | /// Specifies the real host name to log into 42 | pub host_name: Option, 43 | /// Specifies the path of the identity file to be used when authenticating. 44 | /// More than one file can be specified. 45 | /// If more than one file is specified, they will be read in order 46 | pub identity_file: Option>, 47 | /// Specifies a pattern-list of unknown options to be ignored if they are encountered in configuration parsing 48 | pub ignore_unknown: Option>, 49 | /// Specifies the available KEX (Key Exchange) algorithms 50 | pub kex_algorithms: Algorithms, 51 | /// Specifies the MAC (message authentication code) algorithms in order of preference 52 | pub mac: Algorithms, 53 | /// Specifies the port number to connect on the remote host. 54 | pub port: Option, 55 | /// Specifies one or more jump proxies as either [user@]host[:port] or an ssh URI 56 | pub proxy_jump: Option>, 57 | /// Specifies the signature algorithms that will be used for public key authentication 58 | pub pubkey_accepted_algorithms: Algorithms, 59 | /// Specifies whether to try public key authentication using SSH keys 60 | pub pubkey_authentication: Option, 61 | /// Specifies that a TCP port on the remote machine be forwarded over the secure channel 62 | pub remote_forward: Option, 63 | /// Sets a timeout interval in seconds after which if no data has been received from the server, keep alive will be sent 64 | pub server_alive_interval: Option, 65 | /// Specifies whether to send TCP keepalives to the other side 66 | pub tcp_keep_alive: Option, 67 | #[cfg(target_os = "macos")] 68 | /// specifies whether the system should search for passphrases in the user's keychain when attempting to use a particular key 69 | pub use_keychain: Option, 70 | /// Specifies the user to log in as. 71 | pub user: Option, 72 | /// fields that the parser wasn't able to parse 73 | pub ignored_fields: HashMap>, 74 | /// fields that the parser was able to parse but ignored 75 | pub unsupported_fields: HashMap>, 76 | } 77 | 78 | impl HostParams { 79 | /// Create a new [`HostParams`] object with the [`DefaultAlgorithms`] 80 | pub fn new(default_algorithms: &DefaultAlgorithms) -> Self { 81 | Self { 82 | add_keys_to_agent: None, 83 | bind_address: None, 84 | bind_interface: None, 85 | ca_signature_algorithms: Algorithms::new(&default_algorithms.ca_signature_algorithms), 86 | certificate_file: None, 87 | ciphers: Algorithms::new(&default_algorithms.ciphers), 88 | compression: None, 89 | connection_attempts: None, 90 | connect_timeout: None, 91 | forward_agent: None, 92 | host_key_algorithms: Algorithms::new(&default_algorithms.host_key_algorithms), 93 | host_name: None, 94 | identity_file: None, 95 | ignore_unknown: None, 96 | kex_algorithms: Algorithms::new(&default_algorithms.kex_algorithms), 97 | mac: Algorithms::new(&default_algorithms.mac), 98 | port: None, 99 | proxy_jump: None, 100 | pubkey_accepted_algorithms: Algorithms::new( 101 | &default_algorithms.pubkey_accepted_algorithms, 102 | ), 103 | pubkey_authentication: None, 104 | remote_forward: None, 105 | server_alive_interval: None, 106 | tcp_keep_alive: None, 107 | #[cfg(target_os = "macos")] 108 | use_keychain: None, 109 | user: None, 110 | ignored_fields: HashMap::new(), 111 | unsupported_fields: HashMap::new(), 112 | } 113 | } 114 | 115 | /// Return whether a certain `param` is in the ignored list 116 | pub(crate) fn ignored(&self, param: &str) -> bool { 117 | self.ignore_unknown 118 | .as_ref() 119 | .map(|x| x.iter().any(|x| x.as_str() == param)) 120 | .unwrap_or(false) 121 | } 122 | 123 | /// Given a [`HostParams`] object `b`, it will overwrite all the params from `self` only if they are [`None`] 124 | pub fn overwrite_if_none(&mut self, b: &Self) { 125 | self.add_keys_to_agent = self.add_keys_to_agent.or(b.add_keys_to_agent); 126 | self.bind_address = self.bind_address.clone().or_else(|| b.bind_address.clone()); 127 | self.bind_interface = self 128 | .bind_interface 129 | .clone() 130 | .or_else(|| b.bind_interface.clone()); 131 | self.certificate_file = self 132 | .certificate_file 133 | .clone() 134 | .or_else(|| b.certificate_file.clone()); 135 | self.compression = self.compression.or(b.compression); 136 | self.connection_attempts = self.connection_attempts.or(b.connection_attempts); 137 | self.connect_timeout = self.connect_timeout.or(b.connect_timeout); 138 | self.forward_agent = self.forward_agent.or(b.forward_agent); 139 | self.host_name = self.host_name.clone().or_else(|| b.host_name.clone()); 140 | self.identity_file = self 141 | .identity_file 142 | .clone() 143 | .or_else(|| b.identity_file.clone()); 144 | self.ignore_unknown = self 145 | .ignore_unknown 146 | .clone() 147 | .or_else(|| b.ignore_unknown.clone()); 148 | self.port = self.port.or(b.port); 149 | self.proxy_jump = self.proxy_jump.clone().or_else(|| b.proxy_jump.clone()); 150 | self.pubkey_authentication = self.pubkey_authentication.or(b.pubkey_authentication); 151 | self.remote_forward = self.remote_forward.or(b.remote_forward); 152 | self.server_alive_interval = self.server_alive_interval.or(b.server_alive_interval); 153 | #[cfg(target_os = "macos")] 154 | { 155 | self.use_keychain = self.use_keychain.or(b.use_keychain); 156 | } 157 | self.tcp_keep_alive = self.tcp_keep_alive.or(b.tcp_keep_alive); 158 | self.user = self.user.clone().or_else(|| b.user.clone()); 159 | for (ignored_field, args) in &b.ignored_fields { 160 | if !self.ignored_fields.contains_key(ignored_field) { 161 | self.ignored_fields 162 | .insert(ignored_field.to_owned(), args.to_owned()); 163 | } 164 | } 165 | for (unsupported_field, args) in &b.unsupported_fields { 166 | if !self.unsupported_fields.contains_key(unsupported_field) { 167 | self.unsupported_fields 168 | .insert(unsupported_field.to_owned(), args.to_owned()); 169 | } 170 | } 171 | 172 | // merge algos if default and b is not default 173 | if self.ca_signature_algorithms.is_default() && !b.ca_signature_algorithms.is_default() { 174 | self.ca_signature_algorithms = b.ca_signature_algorithms.clone(); 175 | } 176 | if self.ciphers.is_default() && !b.ciphers.is_default() { 177 | self.ciphers = b.ciphers.clone(); 178 | } 179 | if self.host_key_algorithms.is_default() && !b.host_key_algorithms.is_default() { 180 | self.host_key_algorithms = b.host_key_algorithms.clone(); 181 | } 182 | if self.kex_algorithms.is_default() && !b.kex_algorithms.is_default() { 183 | self.kex_algorithms = b.kex_algorithms.clone(); 184 | } 185 | if self.mac.is_default() && !b.mac.is_default() { 186 | self.mac = b.mac.clone(); 187 | } 188 | if self.pubkey_accepted_algorithms.is_default() 189 | && !b.pubkey_accepted_algorithms.is_default() 190 | { 191 | self.pubkey_accepted_algorithms = b.pubkey_accepted_algorithms.clone(); 192 | } 193 | } 194 | } 195 | 196 | #[cfg(test)] 197 | mod tests { 198 | 199 | use std::str::FromStr; 200 | 201 | use pretty_assertions::assert_eq; 202 | 203 | use super::*; 204 | use crate::params::algos::AlgorithmsRule; 205 | 206 | #[test] 207 | fn should_initialize_params() { 208 | let params = HostParams::new(&DefaultAlgorithms::default()); 209 | assert!(params.add_keys_to_agent.is_none()); 210 | assert!(params.bind_address.is_none()); 211 | assert!(params.bind_interface.is_none()); 212 | assert_eq!( 213 | params.ca_signature_algorithms.algorithms(), 214 | DefaultAlgorithms::default().ca_signature_algorithms 215 | ); 216 | assert!(params.certificate_file.is_none()); 217 | assert_eq!( 218 | params.ciphers.algorithms(), 219 | DefaultAlgorithms::default().ciphers 220 | ); 221 | assert!(params.compression.is_none()); 222 | assert!(params.connection_attempts.is_none()); 223 | assert!(params.connect_timeout.is_none()); 224 | assert!(params.forward_agent.is_none()); 225 | assert_eq!( 226 | params.host_key_algorithms.algorithms(), 227 | DefaultAlgorithms::default().host_key_algorithms 228 | ); 229 | assert!(params.host_name.is_none()); 230 | assert!(params.identity_file.is_none()); 231 | assert!(params.ignore_unknown.is_none()); 232 | assert_eq!( 233 | params.kex_algorithms.algorithms(), 234 | DefaultAlgorithms::default().kex_algorithms 235 | ); 236 | assert_eq!(params.mac.algorithms(), DefaultAlgorithms::default().mac); 237 | assert!(params.port.is_none()); 238 | assert!(params.proxy_jump.is_none()); 239 | assert_eq!( 240 | params.pubkey_accepted_algorithms.algorithms(), 241 | DefaultAlgorithms::default().pubkey_accepted_algorithms 242 | ); 243 | assert!(params.pubkey_authentication.is_none()); 244 | assert!(params.remote_forward.is_none()); 245 | assert!(params.server_alive_interval.is_none()); 246 | #[cfg(target_os = "macos")] 247 | assert!(params.use_keychain.is_none()); 248 | assert!(params.tcp_keep_alive.is_none()); 249 | } 250 | 251 | #[test] 252 | fn test_should_overwrite_if_none() { 253 | let mut params = HostParams::new(&DefaultAlgorithms::default()); 254 | params.bind_address = Some(String::from("pippo")); 255 | 256 | let mut b = HostParams::new(&DefaultAlgorithms::default()); 257 | b.bind_address = Some(String::from("pluto")); 258 | b.bind_interface = Some(String::from("tun0")); 259 | b.ciphers 260 | .apply(AlgorithmsRule::from_str("c,d").expect("parse error")); 261 | 262 | params.overwrite_if_none(&b); 263 | assert_eq!(params.bind_address.unwrap(), "pippo"); 264 | assert_eq!(params.bind_interface.unwrap(), "tun0"); 265 | 266 | // algos 267 | assert_eq!( 268 | params.ciphers.algorithms(), 269 | vec!["c".to_string(), "d".to_string()] 270 | ); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![crate_name = "ssh2_config"] 2 | #![crate_type = "lib"] 3 | 4 | //! # ssh2-config 5 | //! 6 | //! ssh2-config a library which provides a parser for the SSH configuration file, 7 | //! to be used in pair with the [ssh2](https://github.com/alexcrichton/ssh2-rs) crate. 8 | //! 9 | //! This library provides a method to parse the configuration file and returns the 10 | //! configuration parsed into a structure. 11 | //! The `SshConfig` structure provides all the attributes which **can** be used to configure the **ssh2 Session** 12 | //! and to resolve the host, port and username. 13 | //! 14 | //! Once the configuration has been parsed you can use the `query(&str)` 15 | //! method to query configuration for a certain host, based on the configured patterns. 16 | //! Even if many attributes are not exposed, since not supported, there is anyway a validation of the configuration, 17 | //! so invalid configuration will result in a parsing error. 18 | //! 19 | //! ## Get started 20 | //! 21 | //! First of you need to add **ssh2-config** to your project dependencies: 22 | //! 23 | //! ```toml 24 | //! ssh2-config = "^0.6" 25 | //! ``` 26 | //! 27 | //! ## Example 28 | //! 29 | //! Here is a basic example: 30 | //! 31 | //! ```rust 32 | //! 33 | //! use ssh2::Session; 34 | //! use ssh2_config::{HostParams, ParseRule, SshConfig}; 35 | //! use std::fs::File; 36 | //! use std::io::BufReader; 37 | //! use std::path::Path; 38 | //! 39 | //! let mut reader = BufReader::new( 40 | //! File::open(Path::new("./assets/ssh.config")) 41 | //! .expect("Could not open configuration file") 42 | //! ); 43 | //! 44 | //! let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration"); 45 | //! 46 | //! // Query parameters for your host 47 | //! // If there's no rule for your host, default params are returned 48 | //! let params = config.query("192.168.1.2"); 49 | //! 50 | //! // ... 51 | //! 52 | //! // serialize configuration to string 53 | //! let s = config.to_string(); 54 | //! 55 | //! ``` 56 | //! 57 | //! --- 58 | //! 59 | //! ## How host parameters are resolved 60 | //! 61 | //! This topic has been debated a lot over the years, so finally since 0.5 this has been fixed to follow the official ssh configuration file rules, as described in the MAN . 62 | //! 63 | //! > Unless noted otherwise, for each parameter, the first obtained value will be used. The configuration files contain sections separated by Host specifications, and that section is only applied for hosts that match one of the patterns given in the specification. The matched host name is usually the one given on the command line (see the CanonicalizeHostname option for exceptions). 64 | //! > 65 | //! > Since the first obtained value for each parameter is used, more host-specific declarations should be given near the beginning of the file, and general defaults at the end. 66 | //! 67 | //! This means that: 68 | //! 69 | //! 1. The first obtained value parsing the configuration top-down will be used 70 | //! 2. Host specific rules ARE not overriding default ones if they are not the first obtained value 71 | //! 3. If you want to achieve default values to be less specific than host specific ones, you should put the default values at the end of the configuration file using `Host *`. 72 | //! 4. Algorithms, so `KexAlgorithms`, `Ciphers`, `MACs` and `HostKeyAlgorithms` use a different resolvers which supports appending, excluding and heading insertions, as described in the man page at ciphers: . 73 | //! 74 | //! ### Resolvers examples 75 | //! 76 | //! ```ssh 77 | //! Compression yes 78 | //! 79 | //! Host 192.168.1.1 80 | //! Compression no 81 | //! ``` 82 | //! 83 | //! If we get rules for `192.168.1.1`, compression will be `yes`, because it's the first obtained value. 84 | //! 85 | //! ```ssh 86 | //! Host 192.168.1.1 87 | //! Compression no 88 | //! 89 | //! Host * 90 | //! Compression yes 91 | //! ``` 92 | //! 93 | //! If we get rules for `192.168.1.1`, compression will be `no`, because it's the first obtained value. 94 | //! 95 | //! If we get rules for `172.168.1.1`, compression will be `yes`, because it's the first obtained value MATCHING the host rule. 96 | //! 97 | //! ```ssh 98 | //! 99 | //! Host 192.168.1.1 100 | //! Ciphers +c 101 | //! ``` 102 | //! 103 | //! If we get rules for `192.168.1.1`, ciphers will be `c` appended to default algorithms, which can be specified in the [`SshConfig`] constructor. 104 | //! 105 | //! ## Configuring default algorithms 106 | //! 107 | //! When you invoke [`SshConfig::default`], the default algorithms are set from openssh source code, which are the following: 108 | //! 109 | //! ```txt 110 | //! ca_signature_algorithms: 111 | //! "ssh-ed25519", 112 | //! "ecdsa-sha2-nistp256", 113 | //! "ecdsa-sha2-nistp384", 114 | //! "ecdsa-sha2-nistp521", 115 | //! "sk-ssh-ed25519@openssh.com", 116 | //! "sk-ecdsa-sha2-nistp256@openssh.com", 117 | //! "rsa-sha2-512", 118 | //! "rsa-sha2-256", 119 | //! 120 | //! ciphers: 121 | //! "chacha20-poly1305@openssh.com", 122 | //! "aes128-ctr,aes192-ctr,aes256-ctr", 123 | //! "aes128-gcm@openssh.com,aes256-gcm@openssh.com", 124 | //! 125 | //! host_key_algorithms: 126 | //! "ssh-ed25519-cert-v01@openssh.com", 127 | //! "ecdsa-sha2-nistp256-cert-v01@openssh.com", 128 | //! "ecdsa-sha2-nistp384-cert-v01@openssh.com", 129 | //! "ecdsa-sha2-nistp521-cert-v01@openssh.com", 130 | //! "sk-ssh-ed25519-cert-v01@openssh.com", 131 | //! "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", 132 | //! "rsa-sha2-512-cert-v01@openssh.com", 133 | //! "rsa-sha2-256-cert-v01@openssh.com", 134 | //! "ssh-ed25519", 135 | //! "ecdsa-sha2-nistp256", 136 | //! "ecdsa-sha2-nistp384", 137 | //! "ecdsa-sha2-nistp521", 138 | //! "sk-ssh-ed25519@openssh.com", 139 | //! "sk-ecdsa-sha2-nistp256@openssh.com", 140 | //! "rsa-sha2-512", 141 | //! "rsa-sha2-256", 142 | //! 143 | //! kex_algorithms: 144 | //! "sntrup761x25519-sha512", 145 | //! "sntrup761x25519-sha512@openssh.com", 146 | //! "mlkem768x25519-sha256", 147 | //! "curve25519-sha256", 148 | //! "curve25519-sha256@libssh.org", 149 | //! "ecdh-sha2-nistp256", 150 | //! "ecdh-sha2-nistp384", 151 | //! "ecdh-sha2-nistp521", 152 | //! "diffie-hellman-group-exchange-sha256", 153 | //! "diffie-hellman-group16-sha512", 154 | //! "diffie-hellman-group18-sha512", 155 | //! "diffie-hellman-group14-sha256", 156 | //! "ssh-ed25519-cert-v01@openssh.com", 157 | //! "ecdsa-sha2-nistp256-cert-v01@openssh.com", 158 | //! "ecdsa-sha2-nistp384-cert-v01@openssh.com", 159 | //! "ecdsa-sha2-nistp521-cert-v01@openssh.com", 160 | //! "sk-ssh-ed25519-cert-v01@openssh.com", 161 | //! "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", 162 | //! "rsa-sha2-512-cert-v01@openssh.com", 163 | //! "rsa-sha2-256-cert-v01@openssh.com", 164 | //! "ssh-ed25519", 165 | //! "ecdsa-sha2-nistp256", 166 | //! "ecdsa-sha2-nistp384", 167 | //! "ecdsa-sha2-nistp521", 168 | //! "sk-ssh-ed25519@openssh.com", 169 | //! "sk-ecdsa-sha2-nistp256@openssh.com", 170 | //! "rsa-sha2-512", 171 | //! "rsa-sha2-256", 172 | //! "chacha20-poly1305@openssh.com", 173 | //! "aes128-ctr,aes192-ctr,aes256-ctr", 174 | //! "aes128-gcm@openssh.com,aes256-gcm@openssh.com", 175 | //! "chacha20-poly1305@openssh.com", 176 | //! "aes128-ctr,aes192-ctr,aes256-ctr", 177 | //! "aes128-gcm@openssh.com,aes256-gcm@openssh.com", 178 | //! "umac-64-etm@openssh.com", 179 | //! "umac-128-etm@openssh.com", 180 | //! "hmac-sha2-256-etm@openssh.com", 181 | //! "hmac-sha2-512-etm@openssh.com", 182 | //! "hmac-sha1-etm@openssh.com", 183 | //! "umac-64@openssh.com", 184 | //! "umac-128@openssh.com", 185 | //! "hmac-sha2-256", 186 | //! "hmac-sha2-512", 187 | //! "hmac-sha1", 188 | //! "umac-64-etm@openssh.com", 189 | //! "umac-128-etm@openssh.com", 190 | //! "hmac-sha2-256-etm@openssh.com", 191 | //! "hmac-sha2-512-etm@openssh.com", 192 | //! "hmac-sha1-etm@openssh.com", 193 | //! "umac-64@openssh.com", 194 | //! "umac-128@openssh.com", 195 | //! "hmac-sha2-256", 196 | //! "hmac-sha2-512", 197 | //! "hmac-sha1", 198 | //! "none,zlib@openssh.com", 199 | //! "none,zlib@openssh.com", 200 | //! 201 | //! mac: 202 | //! "umac-64-etm@openssh.com", 203 | //! "umac-128-etm@openssh.com", 204 | //! "hmac-sha2-256-etm@openssh.com", 205 | //! "hmac-sha2-512-etm@openssh.com", 206 | //! "hmac-sha1-etm@openssh.com", 207 | //! "umac-64@openssh.com", 208 | //! "umac-128@openssh.com", 209 | //! "hmac-sha2-256", 210 | //! "hmac-sha2-512", 211 | //! "hmac-sha1", 212 | //! 213 | //! pubkey_accepted_algorithms: 214 | //! "ssh-ed25519-cert-v01@openssh.com", 215 | //! "ecdsa-sha2-nistp256-cert-v01@openssh.com", 216 | //! "ecdsa-sha2-nistp384-cert-v01@openssh.com", 217 | //! "ecdsa-sha2-nistp521-cert-v01@openssh.com", 218 | //! "sk-ssh-ed25519-cert-v01@openssh.com", 219 | //! "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", 220 | //! "rsa-sha2-512-cert-v01@openssh.com", 221 | //! "rsa-sha2-256-cert-v01@openssh.com", 222 | //! "ssh-ed25519", 223 | //! "ecdsa-sha2-nistp256", 224 | //! "ecdsa-sha2-nistp384", 225 | //! "ecdsa-sha2-nistp521", 226 | //! "sk-ssh-ed25519@openssh.com", 227 | //! "sk-ecdsa-sha2-nistp256@openssh.com", 228 | //! "rsa-sha2-512", 229 | //! "rsa-sha2-256", 230 | //! ``` 231 | //! 232 | //! If you want you can use a custom constructor [`SshConfig::default_algorithms`] to set your own default algorithms. 233 | 234 | #![doc(html_playground_url = "https://play.rust-lang.org")] 235 | 236 | #[macro_use] 237 | extern crate log; 238 | 239 | use std::fmt; 240 | use std::fs::File; 241 | use std::io::{self, BufRead, BufReader}; 242 | use std::path::PathBuf; 243 | use std::time::Duration; 244 | // -- modules 245 | mod default_algorithms; 246 | mod host; 247 | mod params; 248 | mod parser; 249 | mod serializer; 250 | 251 | // -- export 252 | pub use self::default_algorithms::DefaultAlgorithms; 253 | pub use self::host::{Host, HostClause}; 254 | pub use self::params::{Algorithms, HostParams}; 255 | pub use self::parser::{ParseRule, SshParserError, SshParserResult}; 256 | 257 | /// Describes the ssh configuration. 258 | /// Configuration is described in this document: 259 | #[derive(Debug, Clone, PartialEq, Eq, Default)] 260 | pub struct SshConfig { 261 | /// Default algorithms for ssh. 262 | default_algorithms: DefaultAlgorithms, 263 | /// Rulesets for hosts. 264 | /// Default config will be stored with key `*` 265 | hosts: Vec, 266 | } 267 | 268 | impl fmt::Display for SshConfig { 269 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 270 | serializer::SshConfigSerializer::from(self).serialize(f) 271 | } 272 | } 273 | 274 | impl SshConfig { 275 | /// Constructs a new [`SshConfig`] from a list of [`Host`]s. 276 | /// 277 | /// You can later also set the [`DefaultAlgorithms`] using [`SshConfig::default_algorithms`]. 278 | /// 279 | /// ```rust 280 | /// use ssh2_config::{DefaultAlgorithms, Host, SshConfig}; 281 | /// 282 | /// let config = SshConfig::from_hosts(vec![/* put your hosts here */]).default_algorithms(DefaultAlgorithms::default()); 283 | /// ``` 284 | pub fn from_hosts(hosts: Vec) -> Self { 285 | Self { 286 | default_algorithms: DefaultAlgorithms::default(), 287 | hosts, 288 | } 289 | } 290 | 291 | /// Query params for a certain host. Returns [`HostParams`] for the host. 292 | pub fn query>(&self, pattern: S) -> HostParams { 293 | let mut params = HostParams::new(&self.default_algorithms); 294 | // iter keys, overwrite if None top-down 295 | for host in self.hosts.iter() { 296 | if host.intersects(pattern.as_ref()) { 297 | debug!( 298 | "Merging params for host: {:?} into params {params:?}", 299 | host.pattern 300 | ); 301 | params.overwrite_if_none(&host.params); 302 | trace!("Params after merge: {params:?}"); 303 | } 304 | } 305 | // return calculated params 306 | params 307 | } 308 | 309 | /// Get an iterator over the [`Host`]s which intersect with the given host pattern 310 | pub fn intersecting_hosts(&self, pattern: &str) -> impl Iterator { 311 | self.hosts.iter().filter(|host| host.intersects(pattern)) 312 | } 313 | 314 | /// Set default algorithms for ssh. 315 | /// 316 | /// If you want to use the default algorithms from the system, you can use the `Default::default()` method. 317 | pub fn default_algorithms(mut self, algos: DefaultAlgorithms) -> Self { 318 | self.default_algorithms = algos; 319 | 320 | self 321 | } 322 | 323 | /// Parse [`SshConfig`] from stream which implements [`BufRead`] and return parsed configuration or parser error 324 | /// 325 | /// ## Example 326 | /// 327 | /// ```rust,ignore 328 | /// let mut reader = BufReader::new( 329 | /// File::open(Path::new("./assets/ssh.config")) 330 | /// .expect("Could not open configuration file") 331 | /// ); 332 | /// 333 | /// let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration"); 334 | /// ``` 335 | pub fn parse(mut self, reader: &mut impl BufRead, rules: ParseRule) -> SshParserResult { 336 | parser::SshConfigParser::parse(&mut self, reader, rules, None).map(|_| self) 337 | } 338 | 339 | /// Parse `~/.ssh/config`` file and return parsed configuration [`SshConfig`] or parser error 340 | pub fn parse_default_file(rules: ParseRule) -> SshParserResult { 341 | let ssh_folder = dirs::home_dir() 342 | .ok_or_else(|| { 343 | SshParserError::Io(io::Error::new( 344 | io::ErrorKind::NotFound, 345 | "Home folder not found", 346 | )) 347 | })? 348 | .join(".ssh"); 349 | 350 | let mut reader = 351 | BufReader::new(File::open(ssh_folder.join("config")).map_err(SshParserError::Io)?); 352 | 353 | Self::default().parse(&mut reader, rules) 354 | } 355 | 356 | /// Get list of [`Host`]s in the configuration 357 | pub fn get_hosts(&self) -> &Vec { 358 | &self.hosts 359 | } 360 | } 361 | 362 | #[cfg(test)] 363 | fn test_log() { 364 | use std::sync::Once; 365 | 366 | static INIT: Once = Once::new(); 367 | 368 | INIT.call_once(|| { 369 | let _ = env_logger::builder() 370 | .filter_level(log::LevelFilter::Trace) 371 | .is_test(true) 372 | .try_init(); 373 | }); 374 | } 375 | 376 | #[cfg(test)] 377 | mod tests { 378 | 379 | use pretty_assertions::assert_eq; 380 | 381 | use super::*; 382 | 383 | #[test] 384 | fn should_init_ssh_config() { 385 | test_log(); 386 | 387 | let config = SshConfig::default(); 388 | assert_eq!(config.hosts.len(), 0); 389 | assert_eq!( 390 | config.query("192.168.1.2"), 391 | HostParams::new(&DefaultAlgorithms::default()) 392 | ); 393 | } 394 | 395 | #[test] 396 | fn should_parse_default_config() -> Result<(), parser::SshParserError> { 397 | test_log(); 398 | 399 | let _config = SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)?; 400 | Ok(()) 401 | } 402 | 403 | #[test] 404 | fn should_parse_config() -> Result<(), parser::SshParserError> { 405 | test_log(); 406 | 407 | use std::fs::File; 408 | use std::io::BufReader; 409 | use std::path::Path; 410 | 411 | let mut reader = BufReader::new( 412 | File::open(Path::new("./assets/ssh.config")) 413 | .expect("Could not open configuration file"), 414 | ); 415 | 416 | SshConfig::default().parse(&mut reader, ParseRule::STRICT)?; 417 | 418 | Ok(()) 419 | } 420 | 421 | #[test] 422 | fn should_query_ssh_config() { 423 | test_log(); 424 | 425 | let mut config = SshConfig::default(); 426 | // add config 427 | let mut params1 = HostParams::new(&DefaultAlgorithms::default()); 428 | params1.bind_address = Some("0.0.0.0".to_string()); 429 | config.hosts.push(Host::new( 430 | vec![HostClause::new(String::from("192.168.*.*"), false)], 431 | params1.clone(), 432 | )); 433 | let mut params2 = HostParams::new(&DefaultAlgorithms::default()); 434 | params2.bind_interface = Some(String::from("tun0")); 435 | config.hosts.push(Host::new( 436 | vec![HostClause::new(String::from("192.168.10.*"), false)], 437 | params2.clone(), 438 | )); 439 | 440 | let mut params3 = HostParams::new(&DefaultAlgorithms::default()); 441 | params3.host_name = Some("172.26.104.4".to_string()); 442 | config.hosts.push(Host::new( 443 | vec![ 444 | HostClause::new(String::from("172.26.*.*"), false), 445 | HostClause::new(String::from("172.26.104.4"), true), 446 | ], 447 | params3.clone(), 448 | )); 449 | // Query 450 | assert_eq!(config.query("192.168.1.32"), params1); 451 | // merged case 452 | params1.overwrite_if_none(¶ms2); 453 | assert_eq!(config.query("192.168.10.1"), params1); 454 | // Negated case 455 | assert_eq!(config.query("172.26.254.1"), params3); 456 | assert_eq!( 457 | config.query("172.26.104.4"), 458 | HostParams::new(&DefaultAlgorithms::default()) 459 | ); 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh2-config 2 | 3 |

4 | Changelog 5 | · 6 | Get started 7 | · 8 | Documentation 9 |

10 | 11 |

Developed by @veeso

12 |

Current version: 0.6.2 (25/09/2025)

13 | 14 |

15 | License-MIT 20 | Repo stars 25 | Downloads counter 30 | Latest version 35 | 36 | Ko-fi 40 |

41 |

42 | Build 47 | Coveralls 52 | Docs 57 |

58 | 59 | --- 60 | 61 | - [ssh2-config](#ssh2-config) 62 | - [About ssh2-config](#about-ssh2-config) 63 | - [Exposed attributes](#exposed-attributes) 64 | - [Missing features](#missing-features) 65 | - [Get started 🚀](#get-started-) 66 | - [Reading unsupported fields](#reading-unsupported-fields) 67 | - [How host parameters are resolved](#how-host-parameters-are-resolved) 68 | - [Resolvers examples](#resolvers-examples) 69 | - [Configuring default algorithms](#configuring-default-algorithms) 70 | - [Examples](#examples) 71 | - [Support the developer ☕](#support-the-developer-) 72 | - [Contributing and issues 🤝🏻](#contributing-and-issues-) 73 | - [Changelog ⏳](#changelog-) 74 | - [License 📃](#license-) 75 | 76 | --- 77 | 78 | ## About ssh2-config 79 | 80 | ssh2-config a library which provides a parser for the SSH configuration file, to be used in pair with the [ssh2](https://github.com/alexcrichton/ssh2-rs) crate. 81 | 82 | This library provides a method to parse the configuration file and returns the configuration parsed into a structure. 83 | The `SshConfig` structure provides all the attributes which **can** be used to configure the **ssh2 Session** and to resolve 84 | the host, port and username. 85 | 86 | Once the configuration has been parsed you can use the `query(&str)` method to query configuration for a certain host, based on the configured patterns. 87 | 88 | Even if many attributes are not exposed, since not supported, there is anyway a validation of the configuration, so invalid configuration will result in a parsing error. 89 | 90 | ### Exposed attributes 91 | 92 | - **AddKeysToAgent**: you can use this attribute add keys to the SSH agent 93 | - **BindAddress**: you can use this attribute to bind the socket to a certain address 94 | - **BindInterface**: you can use this attribute to bind the socket to a certain network interface 95 | - **CASignatureAlgorithms**: you can use this attribute to handle CA certificates 96 | - **CertificateFile**: you can use this attribute to parse the certificate file in case is necessary 97 | - **Ciphers**: you can use this attribute to set preferred methods with the session method `session.method_pref(MethodType::CryptCs, ...)` and `session.method_pref(MethodType::CryptSc, ...)` 98 | - **Compression**: you can use this attribute to set whether compression is enabled with `session.set_compress(value)` 99 | - **ConnectionAttempts**: you can use this attribute to cycle over connect in order to retry 100 | - **ConnectTimeout**: you can use this attribute to set the connection timeout for the socket 101 | - **ForwardAgent**: you can use this attribute to forward your agent to the remote server 102 | - **HostName**: you can use this attribute to get the real name of the host to connect to 103 | - **IdentityFile**: you can use this attribute to set the keys to try when connecting to remote host. 104 | - **KexAlgorithms**: you can use this attribute to configure Key exchange methods with `session.method_pref(MethodType::Kex, algos.to_string().as_str())` 105 | - **MACs**: you can use this attribute to configure the MAC algos with `session.method_pref(MethodType::MacCs, algos..to_string().as_str())` and `session.method_pref(MethodType::MacSc, algos..to_string().as_str())` 106 | - **Port**: you can use this attribute to resolve the port to connect to 107 | - **ProxyJump**: you can use this attribute to specify hosts to jump via 108 | - **PubkeyAuthentication**: you can use this attribute to set whether to use the pubkey authentication 109 | - **RemoteForward**: you can use this method to implement port forwarding with `session.channel_forward_listen()` 110 | - **ServerAliveInterval**: you can use this method to implement keep alive message interval 111 | - **TcpKeepAlive**: you can use this method to tell whether to send keep alive message 112 | - **UseKeychain**: (macos only) used to tell whether to use keychain to decrypt ssh keys 113 | - **User**: you can use this method to resolve the user to use to log in as 114 | 115 | ### Missing features 116 | 117 | - [Match patterns](http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5#Match) (Host patterns are supported!!!) 118 | - [Tokens](http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5#TOKENS) 119 | 120 | --- 121 | 122 | ## Get started 🚀 123 | 124 | First of all, add ssh2-config to your dependencies 125 | 126 | ```toml 127 | [dependencies] 128 | ssh2-config = "^0.5" 129 | ``` 130 | 131 | then parse the configuration 132 | 133 | ```rust 134 | use ssh2_config::{ParseRule, SshConfig}; 135 | use std::fs::File; 136 | use std::io::BufReader; 137 | 138 | let mut reader = BufReader::new(File::open(config_path).expect("Could not open configuration file")); 139 | let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration"); 140 | 141 | // Query attributes for a certain host 142 | let params = config.query("192.168.1.2"); 143 | ``` 144 | 145 | then you can use the parsed parameters to configure the session: 146 | 147 | ```rust 148 | use ssh2::Session; 149 | use ssh2_config::{HostParams}; 150 | 151 | fn configure_session(session: &mut Session, params: &HostParams) { 152 | if let Some(compress) = params.compression { 153 | session.set_compress(compress); 154 | } 155 | if params.tcp_keep_alive.unwrap_or(false) && params.server_alive_interval.is_some() { 156 | let interval = params.server_alive_interval.unwrap().as_secs() as u32; 157 | session.set_keepalive(true, interval); 158 | } 159 | // KEX 160 | if let Err(err) = session.method_pref( 161 | MethodType::Kex, 162 | params.kex_algorithms.algorithms().join(",").as_str(), 163 | ) { 164 | panic!("Could not set KEX algorithms: {}", err); 165 | } 166 | 167 | // host key 168 | if let Err(err) = session.method_pref( 169 | MethodType::HostKey, 170 | params.host_key_algorithms.algorithms().join(",").as_str(), 171 | ) { 172 | panic!("Could not set host key algorithms: {}", err); 173 | } 174 | 175 | // ciphers 176 | if let Err(err) = session.method_pref( 177 | MethodType::CryptCs, 178 | params.ciphers.algorithms().join(",").as_str(), 179 | ) { 180 | panic!("Could not set crypt algorithms (client-server): {}", err); 181 | } 182 | if let Err(err) = session.method_pref( 183 | MethodType::CryptSc, 184 | params.ciphers.algorithms().join(",").as_str(), 185 | ) { 186 | panic!("Could not set crypt algorithms (server-client): {}", err); 187 | } 188 | 189 | // mac 190 | if let Err(err) = session.method_pref( 191 | MethodType::MacCs, 192 | params.mac.algorithms().join(",").as_str(), 193 | ) { 194 | panic!("Could not set MAC algorithms (client-server): {}", err); 195 | } 196 | if let Err(err) = session.method_pref( 197 | MethodType::MacSc, 198 | params.mac.algorithms().join(",").as_str(), 199 | ) { 200 | panic!("Could not set MAC algorithms (server-client): {}", err); 201 | } 202 | } 203 | 204 | fn auth_with_rsakey( 205 | session: &mut Session, 206 | params: &HostParams, 207 | username: &str, 208 | password: Option<&str> 209 | ) { 210 | for identity_file in params.identity_file.unwrap_or_default().iter() { 211 | if let Ok(_) = session.userauth_pubkey_file(username, None, identity_file, password) { 212 | break; 213 | } 214 | } 215 | } 216 | 217 | ``` 218 | 219 | ### Reading unsupported fields 220 | 221 | As outlined above, ssh2-config does not support all parameters available in the man page of the SSH configuration file. 222 | 223 | If you require these fields you may still access them through the `unsupported_fields` field on the `HostParams` structure like this: 224 | 225 | ```rust 226 | use ssh2_config::{ParseRule, SshConfig}; 227 | use std::fs::File; 228 | use std::io::BufReader; 229 | 230 | let mut reader = BufReader::new(File::open(config_path).expect("Could not open configuration file")); 231 | let config = SshConfig::default().parse(&mut reader, ParseRule::ALLOW_UNSUPPORTED_FIELDS).expect("Failed to parse configuration"); 232 | 233 | // Query attributes for a certain host 234 | let params = config.query("192.168.1.2"); 235 | let forwards = params.unsupported_fields.get("dynamicforward"); 236 | ``` 237 | 238 | --- 239 | 240 | ## How host parameters are resolved 241 | 242 | This topic has been debated a lot over the years, so finally since 0.5 this has been fixed to follow the official ssh configuration file rules, as described in the MAN . 243 | 244 | > Unless noted otherwise, for each parameter, the first obtained value will be used. The configuration files contain sections separated by Host specifications, and that section is only applied for hosts that match one of the patterns given in the specification. The matched host name is usually the one given on the command line (see the CanonicalizeHostname option for exceptions). 245 | > 246 | > Since the first obtained value for each parameter is used, more host-specific declarations should be given near the beginning of the file, and general defaults at the end. 247 | 248 | This means that: 249 | 250 | 1. The first obtained value parsing the configuration top-down will be used 251 | 2. Host specific rules ARE not overriding default ones if they are not the first obtained value 252 | 3. If you want to achieve default values to be less specific than host specific ones, you should put the default values at the end of the configuration file using `Host *`. 253 | 4. Algorithms, so `KexAlgorithms`, `Ciphers`, `MACs` and `HostKeyAlgorithms` use a different resolvers which supports appending, excluding and heading insertions, as described in the man page at ciphers: . They are in case appended to default algorithms, which are either fetched from the openssh source code or set with a constructor. See [configuring default algorithms](#configuring-default-algorithms) for more information. 254 | 255 | ### Resolvers examples 256 | 257 | ```ssh 258 | Compression yes 259 | 260 | Host 192.168.1.1 261 | Compression no 262 | ``` 263 | 264 | If we get rules for `192.168.1.1`, compression will be `yes`, because it's the first obtained value. 265 | 266 | ```ssh 267 | Host 192.168.1.1 268 | Compression no 269 | 270 | Host * 271 | Compression yes 272 | ``` 273 | 274 | If we get rules for `192.168.1.1`, compression will be `no`, because it's the first obtained value. 275 | 276 | If we get rules for `172.168.1.1`, compression will be `yes`, because it's the first obtained value MATCHING the host rule. 277 | 278 | ```ssh 279 | Host 192.168.1.1 280 | Ciphers +c 281 | ``` 282 | 283 | If we get rules for `192.168.1.1`, ciphers will be `a,b,c`, because default is set to `a,b` and `+c` means append `c` to the list. 284 | 285 | --- 286 | 287 | ## Configuring default algorithms 288 | 289 | To reload algos, build ssh2-config with `RELOAD_SSH_ALGO` env variable set. 290 | 291 | When you invoke `SshConfig::default`, the default algorithms are set from openssh source code, which are the following: 292 | 293 | ```txt 294 | ca_signature_algorithms: 295 | "ssh-ed25519", 296 | "ecdsa-sha2-nistp256", 297 | "ecdsa-sha2-nistp384", 298 | "ecdsa-sha2-nistp521", 299 | "sk-ssh-ed25519@openssh.com", 300 | "sk-ecdsa-sha2-nistp256@openssh.com", 301 | "rsa-sha2-512", 302 | "rsa-sha2-256", 303 | 304 | ciphers: 305 | "chacha20-poly1305@openssh.com", 306 | "aes128-ctr,aes192-ctr,aes256-ctr", 307 | "aes128-gcm@openssh.com,aes256-gcm@openssh.com", 308 | 309 | host_key_algorithms: 310 | "ssh-ed25519-cert-v01@openssh.com", 311 | "ecdsa-sha2-nistp256-cert-v01@openssh.com", 312 | "ecdsa-sha2-nistp384-cert-v01@openssh.com", 313 | "ecdsa-sha2-nistp521-cert-v01@openssh.com", 314 | "sk-ssh-ed25519-cert-v01@openssh.com", 315 | "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", 316 | "rsa-sha2-512-cert-v01@openssh.com", 317 | "rsa-sha2-256-cert-v01@openssh.com", 318 | "ssh-ed25519", 319 | "ecdsa-sha2-nistp256", 320 | "ecdsa-sha2-nistp384", 321 | "ecdsa-sha2-nistp521", 322 | "sk-ssh-ed25519@openssh.com", 323 | "sk-ecdsa-sha2-nistp256@openssh.com", 324 | "rsa-sha2-512", 325 | "rsa-sha2-256", 326 | 327 | kex_algorithms: 328 | "sntrup761x25519-sha512", 329 | "sntrup761x25519-sha512@openssh.com", 330 | "mlkem768x25519-sha256", 331 | "curve25519-sha256", 332 | "curve25519-sha256@libssh.org", 333 | "ecdh-sha2-nistp256", 334 | "ecdh-sha2-nistp384", 335 | "ecdh-sha2-nistp521", 336 | "diffie-hellman-group-exchange-sha256", 337 | "diffie-hellman-group16-sha512", 338 | "diffie-hellman-group18-sha512", 339 | "diffie-hellman-group14-sha256", 340 | "ssh-ed25519-cert-v01@openssh.com", 341 | "ecdsa-sha2-nistp256-cert-v01@openssh.com", 342 | "ecdsa-sha2-nistp384-cert-v01@openssh.com", 343 | "ecdsa-sha2-nistp521-cert-v01@openssh.com", 344 | "sk-ssh-ed25519-cert-v01@openssh.com", 345 | "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", 346 | "rsa-sha2-512-cert-v01@openssh.com", 347 | "rsa-sha2-256-cert-v01@openssh.com", 348 | "ssh-ed25519", 349 | "ecdsa-sha2-nistp256", 350 | "ecdsa-sha2-nistp384", 351 | "ecdsa-sha2-nistp521", 352 | "sk-ssh-ed25519@openssh.com", 353 | "sk-ecdsa-sha2-nistp256@openssh.com", 354 | "rsa-sha2-512", 355 | "rsa-sha2-256", 356 | "chacha20-poly1305@openssh.com", 357 | "aes128-ctr,aes192-ctr,aes256-ctr", 358 | "aes128-gcm@openssh.com,aes256-gcm@openssh.com", 359 | "chacha20-poly1305@openssh.com", 360 | "aes128-ctr,aes192-ctr,aes256-ctr", 361 | "aes128-gcm@openssh.com,aes256-gcm@openssh.com", 362 | "umac-64-etm@openssh.com", 363 | "umac-128-etm@openssh.com", 364 | "hmac-sha2-256-etm@openssh.com", 365 | "hmac-sha2-512-etm@openssh.com", 366 | "hmac-sha1-etm@openssh.com", 367 | "umac-64@openssh.com", 368 | "umac-128@openssh.com", 369 | "hmac-sha2-256", 370 | "hmac-sha2-512", 371 | "hmac-sha1", 372 | "umac-64-etm@openssh.com", 373 | "umac-128-etm@openssh.com", 374 | "hmac-sha2-256-etm@openssh.com", 375 | "hmac-sha2-512-etm@openssh.com", 376 | "hmac-sha1-etm@openssh.com", 377 | "umac-64@openssh.com", 378 | "umac-128@openssh.com", 379 | "hmac-sha2-256", 380 | "hmac-sha2-512", 381 | "hmac-sha1", 382 | "none,zlib@openssh.com", 383 | "none,zlib@openssh.com", 384 | 385 | mac: 386 | "umac-64-etm@openssh.com", 387 | "umac-128-etm@openssh.com", 388 | "hmac-sha2-256-etm@openssh.com", 389 | "hmac-sha2-512-etm@openssh.com", 390 | "hmac-sha1-etm@openssh.com", 391 | "umac-64@openssh.com", 392 | "umac-128@openssh.com", 393 | "hmac-sha2-256", 394 | "hmac-sha2-512", 395 | "hmac-sha1", 396 | 397 | pubkey_accepted_algorithms: 398 | "ssh-ed25519-cert-v01@openssh.com", 399 | "ecdsa-sha2-nistp256-cert-v01@openssh.com", 400 | "ecdsa-sha2-nistp384-cert-v01@openssh.com", 401 | "ecdsa-sha2-nistp521-cert-v01@openssh.com", 402 | "sk-ssh-ed25519-cert-v01@openssh.com", 403 | "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", 404 | "rsa-sha2-512-cert-v01@openssh.com", 405 | "rsa-sha2-256-cert-v01@openssh.com", 406 | "ssh-ed25519", 407 | "ecdsa-sha2-nistp256", 408 | "ecdsa-sha2-nistp384", 409 | "ecdsa-sha2-nistp521", 410 | "sk-ssh-ed25519@openssh.com", 411 | "sk-ecdsa-sha2-nistp256@openssh.com", 412 | "rsa-sha2-512", 413 | "rsa-sha2-256", 414 | ``` 415 | 416 | If you want you can use a custom constructor `SshConfig::default().default_algorithms(prefs)` to set your own default algorithms. 417 | 418 | --- 419 | 420 | ### Examples 421 | 422 | You can view a working examples of an implementation of ssh2-config with ssh2 in the examples folder at [client.rs](examples/client.rs). 423 | 424 | You can run the example with 425 | 426 | ```sh 427 | cargo run --example client -- [config-file-path] 428 | ``` 429 | 430 | --- 431 | 432 | ## Support the developer ☕ 433 | 434 | If you like ssh2-config and you're grateful for the work I've done, please consider a little donation 🥳 435 | 436 | You can make a donation with one of these platforms: 437 | 438 | [![ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/veeso) 439 | [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/chrisintin) 440 | 441 | --- 442 | 443 | ## Contributing and issues 🤝🏻 444 | 445 | Contributions, bug reports, new features and questions are welcome! 😉 446 | If you have any question or concern, or you want to suggest a new feature, or you want just want to improve ssh2-config, feel free to open an issue or a PR. 447 | 448 | Please follow [our contributing guidelines](CONTRIBUTING.md) 449 | 450 | --- 451 | 452 | ## Changelog ⏳ 453 | 454 | View ssh2-config's changelog [HERE](CHANGELOG.md) 455 | 456 | --- 457 | 458 | ## License 📃 459 | 460 | ssh2-config is licensed under the MIT license. 461 | 462 | You can read the entire license [HERE](LICENSE) 463 | -------------------------------------------------------------------------------- /src/parser/field.rs: -------------------------------------------------------------------------------- 1 | //! # field 2 | //! 3 | //! Ssh config fields 4 | 5 | use std::fmt; 6 | use std::str::FromStr; 7 | 8 | /// Configuration field. 9 | /// This enum defines ALL THE SUPPORTED fields in ssh config, 10 | /// as described at . 11 | /// Only a few of them are implemented, as described in `HostParams` struct. 12 | #[derive(Debug, Eq, PartialEq)] 13 | pub enum Field { 14 | Host, 15 | AddKeysToAgent, 16 | BindAddress, 17 | BindInterface, 18 | CaSignatureAlgorithms, 19 | CertificateFile, 20 | Ciphers, 21 | Compression, 22 | ConnectionAttempts, 23 | ConnectTimeout, 24 | ForwardAgent, 25 | HostKeyAlgorithms, 26 | HostName, 27 | IdentityFile, 28 | IgnoreUnknown, 29 | KexAlgorithms, 30 | Mac, 31 | Port, 32 | ProxyJump, 33 | PubkeyAcceptedAlgorithms, 34 | PubkeyAuthentication, 35 | RemoteForward, 36 | ServerAliveInterval, 37 | TcpKeepAlive, 38 | #[cfg(target_os = "macos")] 39 | UseKeychain, 40 | User, 41 | // -- not implemented 42 | AddressFamily, 43 | BatchMode, 44 | CanonicalDomains, 45 | CanonicalizeFallbackLock, 46 | CanonicalizeHostname, 47 | CanonicalizeMaxDots, 48 | CanonicalizePermittedCNAMEs, 49 | CheckHostIP, 50 | ClearAllForwardings, 51 | ControlMaster, 52 | ControlPath, 53 | ControlPersist, 54 | DynamicForward, 55 | EnableSSHKeysign, 56 | EscapeChar, 57 | ExitOnForwardFailure, 58 | FingerprintHash, 59 | ForkAfterAuthentication, 60 | ForwardX11, 61 | ForwardX11Timeout, 62 | ForwardX11Trusted, 63 | GatewayPorts, 64 | GlobalKnownHostsFile, 65 | GSSAPIAuthentication, 66 | GSSAPIDelegateCredentials, 67 | HashKnownHosts, 68 | HostbasedAcceptedAlgorithms, 69 | HostbasedAuthentication, 70 | HostbasedKeyTypes, 71 | HostKeyAlias, 72 | IdentitiesOnly, 73 | IdentityAgent, 74 | Include, 75 | IPQoS, 76 | KbdInteractiveAuthentication, 77 | KbdInteractiveDevices, 78 | KnownHostsCommand, 79 | LocalCommand, 80 | LocalForward, 81 | LogLevel, 82 | LogVerbose, 83 | NoHostAuthenticationForLocalhost, 84 | NumberOfPasswordPrompts, 85 | PasswordAuthentication, 86 | PermitLocalCommand, 87 | PermitRemoteOpen, 88 | PKCS11Provider, 89 | PreferredAuthentications, 90 | ProxyCommand, 91 | ProxyUseFdpass, 92 | PubkeyAcceptedKeyTypes, 93 | RekeyLimit, 94 | RequestTTY, 95 | RevokedHostKeys, 96 | SecruityKeyProvider, 97 | SendEnv, 98 | ServerAliveCountMax, 99 | SessionType, 100 | SetEnv, 101 | StdinNull, 102 | StreamLocalBindMask, 103 | StrictHostKeyChecking, 104 | SyslogFacility, 105 | UpdateHostKeys, 106 | UserKnownHostsFile, 107 | VerifyHostKeyDNS, 108 | VisualHostKey, 109 | XAuthLocation, 110 | } 111 | 112 | impl FromStr for Field { 113 | type Err = String; 114 | 115 | fn from_str(s: &str) -> Result { 116 | match s.to_lowercase().as_str() { 117 | "host" => Ok(Self::Host), 118 | "addkeystoagent" => Ok(Self::AddKeysToAgent), 119 | "bindaddress" => Ok(Self::BindAddress), 120 | "bindinterface" => Ok(Self::BindInterface), 121 | "casignaturealgorithms" => Ok(Self::CaSignatureAlgorithms), 122 | "certificatefile" => Ok(Self::CertificateFile), 123 | "ciphers" => Ok(Self::Ciphers), 124 | "compression" => Ok(Self::Compression), 125 | "connectionattempts" => Ok(Self::ConnectionAttempts), 126 | "connecttimeout" => Ok(Self::ConnectTimeout), 127 | "forwardagent" => Ok(Self::ForwardAgent), 128 | "hostkeyalgorithms" => Ok(Self::HostKeyAlgorithms), 129 | "hostname" => Ok(Self::HostName), 130 | "identityfile" => Ok(Self::IdentityFile), 131 | "ignoreunknown" => Ok(Self::IgnoreUnknown), 132 | "kexalgorithms" => Ok(Self::KexAlgorithms), 133 | "macs" => Ok(Self::Mac), 134 | "port" => Ok(Self::Port), 135 | "proxyjump" => Ok(Self::ProxyJump), 136 | "pubkeyacceptedalgorithms" => Ok(Self::PubkeyAcceptedAlgorithms), 137 | "pubkeyauthentication" => Ok(Self::PubkeyAuthentication), 138 | "remoteforward" => Ok(Self::RemoteForward), 139 | "serveraliveinterval" => Ok(Self::ServerAliveInterval), 140 | "tcpkeepalive" => Ok(Self::TcpKeepAlive), 141 | #[cfg(target_os = "macos")] 142 | "usekeychain" => Ok(Self::UseKeychain), 143 | "user" => Ok(Self::User), 144 | // -- not implemented fields 145 | "addressfamily" => Ok(Self::AddressFamily), 146 | "batchmode" => Ok(Self::BatchMode), 147 | "canonicaldomains" => Ok(Self::CanonicalDomains), 148 | "canonicalizefallbacklock" => Ok(Self::CanonicalizeFallbackLock), 149 | "canonicalizehostname" => Ok(Self::CanonicalizeHostname), 150 | "canonicalizemaxdots" => Ok(Self::CanonicalizeMaxDots), 151 | "canonicalizepermittedcnames" => Ok(Self::CanonicalizePermittedCNAMEs), 152 | "checkhostip" => Ok(Self::CheckHostIP), 153 | "clearallforwardings" => Ok(Self::ClearAllForwardings), 154 | "controlmaster" => Ok(Self::ControlMaster), 155 | "controlpath" => Ok(Self::ControlPath), 156 | "controlpersist" => Ok(Self::ControlPersist), 157 | "dynamicforward" => Ok(Self::DynamicForward), 158 | "enablesshkeysign" => Ok(Self::EnableSSHKeysign), 159 | "escapechar" => Ok(Self::EscapeChar), 160 | "exitonforwardfailure" => Ok(Self::ExitOnForwardFailure), 161 | "fingerprinthash" => Ok(Self::FingerprintHash), 162 | "forkafterauthentication" => Ok(Self::ForkAfterAuthentication), 163 | "forwardx11" => Ok(Self::ForwardX11), 164 | "forwardx11timeout" => Ok(Self::ForwardX11Timeout), 165 | "forwardx11trusted" => Ok(Self::ForwardX11Trusted), 166 | "gatewayports" => Ok(Self::GatewayPorts), 167 | "globalknownhostsfile" => Ok(Self::GlobalKnownHostsFile), 168 | "gssapiauthentication" => Ok(Self::GSSAPIAuthentication), 169 | "gssapidelegatecredentials" => Ok(Self::GSSAPIDelegateCredentials), 170 | "hashknownhosts" => Ok(Self::HashKnownHosts), 171 | "hostbasedacceptedalgorithms" => Ok(Self::HostbasedAcceptedAlgorithms), 172 | "hostbasedauthentication" => Ok(Self::HostbasedAuthentication), 173 | "hostkeyalias" => Ok(Self::HostKeyAlias), 174 | "hostbasedkeytypes" => Ok(Self::HostbasedKeyTypes), 175 | "identitiesonly" => Ok(Self::IdentitiesOnly), 176 | "identityagent" => Ok(Self::IdentityAgent), 177 | "include" => Ok(Self::Include), 178 | "ipqos" => Ok(Self::IPQoS), 179 | "kbdinteractiveauthentication" => Ok(Self::KbdInteractiveAuthentication), 180 | "kbdinteractivedevices" => Ok(Self::KbdInteractiveDevices), 181 | "knownhostscommand" => Ok(Self::KnownHostsCommand), 182 | "localcommand" => Ok(Self::LocalCommand), 183 | "localforward" => Ok(Self::LocalForward), 184 | "loglevel" => Ok(Self::LogLevel), 185 | "logverbose" => Ok(Self::LogVerbose), 186 | "nohostauthenticationforlocalhost" => Ok(Self::NoHostAuthenticationForLocalhost), 187 | "numberofpasswordprompts" => Ok(Self::NumberOfPasswordPrompts), 188 | "passwordauthentication" => Ok(Self::PasswordAuthentication), 189 | "permitlocalcommand" => Ok(Self::PermitLocalCommand), 190 | "permitremoteopen" => Ok(Self::PermitRemoteOpen), 191 | "pkcs11provider" => Ok(Self::PKCS11Provider), 192 | "preferredauthentications" => Ok(Self::PreferredAuthentications), 193 | "proxycommand" => Ok(Self::ProxyCommand), 194 | "proxyusefdpass" => Ok(Self::ProxyUseFdpass), 195 | "pubkeyacceptedkeytypes" => Ok(Self::PubkeyAcceptedKeyTypes), 196 | "rekeylimit" => Ok(Self::RekeyLimit), 197 | "requesttty" => Ok(Self::RequestTTY), 198 | "revokedhostkeys" => Ok(Self::RevokedHostKeys), 199 | "secruitykeyprovider" => Ok(Self::SecruityKeyProvider), 200 | "sendenv" => Ok(Self::SendEnv), 201 | "serveralivecountmax" => Ok(Self::ServerAliveCountMax), 202 | "sessiontype" => Ok(Self::SessionType), 203 | "setenv" => Ok(Self::SetEnv), 204 | "stdinnull" => Ok(Self::StdinNull), 205 | "streamlocalbindmask" => Ok(Self::StreamLocalBindMask), 206 | "stricthostkeychecking" => Ok(Self::StrictHostKeyChecking), 207 | "syslogfacility" => Ok(Self::SyslogFacility), 208 | "updatehostkeys" => Ok(Self::UpdateHostKeys), 209 | "userknownhostsfile" => Ok(Self::UserKnownHostsFile), 210 | "verifyhostkeydns" => Ok(Self::VerifyHostKeyDNS), 211 | "visualhostkey" => Ok(Self::VisualHostKey), 212 | "xauthlocation" => Ok(Self::XAuthLocation), 213 | // -- unknwon field 214 | _ => Err(s.to_string()), 215 | } 216 | } 217 | } 218 | 219 | impl fmt::Display for Field { 220 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 221 | let s = match self { 222 | Self::Host => "host", 223 | Self::AddKeysToAgent => "addkeystoagent", 224 | Self::BindAddress => "bindaddress", 225 | Self::BindInterface => "bindinterface", 226 | Self::CaSignatureAlgorithms => "casignaturealgorithms", 227 | Self::CertificateFile => "certificatefile", 228 | Self::Ciphers => "ciphers", 229 | Self::Compression => "compression", 230 | Self::ConnectionAttempts => "connectionattempts", 231 | Self::ConnectTimeout => "connecttimeout", 232 | Self::ForwardAgent => "forwardagent", 233 | Self::HostKeyAlgorithms => "hostkeyalgorithms", 234 | Self::HostName => "hostname", 235 | Self::IdentityFile => "identityfile", 236 | Self::IgnoreUnknown => "ignoreunknown", 237 | Self::KexAlgorithms => "kexalgorithms", 238 | Self::Mac => "macs", 239 | Self::Port => "port", 240 | Self::ProxyJump => "proxyjump", 241 | Self::PubkeyAcceptedAlgorithms => "pubkeyacceptedalgorithms", 242 | Self::PubkeyAuthentication => "pubkeyauthentication", 243 | Self::RemoteForward => "remoteforward", 244 | Self::ServerAliveInterval => "serveraliveinterval", 245 | Self::TcpKeepAlive => "tcpkeepalive", 246 | #[cfg(target_os = "macos")] 247 | Self::UseKeychain => "usekeychain", 248 | Self::User => "user", 249 | // Continuation of the rest of the enum variants 250 | Self::AddressFamily => "addressfamily", 251 | Self::BatchMode => "batchmode", 252 | Self::CanonicalDomains => "canonicaldomains", 253 | Self::CanonicalizeFallbackLock => "canonicalizefallbacklock", 254 | Self::CanonicalizeHostname => "canonicalizehostname", 255 | Self::CanonicalizeMaxDots => "canonicalizemaxdots", 256 | Self::CanonicalizePermittedCNAMEs => "canonicalizepermittedcnames", 257 | Self::CheckHostIP => "checkhostip", 258 | Self::ClearAllForwardings => "clearallforwardings", 259 | Self::ControlMaster => "controlmaster", 260 | Self::ControlPath => "controlpath", 261 | Self::ControlPersist => "controlpersist", 262 | Self::DynamicForward => "dynamicforward", 263 | Self::EnableSSHKeysign => "enablesshkeysign", 264 | Self::EscapeChar => "escapechar", 265 | Self::ExitOnForwardFailure => "exitonforwardfailure", 266 | Self::FingerprintHash => "fingerprinthash", 267 | Self::ForkAfterAuthentication => "forkafterauthentication", 268 | Self::ForwardX11 => "forwardx11", 269 | Self::ForwardX11Timeout => "forwardx11timeout", 270 | Self::ForwardX11Trusted => "forwardx11trusted", 271 | Self::GatewayPorts => "gatewayports", 272 | Self::GlobalKnownHostsFile => "globalknownhostsfile", 273 | Self::GSSAPIAuthentication => "gssapiauthentication", 274 | Self::GSSAPIDelegateCredentials => "gssapidelegatecredentials", 275 | Self::HashKnownHosts => "hashknownhosts", 276 | Self::HostbasedAcceptedAlgorithms => "hostbasedacceptedalgorithms", 277 | Self::HostbasedAuthentication => "hostbasedauthentication", 278 | Self::HostKeyAlias => "hostkeyalias", 279 | Self::HostbasedKeyTypes => "hostbasedkeytypes", 280 | Self::IdentitiesOnly => "identitiesonly", 281 | Self::IdentityAgent => "identityagent", 282 | Self::Include => "include", 283 | Self::IPQoS => "ipqos", 284 | Self::KbdInteractiveAuthentication => "kbdinteractiveauthentication", 285 | Self::KbdInteractiveDevices => "kbdinteractivedevices", 286 | Self::KnownHostsCommand => "knownhostscommand", 287 | Self::LocalCommand => "localcommand", 288 | Self::LocalForward => "localforward", 289 | Self::LogLevel => "loglevel", 290 | Self::LogVerbose => "logverbose", 291 | Self::NoHostAuthenticationForLocalhost => "nohostauthenticationforlocalhost", 292 | Self::NumberOfPasswordPrompts => "numberofpasswordprompts", 293 | Self::PasswordAuthentication => "passwordauthentication", 294 | Self::PermitLocalCommand => "permitlocalcommand", 295 | Self::PermitRemoteOpen => "permitremoteopen", 296 | Self::PKCS11Provider => "pkcs11provider", 297 | Self::PreferredAuthentications => "preferredauthentications", 298 | Self::ProxyCommand => "proxycommand", 299 | Self::ProxyUseFdpass => "proxyusefdpass", 300 | Self::PubkeyAcceptedKeyTypes => "pubkeyacceptedkeytypes", 301 | Self::RekeyLimit => "rekeylimit", 302 | Self::RequestTTY => "requesttty", 303 | Self::RevokedHostKeys => "revokedhostkeys", 304 | Self::SecruityKeyProvider => "secruitykeyprovider", 305 | Self::SendEnv => "sendenv", 306 | Self::ServerAliveCountMax => "serveralivecountmax", 307 | Self::SessionType => "sessiontype", 308 | Self::SetEnv => "setenv", 309 | Self::StdinNull => "stdinnull", 310 | Self::StreamLocalBindMask => "streamlocalbindmask", 311 | Self::StrictHostKeyChecking => "stricthostkeychecking", 312 | Self::SyslogFacility => "syslogfacility", 313 | Self::UpdateHostKeys => "updatehostkeys", 314 | Self::UserKnownHostsFile => "userknownhostsfile", 315 | Self::VerifyHostKeyDNS => "verifyhostkeydns", 316 | Self::VisualHostKey => "visualhostkey", 317 | Self::XAuthLocation => "xauthlocation", 318 | }; 319 | write!(f, "{}", s) 320 | } 321 | } 322 | 323 | #[cfg(test)] 324 | mod tests { 325 | 326 | use pretty_assertions::assert_eq; 327 | 328 | use super::*; 329 | 330 | #[test] 331 | fn should_parse_field_from_string() { 332 | assert_eq!(Field::from_str("Host").ok().unwrap(), Field::Host); 333 | assert_eq!( 334 | Field::from_str("AddKeysToAgent").ok().unwrap(), 335 | Field::AddKeysToAgent 336 | ); 337 | assert_eq!( 338 | Field::from_str("BindAddress").ok().unwrap(), 339 | Field::BindAddress 340 | ); 341 | assert_eq!( 342 | Field::from_str("BindInterface").ok().unwrap(), 343 | Field::BindInterface 344 | ); 345 | assert_eq!( 346 | Field::from_str("CaSignatureAlgorithms").ok().unwrap(), 347 | Field::CaSignatureAlgorithms 348 | ); 349 | assert_eq!( 350 | Field::from_str("CertificateFile").ok().unwrap(), 351 | Field::CertificateFile 352 | ); 353 | assert_eq!(Field::from_str("Ciphers").ok().unwrap(), Field::Ciphers); 354 | assert_eq!( 355 | Field::from_str("Compression").ok().unwrap(), 356 | Field::Compression 357 | ); 358 | assert_eq!( 359 | Field::from_str("ConnectionAttempts").ok().unwrap(), 360 | Field::ConnectionAttempts 361 | ); 362 | assert_eq!( 363 | Field::from_str("ConnectTimeout").ok().unwrap(), 364 | Field::ConnectTimeout 365 | ); 366 | assert_eq!( 367 | Field::from_str("ForwardAgent").ok().unwrap(), 368 | Field::ForwardAgent 369 | ); 370 | assert_eq!(Field::from_str("HostName").ok().unwrap(), Field::HostName); 371 | assert_eq!( 372 | Field::from_str("IdentityFile").ok().unwrap(), 373 | Field::IdentityFile 374 | ); 375 | assert_eq!( 376 | Field::from_str("IgnoreUnknown").ok().unwrap(), 377 | Field::IgnoreUnknown 378 | ); 379 | assert_eq!(Field::from_str("Macs").ok().unwrap(), Field::Mac); 380 | assert_eq!(Field::from_str("ProxyJump").ok().unwrap(), Field::ProxyJump); 381 | assert_eq!( 382 | Field::from_str("PubkeyAcceptedAlgorithms").ok().unwrap(), 383 | Field::PubkeyAcceptedAlgorithms 384 | ); 385 | assert_eq!( 386 | Field::from_str("PubkeyAuthentication").ok().unwrap(), 387 | Field::PubkeyAuthentication 388 | ); 389 | assert_eq!( 390 | Field::from_str("RemoteForward").ok().unwrap(), 391 | Field::RemoteForward 392 | ); 393 | assert_eq!( 394 | Field::from_str("TcpKeepAlive").ok().unwrap(), 395 | Field::TcpKeepAlive 396 | ); 397 | #[cfg(target_os = "macos")] 398 | assert_eq!( 399 | Field::from_str("UseKeychain").ok().unwrap(), 400 | Field::UseKeychain 401 | ); 402 | assert_eq!(Field::from_str("User").ok().unwrap(), Field::User); 403 | assert_eq!( 404 | Field::from_str("AddKeysToAgent").ok().unwrap(), 405 | Field::AddKeysToAgent 406 | ); 407 | assert_eq!( 408 | Field::from_str("AddressFamily").ok().unwrap(), 409 | Field::AddressFamily 410 | ); 411 | assert_eq!(Field::from_str("BatchMode").ok().unwrap(), Field::BatchMode); 412 | assert_eq!( 413 | Field::from_str("CanonicalDomains").ok().unwrap(), 414 | Field::CanonicalDomains 415 | ); 416 | assert_eq!( 417 | Field::from_str("CanonicalizeFallbackLock").ok().unwrap(), 418 | Field::CanonicalizeFallbackLock 419 | ); 420 | assert_eq!( 421 | Field::from_str("CanonicalizeHostname").ok().unwrap(), 422 | Field::CanonicalizeHostname 423 | ); 424 | assert_eq!( 425 | Field::from_str("CanonicalizeMaxDots").ok().unwrap(), 426 | Field::CanonicalizeMaxDots 427 | ); 428 | assert_eq!( 429 | Field::from_str("CanonicalizePermittedCNAMEs").ok().unwrap(), 430 | Field::CanonicalizePermittedCNAMEs 431 | ); 432 | assert_eq!( 433 | Field::from_str("CheckHostIP").ok().unwrap(), 434 | Field::CheckHostIP 435 | ); 436 | assert_eq!( 437 | Field::from_str("ClearAllForwardings").ok().unwrap(), 438 | Field::ClearAllForwardings 439 | ); 440 | assert_eq!( 441 | Field::from_str("ControlMaster").ok().unwrap(), 442 | Field::ControlMaster 443 | ); 444 | assert_eq!( 445 | Field::from_str("ControlPath").ok().unwrap(), 446 | Field::ControlPath 447 | ); 448 | assert_eq!( 449 | Field::from_str("ControlPersist").ok().unwrap(), 450 | Field::ControlPersist 451 | ); 452 | assert_eq!( 453 | Field::from_str("DynamicForward").ok().unwrap(), 454 | Field::DynamicForward 455 | ); 456 | assert_eq!( 457 | Field::from_str("EnableSSHKeysign").ok().unwrap(), 458 | Field::EnableSSHKeysign 459 | ); 460 | assert_eq!( 461 | Field::from_str("EscapeChar").ok().unwrap(), 462 | Field::EscapeChar 463 | ); 464 | assert_eq!( 465 | Field::from_str("ExitOnForwardFailure").ok().unwrap(), 466 | Field::ExitOnForwardFailure 467 | ); 468 | assert_eq!( 469 | Field::from_str("FingerprintHash").ok().unwrap(), 470 | Field::FingerprintHash 471 | ); 472 | assert_eq!( 473 | Field::from_str("ForkAfterAuthentication").ok().unwrap(), 474 | Field::ForkAfterAuthentication 475 | ); 476 | assert_eq!( 477 | Field::from_str("ForwardAgent").ok().unwrap(), 478 | Field::ForwardAgent 479 | ); 480 | assert_eq!( 481 | Field::from_str("ForwardX11").ok().unwrap(), 482 | Field::ForwardX11 483 | ); 484 | assert_eq!( 485 | Field::from_str("ForwardX11Timeout").ok().unwrap(), 486 | Field::ForwardX11Timeout 487 | ); 488 | assert_eq!( 489 | Field::from_str("ForwardX11Trusted").ok().unwrap(), 490 | Field::ForwardX11Trusted, 491 | ); 492 | assert_eq!( 493 | Field::from_str("GatewayPorts").ok().unwrap(), 494 | Field::GatewayPorts 495 | ); 496 | assert_eq!( 497 | Field::from_str("GlobalKnownHostsFile").ok().unwrap(), 498 | Field::GlobalKnownHostsFile 499 | ); 500 | assert_eq!( 501 | Field::from_str("GSSAPIAuthentication").ok().unwrap(), 502 | Field::GSSAPIAuthentication 503 | ); 504 | assert_eq!( 505 | Field::from_str("GSSAPIDelegateCredentials").ok().unwrap(), 506 | Field::GSSAPIDelegateCredentials 507 | ); 508 | assert_eq!( 509 | Field::from_str("HashKnownHosts").ok().unwrap(), 510 | Field::HashKnownHosts 511 | ); 512 | assert_eq!( 513 | Field::from_str("HostbasedAcceptedAlgorithms").ok().unwrap(), 514 | Field::HostbasedAcceptedAlgorithms 515 | ); 516 | assert_eq!( 517 | Field::from_str("HostbasedAuthentication").ok().unwrap(), 518 | Field::HostbasedAuthentication 519 | ); 520 | assert_eq!( 521 | Field::from_str("HostKeyAlgorithms").ok().unwrap(), 522 | Field::HostKeyAlgorithms 523 | ); 524 | assert_eq!( 525 | Field::from_str("HostKeyAlias").ok().unwrap(), 526 | Field::HostKeyAlias 527 | ); 528 | assert_eq!( 529 | Field::from_str("HostbasedKeyTypes").ok().unwrap(), 530 | Field::HostbasedKeyTypes 531 | ); 532 | assert_eq!( 533 | Field::from_str("IdentitiesOnly").ok().unwrap(), 534 | Field::IdentitiesOnly 535 | ); 536 | assert_eq!( 537 | Field::from_str("IdentityAgent").ok().unwrap(), 538 | Field::IdentityAgent 539 | ); 540 | assert_eq!(Field::from_str("Include").ok().unwrap(), Field::Include); 541 | assert_eq!(Field::from_str("IPQoS").ok().unwrap(), Field::IPQoS); 542 | assert_eq!( 543 | Field::from_str("KbdInteractiveAuthentication") 544 | .ok() 545 | .unwrap(), 546 | Field::KbdInteractiveAuthentication 547 | ); 548 | assert_eq!( 549 | Field::from_str("KbdInteractiveDevices").ok().unwrap(), 550 | Field::KbdInteractiveDevices 551 | ); 552 | assert_eq!( 553 | Field::from_str("KnownHostsCommand").ok().unwrap(), 554 | Field::KnownHostsCommand 555 | ); 556 | assert_eq!( 557 | Field::from_str("LocalCommand").ok().unwrap(), 558 | Field::LocalCommand 559 | ); 560 | assert_eq!( 561 | Field::from_str("LocalForward").ok().unwrap(), 562 | Field::LocalForward 563 | ); 564 | assert_eq!(Field::from_str("LogLevel").ok().unwrap(), Field::LogLevel); 565 | assert_eq!( 566 | Field::from_str("LogVerbose").ok().unwrap(), 567 | Field::LogVerbose 568 | ); 569 | assert_eq!( 570 | Field::from_str("NoHostAuthenticationForLocalhost") 571 | .ok() 572 | .unwrap(), 573 | Field::NoHostAuthenticationForLocalhost 574 | ); 575 | assert_eq!( 576 | Field::from_str("NumberOfPasswordPrompts").ok().unwrap(), 577 | Field::NumberOfPasswordPrompts 578 | ); 579 | assert_eq!( 580 | Field::from_str("PasswordAuthentication").ok().unwrap(), 581 | Field::PasswordAuthentication 582 | ); 583 | assert_eq!( 584 | Field::from_str("PermitLocalCommand").ok().unwrap(), 585 | Field::PermitLocalCommand 586 | ); 587 | assert_eq!( 588 | Field::from_str("PermitRemoteOpen").ok().unwrap(), 589 | Field::PermitRemoteOpen 590 | ); 591 | assert_eq!( 592 | Field::from_str("PKCS11Provider").ok().unwrap(), 593 | Field::PKCS11Provider 594 | ); 595 | assert_eq!(Field::from_str("Port").ok().unwrap(), Field::Port); 596 | assert_eq!( 597 | Field::from_str("PreferredAuthentications").ok().unwrap(), 598 | Field::PreferredAuthentications 599 | ); 600 | assert_eq!( 601 | Field::from_str("ProxyCommand").ok().unwrap(), 602 | Field::ProxyCommand 603 | ); 604 | assert_eq!(Field::from_str("ProxyJump").ok().unwrap(), Field::ProxyJump); 605 | assert_eq!( 606 | Field::from_str("ProxyUseFdpass").ok().unwrap(), 607 | Field::ProxyUseFdpass 608 | ); 609 | assert_eq!( 610 | Field::from_str("PubkeyAcceptedKeyTypes").ok().unwrap(), 611 | Field::PubkeyAcceptedKeyTypes 612 | ); 613 | assert_eq!( 614 | Field::from_str("RekeyLimit").ok().unwrap(), 615 | Field::RekeyLimit 616 | ); 617 | assert_eq!( 618 | Field::from_str("RequestTTY").ok().unwrap(), 619 | Field::RequestTTY 620 | ); 621 | assert_eq!( 622 | Field::from_str("RevokedHostKeys").ok().unwrap(), 623 | Field::RevokedHostKeys 624 | ); 625 | assert_eq!( 626 | Field::from_str("SecruityKeyProvider").ok().unwrap(), 627 | Field::SecruityKeyProvider 628 | ); 629 | assert_eq!(Field::from_str("SendEnv").ok().unwrap(), Field::SendEnv); 630 | assert_eq!( 631 | Field::from_str("ServerAliveCountMax").ok().unwrap(), 632 | Field::ServerAliveCountMax 633 | ); 634 | assert_eq!( 635 | Field::from_str("ServerAliveInterval").ok().unwrap(), 636 | Field::ServerAliveInterval 637 | ); 638 | assert_eq!( 639 | Field::from_str("SessionType").ok().unwrap(), 640 | Field::SessionType 641 | ); 642 | assert_eq!(Field::from_str("SetEnv").ok().unwrap(), Field::SetEnv); 643 | assert_eq!(Field::from_str("StdinNull").ok().unwrap(), Field::StdinNull); 644 | assert_eq!( 645 | Field::from_str("StreamLocalBindMask").ok().unwrap(), 646 | Field::StreamLocalBindMask 647 | ); 648 | assert_eq!( 649 | Field::from_str("StrictHostKeyChecking").ok().unwrap(), 650 | Field::StrictHostKeyChecking 651 | ); 652 | assert_eq!( 653 | Field::from_str("SyslogFacility").ok().unwrap(), 654 | Field::SyslogFacility 655 | ); 656 | assert_eq!( 657 | Field::from_str("UpdateHostKeys").ok().unwrap(), 658 | Field::UpdateHostKeys 659 | ); 660 | assert_eq!( 661 | Field::from_str("UserKnownHostsFile").ok().unwrap(), 662 | Field::UserKnownHostsFile 663 | ); 664 | assert_eq!( 665 | Field::from_str("VerifyHostKeyDNS").ok().unwrap(), 666 | Field::VerifyHostKeyDNS 667 | ); 668 | assert_eq!( 669 | Field::from_str("VisualHostKey").ok().unwrap(), 670 | Field::VisualHostKey 671 | ); 672 | assert_eq!( 673 | Field::from_str("XAuthLocation").ok().unwrap(), 674 | Field::XAuthLocation 675 | ); 676 | } 677 | 678 | #[test] 679 | fn should_fail_parsing_field() { 680 | assert!(Field::from_str("CristinaDavena").is_err()); 681 | } 682 | } 683 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | //! # parser 2 | //! 3 | //! Ssh config parser 4 | 5 | use std::fs::File; 6 | use std::io::{BufRead, BufReader, Error as IoError}; 7 | use std::path::PathBuf; 8 | use std::str::FromStr; 9 | use std::time::Duration; 10 | 11 | use bitflags::bitflags; 12 | use glob::glob; 13 | use thiserror::Error; 14 | 15 | use super::{Host, HostClause, HostParams, SshConfig}; 16 | use crate::DefaultAlgorithms; 17 | use crate::params::AlgorithmsRule; 18 | 19 | // modules 20 | mod field; 21 | use field::Field; 22 | 23 | pub type SshParserResult = Result; 24 | 25 | /// [`SshConfigParser::update_host`] result 26 | #[derive(Debug, PartialEq, Eq)] 27 | enum UpdateHost { 28 | /// Update current host 29 | UpdateHost, 30 | /// Add new hosts 31 | NewHosts(Vec), 32 | } 33 | 34 | /// Ssh config parser error 35 | #[derive(Debug, Error)] 36 | pub enum SshParserError { 37 | #[error("expected boolean value ('yes', 'no')")] 38 | ExpectedBoolean, 39 | #[error("expected port number")] 40 | ExpectedPort, 41 | #[error("expected unsigned value")] 42 | ExpectedUnsigned, 43 | #[error("expected algorithms")] 44 | ExpectedAlgorithms, 45 | #[error("expected path")] 46 | ExpectedPath, 47 | #[error("IO error: {0}")] 48 | Io(#[from] IoError), 49 | #[error("glob error: {0}")] 50 | Glob(#[from] glob::GlobError), 51 | #[error("missing argument")] 52 | MissingArgument, 53 | #[error("pattern error: {0}")] 54 | PatternError(#[from] glob::PatternError), 55 | #[error("unknown field: {0}")] 56 | UnknownField(String, Vec), 57 | #[error("unknown field: {0}")] 58 | UnsupportedField(String, Vec), 59 | } 60 | 61 | bitflags! { 62 | /// The parsing mode 63 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 64 | pub struct ParseRule: u8 { 65 | /// Don't allow any invalid field or value 66 | const STRICT = 0b00000000; 67 | /// Allow unknown field 68 | const ALLOW_UNKNOWN_FIELDS = 0b00000001; 69 | /// Allow unsupported fields 70 | const ALLOW_UNSUPPORTED_FIELDS = 0b00000010; 71 | } 72 | } 73 | 74 | // -- parser 75 | 76 | /// Ssh config parser 77 | pub(crate) struct SshConfigParser; 78 | 79 | impl SshConfigParser { 80 | /// Parse reader lines and apply parameters to configuration 81 | pub(crate) fn parse( 82 | config: &mut SshConfig, 83 | reader: &mut impl BufRead, 84 | rules: ParseRule, 85 | ignore_unknown: Option>, 86 | ) -> SshParserResult<()> { 87 | // Options preceding the first `Host` section 88 | // are parsed as command line options; 89 | // overriding all following host-specific options. 90 | // 91 | // See https://github.com/openssh/openssh-portable/blob/master/readconf.c#L1173-L1176 92 | let mut default_params = HostParams::new(&config.default_algorithms); 93 | default_params.ignore_unknown = ignore_unknown; 94 | config.hosts.push(Host::new( 95 | vec![HostClause::new(String::from("*"), false)], 96 | default_params, 97 | )); 98 | 99 | // Current host pointer 100 | let mut current_host = config.hosts.last_mut().unwrap(); 101 | 102 | let mut lines = reader.lines(); 103 | // iter lines 104 | loop { 105 | let line = match lines.next() { 106 | None => break, 107 | Some(Err(err)) => return Err(SshParserError::Io(err)), 108 | Some(Ok(line)) => Self::strip_comments(line.trim()), 109 | }; 110 | if line.is_empty() { 111 | continue; 112 | } 113 | // tokenize 114 | let (field, args) = match Self::tokenize_line(&line) { 115 | Ok((field, args)) => (field, args), 116 | Err(SshParserError::UnknownField(field, args)) 117 | if rules.intersects(ParseRule::ALLOW_UNKNOWN_FIELDS) 118 | || current_host.params.ignored(&field) => 119 | { 120 | current_host.params.ignored_fields.insert(field, args); 121 | continue; 122 | } 123 | Err(SshParserError::UnknownField(field, args)) => { 124 | return Err(SshParserError::UnknownField(field, args)); 125 | } 126 | Err(err) => return Err(err), 127 | }; 128 | // If field is block, init a new block 129 | if field == Field::Host { 130 | // Pass `ignore_unknown` from global overrides down into the tokenizer. 131 | let mut params = HostParams::new(&config.default_algorithms); 132 | params.ignore_unknown = config.hosts[0].params.ignore_unknown.clone(); 133 | let pattern = Self::parse_host(args)?; 134 | trace!("Adding new host: {pattern:?}",); 135 | 136 | // Add a new host 137 | config.hosts.push(Host::new(pattern, params)); 138 | // Update current host pointer 139 | current_host = config.hosts.last_mut().expect("Just added hosts"); 140 | } else { 141 | // Update field 142 | match Self::update_host( 143 | field, 144 | args, 145 | current_host, 146 | rules, 147 | &config.default_algorithms, 148 | ) { 149 | Ok(UpdateHost::UpdateHost) => Ok(()), 150 | Ok(UpdateHost::NewHosts(new_hosts)) => { 151 | trace!("Adding new hosts from 'UpdateHost::NewHosts': {new_hosts:?}",); 152 | config.hosts.extend(new_hosts); 153 | current_host = config.hosts.last_mut().expect("Just added hosts"); 154 | Ok(()) 155 | } 156 | // If we're allowing unsupported fields to be parsed, add them to the map 157 | Err(SshParserError::UnsupportedField(field, args)) 158 | if rules.intersects(ParseRule::ALLOW_UNSUPPORTED_FIELDS) => 159 | { 160 | current_host.params.unsupported_fields.insert(field, args); 161 | Ok(()) 162 | } 163 | // Eat the error here to not break the API with this change 164 | // Also it'd be weird to error on correct ssh_config's just because they're 165 | // not supported by this library 166 | Err(SshParserError::UnsupportedField(_, _)) => Ok(()), 167 | Err(e) => Err(e), 168 | }?; 169 | } 170 | } 171 | 172 | Ok(()) 173 | } 174 | 175 | /// Strip comments from line 176 | fn strip_comments(s: &str) -> String { 177 | if let Some(pos) = s.find('#') { 178 | s[..pos].to_string() 179 | } else { 180 | s.to_string() 181 | } 182 | } 183 | 184 | /// Update current given host with field argument 185 | fn update_host( 186 | field: Field, 187 | args: Vec, 188 | host: &mut Host, 189 | rules: ParseRule, 190 | default_algos: &DefaultAlgorithms, 191 | ) -> SshParserResult { 192 | trace!("parsing field {field:?} with args {args:?}",); 193 | let params = &mut host.params; 194 | match field { 195 | Field::AddKeysToAgent => { 196 | let value = Self::parse_boolean(args)?; 197 | trace!("add_keys_to_agent: {value}",); 198 | params.add_keys_to_agent = Some(value); 199 | } 200 | Field::BindAddress => { 201 | let value = Self::parse_string(args)?; 202 | trace!("bind_address: {value}",); 203 | params.bind_address = Some(value); 204 | } 205 | Field::BindInterface => { 206 | let value = Self::parse_string(args)?; 207 | trace!("bind_interface: {value}",); 208 | params.bind_interface = Some(value); 209 | } 210 | Field::CaSignatureAlgorithms => { 211 | let rule = Self::parse_algos(args)?; 212 | trace!("ca_signature_algorithms: {rule:?}",); 213 | params.ca_signature_algorithms.apply(rule); 214 | } 215 | Field::CertificateFile => { 216 | let value = Self::parse_path(args)?; 217 | trace!("certificate_file: {value:?}",); 218 | params.certificate_file = Some(value); 219 | } 220 | Field::Ciphers => { 221 | let rule = Self::parse_algos(args)?; 222 | trace!("ciphers: {rule:?}",); 223 | params.ciphers.apply(rule); 224 | } 225 | Field::Compression => { 226 | let value = Self::parse_boolean(args)?; 227 | trace!("compression: {value}",); 228 | params.compression = Some(value); 229 | } 230 | Field::ConnectTimeout => { 231 | let value = Self::parse_duration(args)?; 232 | trace!("connect_timeout: {value:?}",); 233 | params.connect_timeout = Some(value); 234 | } 235 | Field::ConnectionAttempts => { 236 | let value = Self::parse_unsigned(args)?; 237 | trace!("connection_attempts: {value}",); 238 | params.connection_attempts = Some(value); 239 | } 240 | Field::ForwardAgent => { 241 | let value = Self::parse_boolean(args)?; 242 | trace!("forward_agent: {value}",); 243 | params.forward_agent = Some(value); 244 | } 245 | Field::Host => { /* already handled before */ } 246 | Field::HostKeyAlgorithms => { 247 | let rule = Self::parse_algos(args)?; 248 | trace!("host_key_algorithm: {rule:?}",); 249 | params.host_key_algorithms.apply(rule); 250 | } 251 | Field::HostName => { 252 | let value = Self::parse_string(args)?; 253 | trace!("host_name: {value}",); 254 | params.host_name = Some(value); 255 | } 256 | Field::Include => { 257 | return Self::include_files( 258 | args, 259 | host, 260 | rules, 261 | default_algos, 262 | host.params.ignore_unknown.clone(), 263 | ) 264 | .map(UpdateHost::NewHosts); 265 | } 266 | Field::IdentityFile => { 267 | let value = Self::parse_path_list(args)?; 268 | trace!("identity_file: {value:?}",); 269 | params.identity_file = Some(value); 270 | } 271 | Field::IgnoreUnknown => { 272 | let value = Self::parse_comma_separated_list(args)?; 273 | trace!("ignore_unknown: {value:?}",); 274 | params.ignore_unknown = Some(value); 275 | } 276 | Field::KexAlgorithms => { 277 | let rule = Self::parse_algos(args)?; 278 | trace!("kex_algorithms: {rule:?}",); 279 | params.kex_algorithms.apply(rule); 280 | } 281 | Field::Mac => { 282 | let rule = Self::parse_algos(args)?; 283 | trace!("mac: {rule:?}",); 284 | params.mac.apply(rule); 285 | } 286 | Field::Port => { 287 | let value = Self::parse_port(args)?; 288 | trace!("port: {value}",); 289 | params.port = Some(value); 290 | } 291 | Field::ProxyJump => { 292 | let rule = Self::parse_comma_separated_list(args)?; 293 | trace!("proxy_jump: {rule:?}",); 294 | params.proxy_jump = Some(rule); 295 | } 296 | Field::PubkeyAcceptedAlgorithms => { 297 | let rule = Self::parse_algos(args)?; 298 | trace!("pubkey_accepted_algorithms: {rule:?}",); 299 | params.pubkey_accepted_algorithms.apply(rule); 300 | } 301 | Field::PubkeyAuthentication => { 302 | let value = Self::parse_boolean(args)?; 303 | trace!("pubkey_authentication: {value}",); 304 | params.pubkey_authentication = Some(value); 305 | } 306 | Field::RemoteForward => { 307 | let value = Self::parse_port(args)?; 308 | trace!("remote_forward: {value}",); 309 | params.remote_forward = Some(value); 310 | } 311 | Field::ServerAliveInterval => { 312 | let value = Self::parse_duration(args)?; 313 | trace!("server_alive_interval: {value:?}",); 314 | params.server_alive_interval = Some(value); 315 | } 316 | Field::TcpKeepAlive => { 317 | let value = Self::parse_boolean(args)?; 318 | trace!("tcp_keep_alive: {value}",); 319 | params.tcp_keep_alive = Some(value); 320 | } 321 | #[cfg(target_os = "macos")] 322 | Field::UseKeychain => { 323 | let value = Self::parse_boolean(args)?; 324 | trace!("use_keychain: {value}",); 325 | params.use_keychain = Some(value); 326 | } 327 | Field::User => { 328 | let value = Self::parse_string(args)?; 329 | trace!("user: {value}",); 330 | params.user = Some(value); 331 | } 332 | // -- unimplemented fields 333 | Field::AddressFamily 334 | | Field::BatchMode 335 | | Field::CanonicalDomains 336 | | Field::CanonicalizeFallbackLock 337 | | Field::CanonicalizeHostname 338 | | Field::CanonicalizeMaxDots 339 | | Field::CanonicalizePermittedCNAMEs 340 | | Field::CheckHostIP 341 | | Field::ClearAllForwardings 342 | | Field::ControlMaster 343 | | Field::ControlPath 344 | | Field::ControlPersist 345 | | Field::DynamicForward 346 | | Field::EnableSSHKeysign 347 | | Field::EscapeChar 348 | | Field::ExitOnForwardFailure 349 | | Field::FingerprintHash 350 | | Field::ForkAfterAuthentication 351 | | Field::ForwardX11 352 | | Field::ForwardX11Timeout 353 | | Field::ForwardX11Trusted 354 | | Field::GatewayPorts 355 | | Field::GlobalKnownHostsFile 356 | | Field::GSSAPIAuthentication 357 | | Field::GSSAPIDelegateCredentials 358 | | Field::HashKnownHosts 359 | | Field::HostbasedAcceptedAlgorithms 360 | | Field::HostbasedAuthentication 361 | | Field::HostKeyAlias 362 | | Field::HostbasedKeyTypes 363 | | Field::IdentitiesOnly 364 | | Field::IdentityAgent 365 | | Field::IPQoS 366 | | Field::KbdInteractiveAuthentication 367 | | Field::KbdInteractiveDevices 368 | | Field::KnownHostsCommand 369 | | Field::LocalCommand 370 | | Field::LocalForward 371 | | Field::LogLevel 372 | | Field::LogVerbose 373 | | Field::NoHostAuthenticationForLocalhost 374 | | Field::NumberOfPasswordPrompts 375 | | Field::PasswordAuthentication 376 | | Field::PermitLocalCommand 377 | | Field::PermitRemoteOpen 378 | | Field::PKCS11Provider 379 | | Field::PreferredAuthentications 380 | | Field::ProxyCommand 381 | | Field::ProxyUseFdpass 382 | | Field::PubkeyAcceptedKeyTypes 383 | | Field::RekeyLimit 384 | | Field::RequestTTY 385 | | Field::RevokedHostKeys 386 | | Field::SecruityKeyProvider 387 | | Field::SendEnv 388 | | Field::ServerAliveCountMax 389 | | Field::SessionType 390 | | Field::SetEnv 391 | | Field::StdinNull 392 | | Field::StreamLocalBindMask 393 | | Field::StrictHostKeyChecking 394 | | Field::SyslogFacility 395 | | Field::UpdateHostKeys 396 | | Field::UserKnownHostsFile 397 | | Field::VerifyHostKeyDNS 398 | | Field::VisualHostKey 399 | | Field::XAuthLocation => { 400 | return Err(SshParserError::UnsupportedField(field.to_string(), args)); 401 | } 402 | } 403 | Ok(UpdateHost::UpdateHost) 404 | } 405 | 406 | /// Resolve the include path for a given path match. 407 | /// 408 | /// If the path match is absolute, it just returns the path as-is; 409 | /// if it is relative, it prepends $HOME/.ssh to it 410 | fn resolve_include_path(path_match: &str) -> String { 411 | #[cfg(windows)] 412 | const PATH_SEPARATOR: &str = "\\"; 413 | #[cfg(unix)] 414 | const PATH_SEPARATOR: &str = "/"; 415 | 416 | // if path match doesn't start with the path separator, prepend it 417 | if path_match.starts_with(PATH_SEPARATOR) { 418 | path_match.to_string() 419 | } else { 420 | // prepend $HOME/.ssh 421 | let home_dir = dirs::home_dir().unwrap_or(PathBuf::from(PATH_SEPARATOR)); 422 | format!( 423 | "{dir}{PATH_SEPARATOR}{path_match}", 424 | dir = home_dir.join(".ssh").display() 425 | ) 426 | } 427 | } 428 | 429 | /// include a file by parsing it and updating host rules by merging the read config to the current one for the host 430 | fn include_files( 431 | args: Vec, 432 | host: &mut Host, 433 | rules: ParseRule, 434 | default_algos: &DefaultAlgorithms, 435 | ignore_unknown: Option>, 436 | ) -> SshParserResult> { 437 | let path_match = Self::resolve_include_path(&Self::parse_string(args)?); 438 | 439 | trace!("include files: {path_match}",); 440 | let files = glob(&path_match)?; 441 | 442 | let mut new_hosts = vec![]; 443 | 444 | for file in files { 445 | let file = file?; 446 | trace!("including file: {}", file.display()); 447 | let mut reader = BufReader::new(File::open(file)?); 448 | let mut sub_config = SshConfig::default().default_algorithms(default_algos.clone()); 449 | Self::parse(&mut sub_config, &mut reader, rules, ignore_unknown.clone())?; 450 | 451 | // merge sub-config into host 452 | for pattern in &host.pattern { 453 | if pattern.negated { 454 | trace!("excluding sub-config for pattern: {pattern:?}",); 455 | continue; 456 | } 457 | trace!("merging sub-config for pattern: {pattern:?}",); 458 | let params = sub_config.query(&pattern.pattern); 459 | host.params.overwrite_if_none(¶ms); 460 | } 461 | 462 | // merge additional hosts 463 | for sub_host in sub_config.hosts.into_iter().skip(1) { 464 | trace!("adding sub-host: {sub_host:?}",); 465 | new_hosts.push(sub_host); 466 | } 467 | } 468 | 469 | Ok(new_hosts) 470 | } 471 | 472 | /// Tokenize line if possible. Returns [`Field`] name and args as a [`Vec`] of [`String`]. 473 | /// 474 | /// All of these lines are valid for tokenization 475 | /// 476 | /// ```txt 477 | /// IgnoreUnknown=Pippo,Pluto 478 | /// ConnectTimeout = 15 479 | /// Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza" 480 | /// Macs="Pasta Carbonara,Pasta con tonno" 481 | /// ``` 482 | /// 483 | /// So lines have syntax `field args...`, `field=args...`, `field "args"`, `field="args"` 484 | fn tokenize_line(line: &str) -> SshParserResult<(Field, Vec)> { 485 | // check what comes first, space or =? 486 | let trimmed_line = line.trim(); 487 | // first token is the field, and it may be separated either by a space or by '=' 488 | let (field, other_tokens) = if trimmed_line.find('=').unwrap_or(usize::MAX) 489 | < trimmed_line.find(char::is_whitespace).unwrap_or(usize::MAX) 490 | { 491 | trimmed_line 492 | .split_once('=') 493 | .ok_or(SshParserError::MissingArgument)? 494 | } else { 495 | trimmed_line 496 | .split_once(char::is_whitespace) 497 | .ok_or(SshParserError::MissingArgument)? 498 | }; 499 | 500 | trace!("tokenized line '{line}' - field '{field}' with args '{other_tokens}'",); 501 | 502 | // other tokens should trim = and whitespace 503 | let other_tokens = other_tokens.trim().trim_start_matches('=').trim(); 504 | trace!("other tokens trimmed: '{other_tokens}'",); 505 | 506 | // if args is quoted, don't split it 507 | let args = if other_tokens.starts_with('"') && other_tokens.ends_with('"') { 508 | trace!("quoted args: '{other_tokens}'",); 509 | vec![other_tokens[1..other_tokens.len() - 1].to_string()] 510 | } else { 511 | trace!("splitting args (non-quoted): '{other_tokens}'",); 512 | // split by whitespace 513 | let tokens = other_tokens.split_whitespace(); 514 | 515 | tokens 516 | .map(|x| x.trim().to_string()) 517 | .filter(|x| !x.is_empty()) 518 | .collect() 519 | }; 520 | 521 | match Field::from_str(field) { 522 | Ok(field) => Ok((field, args)), 523 | Err(_) => Err(SshParserError::UnknownField(field.to_string(), args)), 524 | } 525 | } 526 | 527 | // -- value parsers 528 | 529 | /// parse boolean value 530 | fn parse_boolean(args: Vec) -> SshParserResult { 531 | match args.first().map(|x| x.as_str()) { 532 | Some("yes") => Ok(true), 533 | Some("no") => Ok(false), 534 | Some(_) => Err(SshParserError::ExpectedBoolean), 535 | None => Err(SshParserError::MissingArgument), 536 | } 537 | } 538 | 539 | /// Parse algorithms argument 540 | fn parse_algos(args: Vec) -> SshParserResult { 541 | let first = args.first().ok_or(SshParserError::MissingArgument)?; 542 | 543 | AlgorithmsRule::from_str(first) 544 | } 545 | 546 | /// Parse comma separated list arguments 547 | fn parse_comma_separated_list(args: Vec) -> SshParserResult> { 548 | match args 549 | .first() 550 | .map(|x| x.split(',').map(|x| x.to_string()).collect()) 551 | { 552 | Some(args) => Ok(args), 553 | _ => Err(SshParserError::MissingArgument), 554 | } 555 | } 556 | 557 | /// Parse duration argument 558 | fn parse_duration(args: Vec) -> SshParserResult { 559 | let value = Self::parse_unsigned(args)?; 560 | Ok(Duration::from_secs(value as u64)) 561 | } 562 | 563 | /// Parse host argument 564 | fn parse_host(args: Vec) -> SshParserResult> { 565 | if args.is_empty() { 566 | return Err(SshParserError::MissingArgument); 567 | } 568 | // Collect hosts 569 | Ok(args 570 | .into_iter() 571 | .map(|x| { 572 | let tokens: Vec<&str> = x.split('!').collect(); 573 | if tokens.len() == 2 { 574 | HostClause::new(tokens[1].to_string(), true) 575 | } else { 576 | HostClause::new(tokens[0].to_string(), false) 577 | } 578 | }) 579 | .collect()) 580 | } 581 | 582 | /// Parse a list of paths 583 | fn parse_path_list(args: Vec) -> SshParserResult> { 584 | if args.is_empty() { 585 | return Err(SshParserError::MissingArgument); 586 | } 587 | args.iter() 588 | .map(|x| Self::parse_path_arg(x.as_str())) 589 | .collect() 590 | } 591 | 592 | /// Parse path argument 593 | fn parse_path(args: Vec) -> SshParserResult { 594 | if let Some(s) = args.first() { 595 | Self::parse_path_arg(s) 596 | } else { 597 | Err(SshParserError::MissingArgument) 598 | } 599 | } 600 | 601 | /// Parse path argument 602 | fn parse_path_arg(s: &str) -> SshParserResult { 603 | // Remove tilde 604 | let s = if s.starts_with('~') { 605 | let home_dir = dirs::home_dir() 606 | .unwrap_or_else(|| PathBuf::from("~")) 607 | .to_string_lossy() 608 | .to_string(); 609 | s.replacen('~', &home_dir, 1) 610 | } else { 611 | s.to_string() 612 | }; 613 | Ok(PathBuf::from(s)) 614 | } 615 | 616 | /// Parse port number argument 617 | fn parse_port(args: Vec) -> SshParserResult { 618 | match args.first().map(|x| u16::from_str(x)) { 619 | Some(Ok(val)) => Ok(val), 620 | Some(Err(_)) => Err(SshParserError::ExpectedPort), 621 | None => Err(SshParserError::MissingArgument), 622 | } 623 | } 624 | 625 | /// Parse string argument 626 | fn parse_string(args: Vec) -> SshParserResult { 627 | if let Some(s) = args.into_iter().next() { 628 | Ok(s) 629 | } else { 630 | Err(SshParserError::MissingArgument) 631 | } 632 | } 633 | 634 | /// Parse unsigned argument 635 | fn parse_unsigned(args: Vec) -> SshParserResult { 636 | match args.first().map(|x| usize::from_str(x)) { 637 | Some(Ok(val)) => Ok(val), 638 | Some(Err(_)) => Err(SshParserError::ExpectedUnsigned), 639 | None => Err(SshParserError::MissingArgument), 640 | } 641 | } 642 | } 643 | 644 | #[cfg(test)] 645 | mod tests { 646 | 647 | use std::fs::File; 648 | use std::io::{BufReader, Write}; 649 | use std::path::Path; 650 | 651 | use pretty_assertions::assert_eq; 652 | use tempfile::NamedTempFile; 653 | 654 | use super::*; 655 | use crate::DefaultAlgorithms; 656 | 657 | #[test] 658 | fn should_parse_configuration() -> Result<(), SshParserError> { 659 | crate::test_log(); 660 | let temp = create_ssh_config(); 661 | let file = File::open(temp.path()).expect("Failed to open tempfile"); 662 | let mut reader = BufReader::new(file); 663 | let config = SshConfig::default() 664 | .default_algorithms(DefaultAlgorithms { 665 | ca_signature_algorithms: vec![], 666 | ciphers: vec![], 667 | host_key_algorithms: vec![], 668 | kex_algorithms: vec![], 669 | mac: vec![], 670 | pubkey_accepted_algorithms: vec!["omar-crypt".to_string()], 671 | }) 672 | .parse(&mut reader, ParseRule::STRICT)?; 673 | 674 | // Query openssh cmdline overrides (options preceding the first `Host` section, 675 | // overriding all following options) 676 | let params = config.query("*"); 677 | assert_eq!( 678 | params.ignore_unknown.as_deref().unwrap(), 679 | &["Pippo", "Pluto"] 680 | ); 681 | assert_eq!(params.compression.unwrap(), true); 682 | assert_eq!(params.connection_attempts.unwrap(), 10); 683 | assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60)); 684 | assert_eq!( 685 | params.server_alive_interval.unwrap(), 686 | Duration::from_secs(40) 687 | ); 688 | assert_eq!(params.tcp_keep_alive.unwrap(), true); 689 | assert_eq!(params.ciphers.algorithms(), &["a-manella", "blowfish"]); 690 | assert_eq!( 691 | params.pubkey_accepted_algorithms.algorithms(), 692 | &["desu", "omar-crypt", "fast-omar-crypt"] 693 | ); 694 | 695 | // Query explicit all-hosts fallback options (`Host *`) 696 | assert_eq!(params.ca_signature_algorithms.algorithms(), &["random"]); 697 | assert_eq!( 698 | params.host_key_algorithms.algorithms(), 699 | &["luigi", "mario",] 700 | ); 701 | assert_eq!(params.kex_algorithms.algorithms(), &["desu", "gigi",]); 702 | assert_eq!(params.mac.algorithms(), &["concorde"]); 703 | assert!(params.bind_address.is_none()); 704 | 705 | // Query 172.26.104.4, yielding cmdline overrides, 706 | // explicit `Host 192.168.*.* 172.26.*.* !192.168.1.30` options, 707 | // and all-hosts fallback options. 708 | let params_172_26_104_4 = config.query("172.26.104.4"); 709 | 710 | // cmdline overrides 711 | assert_eq!(params_172_26_104_4.add_keys_to_agent.unwrap(), true); 712 | assert_eq!(params_172_26_104_4.compression.unwrap(), true); 713 | assert_eq!(params_172_26_104_4.connection_attempts.unwrap(), 10); 714 | assert_eq!( 715 | params_172_26_104_4.connect_timeout.unwrap(), 716 | Duration::from_secs(60) 717 | ); 718 | assert_eq!(params_172_26_104_4.tcp_keep_alive.unwrap(), true); 719 | 720 | // all-hosts fallback options, merged with host-specific options 721 | assert_eq!( 722 | params_172_26_104_4.ca_signature_algorithms.algorithms(), 723 | &["random"] 724 | ); 725 | assert_eq!( 726 | params_172_26_104_4.ciphers.algorithms(), 727 | &["a-manella", "blowfish",] 728 | ); 729 | assert_eq!(params_172_26_104_4.mac.algorithms(), &["spyro", "deoxys"]); // use subconfig; defined before * macs 730 | assert_eq!( 731 | params_172_26_104_4.proxy_jump.unwrap(), 732 | &["jump.example.com"] 733 | ); // use subconfig; defined before * macs 734 | assert_eq!( 735 | params_172_26_104_4 736 | .pubkey_accepted_algorithms 737 | .algorithms() 738 | .is_empty(), // should have removed omar-crypt 739 | true 740 | ); 741 | assert_eq!( 742 | params_172_26_104_4.bind_address.as_deref().unwrap(), 743 | "10.8.0.10" 744 | ); 745 | assert_eq!( 746 | params_172_26_104_4.bind_interface.as_deref().unwrap(), 747 | "tun0" 748 | ); 749 | assert_eq!(params_172_26_104_4.port.unwrap(), 2222); 750 | assert_eq!( 751 | params_172_26_104_4.identity_file.as_deref().unwrap(), 752 | vec![ 753 | Path::new("/home/root/.ssh/pippo.key"), 754 | Path::new("/home/root/.ssh/pluto.key") 755 | ] 756 | ); 757 | assert_eq!(params_172_26_104_4.user.as_deref().unwrap(), "omar"); 758 | 759 | // Query tostapane 760 | let params_tostapane = config.query("tostapane"); 761 | assert_eq!(params_tostapane.compression.unwrap(), true); // it takes the first value defined, which is `yes` 762 | assert_eq!(params_tostapane.connection_attempts.unwrap(), 10); 763 | assert_eq!( 764 | params_tostapane.connect_timeout.unwrap(), 765 | Duration::from_secs(60) 766 | ); 767 | assert_eq!(params_tostapane.tcp_keep_alive.unwrap(), true); 768 | assert_eq!(params_tostapane.remote_forward.unwrap(), 88); 769 | assert_eq!(params_tostapane.user.as_deref().unwrap(), "ciro-esposito"); 770 | 771 | // all-hosts fallback options 772 | assert_eq!( 773 | params_tostapane.ca_signature_algorithms.algorithms(), 774 | &["random"] 775 | ); 776 | assert_eq!( 777 | params_tostapane.ciphers.algorithms(), 778 | &["a-manella", "blowfish",] 779 | ); 780 | assert_eq!( 781 | params_tostapane.mac.algorithms(), 782 | vec!["spyro".to_string(), "deoxys".to_string(),] 783 | ); 784 | assert_eq!( 785 | params_tostapane.proxy_jump.unwrap(), 786 | vec![ 787 | "jump1.example.com".to_string(), 788 | "jump2.example.com".to_string(), 789 | ] 790 | ); 791 | assert_eq!( 792 | params_tostapane.pubkey_accepted_algorithms.algorithms(), 793 | &["desu", "omar-crypt", "fast-omar-crypt"] 794 | ); 795 | 796 | // query 192.168.1.30 797 | let params_192_168_1_30 = config.query("192.168.1.30"); 798 | 799 | // host-specific options 800 | assert_eq!(params_192_168_1_30.user.as_deref().unwrap(), "nutellaro"); 801 | assert_eq!(params_192_168_1_30.remote_forward.unwrap(), 123); 802 | 803 | // cmdline overrides 804 | assert_eq!(params_192_168_1_30.compression.unwrap(), true); 805 | assert_eq!(params_192_168_1_30.connection_attempts.unwrap(), 10); 806 | assert_eq!( 807 | params_192_168_1_30.connect_timeout.unwrap(), 808 | Duration::from_secs(60) 809 | ); 810 | assert_eq!(params_192_168_1_30.tcp_keep_alive.unwrap(), true); 811 | 812 | // all-hosts fallback options 813 | assert_eq!( 814 | params_192_168_1_30.ca_signature_algorithms.algorithms(), 815 | &["random"] 816 | ); 817 | assert_eq!( 818 | params_192_168_1_30.ciphers.algorithms(), 819 | &["a-manella", "blowfish"] 820 | ); 821 | assert_eq!(params_192_168_1_30.mac.algorithms(), &["concorde"]); 822 | assert_eq!( 823 | params_192_168_1_30.pubkey_accepted_algorithms.algorithms(), 824 | &["desu", "omar-crypt", "fast-omar-crypt"] 825 | ); 826 | 827 | Ok(()) 828 | } 829 | 830 | #[test] 831 | fn should_allow_unknown_field() -> Result<(), SshParserError> { 832 | crate::test_log(); 833 | let temp = create_ssh_config_with_unknown_fields(); 834 | let file = File::open(temp.path()).expect("Failed to open tempfile"); 835 | let mut reader = BufReader::new(file); 836 | let _config = SshConfig::default() 837 | .default_algorithms(DefaultAlgorithms::empty()) 838 | .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)?; 839 | 840 | Ok(()) 841 | } 842 | 843 | #[test] 844 | fn should_not_allow_unknown_field() { 845 | crate::test_log(); 846 | let temp = create_ssh_config_with_unknown_fields(); 847 | let file = File::open(temp.path()).expect("Failed to open tempfile"); 848 | let mut reader = BufReader::new(file); 849 | assert!(matches!( 850 | SshConfig::default() 851 | .default_algorithms(DefaultAlgorithms::empty()) 852 | .parse(&mut reader, ParseRule::STRICT) 853 | .unwrap_err(), 854 | SshParserError::UnknownField(..) 855 | )); 856 | } 857 | 858 | #[test] 859 | fn should_store_unknown_fields() { 860 | crate::test_log(); 861 | let temp = create_ssh_config_with_unknown_fields(); 862 | let file = File::open(temp.path()).expect("Failed to open tempfile"); 863 | let mut reader = BufReader::new(file); 864 | let config = SshConfig::default() 865 | .default_algorithms(DefaultAlgorithms::empty()) 866 | .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS) 867 | .unwrap(); 868 | 869 | let host = config.query("cross-platform"); 870 | assert_eq!( 871 | host.ignored_fields.get("Piropero").unwrap(), 872 | &vec![String::from("yes")] 873 | ); 874 | } 875 | 876 | #[test] 877 | fn should_parse_inversed_ssh_config() { 878 | crate::test_log(); 879 | let temp = create_inverted_ssh_config(); 880 | let file = File::open(temp.path()).expect("Failed to open tempfile"); 881 | let mut reader = BufReader::new(file); 882 | let config = SshConfig::default() 883 | .default_algorithms(DefaultAlgorithms::empty()) 884 | .parse(&mut reader, ParseRule::STRICT) 885 | .unwrap(); 886 | 887 | let home_dir = dirs::home_dir() 888 | .unwrap_or_else(|| PathBuf::from("~")) 889 | .to_string_lossy() 890 | .to_string(); 891 | 892 | let remote_host = config.query("remote-host"); 893 | 894 | // From `*-host` 895 | assert_eq!( 896 | remote_host.identity_file.unwrap()[0].as_path(), 897 | Path::new(format!("{home_dir}/.ssh/id_rsa_good").as_str()) // because it's the first in the file 898 | ); 899 | 900 | // From `remote-*` 901 | assert_eq!(remote_host.host_name.unwrap(), "hostname.com"); 902 | assert_eq!(remote_host.user.unwrap(), "user"); 903 | 904 | // From `*` 905 | assert_eq!( 906 | remote_host.connect_timeout.unwrap(), 907 | Duration::from_secs(15) 908 | ); 909 | } 910 | 911 | #[test] 912 | fn should_parse_configuration_with_hosts() { 913 | crate::test_log(); 914 | let temp = create_ssh_config_with_comments(); 915 | 916 | let file = File::open(temp.path()).expect("Failed to open tempfile"); 917 | let mut reader = BufReader::new(file); 918 | let config = SshConfig::default() 919 | .default_algorithms(DefaultAlgorithms::empty()) 920 | .parse(&mut reader, ParseRule::STRICT) 921 | .unwrap(); 922 | 923 | let hostname = config.query("cross-platform").host_name.unwrap(); 924 | assert_eq!(&hostname, "hostname.com"); 925 | 926 | assert!(config.query("this").host_name.is_none()); 927 | } 928 | 929 | #[test] 930 | fn should_update_host_bind_address() -> Result<(), SshParserError> { 931 | crate::test_log(); 932 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 933 | SshConfigParser::update_host( 934 | Field::BindAddress, 935 | vec![String::from("127.0.0.1")], 936 | &mut host, 937 | ParseRule::ALLOW_UNKNOWN_FIELDS, 938 | &DefaultAlgorithms::empty(), 939 | )?; 940 | assert_eq!(host.params.bind_address.as_deref().unwrap(), "127.0.0.1"); 941 | Ok(()) 942 | } 943 | 944 | #[test] 945 | fn should_update_host_bind_interface() -> Result<(), SshParserError> { 946 | crate::test_log(); 947 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 948 | SshConfigParser::update_host( 949 | Field::BindInterface, 950 | vec![String::from("aaa")], 951 | &mut host, 952 | ParseRule::ALLOW_UNKNOWN_FIELDS, 953 | &DefaultAlgorithms::empty(), 954 | )?; 955 | assert_eq!(host.params.bind_interface.as_deref().unwrap(), "aaa"); 956 | Ok(()) 957 | } 958 | 959 | #[test] 960 | fn should_update_host_ca_signature_algos() -> Result<(), SshParserError> { 961 | crate::test_log(); 962 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 963 | SshConfigParser::update_host( 964 | Field::CaSignatureAlgorithms, 965 | vec![String::from("a,b,c")], 966 | &mut host, 967 | ParseRule::ALLOW_UNKNOWN_FIELDS, 968 | &DefaultAlgorithms::empty(), 969 | )?; 970 | assert_eq!( 971 | host.params.ca_signature_algorithms.algorithms(), 972 | &["a", "b", "c"] 973 | ); 974 | Ok(()) 975 | } 976 | 977 | #[test] 978 | fn should_update_host_certificate_file() -> Result<(), SshParserError> { 979 | crate::test_log(); 980 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 981 | SshConfigParser::update_host( 982 | Field::CertificateFile, 983 | vec![String::from("/tmp/a.crt")], 984 | &mut host, 985 | ParseRule::ALLOW_UNKNOWN_FIELDS, 986 | &DefaultAlgorithms::empty(), 987 | )?; 988 | assert_eq!( 989 | host.params.certificate_file.as_deref().unwrap(), 990 | Path::new("/tmp/a.crt") 991 | ); 992 | Ok(()) 993 | } 994 | 995 | #[test] 996 | fn should_update_host_ciphers() -> Result<(), SshParserError> { 997 | crate::test_log(); 998 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 999 | SshConfigParser::update_host( 1000 | Field::Ciphers, 1001 | vec![String::from("a,b,c")], 1002 | &mut host, 1003 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1004 | &DefaultAlgorithms::empty(), 1005 | )?; 1006 | assert_eq!(host.params.ciphers.algorithms(), &["a", "b", "c"]); 1007 | Ok(()) 1008 | } 1009 | 1010 | #[test] 1011 | fn should_update_host_compression() -> Result<(), SshParserError> { 1012 | crate::test_log(); 1013 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1014 | SshConfigParser::update_host( 1015 | Field::Compression, 1016 | vec![String::from("yes")], 1017 | &mut host, 1018 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1019 | &DefaultAlgorithms::empty(), 1020 | )?; 1021 | assert_eq!(host.params.compression.unwrap(), true); 1022 | Ok(()) 1023 | } 1024 | 1025 | #[test] 1026 | fn should_update_host_connection_attempts() -> Result<(), SshParserError> { 1027 | crate::test_log(); 1028 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1029 | SshConfigParser::update_host( 1030 | Field::ConnectionAttempts, 1031 | vec![String::from("4")], 1032 | &mut host, 1033 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1034 | &DefaultAlgorithms::empty(), 1035 | )?; 1036 | assert_eq!(host.params.connection_attempts.unwrap(), 4); 1037 | Ok(()) 1038 | } 1039 | 1040 | #[test] 1041 | fn should_update_host_connection_timeout() -> Result<(), SshParserError> { 1042 | crate::test_log(); 1043 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1044 | SshConfigParser::update_host( 1045 | Field::ConnectTimeout, 1046 | vec![String::from("10")], 1047 | &mut host, 1048 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1049 | &DefaultAlgorithms::empty(), 1050 | )?; 1051 | assert_eq!( 1052 | host.params.connect_timeout.unwrap(), 1053 | Duration::from_secs(10) 1054 | ); 1055 | Ok(()) 1056 | } 1057 | 1058 | #[test] 1059 | fn should_update_host_key_algorithms() -> Result<(), SshParserError> { 1060 | crate::test_log(); 1061 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1062 | SshConfigParser::update_host( 1063 | Field::HostKeyAlgorithms, 1064 | vec![String::from("a,b,c")], 1065 | &mut host, 1066 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1067 | &DefaultAlgorithms::empty(), 1068 | )?; 1069 | assert_eq!( 1070 | host.params.host_key_algorithms.algorithms(), 1071 | &["a", "b", "c"] 1072 | ); 1073 | Ok(()) 1074 | } 1075 | 1076 | #[test] 1077 | fn should_update_host_host_name() -> Result<(), SshParserError> { 1078 | crate::test_log(); 1079 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1080 | SshConfigParser::update_host( 1081 | Field::HostName, 1082 | vec![String::from("192.168.1.1")], 1083 | &mut host, 1084 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1085 | &DefaultAlgorithms::empty(), 1086 | )?; 1087 | assert_eq!(host.params.host_name.as_deref().unwrap(), "192.168.1.1"); 1088 | Ok(()) 1089 | } 1090 | 1091 | #[test] 1092 | fn should_update_host_ignore_unknown() -> Result<(), SshParserError> { 1093 | crate::test_log(); 1094 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1095 | SshConfigParser::update_host( 1096 | Field::IgnoreUnknown, 1097 | vec![String::from("a,b,c")], 1098 | &mut host, 1099 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1100 | &DefaultAlgorithms::empty(), 1101 | )?; 1102 | assert_eq!( 1103 | host.params.ignore_unknown.as_deref().unwrap(), 1104 | &["a", "b", "c"] 1105 | ); 1106 | Ok(()) 1107 | } 1108 | 1109 | #[test] 1110 | fn should_update_kex_algorithms() -> Result<(), SshParserError> { 1111 | crate::test_log(); 1112 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1113 | SshConfigParser::update_host( 1114 | Field::KexAlgorithms, 1115 | vec![String::from("a,b,c")], 1116 | &mut host, 1117 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1118 | &DefaultAlgorithms::empty(), 1119 | )?; 1120 | assert_eq!(host.params.kex_algorithms.algorithms(), &["a", "b", "c"]); 1121 | Ok(()) 1122 | } 1123 | 1124 | #[test] 1125 | fn should_update_host_mac() -> Result<(), SshParserError> { 1126 | crate::test_log(); 1127 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1128 | SshConfigParser::update_host( 1129 | Field::Mac, 1130 | vec![String::from("a,b,c")], 1131 | &mut host, 1132 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1133 | &DefaultAlgorithms::empty(), 1134 | )?; 1135 | assert_eq!(host.params.mac.algorithms(), &["a", "b", "c"]); 1136 | Ok(()) 1137 | } 1138 | 1139 | #[test] 1140 | fn should_update_host_port() -> Result<(), SshParserError> { 1141 | crate::test_log(); 1142 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1143 | SshConfigParser::update_host( 1144 | Field::Port, 1145 | vec![String::from("2222")], 1146 | &mut host, 1147 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1148 | &DefaultAlgorithms::empty(), 1149 | )?; 1150 | assert_eq!(host.params.port.unwrap(), 2222); 1151 | Ok(()) 1152 | } 1153 | 1154 | #[test] 1155 | fn should_update_host_pubkey_accepted_algos() -> Result<(), SshParserError> { 1156 | crate::test_log(); 1157 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1158 | SshConfigParser::update_host( 1159 | Field::PubkeyAcceptedAlgorithms, 1160 | vec![String::from("a,b,c")], 1161 | &mut host, 1162 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1163 | &DefaultAlgorithms::empty(), 1164 | )?; 1165 | assert_eq!( 1166 | host.params.pubkey_accepted_algorithms.algorithms(), 1167 | &["a", "b", "c"] 1168 | ); 1169 | Ok(()) 1170 | } 1171 | 1172 | #[test] 1173 | fn should_update_host_pubkey_authentication() -> Result<(), SshParserError> { 1174 | crate::test_log(); 1175 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1176 | SshConfigParser::update_host( 1177 | Field::PubkeyAuthentication, 1178 | vec![String::from("yes")], 1179 | &mut host, 1180 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1181 | &DefaultAlgorithms::empty(), 1182 | )?; 1183 | assert_eq!(host.params.pubkey_authentication.unwrap(), true); 1184 | Ok(()) 1185 | } 1186 | 1187 | #[test] 1188 | fn should_update_host_remote_forward() -> Result<(), SshParserError> { 1189 | crate::test_log(); 1190 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1191 | SshConfigParser::update_host( 1192 | Field::RemoteForward, 1193 | vec![String::from("3005")], 1194 | &mut host, 1195 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1196 | &DefaultAlgorithms::empty(), 1197 | )?; 1198 | assert_eq!(host.params.remote_forward.unwrap(), 3005); 1199 | Ok(()) 1200 | } 1201 | 1202 | #[test] 1203 | fn should_update_host_server_alive_interval() -> Result<(), SshParserError> { 1204 | crate::test_log(); 1205 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1206 | SshConfigParser::update_host( 1207 | Field::ServerAliveInterval, 1208 | vec![String::from("40")], 1209 | &mut host, 1210 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1211 | &DefaultAlgorithms::empty(), 1212 | )?; 1213 | assert_eq!( 1214 | host.params.server_alive_interval.unwrap(), 1215 | Duration::from_secs(40) 1216 | ); 1217 | Ok(()) 1218 | } 1219 | 1220 | #[test] 1221 | fn should_update_host_tcp_keep_alive() -> Result<(), SshParserError> { 1222 | crate::test_log(); 1223 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1224 | SshConfigParser::update_host( 1225 | Field::TcpKeepAlive, 1226 | vec![String::from("no")], 1227 | &mut host, 1228 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1229 | &DefaultAlgorithms::empty(), 1230 | )?; 1231 | assert_eq!(host.params.tcp_keep_alive.unwrap(), false); 1232 | Ok(()) 1233 | } 1234 | 1235 | #[test] 1236 | fn should_update_host_user() -> Result<(), SshParserError> { 1237 | crate::test_log(); 1238 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1239 | SshConfigParser::update_host( 1240 | Field::User, 1241 | vec![String::from("pippo")], 1242 | &mut host, 1243 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1244 | &DefaultAlgorithms::empty(), 1245 | )?; 1246 | assert_eq!(host.params.user.as_deref().unwrap(), "pippo"); 1247 | Ok(()) 1248 | } 1249 | 1250 | #[test] 1251 | fn should_not_update_host_if_unknown() -> Result<(), SshParserError> { 1252 | crate::test_log(); 1253 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1254 | let result = SshConfigParser::update_host( 1255 | Field::PasswordAuthentication, 1256 | vec![String::from("yes")], 1257 | &mut host, 1258 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1259 | &DefaultAlgorithms::empty(), 1260 | ); 1261 | 1262 | match result { 1263 | Ok(_) | Err(SshParserError::UnsupportedField(_, _)) => Ok(()), 1264 | Err(e) => Err(e), 1265 | }?; 1266 | 1267 | assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty())); 1268 | Ok(()) 1269 | } 1270 | 1271 | #[test] 1272 | fn should_update_host_if_unsupported() -> Result<(), SshParserError> { 1273 | crate::test_log(); 1274 | let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty())); 1275 | let result = SshConfigParser::update_host( 1276 | Field::PasswordAuthentication, 1277 | vec![String::from("yes")], 1278 | &mut host, 1279 | ParseRule::ALLOW_UNKNOWN_FIELDS, 1280 | &DefaultAlgorithms::empty(), 1281 | ); 1282 | 1283 | match result { 1284 | Err(SshParserError::UnsupportedField(field, _)) => { 1285 | assert_eq!(field, "passwordauthentication"); 1286 | Ok(()) 1287 | } 1288 | Ok(_) => Ok(()), 1289 | Err(e) => Err(e), 1290 | }?; 1291 | 1292 | assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty())); 1293 | Ok(()) 1294 | } 1295 | 1296 | #[test] 1297 | fn should_tokenize_line() -> Result<(), SshParserError> { 1298 | crate::test_log(); 1299 | assert_eq!( 1300 | SshConfigParser::tokenize_line("HostName 192.168.*.* 172.26.*.*")?, 1301 | ( 1302 | Field::HostName, 1303 | vec![String::from("192.168.*.*"), String::from("172.26.*.*")] 1304 | ) 1305 | ); 1306 | // Tokenize line with spaces 1307 | assert_eq!( 1308 | SshConfigParser::tokenize_line( 1309 | " HostName 192.168.*.* 172.26.*.* " 1310 | )?, 1311 | ( 1312 | Field::HostName, 1313 | vec![String::from("192.168.*.*"), String::from("172.26.*.*")] 1314 | ) 1315 | ); 1316 | Ok(()) 1317 | } 1318 | 1319 | #[test] 1320 | fn should_not_tokenize_line() { 1321 | crate::test_log(); 1322 | assert!(matches!( 1323 | SshConfigParser::tokenize_line("Omar yes").unwrap_err(), 1324 | SshParserError::UnknownField(..) 1325 | )); 1326 | } 1327 | 1328 | #[test] 1329 | fn should_fail_parsing_field() { 1330 | crate::test_log(); 1331 | 1332 | assert!(matches!( 1333 | SshConfigParser::tokenize_line(" ").unwrap_err(), 1334 | SshParserError::MissingArgument 1335 | )); 1336 | } 1337 | 1338 | #[test] 1339 | fn should_parse_boolean() -> Result<(), SshParserError> { 1340 | crate::test_log(); 1341 | assert_eq!( 1342 | SshConfigParser::parse_boolean(vec![String::from("yes")])?, 1343 | true 1344 | ); 1345 | assert_eq!( 1346 | SshConfigParser::parse_boolean(vec![String::from("no")])?, 1347 | false 1348 | ); 1349 | Ok(()) 1350 | } 1351 | 1352 | #[test] 1353 | fn should_fail_parsing_boolean() { 1354 | crate::test_log(); 1355 | assert!(matches!( 1356 | SshConfigParser::parse_boolean(vec!["boh".to_string()]).unwrap_err(), 1357 | SshParserError::ExpectedBoolean 1358 | )); 1359 | assert!(matches!( 1360 | SshConfigParser::parse_boolean(vec![]).unwrap_err(), 1361 | SshParserError::MissingArgument 1362 | )); 1363 | } 1364 | 1365 | #[test] 1366 | fn should_parse_algos() -> Result<(), SshParserError> { 1367 | crate::test_log(); 1368 | assert_eq!( 1369 | SshConfigParser::parse_algos(vec![String::from("a,b,c,d")])?, 1370 | AlgorithmsRule::Set(vec![ 1371 | "a".to_string(), 1372 | "b".to_string(), 1373 | "c".to_string(), 1374 | "d".to_string(), 1375 | ]) 1376 | ); 1377 | 1378 | assert_eq!( 1379 | SshConfigParser::parse_algos(vec![String::from("a")])?, 1380 | AlgorithmsRule::Set(vec!["a".to_string()]) 1381 | ); 1382 | 1383 | assert_eq!( 1384 | SshConfigParser::parse_algos(vec![String::from("+a,b")])?, 1385 | AlgorithmsRule::Append(vec!["a".to_string(), "b".to_string()]) 1386 | ); 1387 | 1388 | Ok(()) 1389 | } 1390 | 1391 | #[test] 1392 | fn should_parse_comma_separated_list() -> Result<(), SshParserError> { 1393 | crate::test_log(); 1394 | assert_eq!( 1395 | SshConfigParser::parse_comma_separated_list(vec![String::from("a,b,c,d")])?, 1396 | vec![ 1397 | "a".to_string(), 1398 | "b".to_string(), 1399 | "c".to_string(), 1400 | "d".to_string(), 1401 | ] 1402 | ); 1403 | assert_eq!( 1404 | SshConfigParser::parse_comma_separated_list(vec![String::from("a")])?, 1405 | vec!["a".to_string()] 1406 | ); 1407 | Ok(()) 1408 | } 1409 | 1410 | #[test] 1411 | fn should_fail_parsing_comma_separated_list() { 1412 | crate::test_log(); 1413 | assert!(matches!( 1414 | SshConfigParser::parse_comma_separated_list(vec![]).unwrap_err(), 1415 | SshParserError::MissingArgument 1416 | )); 1417 | } 1418 | 1419 | #[test] 1420 | fn should_parse_duration() -> Result<(), SshParserError> { 1421 | crate::test_log(); 1422 | assert_eq!( 1423 | SshConfigParser::parse_duration(vec![String::from("60")])?, 1424 | Duration::from_secs(60) 1425 | ); 1426 | Ok(()) 1427 | } 1428 | 1429 | #[test] 1430 | fn should_fail_parsing_duration() { 1431 | crate::test_log(); 1432 | assert!(matches!( 1433 | SshConfigParser::parse_duration(vec![String::from("AAA")]).unwrap_err(), 1434 | SshParserError::ExpectedUnsigned 1435 | )); 1436 | assert!(matches!( 1437 | SshConfigParser::parse_duration(vec![]).unwrap_err(), 1438 | SshParserError::MissingArgument 1439 | )); 1440 | } 1441 | 1442 | #[test] 1443 | fn should_parse_host() -> Result<(), SshParserError> { 1444 | crate::test_log(); 1445 | assert_eq!( 1446 | SshConfigParser::parse_host(vec![ 1447 | String::from("192.168.*.*"), 1448 | String::from("!192.168.1.1"), 1449 | String::from("172.26.104.*"), 1450 | String::from("!172.26.104.10"), 1451 | ])?, 1452 | vec![ 1453 | HostClause::new(String::from("192.168.*.*"), false), 1454 | HostClause::new(String::from("192.168.1.1"), true), 1455 | HostClause::new(String::from("172.26.104.*"), false), 1456 | HostClause::new(String::from("172.26.104.10"), true), 1457 | ] 1458 | ); 1459 | Ok(()) 1460 | } 1461 | 1462 | #[test] 1463 | fn should_fail_parsing_host() { 1464 | crate::test_log(); 1465 | assert!(matches!( 1466 | SshConfigParser::parse_host(vec![]).unwrap_err(), 1467 | SshParserError::MissingArgument 1468 | )); 1469 | } 1470 | 1471 | #[test] 1472 | fn should_parse_path() -> Result<(), SshParserError> { 1473 | crate::test_log(); 1474 | assert_eq!( 1475 | SshConfigParser::parse_path(vec![String::from("/tmp/a.txt")])?, 1476 | PathBuf::from("/tmp/a.txt") 1477 | ); 1478 | Ok(()) 1479 | } 1480 | 1481 | #[test] 1482 | fn should_parse_path_and_resolve_tilde() -> Result<(), SshParserError> { 1483 | crate::test_log(); 1484 | let mut expected = dirs::home_dir().unwrap(); 1485 | expected.push(".ssh/id_dsa"); 1486 | assert_eq!( 1487 | SshConfigParser::parse_path(vec![String::from("~/.ssh/id_dsa")])?, 1488 | expected 1489 | ); 1490 | Ok(()) 1491 | } 1492 | 1493 | #[test] 1494 | fn should_parse_path_list() -> Result<(), SshParserError> { 1495 | crate::test_log(); 1496 | assert_eq!( 1497 | SshConfigParser::parse_path_list(vec![ 1498 | String::from("/tmp/a.txt"), 1499 | String::from("/tmp/b.txt") 1500 | ])?, 1501 | vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")] 1502 | ); 1503 | Ok(()) 1504 | } 1505 | 1506 | #[test] 1507 | fn should_fail_parse_path_list() { 1508 | crate::test_log(); 1509 | assert!(matches!( 1510 | SshConfigParser::parse_path_list(vec![]).unwrap_err(), 1511 | SshParserError::MissingArgument 1512 | )); 1513 | } 1514 | 1515 | #[test] 1516 | fn should_fail_parsing_path() { 1517 | crate::test_log(); 1518 | assert!(matches!( 1519 | SshConfigParser::parse_path(vec![]).unwrap_err(), 1520 | SshParserError::MissingArgument 1521 | )); 1522 | } 1523 | 1524 | #[test] 1525 | fn should_parse_port() -> Result<(), SshParserError> { 1526 | crate::test_log(); 1527 | assert_eq!(SshConfigParser::parse_port(vec![String::from("22")])?, 22); 1528 | Ok(()) 1529 | } 1530 | 1531 | #[test] 1532 | fn should_fail_parsing_port() { 1533 | crate::test_log(); 1534 | assert!(matches!( 1535 | SshConfigParser::parse_port(vec![String::from("1234567")]).unwrap_err(), 1536 | SshParserError::ExpectedPort 1537 | )); 1538 | assert!(matches!( 1539 | SshConfigParser::parse_port(vec![]).unwrap_err(), 1540 | SshParserError::MissingArgument 1541 | )); 1542 | } 1543 | 1544 | #[test] 1545 | fn should_parse_string() -> Result<(), SshParserError> { 1546 | crate::test_log(); 1547 | assert_eq!( 1548 | SshConfigParser::parse_string(vec![String::from("foobar")])?, 1549 | String::from("foobar") 1550 | ); 1551 | Ok(()) 1552 | } 1553 | 1554 | #[test] 1555 | fn should_fail_parsing_string() { 1556 | crate::test_log(); 1557 | assert!(matches!( 1558 | SshConfigParser::parse_string(vec![]).unwrap_err(), 1559 | SshParserError::MissingArgument 1560 | )); 1561 | } 1562 | 1563 | #[test] 1564 | fn should_parse_unsigned() -> Result<(), SshParserError> { 1565 | crate::test_log(); 1566 | assert_eq!( 1567 | SshConfigParser::parse_unsigned(vec![String::from("43")])?, 1568 | 43 1569 | ); 1570 | Ok(()) 1571 | } 1572 | 1573 | #[test] 1574 | fn should_fail_parsing_unsigned() { 1575 | crate::test_log(); 1576 | assert!(matches!( 1577 | SshConfigParser::parse_unsigned(vec![String::from("abc")]).unwrap_err(), 1578 | SshParserError::ExpectedUnsigned 1579 | )); 1580 | assert!(matches!( 1581 | SshConfigParser::parse_unsigned(vec![]).unwrap_err(), 1582 | SshParserError::MissingArgument 1583 | )); 1584 | } 1585 | 1586 | #[test] 1587 | fn should_strip_comments() { 1588 | crate::test_log(); 1589 | 1590 | assert_eq!( 1591 | SshConfigParser::strip_comments("host my_host # this is my fav host").as_str(), 1592 | "host my_host " 1593 | ); 1594 | assert_eq!( 1595 | SshConfigParser::strip_comments("# this is a comment").as_str(), 1596 | "" 1597 | ); 1598 | } 1599 | 1600 | #[test] 1601 | fn test_should_parse_config_with_quotes_and_eq() { 1602 | crate::test_log(); 1603 | 1604 | let config = create_ssh_config_with_quotes_and_eq(); 1605 | let file = File::open(config.path()).expect("Failed to open tempfile"); 1606 | let mut reader = BufReader::new(file); 1607 | 1608 | let config = SshConfig::default() 1609 | .default_algorithms(DefaultAlgorithms::empty()) 1610 | .parse(&mut reader, ParseRule::STRICT) 1611 | .expect("Failed to parse config"); 1612 | 1613 | let params = config.query("foo"); 1614 | 1615 | // connect timeout is 15 1616 | assert_eq!( 1617 | params.connect_timeout.expect("unspec connect timeout"), 1618 | Duration::from_secs(15) 1619 | ); 1620 | assert_eq!( 1621 | params 1622 | .ignore_unknown 1623 | .as_deref() 1624 | .expect("unspec ignore unknown"), 1625 | &["Pippo", "Pluto"] 1626 | ); 1627 | assert_eq!( 1628 | params 1629 | .ciphers 1630 | .algorithms() 1631 | .iter() 1632 | .map(|x| x.as_str()) 1633 | .collect::>(), 1634 | &["Pepperoni Pizza", "Margherita Pizza", "Hawaiian Pizza"] 1635 | ); 1636 | assert_eq!( 1637 | params 1638 | .mac 1639 | .algorithms() 1640 | .iter() 1641 | .map(|x| x.as_str()) 1642 | .collect::>(), 1643 | &["Pasta Carbonara", "Pasta con tonno"] 1644 | ); 1645 | } 1646 | 1647 | #[test] 1648 | fn test_should_resolve_absolute_include_path() { 1649 | crate::test_log(); 1650 | 1651 | let expected = PathBuf::from("/tmp/config.local"); 1652 | 1653 | let s = "/tmp/config.local"; 1654 | let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s)); 1655 | assert_eq!(resolved, expected); 1656 | } 1657 | 1658 | #[test] 1659 | fn test_should_resolve_relative_include_path() { 1660 | crate::test_log(); 1661 | 1662 | let expected = dirs::home_dir() 1663 | .unwrap_or_else(|| PathBuf::from("~")) 1664 | .join(".ssh") 1665 | .join("config.local"); 1666 | 1667 | let s = "config.local"; 1668 | let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s)); 1669 | assert_eq!(resolved, expected); 1670 | } 1671 | 1672 | fn create_ssh_config_with_quotes_and_eq() -> NamedTempFile { 1673 | let mut tmpfile: tempfile::NamedTempFile = 1674 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1675 | let config = r##" 1676 | # ssh config 1677 | # written by veeso 1678 | 1679 | 1680 | # I put a comment here just to annoy 1681 | 1682 | IgnoreUnknown=Pippo,Pluto 1683 | ConnectTimeout = 15 1684 | Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza" 1685 | Macs="Pasta Carbonara,Pasta con tonno" 1686 | "##; 1687 | tmpfile.write_all(config.as_bytes()).unwrap(); 1688 | tmpfile 1689 | } 1690 | 1691 | fn create_ssh_config() -> NamedTempFile { 1692 | let mut tmpfile: tempfile::NamedTempFile = 1693 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1694 | let config = r##" 1695 | # ssh config 1696 | # written by veeso 1697 | 1698 | 1699 | # I put a comment here just to annoy 1700 | 1701 | IgnoreUnknown Pippo,Pluto 1702 | 1703 | Compression yes 1704 | ConnectionAttempts 10 1705 | ConnectTimeout 60 1706 | ServerAliveInterval 40 1707 | TcpKeepAlive yes 1708 | Ciphers +a-manella,blowfish 1709 | 1710 | # Let's start defining some hosts 1711 | 1712 | Host 192.168.*.* 172.26.*.* !192.168.1.30 1713 | User omar 1714 | # ForwardX11 is actually not supported; I just want to see that it wont' fail parsing 1715 | ForwardX11 yes 1716 | BindAddress 10.8.0.10 1717 | BindInterface tun0 1718 | AddKeysToAgent yes 1719 | Ciphers +coi-piedi,cazdecan,triestin-stretto 1720 | IdentityFile /home/root/.ssh/pippo.key /home/root/.ssh/pluto.key 1721 | Macs spyro,deoxys 1722 | Port 2222 1723 | PubkeyAcceptedAlgorithms -omar-crypt 1724 | ProxyJump jump.example.com 1725 | 1726 | Host tostapane 1727 | User ciro-esposito 1728 | HostName 192.168.24.32 1729 | RemoteForward 88 1730 | Compression no 1731 | Pippo yes 1732 | Pluto 56 1733 | ProxyJump jump1.example.com,jump2.example.com 1734 | Macs +spyro,deoxys 1735 | 1736 | Host 192.168.1.30 1737 | User nutellaro 1738 | RemoteForward 123 1739 | 1740 | Host * 1741 | CaSignatureAlgorithms random 1742 | HostKeyAlgorithms luigi,mario 1743 | KexAlgorithms desu,gigi 1744 | Macs concorde 1745 | PubkeyAcceptedAlgorithms desu,omar-crypt,fast-omar-crypt 1746 | "##; 1747 | tmpfile.write_all(config.as_bytes()).unwrap(); 1748 | tmpfile 1749 | } 1750 | 1751 | fn create_inverted_ssh_config() -> NamedTempFile { 1752 | let mut tmpfile: tempfile::NamedTempFile = 1753 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1754 | let config = r##" 1755 | Host *-host 1756 | IdentityFile ~/.ssh/id_rsa_good 1757 | 1758 | Host remote-* 1759 | HostName hostname.com 1760 | User user 1761 | IdentityFile ~/.ssh/id_rsa_bad 1762 | 1763 | Host * 1764 | ConnectTimeout 15 1765 | IdentityFile ~/.ssh/id_rsa_ugly 1766 | "##; 1767 | tmpfile.write_all(config.as_bytes()).unwrap(); 1768 | tmpfile 1769 | } 1770 | 1771 | fn create_ssh_config_with_comments() -> NamedTempFile { 1772 | let mut tmpfile: tempfile::NamedTempFile = 1773 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1774 | let config = r##" 1775 | Host cross-platform # this is my fav host 1776 | HostName hostname.com 1777 | User user 1778 | IdentityFile ~/.ssh/id_rsa_good 1779 | 1780 | Host * 1781 | AddKeysToAgent yes 1782 | IdentityFile ~/.ssh/id_rsa_bad 1783 | "##; 1784 | tmpfile.write_all(config.as_bytes()).unwrap(); 1785 | tmpfile 1786 | } 1787 | 1788 | fn create_ssh_config_with_unknown_fields() -> NamedTempFile { 1789 | let mut tmpfile: tempfile::NamedTempFile = 1790 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1791 | let config = r##" 1792 | Host cross-platform # this is my fav host 1793 | HostName hostname.com 1794 | User user 1795 | IdentityFile ~/.ssh/id_rsa_good 1796 | Piropero yes 1797 | 1798 | Host * 1799 | AddKeysToAgent yes 1800 | IdentityFile ~/.ssh/id_rsa_bad 1801 | "##; 1802 | tmpfile.write_all(config.as_bytes()).unwrap(); 1803 | tmpfile 1804 | } 1805 | 1806 | #[test] 1807 | fn test_should_parse_config_with_include() { 1808 | crate::test_log(); 1809 | 1810 | let config = create_include_config(); 1811 | let file = File::open(config.config.path()).expect("Failed to open tempfile"); 1812 | let mut reader = BufReader::new(file); 1813 | 1814 | let config = SshConfig::default() 1815 | .default_algorithms(DefaultAlgorithms::empty()) 1816 | .parse(&mut reader, ParseRule::STRICT) 1817 | .expect("Failed to parse config"); 1818 | 1819 | let default_params = config.query("unknown-host"); 1820 | // verify default params 1821 | assert_eq!( 1822 | default_params.connect_timeout.unwrap(), 1823 | Duration::from_secs(60) // first read 1824 | ); 1825 | assert_eq!( 1826 | default_params.server_alive_interval.unwrap(), 1827 | Duration::from_secs(40) // first read 1828 | ); 1829 | assert_eq!(default_params.tcp_keep_alive.unwrap(), true); 1830 | assert_eq!(default_params.ciphers.algorithms().is_empty(), true); 1831 | assert_eq!( 1832 | default_params.ignore_unknown.as_deref().unwrap(), 1833 | &["Pippo", "Pluto"] 1834 | ); 1835 | assert_eq!(default_params.compression.unwrap(), true); 1836 | assert_eq!(default_params.connection_attempts.unwrap(), 10); 1837 | 1838 | // verify include 1 overwrites the default value 1839 | let glob_params = config.query("192.168.1.1"); 1840 | assert_eq!( 1841 | glob_params.connect_timeout.unwrap(), 1842 | Duration::from_secs(60) 1843 | ); 1844 | assert_eq!( 1845 | glob_params.server_alive_interval.unwrap(), 1846 | Duration::from_secs(40) // first read 1847 | ); 1848 | assert_eq!(glob_params.tcp_keep_alive.unwrap(), true); 1849 | assert_eq!(glob_params.ciphers.algorithms().is_empty(), true); 1850 | 1851 | // verify tostapane 1852 | let tostapane_params = config.query("tostapane"); 1853 | assert_eq!( 1854 | tostapane_params.connect_timeout.unwrap(), 1855 | Duration::from_secs(60) // first read 1856 | ); 1857 | assert_eq!( 1858 | tostapane_params.server_alive_interval.unwrap(), 1859 | Duration::from_secs(40) // first read 1860 | ); 1861 | assert_eq!(tostapane_params.tcp_keep_alive.unwrap(), true); 1862 | // verify ciphers 1863 | assert_eq!( 1864 | tostapane_params.ciphers.algorithms(), 1865 | &[ 1866 | "a-manella", 1867 | "blowfish", 1868 | "coi-piedi", 1869 | "cazdecan", 1870 | "triestin-stretto" 1871 | ] 1872 | ); 1873 | 1874 | // verify included host (microwave) 1875 | let microwave_params = config.query("microwave"); 1876 | assert_eq!( 1877 | microwave_params.connect_timeout.unwrap(), 1878 | Duration::from_secs(60) // (not) updated in inc4 1879 | ); 1880 | assert_eq!( 1881 | microwave_params.server_alive_interval.unwrap(), 1882 | Duration::from_secs(40) // (not) updated in inc4 1883 | ); 1884 | assert_eq!( 1885 | microwave_params.port.unwrap(), 1886 | 345 // updated in inc4 1887 | ); 1888 | assert_eq!(microwave_params.tcp_keep_alive.unwrap(), true); 1889 | assert_eq!(microwave_params.ciphers.algorithms().is_empty(), true); 1890 | assert_eq!(microwave_params.user.as_deref().unwrap(), "mario-rossi"); 1891 | assert_eq!( 1892 | microwave_params.host_name.as_deref().unwrap(), 1893 | "192.168.24.33" 1894 | ); 1895 | assert_eq!(microwave_params.remote_forward.unwrap(), 88); 1896 | assert_eq!(microwave_params.compression.unwrap(), true); 1897 | 1898 | // verify included host (fridge) 1899 | let fridge_params = config.query("fridge"); 1900 | assert_eq!( 1901 | fridge_params.connect_timeout.unwrap(), 1902 | Duration::from_secs(60) 1903 | ); // default 1904 | assert_eq!( 1905 | fridge_params.server_alive_interval.unwrap(), 1906 | Duration::from_secs(40) 1907 | ); // default 1908 | assert_eq!(fridge_params.tcp_keep_alive.unwrap(), true); 1909 | assert_eq!(fridge_params.ciphers.algorithms().is_empty(), true); 1910 | assert_eq!(fridge_params.user.as_deref().unwrap(), "luigi-verdi"); 1911 | assert_eq!(fridge_params.host_name.as_deref().unwrap(), "192.168.24.34"); 1912 | } 1913 | 1914 | #[allow(dead_code)] 1915 | struct ConfigWithInclude { 1916 | config: NamedTempFile, 1917 | inc1: NamedTempFile, 1918 | inc2: NamedTempFile, 1919 | inc3: NamedTempFile, 1920 | inc4: NamedTempFile, 1921 | } 1922 | 1923 | fn create_include_config() -> ConfigWithInclude { 1924 | let mut config_file: tempfile::NamedTempFile = 1925 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1926 | let mut inc1_file: tempfile::NamedTempFile = 1927 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1928 | let mut inc2_file: tempfile::NamedTempFile = 1929 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1930 | let mut inc3_file: tempfile::NamedTempFile = 1931 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1932 | let mut inc4_file: tempfile::NamedTempFile = 1933 | tempfile::NamedTempFile::new().expect("Failed to create tempfile"); 1934 | 1935 | let config = format!( 1936 | r##" 1937 | # ssh config 1938 | # written by veeso 1939 | 1940 | 1941 | # I put a comment here just to annoy 1942 | 1943 | IgnoreUnknown Pippo,Pluto 1944 | 1945 | Compression yes 1946 | ConnectionAttempts 10 1947 | ConnectTimeout 60 1948 | ServerAliveInterval 40 1949 | Include {inc1} 1950 | 1951 | # Let's start defining some hosts 1952 | 1953 | Host tostapane 1954 | User ciro-esposito 1955 | HostName 192.168.24.32 1956 | RemoteForward 88 1957 | Compression no 1958 | # Ignore unknown fields should be inherited from the global section 1959 | Pippo yes 1960 | Pluto 56 1961 | Include {inc2} 1962 | 1963 | Include {inc3} 1964 | Include {inc4} 1965 | "##, 1966 | inc1 = inc1_file.path().display(), 1967 | inc2 = inc2_file.path().display(), 1968 | inc3 = inc3_file.path().display(), 1969 | inc4 = inc4_file.path().display(), 1970 | ); 1971 | config_file.write_all(config.as_bytes()).unwrap(); 1972 | 1973 | // write include 1 1974 | let inc1 = r##" 1975 | ConnectTimeout 60 1976 | ServerAliveInterval 60 1977 | TcpKeepAlive yes 1978 | "##; 1979 | inc1_file.write_all(inc1.as_bytes()).unwrap(); 1980 | 1981 | // write include 2 1982 | let inc2 = r##" 1983 | ConnectTimeout 180 1984 | ServerAliveInterval 180 1985 | Ciphers +a-manella,blowfish,coi-piedi,cazdecan,triestin-stretto 1986 | "##; 1987 | inc2_file.write_all(inc2.as_bytes()).unwrap(); 1988 | 1989 | // write include 3 with host directive 1990 | let inc3 = r##" 1991 | Host microwave 1992 | User mario-rossi 1993 | HostName 192.168.24.33 1994 | RemoteForward 88 1995 | Compression no 1996 | # Ignore unknown fields should be inherited from the global section 1997 | Pippo yes 1998 | Pluto 56 1999 | "##; 2000 | inc3_file.write_all(inc3.as_bytes()).unwrap(); 2001 | 2002 | // write include 4 which updates a param from microwave and then create a new host 2003 | let inc4 = r##" 2004 | # Update microwave 2005 | ServerAliveInterval 30 2006 | Port 345 2007 | 2008 | # Force microwave update (it won't work) 2009 | Host microwave 2010 | ConnectTimeout 30 2011 | 2012 | Host fridge 2013 | User luigi-verdi 2014 | HostName 192.168.24.34 2015 | RemoteForward 88 2016 | Compression no 2017 | "##; 2018 | inc4_file.write_all(inc4.as_bytes()).unwrap(); 2019 | 2020 | ConfigWithInclude { 2021 | config: config_file, 2022 | inc1: inc1_file, 2023 | inc2: inc2_file, 2024 | inc3: inc3_file, 2025 | inc4: inc4_file, 2026 | } 2027 | } 2028 | } 2029 | --------------------------------------------------------------------------------