Coder Social home page Coder Social logo

ssh2-config's Introduction

ssh2-config

Changelog ยท Get started ยท Documentation

Developed by @veeso

Current version: 0.2.3 (05/12/2023)

License-MIT Repo stars Downloads counter Latest version Ko-fi

Build Coveralls Docs



About ssh2-config

ssh2-config a library which provides a parser for the SSH configuration file, to be used in pair with the ssh2 crate.

This library provides a method to parse the configuration file and returns the configuration parsed into a structure. The SshConfig structure provides all the attributes which can be used to configure the ssh2 Session and to resolve the host, port and username.

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.

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.

Exposed attributes

  • BindAddress: you can use this attribute to bind the socket to a certain address
  • BindInterface: you can use this attribute to bind the socket to a certain network interface
  • CASignatureAlgorithms: you can use this attribute to handle CA certificates
  • CertificateFile: you can use this attribute to parse the certificate file in case is necessary
  • 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, ...)
  • Compression: you can use this attribute to set whether compression is enabled with session.set_compress(value)
  • ConnectionAttempts: you can use this attribute to cycle over connect in order to retry
  • ConnectTimeout: you can use this attribute to set the connection timeout for the socket
  • HostName: you can use this attribute to get the real name of the host to connect to
  • IdentityFile: you can use this attribute to set the keys to try when connecting to remote host.
  • KexAlgorithms: you can use this attribute to configure Key exchange methods with session.method_pref(MethodType::Kex, algos.join(",").as_str())
  • MACs: you can use this attribute to configure the MAC algos with session.method_pref(MethodType::MacCs, algos.join(",").as_str()) and session.method_pref(MethodType::MacSc, algos.join(",").as_str())
  • Port: you can use this attribute to resolve the port to connect to
  • PubkeyAuthentication: you can use this attribute to set whether to use the pubkey authentication
  • RemoteForward: you can use this method to implement port forwarding with session.channel_forward_listen()
  • ServerAliveInterval: you can use this method to implement keep alive message interval
  • TcpKeepAlive: you can use this method to tell whether to send keep alive message
  • UseKeychain: (macos only) used to tell whether to use keychain to decrypt ssh keys
  • User: you can use this method to resolve the user to use to log in as

Missing features


Get started ๐Ÿš€

First of all, add ssh2-config to your dependencies

[dependencies]
ssh2-config = "^0.2"

then parse the configuration

use ssh2_config::{ParseRule, SshConfig};
use std::fs::File;
use std::io::BufReader;

let mut reader = BufReader::new(File::open(config_path).expect("Could not open configuration file"));
let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration");

// Query attributes for a certain host
let params = config.query("192.168.1.2");

then you can use the parsed parameters to configure the session:

use ssh2::Session;
use ssh2_config::{HostParams};

fn configure_session(session: &mut Session, params: &HostParams) {
    if let Some(compress) = params.compression {
        session.set_compress(compress);
    }
    if params.tcp_keep_alive.unwrap_or(false) && params.server_alive_interval.is_some() {
        let interval = params.server_alive_interval.unwrap().as_secs() as u32;
        session.set_keepalive(true, interval);
    }
    // algos
    if let Some(algos) = params.kex_algorithms.as_deref() {
        if let Err(err) = session.method_pref(MethodType::Kex, algos.join(",").as_str()) {
            panic!("Could not set KEX algorithms: {}", err);
        }
    }
    if let Some(algos) = params.host_key_algorithms.as_deref() {
        if let Err(err) = session.method_pref(MethodType::HostKey, algos.join(",").as_str()) {
            panic!("Could not set host key algorithms: {}", err);
        }
    }
    if let Some(algos) = params.ciphers.as_deref() {
        if let Err(err) = session.method_pref(MethodType::CryptCs, algos.join(",").as_str()) {
            panic!("Could not set crypt algorithms (client-server): {}", err);
        }
        if let Err(err) = session.method_pref(MethodType::CryptSc, algos.join(",").as_str()) {
            panic!("Could not set crypt algorithms (server-client): {}", err);
        }
    }
    if let Some(algos) = params.mac.as_deref() {
        if let Err(err) = session.method_pref(MethodType::MacCs, algos.join(",").as_str()) {
            panic!("Could not set MAC algorithms (client-server): {}", err);
        }
        if let Err(err) = session.method_pref(MethodType::MacSc, algos.join(",").as_str()) {
            panic!("Could not set MAC algorithms (server-client): {}", err);
        }
    }
}

fn auth_with_rsakey(
    session: &mut Session,
    params: &HostParams,
    username: &str,
    password: Option<&str>
) {
    for identity_file in params.identity_file.unwrap_or_default().iter() {
        if let Ok(_) = session.userauth_pubkey_file(username, None, identity_file, password) {
            break;
        } 
    }
}

