Skip to main content

edgehog_device_runtime_forwarder/
astarte.rs

1// Copyright 2024 SECO Mind Srl
2// SPDX-License-Identifier: Apache-2.0
3
4//! Implement the interaction with the [Astarte rust SDK](astarte_device_sdk).
5//!
6//! Module responsible for handling a connection between a Device and Astarte.
7
8use std::hash::Hash;
9
10use astarte_device_sdk::{Error as SdkError, FromEvent, IntoAstarteObject};
11use url::{ParseError, Url};
12
13/// Astarte errors.
14#[non_exhaustive]
15#[derive(displaydoc::Display, thiserror::Error, Debug)]
16pub enum AstarteError {
17    /// Error occurring when different fields from those of the mapping are received.
18    Sdk(#[from] SdkError),
19
20    /// Missing session token.
21    MissingSessionToken,
22
23    /// Error while parsing an url, `{0}`.
24    ParseUrl(#[from] ParseError),
25}
26
27/// Struct representing the fields of an aggregated object the Astarte server can send to the device.
28#[derive(Debug, Clone, Eq, PartialEq, Hash, FromEvent, IntoAstarteObject)]
29#[from_event(
30    interface = "io.edgehog.devicemanager.ForwarderSessionRequest",
31    path = "/request",
32    aggregation = "object"
33)]
34pub struct SessionInfo {
35    /// Hostname or IP address.
36    pub host: String,
37    /// Port number.
38    pub port: i32,
39    /// Session token.
40    pub session_token: String,
41    /// Flag to enable secure session establishment
42    pub secure: bool,
43}
44
45impl TryFrom<&SessionInfo> for Url {
46    type Error = AstarteError;
47
48    fn try_from(value: &SessionInfo) -> Result<Self, Self::Error> {
49        if value.session_token.is_empty() {
50            return Err(AstarteError::MissingSessionToken);
51        }
52
53        let schema = if value.secure { "wss" } else { "ws" };
54
55        Url::parse_with_params(
56            &format!(
57                "{}://{}:{}/device/websocket",
58                schema, value.host, value.port
59            ),
60            &[("session", &value.session_token)],
61        )
62        .map_err(AstarteError::ParseUrl)
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use astarte_device_sdk::aggregate::AstarteObject;
70    use astarte_device_sdk::chrono::Utc;
71    use astarte_device_sdk::types::AstarteData;
72    use astarte_device_sdk::{DeviceEvent, Value};
73    use std::net::Ipv4Addr;
74    use url::Host;
75
76    fn create_sinfo(session_token: &str) -> SessionInfo {
77        SessionInfo {
78            host: Ipv4Addr::LOCALHOST.to_string(),
79            port: 8080,
80            session_token: session_token.to_string(),
81            secure: false,
82        }
83    }
84
85    fn create_astarte_event(
86        host: &str,
87        port: i32,
88        session_token: &str,
89        secure: bool,
90    ) -> DeviceEvent {
91        let mut data = AstarteObject::new();
92
93        data.insert("host".to_string(), AstarteData::String(host.to_string()));
94        data.insert("port".to_string(), AstarteData::Integer(port));
95        data.insert(
96            "session_token".to_string(),
97            AstarteData::String(session_token.to_string()),
98        );
99        data.insert("secure".to_string(), AstarteData::Boolean(secure));
100
101        DeviceEvent {
102            interface: "io.edgehog.devicemanager.ForwarderSessionRequest".to_string(),
103            path: "/request".to_string(),
104            data: Value::Object {
105                data,
106                timestamp: Utc::now(),
107            },
108        }
109    }
110
111    #[test]
112    fn test_astarte_aggregate() {
113        let sinfo = create_sinfo("test_token");
114
115        let expected = [
116            ("host", AstarteData::String("127.0.0.1".to_string())),
117            ("port", AstarteData::Integer(8080)),
118            (
119                "session_token",
120                AstarteData::String("test_token".to_string()),
121            ),
122        ];
123
124        let res: Result<AstarteObject, astarte_device_sdk::Error> = sinfo.try_into();
125
126        assert!(res.is_ok());
127
128        let res = res.unwrap();
129
130        for (key, exp_val) in expected {
131            assert_eq!(*res.get(key).unwrap(), exp_val);
132        }
133    }
134
135    #[test]
136    fn test_try_from_sinfo() {
137        // empty session token generates error
138        let mut sinfo = create_sinfo("");
139
140        assert!(Url::try_from(&sinfo).is_err());
141
142        // ok
143        sinfo = create_sinfo("test_token");
144
145        let case = Url::try_from(&sinfo).unwrap();
146
147        assert_eq!(case.host(), Some(Host::Ipv4(Ipv4Addr::LOCALHOST)));
148        assert_eq!(case.port(), Some(8080));
149        assert_eq!(case.query(), Some("session=test_token"));
150    }
151
152    #[test]
153    fn test_retrieve_sinfo() {
154        let err_cases = [
155            create_astarte_event("", 8080, "test_token", false),
156            create_astarte_event("127.0.0.1", -1, "test_token", false),
157            create_astarte_event("127.0.0.1", 8080, "", false),
158        ];
159
160        for event in err_cases {
161            let sinfo = SessionInfo::from_event(event).unwrap();
162            assert!(Url::try_from(&sinfo).is_err());
163        }
164
165        let event = create_astarte_event("127.0.0.1", 8080, "test_token", false);
166        let sinfo = SessionInfo::from_event(event).unwrap();
167
168        assert_eq!(sinfo.host, Ipv4Addr::LOCALHOST.to_string());
169        assert_eq!(sinfo.port, 8080);
170        assert_eq!(sinfo.session_token, "test_token".to_string());
171        assert!(!sinfo.secure);
172
173        let url = Url::try_from(&sinfo).unwrap();
174        let exp = Url::try_from("ws://127.0.0.1:8080/device/websocket?session=test_token").unwrap();
175        assert_eq!(exp, url);
176    }
177}