nethsm/
connection.rs

1//! Components for NetHSM connection handling.
2
3use std::{fmt::Display, str::FromStr};
4
5use serde::{Deserialize, Serialize};
6
7use crate::ConnectionSecurity;
8#[cfg(doc)]
9use crate::NetHsm;
10
11/// An error that may occur when working with NetHSM connections.
12#[derive(Debug, thiserror::Error)]
13pub enum Error {
14    /// The format of a URL is invalid.
15    ///
16    /// A [`url::Url`] could be created, but one of the additional constraints imposed by [`Url`]
17    /// can not be met.
18    #[error("The format of URL {url} is invalid because {context}")]
19    UrlInvalidFormat {
20        /// The [`url::Url`] for which one of the [`Url`] constraints can not be met.
21        url: url::Url,
22
23        /// The context in which the error occurred.
24        ///
25        /// This is meant to complete the sentence "The format of URL {url} is invalid because ".
26        context: &'static str,
27    },
28
29    /// A URL can not be parsed.
30    #[error("URL parser error:\n{0}")]
31    UrlParse(#[from] url::ParseError),
32}
33
34/// The connection to a NetHSM device.
35///
36/// Contains the [`Url`] and [`ConnectionSecurity`] for a [`NetHsm`] device.
37#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
38pub struct Connection {
39    pub(crate) url: Url,
40    pub(crate) tls_security: ConnectionSecurity,
41}
42
43impl Connection {
44    /// Creates a new [`Connection`]
45    pub fn new(url: Url, tls_security: ConnectionSecurity) -> Self {
46        Self { url, tls_security }
47    }
48
49    /// Returns a reference to the contained [`Url`].
50    pub fn url(&self) -> &Url {
51        &self.url
52    }
53
54    /// Returns a reference to the contained [`ConnectionSecurity`].
55    pub fn tls_security(&self) -> &ConnectionSecurity {
56        &self.tls_security
57    }
58}
59
60impl Display for Connection {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(f, "{} (TLS security: {})", self.url, self.tls_security)
63    }
64}
65
66/// The URL used for connecting to a NetHSM instance.
67///
68/// Wraps [`url::Url`] but offers stricter constraints.
69/// The URL
70///
71/// * must use https
72/// * must have a host
73/// * must not contain a password, user or query
74#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
75#[serde(try_from = "String")]
76pub struct Url(url::Url);
77
78impl Url {
79    /// Creates a new Url.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use nethsm::Url;
85    ///
86    /// # fn main() -> testresult::TestResult {
87    /// Url::new("https://example.org/api/v1")?;
88    /// Url::new("https://127.0.0.1:8443/api/v1")?;
89    ///
90    /// // errors when not using https
91    /// assert!(Url::new("http://example.org/api/v1").is_err());
92    ///
93    /// // errors when using query, user or password
94    /// assert!(Url::new("https://example.org/api/v1?something").is_err());
95    /// # Ok(())
96    /// # }
97    /// ```
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if
102    /// * https is not used
103    /// * a host is not defined
104    /// * the URL contains a password, user or query
105    pub fn new(url: &str) -> Result<Self, crate::Error> {
106        let url = url::Url::parse(url).map_err(Error::UrlParse)?;
107        if !url.scheme().eq("https") {
108            Err(Error::UrlInvalidFormat {
109                url,
110                context: "a URL must use TLS",
111            }
112            .into())
113        } else if !url.has_host() {
114            Err(Error::UrlInvalidFormat {
115                url,
116                context: "a URL must have a host component",
117            }
118            .into())
119        } else if url.password().is_some() {
120            Err(Error::UrlInvalidFormat {
121                url,
122                context: "a URL must not have a password component",
123            }
124            .into())
125        } else if !url.username().is_empty() {
126            Err(Error::UrlInvalidFormat {
127                url,
128                context: "a URL must not have a user component",
129            }
130            .into())
131        } else if url.query().is_some() {
132            Err(Error::UrlInvalidFormat {
133                url,
134                context: "a URL must not have a query component",
135            }
136            .into())
137        } else {
138            Ok(Self(url))
139        }
140    }
141}
142
143impl Display for Url {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        write!(f, "{}", self.0)
146    }
147}
148
149impl TryFrom<&str> for Url {
150    type Error = crate::Error;
151
152    fn try_from(value: &str) -> Result<Self, crate::Error> {
153        Self::new(value)
154    }
155}
156
157impl TryFrom<String> for Url {
158    type Error = crate::Error;
159
160    fn try_from(value: String) -> Result<Self, crate::Error> {
161        Self::new(&value)
162    }
163}
164
165impl FromStr for Url {
166    type Err = crate::Error;
167
168    fn from_str(s: &str) -> Result<Self, Self::Err> {
169        Self::new(s)
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use rstest::rstest;
176    use testresult::TestResult;
177
178    use super::*;
179
180    #[rstest]
181    #[case(ConnectionSecurity::Unsafe, "unsafe")]
182    #[case(ConnectionSecurity::Native, "native")]
183    fn connection_display(
184        #[case] connection_security: ConnectionSecurity,
185        #[case] expected_str: &str,
186    ) -> TestResult {
187        let url = "https://example.org/";
188        let connection = Connection::new(url.parse()?, connection_security);
189        assert_eq!(
190            format!("{connection}"),
191            format!("{url} (TLS security: {expected_str})")
192        );
193
194        Ok(())
195    }
196}