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#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15#[non_exhaustive]
16pub struct Connection {
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub identifier: Option<String>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub parent_identifier: Option<String>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub name: Option<String>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub protocol: Option<String>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub parameters: Option<HashMap<String, String>>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub attributes: Option<HashMap<String, String>>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub active_connections: Option<i32>,
44}
45
46#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49#[non_exhaustive]
50pub struct ActiveConnection {
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub identifier: Option<String>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub connection_identifier: Option<String>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub start_date: Option<i64>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub remote_host: Option<String>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub username: Option<String>,
70}
71
72impl GuacamoleClient {
73 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 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 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 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 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 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 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 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 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 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}