Skip to main content

guacamole_client/
connection.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::GuacamoleClient;
6use crate::error::Result;
7use crate::history::HistoryEntry;
8use crate::patch::PatchOperation;
9use crate::sharing_profile::SharingProfileSummary;
10use crate::validation::validate_connection_id;
11
12/// A Guacamole connection.
13#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15#[non_exhaustive]
16pub struct Connection {
17    /// The unique numeric identifier (as a string).
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub identifier: Option<String>,
20
21    /// Identifier of the parent connection group (`"ROOT"` for top-level).
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub parent_identifier: Option<String>,
24
25    /// Human-readable name of the connection.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub name: Option<String>,
28
29    /// Protocol used by this connection (e.g. `"ssh"`, `"rdp"`, `"vnc"`).
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub protocol: Option<String>,
32
33    /// Connection parameters (hostname, port, username, etc.).
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub parameters: Option<HashMap<String, String>>,
36
37    /// Arbitrary connection attributes.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub attributes: Option<HashMap<String, String>>,
40
41    /// Number of active connections using this connection.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub active_connections: Option<i32>,
44}
45
46/// An active Guacamole connection (a session currently in use).
47#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49#[non_exhaustive]
50pub struct ActiveConnection {
51    /// The unique identifier for this active connection.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub identifier: Option<String>,
54
55    /// The identifier of the underlying connection definition.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub connection_identifier: Option<String>,
58
59    /// Start timestamp in milliseconds since epoch.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub start_date: Option<i64>,
62
63    /// Remote host of the client.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub remote_host: Option<String>,
66
67    /// Username of the user who opened this connection.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub username: Option<String>,
70}
71
72impl GuacamoleClient {
73    /// Lists all connections in the given data source.
74    pub async fn list_connections(
75        &self,
76        data_source: Option<&str>,
77    ) -> Result<HashMap<String, Connection>> {
78        let ds = self.resolve_data_source(data_source)?;
79        let url = self.url(&format!("/api/session/data/{ds}/connections"))?;
80        let response = self.http.get(&url).send().await?;
81        Self::parse_response(response, "connections").await
82    }
83
84    /// Retrieves a single connection by its identifier.
85    pub async fn get_connection(
86        &self,
87        data_source: Option<&str>,
88        connection_id: &str,
89    ) -> Result<Connection> {
90        validate_connection_id(connection_id)?;
91        let ds = self.resolve_data_source(data_source)?;
92        let url = self.url(&format!(
93            "/api/session/data/{ds}/connections/{connection_id}"
94        ))?;
95        let response = self.http.get(&url).send().await?;
96        Self::parse_response(response, &format!("connection {connection_id}")).await
97    }
98
99    /// Retrieves the connection parameters (hostname, port, credentials, etc.).
100    pub async fn get_connection_parameters(
101        &self,
102        data_source: Option<&str>,
103        connection_id: &str,
104    ) -> Result<HashMap<String, String>> {
105        validate_connection_id(connection_id)?;
106        let ds = self.resolve_data_source(data_source)?;
107        let url = self.url(&format!(
108            "/api/session/data/{ds}/connections/{connection_id}/parameters"
109        ))?;
110        let response = self.http.get(&url).send().await?;
111        Self::parse_response(response, &format!("connection {connection_id} parameters"))
112            .await
113    }
114
115    /// Retrieves the history for a specific connection.
116    pub async fn get_connection_history(
117        &self,
118        data_source: Option<&str>,
119        connection_id: &str,
120    ) -> Result<Vec<HistoryEntry>> {
121        validate_connection_id(connection_id)?;
122        let ds = self.resolve_data_source(data_source)?;
123        let url = self.url(&format!(
124            "/api/session/data/{ds}/connections/{connection_id}/history"
125        ))?;
126        let response = self.http.get(&url).send().await?;
127        Self::parse_response(response, &format!("connection {connection_id} history"))
128            .await
129    }
130
131    /// Retrieves the sharing profiles associated with a connection.
132    pub async fn get_connection_sharing_profiles(
133        &self,
134        data_source: Option<&str>,
135        connection_id: &str,
136    ) -> Result<HashMap<String, SharingProfileSummary>> {
137        validate_connection_id(connection_id)?;
138        let ds = self.resolve_data_source(data_source)?;
139        let url = self.url(&format!(
140            "/api/session/data/{ds}/connections/{connection_id}/sharingProfiles"
141        ))?;
142        let response = self.http.get(&url).send().await?;
143        Self::parse_response(
144            response,
145            &format!("connection {connection_id} sharing profiles"),
146        )
147        .await
148    }
149
150    /// Creates a new connection. Returns the created connection (with assigned identifier).
151    pub async fn create_connection(
152        &self,
153        data_source: Option<&str>,
154        connection: &Connection,
155    ) -> Result<Connection> {
156        let ds = self.resolve_data_source(data_source)?;
157        let url = self.url(&format!("/api/session/data/{ds}/connections"))?;
158        let response = self.http.post(&url).json(connection).send().await?;
159        Self::parse_response(response, "create connection").await
160    }
161
162    /// Updates an existing connection. Returns `()` on success (204 No Content).
163    pub async fn update_connection(
164        &self,
165        data_source: Option<&str>,
166        connection_id: &str,
167        connection: &Connection,
168    ) -> Result<()> {
169        validate_connection_id(connection_id)?;
170        let ds = self.resolve_data_source(data_source)?;
171        let url = self.url(&format!(
172            "/api/session/data/{ds}/connections/{connection_id}"
173        ))?;
174        let response = self.http.put(&url).json(connection).send().await?;
175        Self::handle_error(response, &format!("connection {connection_id}")).await?;
176        Ok(())
177    }
178
179    /// Deletes a connection by its identifier. Returns `()` on success (204 No Content).
180    pub async fn delete_connection(
181        &self,
182        data_source: Option<&str>,
183        connection_id: &str,
184    ) -> Result<()> {
185        validate_connection_id(connection_id)?;
186        let ds = self.resolve_data_source(data_source)?;
187        let url = self.url(&format!(
188            "/api/session/data/{ds}/connections/{connection_id}"
189        ))?;
190        let response = self.http.delete(&url).send().await?;
191        Self::handle_error(response, &format!("connection {connection_id}")).await?;
192        Ok(())
193    }
194
195    /// Lists all active connections in the given data source.
196    pub async fn list_active_connections(
197        &self,
198        data_source: Option<&str>,
199    ) -> Result<HashMap<String, ActiveConnection>> {
200        let ds = self.resolve_data_source(data_source)?;
201        let url = self.url(&format!(
202            "/api/session/data/{ds}/activeConnections"
203        ))?;
204        let response = self.http.get(&url).send().await?;
205        Self::parse_response(response, "active connections").await
206    }
207
208    /// Kills active connections using JSON Patch operations.
209    pub async fn kill_connections(
210        &self,
211        data_source: Option<&str>,
212        patches: &[PatchOperation],
213    ) -> Result<()> {
214        let ds = self.resolve_data_source(data_source)?;
215        let url = self.url(&format!(
216            "/api/session/data/{ds}/activeConnections"
217        ))?;
218        let response = self.http.patch(&url).json(patches).send().await?;
219        Self::handle_error(response, "kill connections").await?;
220        Ok(())
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn connection_serde_roundtrip() {
230        let conn = Connection {
231            identifier: Some("1".to_string()),
232            parent_identifier: Some("ROOT".to_string()),
233            name: Some("my-server".to_string()),
234            protocol: Some("ssh".to_string()),
235            parameters: Some(HashMap::from([
236                ("hostname".to_string(), "10.0.0.5".to_string()),
237                ("port".to_string(), "22".to_string()),
238            ])),
239            attributes: Some(HashMap::new()),
240            active_connections: Some(0),
241        };
242        let json = serde_json::to_string(&conn).unwrap();
243        let deserialized: Connection = serde_json::from_str(&json).unwrap();
244        assert_eq!(conn, deserialized);
245    }
246
247    #[test]
248    fn connection_camel_case_keys() {
249        let conn = Connection {
250            parent_identifier: Some("ROOT".to_string()),
251            active_connections: Some(2),
252            ..Default::default()
253        };
254        let json = serde_json::to_value(&conn).unwrap();
255        assert!(json.get("parentIdentifier").is_some());
256        assert!(json.get("activeConnections").is_some());
257    }
258
259    #[test]
260    fn connection_skip_none_fields() {
261        let conn = Connection::default();
262        let json = serde_json::to_value(&conn).unwrap();
263        let obj = json.as_object().unwrap();
264        assert!(obj.is_empty());
265    }
266
267    #[test]
268    fn deserialize_connection_from_api_json() {
269        let json = r#"{
270            "identifier": "1",
271            "parentIdentifier": "ROOT",
272            "name": "my-server",
273            "protocol": "ssh",
274            "parameters": {},
275            "attributes": {},
276            "activeConnections": 0
277        }"#;
278        let conn: Connection = serde_json::from_str(json).unwrap();
279        assert_eq!(conn.identifier.as_deref(), Some("1"));
280        assert_eq!(conn.parent_identifier.as_deref(), Some("ROOT"));
281        assert_eq!(conn.protocol.as_deref(), Some("ssh"));
282        assert_eq!(conn.active_connections, Some(0));
283    }
284
285    #[test]
286    fn active_connection_serde_roundtrip() {
287        let ac = ActiveConnection {
288            identifier: Some("abc-123".to_string()),
289            connection_identifier: Some("1".to_string()),
290            start_date: Some(1_700_000_000_000),
291            remote_host: Some("10.0.0.1".to_string()),
292            username: Some("admin".to_string()),
293        };
294        let json = serde_json::to_string(&ac).unwrap();
295        let deserialized: ActiveConnection = serde_json::from_str(&json).unwrap();
296        assert_eq!(ac, deserialized);
297    }
298
299    #[test]
300    fn active_connection_camel_case_keys() {
301        let ac = ActiveConnection {
302            connection_identifier: Some("1".to_string()),
303            start_date: Some(1),
304            remote_host: Some("host".to_string()),
305            ..Default::default()
306        };
307        let json = serde_json::to_value(&ac).unwrap();
308        assert!(json.get("connectionIdentifier").is_some());
309        assert!(json.get("startDate").is_some());
310        assert!(json.get("remoteHost").is_some());
311    }
312
313    #[test]
314    fn active_connection_skip_none_fields() {
315        let ac = ActiveConnection::default();
316        let json = serde_json::to_value(&ac).unwrap();
317        let obj = json.as_object().unwrap();
318        assert!(obj.is_empty());
319    }
320
321    #[test]
322    fn deserialize_connection_unknown_fields_ignored() {
323        let json = r#"{"identifier": "1", "unknownField": "value"}"#;
324        let conn: Connection = serde_json::from_str(json).unwrap();
325        assert_eq!(conn.identifier.as_deref(), Some("1"));
326    }
327
328    #[test]
329    fn deserialize_active_connection_from_api_json() {
330        let json = r#"{
331            "identifier": "abc-def-123",
332            "connectionIdentifier": "42",
333            "startDate": 1700000000000,
334            "remoteHost": "192.168.1.100",
335            "username": "guacadmin"
336        }"#;
337        let ac: ActiveConnection = serde_json::from_str(json).unwrap();
338        assert_eq!(ac.identifier.as_deref(), Some("abc-def-123"));
339        assert_eq!(ac.connection_identifier.as_deref(), Some("42"));
340        assert_eq!(ac.start_date, Some(1_700_000_000_000));
341        assert_eq!(ac.remote_host.as_deref(), Some("192.168.1.100"));
342        assert_eq!(ac.username.as_deref(), Some("guacadmin"));
343    }
344
345    #[test]
346    fn active_connection_unknown_fields_ignored() {
347        let json = r#"{"identifier": "abc", "unknownField": 99}"#;
348        let ac: ActiveConnection = serde_json::from_str(json).unwrap();
349        assert_eq!(ac.identifier.as_deref(), Some("abc"));
350    }
351}