1use 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#[derive(Debug, thiserror::Error)]
14pub enum Error {
15 #[error("The format of URL {url} is invalid because {context}")]
20 UrlInvalidFormat {
21 url: url::Url,
23
24 context: &'static str,
28 },
29
30 #[error("URL parser error:\n{0}")]
32 UrlParse(#[from] url::ParseError),
33}
34
35#[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 pub fn new(url: Url, tls_security: ConnectionSecurity) -> Self {
47 Self { url, tls_security }
48 }
49
50 pub fn url(&self) -> &Url {
52 &self.url
53 }
54
55 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#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
138#[serde(try_from = "String")]
139pub struct Url(url::Url);
140
141impl Url {
142 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}