pijul_org / thrussh

Thrussh-config, a new crate to parse .ssh/config and proxy SSH connections

By pmeunier on September 27, 2018
This patch is not signed.
9jwKUj73aZ3HFbcbQq3jMnid9WbhfR8sjeJNmVLPJ8Jyxe81GNkkNgcVC3ejFSP78rx5KPwR6GxxnpUkbwoe8toR
This patch is in the following branches:
master
In file Cargo.toml
1
2

3
4
5
6
7
8
9
[workspace]

members = [ "thrussh-keys", "thrussh", "thrussh-libsodium", "thrussh-agent" ]
members = [ "thrussh-keys", "thrussh", "thrussh-libsodium", "thrussh-agent", "thrussh-config" ]

[replace]

"thrussh-keys:0.11.1" = { path = "thrussh-keys" }
"thrussh-config:0.1.0" = { path = "thrussh-config" }
"thrussh-libsodium:0.1.2" = { path = "thrussh-libsodium" }


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version = "0.1.0"
error-chain = "0.12"
[package]
name = "thrussh-config"
description = "Utilities to parse .ssh/config files, including helpers to implement ProxyCommand in Thrussh."
version = "0.1.2"
authors = ["Pierre-√Čtienne Meunier <pe@pijul.org>"]
include = [ "Cargo.toml", "src/lib.rs", "src/proxy.rs" ]
license = "Apache-2.0"
documentation = "https://docs.rs/thrussh-config"
repository = "https://nest.pijul.com/pijul_org/thrussh"

