remotefs_ssh/ssh/
config.rs1use std::fs::File;
6use std::io::BufReader;
7use std::path::Path;
8use std::time::Duration;
9
10use remotefs::{RemoteError, RemoteErrorType, RemoteResult};
11use ssh2_config::{DefaultAlgorithms, HostParams, ParseRule, SshConfig};
12
13use super::SshOpts;
14
15pub struct Config {
17 pub params: HostParams,
18 pub host: String,
19 pub resolved_host: String,
21 pub address: String,
23 pub username: String,
24 pub connection_timeout: Duration,
25 pub connection_attempts: usize,
26}
27
28impl Config {
29 fn from_params(params: HostParams, opts: &SshOpts) -> Self {
33 Config {
34 host: opts.host.to_string(),
35 resolved_host: Self::resolve_host(¶ms, opts),
36 address: Self::resolve_address(¶ms, opts),
37 username: Self::resolve_username(¶ms, opts),
38 connection_timeout: Self::resolve_connection_timeout(¶ms, opts),
39 connection_attempts: Self::resolve_connection_attempts(¶ms),
40 params,
41 }
42 }
43
44 fn parse(p: &Path, host: &str, rules: ParseRule) -> RemoteResult<HostParams> {
46 trace!("Parsing configuration at {}", p.display());
47 let mut reader = BufReader::new(File::open(p).map_err(|e| {
48 RemoteError::new_ex(
49 RemoteErrorType::IoError,
50 format!("Could not open configuration file: {e}"),
51 )
52 })?);
53 SshConfig::default()
54 .parse(&mut reader, rules)
55 .map_err(|e| {
56 RemoteError::new_ex(
57 RemoteErrorType::IoError,
58 format!("Could not parse configuration file: {e}"),
59 )
60 })
61 .map(|x| x.query(host))
62 }
63
64 fn resolve_host(params: &HostParams, opts: &SshOpts) -> String {
66 match params.host_name.as_deref() {
68 Some(h) => h.to_string(),
69 None => opts.host.to_string(),
70 }
71 }
72
73 fn resolve_address(params: &HostParams, opts: &SshOpts) -> String {
75 let host = Self::resolve_host(params, opts);
76 let port = match opts.port {
78 None => params.port.unwrap_or(22),
79 Some(p) => p,
80 };
81 format!("{host}:{port}")
82 }
83
84 fn resolve_username(params: &HostParams, opts: &SshOpts) -> String {
89 match opts.username.as_ref() {
90 Some(u) => u.to_string(),
91 None => params.user.as_deref().unwrap_or("").to_string(),
92 }
93 }
94
95 fn resolve_connection_timeout(params: &HostParams, opts: &SshOpts) -> Duration {
97 match opts.connection_timeout {
98 Some(t) => t,
99 None => params
100 .connect_timeout
101 .unwrap_or_else(|| Duration::from_secs(30)),
102 }
103 }
104
105 fn resolve_connection_attempts(params: &HostParams) -> usize {
108 params.connection_attempts.unwrap_or(1)
109 }
110}
111
112impl TryFrom<&SshOpts> for Config {
113 type Error = RemoteError;
114
115 fn try_from(opts: &SshOpts) -> Result<Self, Self::Error> {
116 if let Some(p) = opts.config_file.as_deref() {
117 let params = Self::parse(p, opts.host.as_str(), opts.parse_rules)?;
118 Ok(Self::from_params(params, opts))
119 } else {
120 let params = HostParams::new(&DefaultAlgorithms::default());
121 Ok(Self::from_params(params, opts))
122 }
123 }
124}
125
126#[cfg(test)]
127mod test {
128
129 use pretty_assertions::{assert_eq, assert_ne};
130
131 use super::*;
132 use crate::mock::ssh as ssh_mock;
133
134 #[test]
135 fn should_init_config_from_default_ssh_opts() {
136 let opts = SshOpts::new("192.168.1.1");
137 let config = Config::try_from(&opts).ok().unwrap();
138 assert_eq!(config.connection_attempts, 1);
139 assert_eq!(config.connection_timeout, Duration::from_secs(30));
140 assert_eq!(config.address.as_str(), "192.168.1.1:22");
141 assert_eq!(config.host.as_str(), "192.168.1.1");
142 assert!(config.username.is_empty());
143 assert_eq!(
144 config.params,
145 HostParams::new(&DefaultAlgorithms::default())
146 );
147 }
148
149 #[test]
150 fn should_init_config_from_custom_opts() {
151 let opts = SshOpts::new("192.168.1.1")
152 .connection_timeout(Duration::from_secs(10))
153 .port(2222)
154 .username("omar");
155 let config = Config::try_from(&opts).ok().unwrap();
156 assert_eq!(config.connection_attempts, 1);
157 assert_eq!(config.connection_timeout, Duration::from_secs(10));
158 assert_eq!(config.host.as_str(), "192.168.1.1");
159 assert_eq!(config.address.as_str(), "192.168.1.1:2222");
160 assert_eq!(config.username.as_str(), "omar");
161 assert_eq!(
162 config.params,
163 HostParams::new(&DefaultAlgorithms::default())
164 );
165 }
166
167 #[test]
168 fn should_init_config_from_file() {
169 let config_file = ssh_mock::create_ssh_config(22);
170 let opts = SshOpts::new("sftp").config_file(config_file.path(), ParseRule::STRICT);
171 let config = Config::try_from(&opts).ok().unwrap();
172 assert_eq!(config.connection_attempts, 3);
173 assert_eq!(config.connection_timeout, Duration::from_secs(60));
174 assert_eq!(config.host.as_str(), "sftp");
175 assert_eq!(config.resolved_host.as_str(), "127.0.0.1");
176 assert_eq!(config.address.as_str(), "127.0.0.1:22");
177 assert_eq!(config.username.as_str(), "sftp");
178 assert_ne!(
179 config.params,
180 HostParams::new(&DefaultAlgorithms::default())
181 );
182 }
183
184 #[test]
185 fn should_init_config_from_file_with_override() {
186 let config_file = ssh_mock::create_ssh_config(22);
187 let opts = SshOpts::new("sftp")
188 .config_file(config_file.path(), ParseRule::STRICT)
189 .connection_timeout(Duration::from_secs(10))
190 .port(22)
191 .username("omar");
192 let config = Config::try_from(&opts).ok().unwrap();
193 assert_eq!(config.connection_attempts, 3);
194 assert_eq!(config.connection_timeout, Duration::from_secs(10));
195 assert_eq!(config.host.as_str(), "sftp");
196 assert_eq!(config.resolved_host.as_str(), "127.0.0.1");
197 assert_eq!(config.address.as_str(), "127.0.0.1:22");
198 assert_eq!(config.username.as_str(), "omar");
199 assert_ne!(
200 config.params,
201 HostParams::new(&DefaultAlgorithms::default())
202 );
203 }
204}