distant_core/
credentials.rs

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/// Represents credentials used for a distant server that is maintaining a single key
16/// across all connections
17#[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    /// Converts credentials into string in the form of `distant://[username]:{key}@{host}:{port}`
27    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 we are IPv6, we need to include square brackets
37        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    /// Parse `distant://[username]:{key}@{host}:{port}` as credentials. Note that this requires the
51    /// `distant` scheme to be included. If parsing without scheme is desired, call the
52    /// [`DistantSingleKeyCredentials::try_from_uri_ref`] method instead with `require_scheme`
53    /// set to false
54    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        // Can be scheme-less or explicitly distant
60        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    /// Searches a str for `distant://[username]:{key}@{host}:{port}`, returning the first matching
104    /// credentials set if found, failing if anything is found immediately before or after the
105    /// credentials that is not whitespace or control characters
106    ///
107    /// If `strict` is false, then the scheme can be preceded by any character
108    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            // Start at the scheme
113            let (before, s) = s.split_at(i);
114
115            // Check character preceding the scheme to make sure it isn't a different scheme
116            // Only whitespace or control characters preceding are okay, anything else is skipped
117            if strict && !before.is_empty() && !before.ends_with(is_boundary) {
118                continue;
119            }
120
121            // Consume until we reach whitespace or control, which indicates the potential end
122            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    /// Equivalent to [`find(s, true)`].
137    ///
138    /// [`find(s, true)`]: DistantSingleKeyCredentials::find
139    #[inline]
140    pub fn find_strict(s: &str) -> Option<DistantSingleKeyCredentials> {
141        Self::find(s, true)
142    }
143
144    /// Equivalent to [`find(s, false)`].
145    ///
146    /// [`find(s, false)`]: DistantSingleKeyCredentials::find
147    #[inline]
148    pub fn find_lax(s: &str) -> Option<DistantSingleKeyCredentials> {
149        Self::find(s, false)
150    }
151
152    /// Converts credentials into a [`Destination`] of the form
153    /// `distant://[username]:{key}@{host}:{port}`, failing if the credentials would not produce a
154    /// valid [`Destination`]
155    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}