1use std::convert::TryFrom;
2use std::str::FromStr;
3use std::{fmt, io};
4
5use distant_net::common::{Destination, Host, SecretKey32};
6use serde::de::Deserializer;
7use serde::ser::Serializer;
8use serde::{Deserialize, Serialize};
9
10use crate::serde_str::{deserialize_from_str, serialize_to_str};
11
12const SCHEME: &str = "distant";
13const SCHEME_WITH_SEP: &str = "distant://";
14
15#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct DistantSingleKeyCredentials {
19 pub host: Host,
20 pub port: u16,
21 pub key: SecretKey32,
22 pub username: Option<String>,
23}
24
25impl fmt::Display for DistantSingleKeyCredentials {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 write!(f, "{SCHEME}://")?;
29
30 if let Some(username) = self.username.as_ref() {
31 write!(f, "{username}")?;
32 }
33
34 write!(f, ":{}@", self.key)?;
35
36 if self.host.is_ipv6() {
38 write!(f, "[{}]", self.host)?;
39 } else {
40 write!(f, "{}", self.host)?;
41 }
42
43 write!(f, ":{}", self.port)
44 }
45}
46
47impl FromStr for DistantSingleKeyCredentials {
48 type Err = io::Error;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 let destination: Destination = s
56 .parse()
57 .map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?;
58
59 if let Some(scheme) = destination.scheme.as_deref() {
61 if scheme != SCHEME {
62 return Err(io::Error::new(
63 io::ErrorKind::InvalidData,
64 format!("Unexpected scheme: {scheme}"),
65 ));
66 }
67 }
68
69 Ok(Self {
70 host: destination.host,
71 port: destination
72 .port
73 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing port"))?,
74 key: destination
75 .password
76 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing key"))?
77 .parse()
78 .map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?,
79 username: destination.username,
80 })
81 }
82}
83
84impl Serialize for DistantSingleKeyCredentials {
85 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
86 where
87 S: Serializer,
88 {
89 serialize_to_str(self, serializer)
90 }
91}
92
93impl<'de> Deserialize<'de> for DistantSingleKeyCredentials {
94 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95 where
96 D: Deserializer<'de>,
97 {
98 deserialize_from_str(deserializer)
99 }
100}
101
102impl DistantSingleKeyCredentials {
103 pub fn find(s: &str, strict: bool) -> Option<DistantSingleKeyCredentials> {
109 let is_boundary = |c| char::is_whitespace(c) || char::is_control(c);
110
111 for (i, _) in s.match_indices(SCHEME_WITH_SEP) {
112 let (before, s) = s.split_at(i);
114
115 if strict && !before.is_empty() && !before.ends_with(is_boundary) {
118 continue;
119 }
120
121 let s = match s.find(is_boundary) {
123 Some(i) => &s[..i],
124 None => s,
125 };
126
127 match s.parse::<Self>() {
128 Ok(this) => return Some(this),
129 Err(_) => continue,
130 }
131 }
132
133 None
134 }
135
136 #[inline]
140 pub fn find_strict(s: &str) -> Option<DistantSingleKeyCredentials> {
141 Self::find(s, true)
142 }
143
144 #[inline]
148 pub fn find_lax(s: &str) -> Option<DistantSingleKeyCredentials> {
149 Self::find(s, false)
150 }
151
152 pub fn try_to_destination(&self) -> io::Result<Destination> {
156 TryFrom::try_from(self.clone())
157 }
158}
159
160impl TryFrom<DistantSingleKeyCredentials> for Destination {
161 type Error = io::Error;
162
163 fn try_from(credentials: DistantSingleKeyCredentials) -> Result<Self, Self::Error> {
164 Ok(Destination {
165 scheme: Some("distant".to_string()),
166 username: credentials.username,
167 password: Some(credentials.key.to_string()),
168 host: credentials.host,
169 port: Some(credentials.port),
170 })
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use std::net::{Ipv4Addr, Ipv6Addr};
177
178 use once_cell::sync::Lazy;
179 use test_log::test;
180
181 use super::*;
182
183 const HOST: &str = "testhost";
184 const PORT: u16 = 12345;
185
186 const USER: &str = "testuser";
187 static KEY: Lazy<String> = Lazy::new(|| SecretKey32::default().to_string());
188
189 static CREDENTIALS_STR_NO_USER: Lazy<String> = Lazy::new(|| {
190 let key = KEY.as_str();
191 format!("distant://:{key}@{HOST}:{PORT}")
192 });
193 static CREDENTIALS_STR_USER: Lazy<String> = Lazy::new(|| {
194 let key = KEY.as_str();
195 format!("distant://{USER}:{key}@{HOST}:{PORT}")
196 });
197
198 static CREDENTIALS_NO_USER: Lazy<DistantSingleKeyCredentials> =
199 Lazy::new(|| CREDENTIALS_STR_NO_USER.parse().unwrap());
200 static CREDENTIALS_USER: Lazy<DistantSingleKeyCredentials> =
201 Lazy::new(|| CREDENTIALS_STR_USER.parse().unwrap());
202
203 #[test]
204 fn find_should_return_some_key_if_string_is_exact_match() {
205 let credentials = DistantSingleKeyCredentials::find(CREDENTIALS_STR_NO_USER.as_str(), true);
206 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
207
208 let credentials = DistantSingleKeyCredentials::find(CREDENTIALS_STR_USER.as_str(), true);
209 assert_eq!(credentials.unwrap(), *CREDENTIALS_USER);
210 }
211
212 #[test]
213 fn find_should_return_some_key_if_there_is_a_match_with_only_whitespace_on_either_side() {
214 let s = format!(" {} ", CREDENTIALS_STR_NO_USER.as_str());
215 let credentials = DistantSingleKeyCredentials::find(&s, true);
216 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
217
218 let s = format!("\r{}\r", CREDENTIALS_STR_NO_USER.as_str());
219 let credentials = DistantSingleKeyCredentials::find(&s, true);
220 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
221
222 let s = format!("\t{}\t", CREDENTIALS_STR_NO_USER.as_str());
223 let credentials = DistantSingleKeyCredentials::find(&s, true);
224 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
225
226 let s = format!("\n{}\n", CREDENTIALS_STR_NO_USER.as_str());
227 let credentials = DistantSingleKeyCredentials::find(&s, true);
228 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
229 }
230
231 #[test]
232 fn find_should_return_some_key_if_there_is_a_match_with_only_control_characters_on_either_side()
233 {
234 let s = format!("\x1b{} \x1b", CREDENTIALS_STR_NO_USER.as_str());
235 let credentials = DistantSingleKeyCredentials::find(&s, true);
236 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
237 }
238
239 #[test]
240 fn find_should_return_first_match_found_in_str() {
241 let s = format!(
242 "{} {}",
243 CREDENTIALS_STR_NO_USER.as_str(),
244 CREDENTIALS_STR_USER.as_str()
245 );
246 let credentials = DistantSingleKeyCredentials::find(&s, true);
247 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
248 }
249
250 #[test]
251 fn find_should_return_first_valid_match_found_in_str() {
252 let s = format!(
253 "a{}a {} b{}b",
254 CREDENTIALS_STR_NO_USER.as_str(),
255 CREDENTIALS_STR_NO_USER.as_str(),
256 CREDENTIALS_STR_NO_USER.as_str()
257 );
258 let credentials = DistantSingleKeyCredentials::find(&s, true);
259 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
260 }
261
262 #[test]
263 fn find_with_strict_false_should_ignore_any_character_preceding_scheme() {
264 let s = format!("a{}", CREDENTIALS_STR_NO_USER.as_str());
265 let credentials = DistantSingleKeyCredentials::find(&s, false);
266 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
267
268 let s = format!(
269 "a{} b{}",
270 CREDENTIALS_STR_NO_USER.as_str(),
271 CREDENTIALS_STR_NO_USER.as_str()
272 );
273 let credentials = DistantSingleKeyCredentials::find(&s, false);
274 assert_eq!(credentials.unwrap(), *CREDENTIALS_NO_USER);
275 }
276
277 #[test]
278 fn find_with_strict_true_should_not_find_if_non_whitespace_and_control_preceding_scheme() {
279 let s = format!("a{}", CREDENTIALS_STR_NO_USER.as_str());
280 let credentials = DistantSingleKeyCredentials::find(&s, true);
281 assert_eq!(credentials, None);
282
283 let s = format!(
284 "a{} b{}",
285 CREDENTIALS_STR_NO_USER.as_str(),
286 CREDENTIALS_STR_NO_USER.as_str()
287 );
288 let credentials = DistantSingleKeyCredentials::find(&s, true);
289 assert_eq!(credentials, None);
290 }
291
292 #[test]
293 fn find_should_return_none_if_no_match_found() {
294 let s = "abc";
295 let credentials = DistantSingleKeyCredentials::find(s, true);
296 assert_eq!(credentials, None);
297
298 let s = "abc";
299 let credentials = DistantSingleKeyCredentials::find(s, false);
300 assert_eq!(credentials, None);
301 }
302
303 #[test]
304 fn display_should_not_wrap_ipv4_address() {
305 let key = KEY.as_str();
306 let credentials = DistantSingleKeyCredentials {
307 host: Host::Ipv4(Ipv4Addr::LOCALHOST),
308 port: 12345,
309 username: None,
310 key: key.parse().unwrap(),
311 };
312
313 assert_eq!(
314 credentials.to_string(),
315 format!("{SCHEME}://:{key}@127.0.0.1:12345")
316 );
317 }
318
319 #[test]
320 fn display_should_wrap_ipv6_address_in_square_brackets() {
321 let key = KEY.as_str();
322 let credentials = DistantSingleKeyCredentials {
323 host: Host::Ipv6(Ipv6Addr::LOCALHOST),
324 port: 12345,
325 username: None,
326 key: key.parse().unwrap(),
327 };
328
329 assert_eq!(
330 credentials.to_string(),
331 format!("{SCHEME}://:{key}@[::1]:12345")
332 );
333 }
334}