[dependencies]
futures = "0.1"
tokio = "0.1"
thrussh = "0.20"
regex = "1.0"
lazy_static = "1.0"
log = "0.4"
dirs = "1.0"



















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#[macro_use]
extern crate error_chain;
error_chain!{
    foreign_links {
        IO(std::io::Error);
    errors {
        HostNotFound {
        NoHome {
pub fn parse_home(host: &str) -> Result<Config> {
        return Err(ErrorKind::NoHome.into())
pub fn parse_path<P:AsRef<Path>>(path: P, host: &str) -> Result<Config> {
pub fn parse(file: &str, host: &str) -> Result<Config> {
                    "user" => config.user = Some(value.trim_left().to_string()),
                    "hostname" => config.host_name = Some(value.trim_left().to_string()),
                    "port" => config.port = value.trim_left().parse().ok(),
                    "identityfile" => config.identity_file = Some(value.trim_left().to_string()),
                    "proxycommand" => config.proxy_command = Some(value.trim_left().to_string()),
                        if value.trim_left() == host {
        Err(ErrorKind::HostNotFound.into())
extern crate tokio;
extern crate futures;
extern crate thrussh;
extern crate regex;
#[macro_use]
extern crate log;
extern crate dirs;

use std::io::Read;
use std::path::Path;

#[derive(Debug)]
pub enum Error {
    IO(std::io::Error),
    HostNotFound,
    NoHome
}

impl std::convert::From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Error::IO(e)
    }
}
impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match *self {
            Error::IO(ref e) => e.fmt(f),
            Error::HostNotFound => write!(f, "Host not found"),
            Error::NoHome => write!(f, "No home directory"),
        }
    }
}
impl std::error::Error for Error {
    fn description(&self) -> &str {
        match *self {
            Error::IO(ref e) => e.description(),
            Error::HostNotFound => "Host not found",
            Error::NoHome => "No home directory",
        }
    }
    fn cause(&self) -> Option<&std::error::Error> {
        if let Error::IO(ref e) = *self {
            Some(e)
        } else {
            None
        }
    }
}

mod proxy;
pub use proxy::*;

#[derive(Debug, Default)]
pub struct Config {
    pub user: Option<String>,
    pub host_name: Option<String>,
    pub port: Option<u16>,
    pub identity_file: Option<String>,
    pub proxy_command: Option<String>,
    pub add_keys_to_agent: AddKeysToAgent,
}

impl Config {
    pub fn update_proxy_command(&mut self) {
        if let Some(ref h) = self.host_name {
            if let Some(ref mut prox) = self.proxy_command {
                *prox = prox.replace("%h", h);
            }
        }
        if let Some(ref p) = self.port {
            if let Some(ref mut prox) = self.proxy_command {
                *prox = prox.replace("%p", &format!("{}", p));
            }
        }
    }
}

pub fn parse_home(host: &str) -> Result<Config, Error> {
    let mut home = if let Some(home) = dirs::home_dir() {
        home
    } else {
        return Err(Error::NoHome)
    };
    home.push(".ssh");
    home.push("config");
    parse_path(&home, host)
}

pub fn parse_path<P:AsRef<Path>>(path: P, host: &str) -> Result<Config, Error> {
    let mut s = String::new();
    let mut b = std::fs::File::open(path)?;
    b.read_to_string(&mut s)?;
    parse(&s, host)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddKeysToAgent {
    Yes,
    Confirm,
    Ask,
    No
}

impl Default for AddKeysToAgent {
    fn default() -> Self {
        AddKeysToAgent::No
    }
}

pub fn parse(file: &str, host: &str) -> Result<Config, Error> {
    let mut config: Option<Config> = None;
    for line in file.lines() {
        let line = line.trim();
        if let Some(n) = line.find(' ') {
            let (key, value) = line.split_at(n);
            let lower = key.to_lowercase();
            if let Some(ref mut config) = config {
                match lower.as_str() {
                    "host" => break,
                    "user" => config.user = Some(value.trim_start().to_string()),
                    "hostname" => config.host_name = Some(value.trim_start().to_string()),
                    "port" => config.port = value.trim_start().parse().ok(),
                    "identityfile" => config.identity_file = Some(value.trim_start().to_string()),
                    "proxycommand" => config.proxy_command = Some(value.trim_start().to_string()),
                    "addkeystoagent" => {
                        match value.to_lowercase().as_str() {
                            "yes" => config.add_keys_to_agent = AddKeysToAgent::Yes,
                            "confirm" => config.add_keys_to_agent = AddKeysToAgent::Confirm,
                            "ask" => config.add_keys_to_agent = AddKeysToAgent::Ask,
                            _ => config.add_keys_to_agent = AddKeysToAgent::No,
                        }
                    },
                    key => {
                        debug!("{:?}", key);
                    }
                }
            } else {
                match lower.as_str() {
                    "host" => {
                        if value.trim_start() == host {
                            config = Some(Config::default())
                        }
                    }
                    _ => {}
                }
            }
        }

    }
    if let Some(config) = config {
        Ok(config)
    } else {
        Err(Error::HostNotFound)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use std;
use tokio;
use futures::{Poll, Async, Future};
use tokio::net::tcp::TcpStream;
use std::net::SocketAddr;
use std::process::{Stdio, Command};
use thrussh;
use std::io::Write;

/// A type to implement either a TCP socket, or proxying through an external command.
pub enum Stream {
    #[allow(missing_docs)]
    Child(std::process::Child),
    #[allow(missing_docs)]
    Tcp(TcpStream)
}

pub struct ConnectFuture(Option<ConnectFuture_>);
enum ConnectFuture_ {
    Tcp(tokio::net::tcp::ConnectFuture),
    Child(std::process::Child),
}

impl Future for ConnectFuture {
    type Item = Stream;
    type Error = tokio::io::Error;
    fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
        match self.0.take().unwrap() {
            ConnectFuture_::Tcp(mut tcp) => {
                match tcp.poll()? {
                    Async::Ready(tcp) => Ok(Async::Ready(Stream::Tcp(tcp))),
                    Async::NotReady => {
                        self.0 = Some(ConnectFuture_::Tcp(tcp));
                        Ok(Async::NotReady)
                    }
                }
            },
            ConnectFuture_::Child(child) => Ok(Async::Ready(Stream::Child(child)))
        }
    }
}

impl Stream {
    /// Connect a direct TCP stream (as opposed to a proxied one).
    pub fn tcp_connect(addr: &SocketAddr) -> ConnectFuture {
        ConnectFuture(Some(ConnectFuture_::Tcp(tokio::net::tcp::TcpStream::connect(addr))))
    }
    /// Connect through a proxy command.
    pub fn proxy_command(cmd: &str, args: &[&str]) -> ConnectFuture {
        ConnectFuture(Some(ConnectFuture_::Child(
            Command::new(cmd)
                .stdin(Stdio::piped())
                .stdout(Stdio::piped())
                .args(args)
                .spawn().unwrap()
        )))
    }
}

impl tokio::io::Read for Stream {
    fn read(&mut self, r: &mut [u8]) -> std::io::Result<usize> {
        match *self {
            Stream::Child(ref mut c) => c.stdout.as_mut().unwrap().read(r),
            Stream::Tcp(ref mut t) => t.read(r)
        }
    }
}

impl tokio::io::AsyncWrite for Stream {
    fn shutdown(&mut self) -> Result<Async<()>, std::io::Error> {
        match *self {
            Stream::Child(ref mut c) => {
                c.stdin.take();
                Ok(Async::Ready(()))
            },
            Stream::Tcp(ref mut t) => t.shutdown()
        }
    }
    fn poll_write(&mut self, r: &[u8]) -> Result<Async<usize>, std::io::Error> {
        match *self {
            Stream::Child(ref mut c) => {
                match c.stdin.as_mut().unwrap().write(r) {
                    Ok(n) => Ok(Async::Ready(n)),
                    Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(Async::NotReady),
                    Err(e) => Err(e)
                }
            },
            Stream::Tcp(ref mut t) => t.poll_write(r)
        }
    }
    fn poll_flush(&mut self) -> Result<Async<()>, std::io::Error> {
        match *self {
            Stream::Child(ref mut c) => {
                match c.stdin.as_mut().unwrap().flush() {
                    Ok(n) => Ok(Async::Ready(n)),
                    Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(Async::NotReady),
                    Err(e) => Err(e)
                }
            },
            Stream::Tcp(ref mut t) => t.poll_flush()
        }
    }
}

impl std::io::Write for Stream {
    fn write(&mut self, r: &[u8]) -> std::io::Result<usize> {
        match *self {
            Stream::Child(ref mut c) => c.stdin.as_mut().unwrap().write(r),
            Stream::Tcp(ref mut t) => t.write(r)
        }
    }
    fn flush(&mut self) -> std::io::Result<()> {
        match *self {
            Stream::Child(ref mut c) => c.stdin.as_mut().unwrap().flush(),
            Stream::Tcp(ref mut t) => t.flush()
        }
    }
}

impl tokio::io::AsyncRead for Stream{}
impl thrussh::Tcp for Stream {

}