edgehog_device_runtime/data/
astarte_device_sdk_lib.rs1use std::path::Path;
22
23use astarte_device_sdk::builder::{BuilderError, DeviceBuilder};
24use astarte_device_sdk::error::Error as AstarteError;
25use astarte_device_sdk::introspection::AddInterfaceError;
26use astarte_device_sdk::prelude::*;
27use astarte_device_sdk::store::SqliteStore;
28use astarte_device_sdk::transport::mqtt::{Credential, Mqtt, MqttConfig};
29use astarte_device_sdk::DeviceClient;
30use serde::Deserialize;
31use tokio::task::JoinSet;
32use url::Url;
33
34use crate::repository::file_state_repository::{FileStateError, FileStateRepository};
35use crate::repository::StateRepository;
36
37#[derive(Debug, thiserror::Error, displaydoc::Display)]
39pub enum DeviceSdkError {
40 MissingDeviceId,
42 #[cfg(all(feature = "zbus", target_os = "linux"))]
44 Zbus(#[from] zbus::Error),
45 ReadSecret(#[source] FileStateError),
47 MissingCredentialSecret,
49 Interfaces(#[from] AddInterfaceError),
51 Builder(#[from] BuilderError),
53 Connect(#[source] AstarteError),
55}
56
57#[derive(Debug, Deserialize, Clone)]
58pub struct AstarteDeviceSdkConfigOptions {
59 pub realm: String,
60 pub device_id: Option<String>,
61 pub credentials_secret: Option<String>,
62 pub pairing_url: Url,
63 pub pairing_token: Option<String>,
64 #[serde(default)]
65 pub ignore_ssl: bool,
66}
67
68impl AstarteDeviceSdkConfigOptions {
69 async fn device_id_or_from_dbus(&self) -> Result<String, DeviceSdkError> {
70 if let Some(id) = self.device_id.as_ref().filter(|id| !id.is_empty()) {
71 return Ok(id.clone());
72 }
73
74 cfg_if::cfg_if! {
75 if #[cfg(all(feature = "zbus", target_os = "linux"))] {
76 hardware_id_from_dbus()
77 .await?
78 .ok_or(DeviceSdkError::MissingDeviceId)
79 } else {
80 Err(DeviceSdkError::MissingDeviceId)
81 }
82 }
83 }
84
85 async fn credentials_secret(
86 &self,
87 device_id: &str,
88 store_directory: impl AsRef<Path>,
89 ) -> Result<Credential, DeviceSdkError> {
90 let cred = self.credentials_secret.as_ref().filter(|id| !id.is_empty());
91
92 if let Some(secret) = cred {
93 return Ok(Credential::secret(secret));
94 }
95
96 let registry = FileStateRepository::new(
97 store_directory.as_ref(),
98 format!("credentials_{device_id}.json"),
99 );
100
101 if StateRepository::<String>::exists(®istry).await {
102 return registry
103 .read()
104 .await
105 .map(Credential::secret)
106 .map_err(DeviceSdkError::ReadSecret);
107 }
108
109 if let Some(token) = &self.pairing_token {
110 return Ok(Credential::paring_token(token));
111 }
112
113 Err(DeviceSdkError::MissingCredentialSecret)
114 }
115
116 pub async fn connect<P>(
117 &self,
118 tasks: &mut JoinSet<stable_eyre::Result<()>>,
119 store: SqliteStore,
120 store_dir: P,
121 interface_dir: P,
122 ) -> Result<DeviceClient<Mqtt<SqliteStore>>, DeviceSdkError>
123 where
124 P: AsRef<Path>,
125 {
126 let device_id = self.device_id_or_from_dbus().await?;
127
128 let credentials_secret = self.credentials_secret(&device_id, &store_dir).await?;
129
130 let mut mqtt_cfg = MqttConfig::new(
131 &self.realm,
132 &device_id,
133 credentials_secret,
134 self.pairing_url.clone(),
135 );
136
137 if self.ignore_ssl {
138 mqtt_cfg.ignore_ssl_errors();
139 }
140
141 let (client, connection) = DeviceBuilder::new()
142 .store(store)
143 .interface_directory(interface_dir)?
144 .writable_dir(store_dir)?
145 .connection(mqtt_cfg)
146 .build()
147 .await
148 .map_err(DeviceSdkError::Connect)?;
149
150 tasks.spawn(async move { connection.handle_events().await.map_err(Into::into) });
151
152 Ok(client)
153 }
154}
155
156#[cfg(all(feature = "zbus", target_os = "linux"))]
157pub async fn hardware_id_from_dbus() -> Result<Option<String>, DeviceSdkError> {
158 let connection = zbus::Connection::system().await?;
159 let proxy = crate::device::DeviceProxy::new(&connection).await?;
160 let hardware_id: String = proxy.get_hardware_id("").await?;
161
162 if hardware_id.is_empty() {
163 return Ok(None);
164 }
165
166 Ok(Some(hardware_id))
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 use tempdir::TempDir;
174
175 #[tokio::test]
176 async fn device_id_test() {
177 let opts = AstarteDeviceSdkConfigOptions {
178 realm: "foo".to_string(),
179 device_id: Some("target".to_string()),
180 credentials_secret: None,
181 pairing_url: Url::parse("http://[::]").unwrap(),
182 pairing_token: None,
183 ignore_ssl: false,
184 };
185
186 let id = opts.device_id_or_from_dbus().await.unwrap();
187 assert_eq!(id, "target");
188 }
189
190 #[tokio::test]
191 async fn credentials_secret_test() {
192 let _dir = TempDir::new("sdk_cred").unwrap();
193 let path = _dir.path().to_owned();
194
195 let options = AstarteDeviceSdkConfigOptions {
196 realm: "".to_string(),
197 device_id: None,
198 credentials_secret: Some("credentials_secret".to_string()),
199 pairing_url: Url::parse("http://[::]").unwrap(),
200 pairing_token: None,
201 ignore_ssl: false,
202 };
203
204 let secret = options.credentials_secret("device_id", path).await.unwrap();
205
206 assert_eq!(secret, Credential::secret("credentials_secret"));
207 }
208
209 #[tokio::test]
210 async fn credentials_pairing_token() {
211 let _dir = TempDir::new("sdk_cred").unwrap();
212 let path = _dir.path().to_owned();
213
214 let options = AstarteDeviceSdkConfigOptions {
215 realm: "".to_string(),
216 device_id: None,
217 credentials_secret: None,
218 pairing_url: Url::parse("http://[::]").unwrap(),
219 pairing_token: Some("pairing_token".to_string()),
220 ignore_ssl: false,
221 };
222
223 let secret = options.credentials_secret("device_id", path).await.unwrap();
224
225 assert_eq!(secret, Credential::paring_token("pairing_token"));
226 }
227
228 #[tokio::test]
229 async fn not_enough_arguments_credentials_secret_test() {
230 let _dir = TempDir::new("sdk_cred").unwrap();
231 let path = _dir.path().to_owned();
232
233 let options = AstarteDeviceSdkConfigOptions {
234 realm: "".to_string(),
235 device_id: None,
236 credentials_secret: None,
237 pairing_url: Url::parse("http://[::]").unwrap(),
238 pairing_token: None,
239 ignore_ssl: false,
240 };
241
242 let res = options.credentials_secret("device_id", &path).await;
243
244 assert!(res.is_err());
245 }
246
247 #[tokio::test]
248 async fn get_credentials_secret_persistence_fail() {
249 let _dir = TempDir::new("sdk_cred").unwrap();
250 let path = _dir.path().to_owned();
251
252 let device_id = "device_id";
253
254 let path = path.join(format!("credentials_{device_id}.json"));
255
256 tokio::fs::write(&path, b"\0").await.unwrap();
257
258 let options = AstarteDeviceSdkConfigOptions {
259 realm: "".to_string(),
260 device_id: Some(device_id.to_owned()),
261 credentials_secret: None,
262 pairing_url: Url::parse("http://[::]").unwrap(),
263 pairing_token: None,
264 ignore_ssl: true,
265 };
266
267 let res = options.credentials_secret(device_id, path).await;
268 assert!(res.is_err());
269 }
270
271 #[tokio::test]
272 async fn get_credentials_secret_persistence_success() {
273 let _dir = TempDir::new("sdk_cred").unwrap();
274 let path = _dir.path().to_owned();
275
276 let device_id = "device_id";
277
278 let full_path = path.join(format!("credentials_{device_id}.json"));
279
280 let exp = "credential_secret";
281
282 tokio::fs::write(&full_path, format!("\"{exp}\""))
283 .await
284 .unwrap();
285
286 let options = AstarteDeviceSdkConfigOptions {
287 realm: "".to_string(),
288 device_id: Some(device_id.to_owned()),
289 credentials_secret: None,
290 pairing_url: Url::parse("http://[::]").unwrap(),
291 pairing_token: None,
292 ignore_ssl: false,
293 };
294
295 let secret = options.credentials_secret(device_id, path).await.unwrap();
296
297 assert_eq!(secret, Credential::secret(exp));
298 }
299}