Skip to main content

nethsm/
connection.rs

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