1use log::debug;
2use std::io::Read;
3use std::net::ToSocketAddrs;
4use std::path::Path;
5use thiserror::*;
6
7#[derive(Debug, Error)]
8pub enum Error {
10 #[error("Host not found")]
11 HostNotFound,
12 #[error("No home directory")]
13 NoHome,
14 #[error("{}", source)]
15 Io {
16 #[from]
17 source: std::io::Error,
18 },
19}
20
21mod proxy;
22pub use proxy::*;
23
24#[derive(Debug)]
25pub struct Config {
26 pub user: String,
27 pub host_name: String,
28 pub port: u16,
29 pub identity_file: Option<String>,
30 pub proxy_command: Option<String>,
31 pub add_keys_to_agent: AddKeysToAgent,
32}
33
34impl Config {
35 pub fn default(host_name: &str) -> Self {
36 Config {
37 user: whoami::username(),
38 host_name: host_name.to_string(),
39 port: 22,
40 identity_file: None,
41 proxy_command: None,
42 add_keys_to_agent: AddKeysToAgent::default(),
43 }
44 }
45}
46
47impl Config {
48 fn update_proxy_command(&mut self) {
49 if let Some(ref mut prox) = self.proxy_command {
50 *prox = prox.replace("%h", &self.host_name);
51 *prox = prox.replace("%p", &format!("{}", self.port));
52 }
53 }
54
55 pub async fn stream(&mut self) -> Result<Stream, std::io::Error> {
56 self.update_proxy_command();
57 if let Some(ref proxy_command) = self.proxy_command {
58 let cmd: Vec<&str> = proxy_command.split(' ').collect();
59 Stream::proxy_command(cmd[0], &cmd[1..]).await
60 } else {
61 Stream::tcp_connect(
62 &(self.host_name.as_str(), self.port)
63 .to_socket_addrs()?
64 .next()
65 .unwrap(),
66 )
67 .await
68 }
69 }
70}
71
72pub fn parse_home(host: &str) -> Result<Config, Error> {
73 let mut home = if let Some(home) = dirs_next::home_dir() {
74 home
75 } else {
76 return Err(Error::NoHome);
77 };
78 home.push(".ssh");
79 home.push("config");
80 parse_path(&home, host)
81}
82
83pub fn parse_path<P: AsRef<Path>>(path: P, host: &str) -> Result<Config, Error> {
84 let mut s = String::new();
85 let mut b = std::fs::File::open(path)?;
86 b.read_to_string(&mut s)?;
87 parse(&s, host)
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum AddKeysToAgent {
92 Yes,
93 Confirm,
94 Ask,
95 No,
96}
97
98impl Default for AddKeysToAgent {
99 fn default() -> Self {
100 AddKeysToAgent::No
101 }
102}
103
104pub fn parse(file: &str, host: &str) -> Result<Config, Error> {
105 let mut config: Option<Config> = None;
106 for line in file.lines() {
107 let line = line.trim();
108 if let Some(n) = line.find(' ') {
109 let (key, value) = line.split_at(n);
110 let lower = key.to_lowercase();
111 if let Some(ref mut config) = config {
112 match lower.as_str() {
113 "host" => break,
114 "user" => {
115 config.user.clear();
116 config.user.push_str(value.trim_start());
117 }
118 "hostname" => {
119 config.host_name.clear();
120 config.host_name.push_str(value.trim_start())
121 }
122 "port" => {
123 if let Ok(port) = value.trim_start().parse() {
124 config.port = port
125 }
126 }
127 "identityfile" => {
128 let id = value.trim_start();
129 if id.starts_with("~/") {
130 if let Some(mut home) = dirs_next::home_dir() {
131 home.push(id.split_at(2).1);
132 config.identity_file = Some(home.to_str().unwrap().to_string());
133 } else {
134 return Err(Error::NoHome);
135 }
136 } else {
137 config.identity_file = Some(id.to_string())
138 }
139 }
140 "proxycommand" => config.proxy_command = Some(value.trim_start().to_string()),
141 "addkeystoagent" => match value.to_lowercase().as_str() {
142 "yes" => config.add_keys_to_agent = AddKeysToAgent::Yes,
143 "confirm" => config.add_keys_to_agent = AddKeysToAgent::Confirm,
144 "ask" => config.add_keys_to_agent = AddKeysToAgent::Ask,
145 _ => config.add_keys_to_agent = AddKeysToAgent::No,
146 },
147 key => {
148 debug!("{:?}", key);
149 }
150 }
151 } else {
152 match lower.as_str() {
153 "host" => {
154 if value.trim_start() == host {
155 let mut c = Config::default(host);
156 c.port = 22;
157 config = Some(c)
158 }
159 }
160 _ => {}
161 }
162 }
163 }
164 }
165 if let Some(config) = config {
166 Ok(config)
167 } else {
168 Err(Error::HostNotFound)
169 }
170}