Examples

You can view a working examples of an implementation of ssh2-config with ssh2 in the examples folder at client.rs.

You can run the example with

cargo run --example client -- <remote-host> [config-file-path]

Support the developer โ˜•

If you like ssh2-config and you're grateful for the work I've done, please consider a little donation ๐Ÿฅณ

You can make a donation with one of these platforms:

ko-fi PayPal bitcoin


Contributing and issues ๐Ÿค๐Ÿป

Contributions, bug reports, new features and questions are welcome! ๐Ÿ˜‰ 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.

Please follow our contributing guidelines


Changelog โณ

View ssh2-config's changelog HERE


License ๐Ÿ“ƒ

ssh2-config is licensed under the MIT license.

You can read the entire license HERE

ssh2-config's People

Contributors

dev-ardi avatar leoniephiline avatar veeso avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

ssh2-config's Issues

[QUESTION] - TITLE

bitflags! {
    /// The parsing mode
    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
    pub struct ParseRule: u8 {
        /// Don't allow any invalid field or value
        const STRICT = 0b00000000;
        /// Allow unknown field
        const ALLOW_UNKNOWN_FIELDS = 0b00000001;
    }
}

Why didn't you just use an enum?

[Feature Request] - Allow converting `SshConfig` to String

Description

I've been working on an alternative ssh-agent for a bit, and have been using your crate to parse ~/.ssh/config to allow importing OpenSSH keys into my program automatically.

I wanted to make it possible for my program to write a modified configuration when a user generates a new key using my program, and would still like to keep using your crate since I found it fairly simple and easy to use. For this, I think the first feature would be to allow writing SshConfig to a file, or even simpler, just allow a conversion to String. This would be sufficient for me, for example, to just append some template with the new key information, or, if it would be within the scope of this project, we could allow modifying the config within this crate itself for better type safety.

Implementation

Implement the .to_string() method, which will contain the raw SSH config in string format, ready to be written to a file or modified if necessary.

unlock SshConfig::parse_default_file in windows

Description

This program works just fine for me in windows

use anyhow::*;
use ssh2_config::{ParseRule, SshConfig};

fn main() -> Result<(), anyhow::Error> {
    let config = SshConfig::parse_default_file(ParseRule::STRICT)?;

    let hostname_tuple = config
        .get_hosts()
        .iter()
        .map(|x| (x.pattern[0].pattern.as_str(), x.params.host_name.as_deref()));

    dbg!(hostname_tuple.collect::<Vec<_>>());

    Ok(())
}

There's no reason to hide this API for windows users.

Too many unnecessary copies

Description

Usage of String and Vec where &str and impl Iterator would suffice.

Example:

    fn parse_string(args: Vec<String>) -> SshParserResult<String> {
        if let Some(s) = args.get(0) {
            Ok(s.to_string())
        } else {
            Err(SshParserError::MissingArgument)
        }
    }

would be the same as

    fn parse_str<'a>(mut args: impl Iterator<Item = &'a str>) -> SshParserResult<&'a str> {
        if let Some(s) = args.next() {
            Ok(s)
        } else {
            Err(SshParserError::MissingArgument)
        }
    }

Without copying any data.

[BUG] - Hosts must be sorted by parsing order, not alphabetically by pattern; merge precedence must be reversed

Description

This issue has been brought up before as #2 and was closed by #3.

In #3, host sorting was implemented, sorting by their host patterns in alphabetical order.

However, this approach to sorting host rules is incorrect.

https://linux.die.net/man/5/ssh_config states (emphasis mine):

ssh obtains configuration data from the following sources in the following order:

  1. command-line options
  2. user's configuration file (~/.ssh/config)
  3. system-wide configuration file (/etc/ssh/ssh_config)

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 the one given on the command line.

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.

This means that in the ssh2-config crate:

  • hosts should not be sorted after parsing at all (after reading & parsing the config file, the hosts are already sorted by precedence in descending order, through their order of appearance in the config file),
  • and HostParams::merge should give precedence to self, rather than to b.

Steps to reproduce

SSH example config file (taken from #2 and adjusted):

Host *-host
  IdentityFile ~/.ssh/id_rsa_good

Host remote-*
  HostName hostname.com
  User user
  IdentityFile ~/.ssh/id_rsa_bad

Host *
  AddKeysToAgent yes
  IdentityFile ~/.ssh/id_rsa_ugly

Cargo.toml:

[package]
name = "incorrect_hosts_ordering"
version = "0.1.0"
edition = "2021"

[dependencies]
ssh2-config = "0.2.2"

src/main.rs (taken from #2 and adjusted):

use ssh2_config::{ParseRule, SshConfig};
use std::io::BufReader;

fn main() {
    // Parsing ssh config
    let config_path = "config";
    let config = std::fs::File::open(config_path).unwrap();
    let mut buf_rdr_conf = BufReader::new(config);
    let ssh_config = SshConfig::default()
        .parse(&mut buf_rdr_conf, ParseRule::ALLOW_UNKNOWN_FIELDS)
        .unwrap();

    // Get settings from config
    let config = ssh_config.query("remote-host");
    let host_name = config.host_name.unwrap();
    eprintln!("host = {host_name}");
    let mut identity_paths = config.identity_file.unwrap();
    eprintln!("available identity paths = {identity_paths:#?}");

    let identity_path = identity_paths.pop().unwrap();
    eprintln!("identity path = {}", identity_path.display());
    let user_name = config.user.unwrap();
    eprintln!("user = {user_name}");
}

Actual result

id_rsa_bad is incorrectly selected, instead of id_rsa_good:

$ cargo -q run
host = hostname.com
available identity paths = [
    "/home/user/.ssh/id_rsa_bad",
]
identity path = /home/user/.ssh/id_rsa_bad
user = user

The reason is that the incorrect algorithm creates the following wrong sorting order: *, *-host, remote-*. It then iterates through the hosts in this order, overriding previous values with new values.

Correct would be to keep the correct order *-host, remote-*, *. Then iterate through the hosts in this order, filling in missing previous values with new values.

Expected behaviour

id_rsa_good should be selected, rather than id_rsa_bad:

$ cargo -q run
host = hostname.com
available identity paths = [
    "/home/user/.ssh/id_rsa_good",
]
identity path = /home/user/.ssh/id_rsa_good
user = user

Proposed solution

  1. Do not sort hosts at all.
  2. Change the HostParams::merge method: Currently it overrides params in self with params from b. Instead, it must fill in params missing (None) in self with params set (Some) in b.

This has an additional advantage:

The ssh parameter precedence is 1. CLI args, 2. ~/.ssh/config, 3. /etc/ssh/ssh_config.

By not sorting hosts and correcting the precedence in HostParams::merge, multiple config files can simply read (or concatenated before parsing) in order of decreasing precendence:

  1. Preparing a HostParams from CLI args,
  2. then filling in (not overriding) with HostParams from ~/.ssh/config,
  3. then filling in (not overriding) with HostParams from /etc/ssh/ssh_config.

[Feature Request] Expose ignored fields

Description

It would be nice to have them done, but until that's done I think it's better to have a HashMap of Key, Arguments instead of just ignoring them.

Implementation

Modify the struct so that it holds the hashmap and go through all of the Field variants. This is not too hard to do with a vim macro.

[BUG] - Sometimes an unexpected identity file path is parsed

Description

The wrong identity file path is parsed. When the wrong identity file path is present, there is a configuration in which the Host parameter is just a wildcard. If there are multiple wildcards, the last one is used when the wrong identity file path is parsed.

Steps to reproduce

Put the following in your ~/.ssh/config file

Host remote_host
  HostName hostname.com
  User user
  IdentityFile ~/.ssh/id_rsa_good

Host *
  AddKeysToAgent yes
  IdentityFile ~/.ssh/id_rsa_bad

Parse the config with the following code:

use ssh2_config::SshConfig;
use std::io::BufReader;

// Parsing ssh config
let config_path = <path to your config>;
let config = std::fs::File::open(config_path).unwrap();
let mut buf_rdr_conf = BufReader::new(config);
let ssh_config = SshConfig::default().parse(&mut buf_rdr_conf).unwrap();

// Get settings from config
let config = ssh_config.query("remote_host");
let host_name = config.host_name.unwrap();
eprintln!("host = {host_name}");
let mut identity_paths = config.identity_file.unwrap();
eprintln!("available identity paths = {identity_paths:#?}");

let identity_path = identity_paths.pop().unwrap();
eprintln!("identity path = {}", identity_path.display());
let user_name = config.user.unwrap();
eprintln!("user = {user_name}");

Note that the program prints other parameters just to be sure that all the others are correct.
Up until now, I've never had an issue with the others, but maybe that's because I don't actually use any other overlapping parameters in the wildcard configuration.

Expected behaviour

Program prints

identity path = ~/.ssh/id_rsa_good

Actual behavior

Program prints

identity path = ~/.ssh/id_rsa_bad

Environment

  • OS: MacOS Ventura
  • Architecture: Arm
  • Rust version: 1.64.0
  • remotefs version: N/A
  • Protocol used: N/A
  • Remote server version and name: N/A

Additional information

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.