1use crate::core_error::CoreError;
2use crate::model::{
3 Country, DpiApplication, DpiCategory, EntityId, RadiusProfile, VpnServer, VpnTunnel,
4 WanInterface,
5};
6
7use super::super::{
8 Controller, integration_client_context, integration_site_context, require_uuid,
9};
10
11impl Controller {
12 pub async fn list_vpn_servers(&self) -> Result<Vec<VpnServer>, CoreError> {
13 let (client, site_id) = integration_site_context(self, "list_vpn_servers").await?;
14 let raw = client
15 .paginate_all(200, |offset, limit| {
16 client.list_vpn_servers(&site_id, offset, limit)
17 })
18 .await?;
19 Ok(raw
20 .into_iter()
21 .map(|server| parse_vpn_server(&server.fields))
22 .collect())
23 }
24
25 pub async fn list_vpn_tunnels(&self) -> Result<Vec<VpnTunnel>, CoreError> {
26 let (client, site_id) = integration_site_context(self, "list_vpn_tunnels").await?;
27 let raw = client
28 .paginate_all(200, |offset, limit| {
29 client.list_vpn_tunnels(&site_id, offset, limit)
30 })
31 .await?;
32 Ok(raw
33 .into_iter()
34 .map(|tunnel| parse_vpn_tunnel(&tunnel.fields))
35 .collect())
36 }
37
38 pub async fn list_wans(&self) -> Result<Vec<WanInterface>, CoreError> {
39 let (client, site_id) = integration_site_context(self, "list_wans").await?;
40 let raw = client
41 .paginate_all(200, |offset, limit| {
42 client.list_wans(&site_id, offset, limit)
43 })
44 .await?;
45 Ok(raw
46 .into_iter()
47 .map(|wan| {
48 let parse_ip = |key: &str| -> Option<std::net::IpAddr> {
49 wan.fields
50 .get(key)
51 .and_then(|value| value.as_str())
52 .and_then(|value| value.parse().ok())
53 };
54 let dns = wan
55 .fields
56 .get("dns")
57 .and_then(|value| value.as_array())
58 .map(|values| {
59 values
60 .iter()
61 .filter_map(|value| value.as_str().and_then(|value| value.parse().ok()))
62 .collect()
63 })
64 .unwrap_or_default();
65 WanInterface {
66 id: parse_integration_entity_id(&wan.fields),
67 name: wan
68 .fields
69 .get("name")
70 .and_then(|value| value.as_str())
71 .map(String::from),
72 ip: parse_ip("ipAddress").or_else(|| parse_ip("ip")),
73 gateway: parse_ip("gateway"),
74 dns,
75 }
76 })
77 .collect())
78 }
79
80 pub async fn list_dpi_categories(&self) -> Result<Vec<DpiCategory>, CoreError> {
81 let (client, site_id) = integration_site_context(self, "list_dpi_categories").await?;
82 let raw = client
83 .paginate_all(200, |offset, limit| {
84 client.list_dpi_categories(&site_id, offset, limit)
85 })
86 .await?;
87 Ok(raw
88 .into_iter()
89 .map(|category| {
90 #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
91 let id = category
92 .fields
93 .get("id")
94 .and_then(serde_json::Value::as_u64)
95 .unwrap_or(0) as u32;
96 DpiCategory {
97 id,
98 name: category
99 .fields
100 .get("name")
101 .and_then(|value| value.as_str())
102 .unwrap_or("Unknown")
103 .to_owned(),
104 tx_bytes: category
105 .fields
106 .get("txBytes")
107 .and_then(serde_json::Value::as_u64)
108 .unwrap_or(0),
109 rx_bytes: category
110 .fields
111 .get("rxBytes")
112 .and_then(serde_json::Value::as_u64)
113 .unwrap_or(0),
114 apps: Vec::new(),
115 }
116 })
117 .collect())
118 }
119
120 pub async fn list_dpi_applications(&self) -> Result<Vec<DpiApplication>, CoreError> {
121 let (client, site_id) = integration_site_context(self, "list_dpi_applications").await?;
122 let raw = client
123 .paginate_all(200, |offset, limit| {
124 client.list_dpi_applications(&site_id, offset, limit)
125 })
126 .await?;
127 Ok(raw
128 .into_iter()
129 .map(|application| {
130 #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
131 let id = application
132 .fields
133 .get("id")
134 .and_then(serde_json::Value::as_u64)
135 .unwrap_or(0) as u32;
136 DpiApplication {
137 id,
138 name: application
139 .fields
140 .get("name")
141 .and_then(|value| value.as_str())
142 .unwrap_or("Unknown")
143 .to_owned(),
144 #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
145 category_id: application
146 .fields
147 .get("categoryId")
148 .and_then(serde_json::Value::as_u64)
149 .unwrap_or(0) as u32,
150 tx_bytes: application
151 .fields
152 .get("txBytes")
153 .and_then(serde_json::Value::as_u64)
154 .unwrap_or(0),
155 rx_bytes: application
156 .fields
157 .get("rxBytes")
158 .and_then(serde_json::Value::as_u64)
159 .unwrap_or(0),
160 }
161 })
162 .collect())
163 }
164
165 pub async fn list_radius_profiles(&self) -> Result<Vec<RadiusProfile>, CoreError> {
166 let (client, site_id) = integration_site_context(self, "list_radius_profiles").await?;
167 let raw = client
168 .paginate_all(200, |offset, limit| {
169 client.list_radius_profiles(&site_id, offset, limit)
170 })
171 .await?;
172 Ok(raw
173 .into_iter()
174 .map(|profile| RadiusProfile {
175 id: parse_integration_entity_id(&profile.fields),
176 name: profile
177 .fields
178 .get("name")
179 .and_then(|value| value.as_str())
180 .unwrap_or("Unknown")
181 .to_owned(),
182 })
183 .collect())
184 }
185
186 pub async fn list_countries(&self) -> Result<Vec<Country>, CoreError> {
187 let client = integration_client_context(self, "list_countries").await?;
188 let raw = client
189 .paginate_all(200, |offset, limit| client.list_countries(offset, limit))
190 .await?;
191 Ok(raw
192 .into_iter()
193 .map(|country| Country {
194 code: country
195 .fields
196 .get("code")
197 .and_then(|value| value.as_str())
198 .unwrap_or("")
199 .to_owned(),
200 name: country
201 .fields
202 .get("name")
203 .and_then(|value| value.as_str())
204 .unwrap_or("Unknown")
205 .to_owned(),
206 })
207 .collect())
208 }
209
210 pub async fn get_network_references(
211 &self,
212 network_id: &EntityId,
213 ) -> Result<serde_json::Value, CoreError> {
214 let (client, site_id) = integration_site_context(self, "get_network_references").await?;
215 let uuid = require_uuid(network_id)?;
216 let refs = client.get_network_references(&site_id, &uuid).await?;
217 Ok(serde_json::to_value(refs).unwrap_or_default())
218 }
219}
220
221fn parse_integration_entity_id(
222 fields: &std::collections::HashMap<String, serde_json::Value>,
223) -> EntityId {
224 fields
225 .get("id")
226 .and_then(|value| value.as_str())
227 .and_then(|value| uuid::Uuid::parse_str(value).ok())
228 .map_or_else(|| EntityId::Legacy("unknown".into()), EntityId::Uuid)
229}
230
231fn parse_vpn_server(fields: &std::collections::HashMap<String, serde_json::Value>) -> VpnServer {
232 VpnServer {
233 id: parse_integration_entity_id(fields),
234 name: field_string(fields, &["name"]),
235 server_type: field_string(fields, &["type", "serverType"])
236 .unwrap_or_else(|| "UNKNOWN".into()),
237 enabled: field_bool(fields, &["enabled"]),
238 subnet: field_string(fields, &["subnet", "addressRange"]),
239 port: field_u16(fields, &["port"]),
240 wan_ip: field_string(fields, &["wanIp", "wanIP", "wanAddress"]),
241 connected_clients: field_u32(
242 fields,
243 &["connectedClients", "connectedClientCount", "numClients"],
244 ),
245 protocol: field_string(fields, &["protocol", "transportProtocol"]),
246 extra: collect_extra(fields),
247 }
248}
249
250fn parse_vpn_tunnel(fields: &std::collections::HashMap<String, serde_json::Value>) -> VpnTunnel {
251 let local_subnets = field_string_list(fields, &["localNetworks", "localSubnets"]);
252 let remote_subnets = field_string_list(fields, &["remoteNetworks", "remoteSubnets"]);
253
254 VpnTunnel {
255 id: parse_integration_entity_id(fields),
256 name: field_string(fields, &["name"]),
257 tunnel_type: field_string(fields, &["type", "tunnelType"])
258 .unwrap_or_else(|| "UNKNOWN".into()),
259 enabled: field_bool(fields, &["enabled"]),
260 peer_address: field_string(
261 fields,
262 &["peerIp", "peerAddress", "remoteAddress", "remoteHost"],
263 ),
264 local_subnets,
265 remote_subnets,
266 has_psk: fields
267 .get("psk")
268 .or_else(|| fields.get("preSharedKey"))
269 .is_some_and(|value| !value.is_null()),
270 ike_version: field_string(fields, &["ikeVersion", "ike"]),
271 extra: collect_extra(fields),
272 }
273}
274
275fn field_string(
276 fields: &std::collections::HashMap<String, serde_json::Value>,
277 keys: &[&str],
278) -> Option<String> {
279 keys.iter().find_map(|key| {
280 fields.get(*key).and_then(|value| match value {
281 serde_json::Value::String(value) => Some(value.clone()),
282 serde_json::Value::Number(value) => Some(value.to_string()),
283 _ => None,
284 })
285 })
286}
287
288fn field_bool(
289 fields: &std::collections::HashMap<String, serde_json::Value>,
290 keys: &[&str],
291) -> Option<bool> {
292 keys.iter()
293 .find_map(|key| fields.get(*key).and_then(serde_json::Value::as_bool))
294}
295
296fn field_u16(
297 fields: &std::collections::HashMap<String, serde_json::Value>,
298 keys: &[&str],
299) -> Option<u16> {
300 keys.iter().find_map(|key| {
301 fields
302 .get(*key)
303 .and_then(serde_json::Value::as_u64)
304 .and_then(|value| u16::try_from(value).ok())
305 })
306}
307
308fn field_u32(
309 fields: &std::collections::HashMap<String, serde_json::Value>,
310 keys: &[&str],
311) -> Option<u32> {
312 keys.iter().find_map(|key| {
313 fields
314 .get(*key)
315 .and_then(serde_json::Value::as_u64)
316 .and_then(|value| u32::try_from(value).ok())
317 })
318}
319
320fn field_string_list(
321 fields: &std::collections::HashMap<String, serde_json::Value>,
322 keys: &[&str],
323) -> Vec<String> {
324 keys.iter()
325 .filter_map(|key| fields.get(*key))
326 .flat_map(|value| match value {
327 serde_json::Value::String(value) => vec![value.clone()],
328 serde_json::Value::Array(values) => values
329 .iter()
330 .filter_map(|value| match value {
331 serde_json::Value::String(value) => Some(value.clone()),
332 serde_json::Value::Number(value) => Some(value.to_string()),
333 _ => None,
334 })
335 .collect(),
336 _ => Vec::new(),
337 })
338 .collect()
339}
340
341fn collect_extra(
342 fields: &std::collections::HashMap<String, serde_json::Value>,
343) -> serde_json::Map<String, serde_json::Value> {
344 fields
345 .iter()
346 .map(|(key, value)| (key.clone(), value.clone()))
347 .collect()
348}
349
350#[cfg(test)]
351mod tests {
352 use super::{
353 field_string_list, parse_integration_entity_id, parse_vpn_server, parse_vpn_tunnel,
354 };
355 use crate::EntityId;
356
357 #[test]
358 fn parse_integration_entity_id_prefers_valid_uuid() {
359 let fields: std::collections::HashMap<String, serde_json::Value> =
360 serde_json::from_value(serde_json::json!({
361 "id": "11111111-1111-1111-1111-111111111111"
362 }))
363 .expect("hash map");
364
365 let id = parse_integration_entity_id(&fields);
366 assert_eq!(id.to_string(), "11111111-1111-1111-1111-111111111111");
367 assert!(id.as_uuid().is_some());
368 }
369
370 #[test]
371 fn parse_integration_entity_id_falls_back_to_legacy_unknown() {
372 let fields: std::collections::HashMap<String, serde_json::Value> =
373 serde_json::from_value(serde_json::json!({})).expect("hash map");
374 let id = parse_integration_entity_id(&fields);
375
376 assert_eq!(id, EntityId::Legacy("unknown".into()));
377 }
378
379 #[test]
380 fn parse_vpn_server_uses_fallback_fields() {
381 let fields: std::collections::HashMap<String, serde_json::Value> =
382 serde_json::from_value(serde_json::json!({
383 "id": "11111111-1111-1111-1111-111111111111",
384 "name": "Home WG",
385 "serverType": "WIREGUARD",
386 "enabled": true,
387 "addressRange": "10.8.0.0/24",
388 "port": 51820,
389 "wanAddress": "198.51.100.10",
390 "numClients": 4,
391 "transportProtocol": "UDP"
392 }))
393 .expect("hash map");
394
395 let server = parse_vpn_server(&fields);
396
397 assert_eq!(server.name.as_deref(), Some("Home WG"));
398 assert_eq!(server.server_type, "WIREGUARD");
399 assert_eq!(server.subnet.as_deref(), Some("10.8.0.0/24"));
400 assert_eq!(server.port, Some(51_820));
401 assert_eq!(server.wan_ip.as_deref(), Some("198.51.100.10"));
402 assert_eq!(server.connected_clients, Some(4));
403 assert_eq!(server.protocol.as_deref(), Some("UDP"));
404 assert_eq!(server.extra.len(), fields.len());
405 }
406
407 #[test]
408 fn parse_vpn_tunnel_combines_subnet_aliases() {
409 let fields: std::collections::HashMap<String, serde_json::Value> =
410 serde_json::from_value(serde_json::json!({
411 "id": "22222222-2222-2222-2222-222222222222",
412 "name": "Branch Tunnel",
413 "tunnelType": "IPSEC",
414 "enabled": true,
415 "remoteAddress": "203.0.113.20",
416 "localNetworks": ["10.0.0.0/24"],
417 "localSubnets": ["10.0.1.0/24"],
418 "remoteNetworks": ["10.10.0.0/24"],
419 "remoteSubnets": ["10.10.1.0/24"],
420 "preSharedKey": "redacted",
421 "ike": 2
422 }))
423 .expect("hash map");
424
425 let tunnel = parse_vpn_tunnel(&fields);
426
427 assert_eq!(tunnel.name.as_deref(), Some("Branch Tunnel"));
428 assert_eq!(tunnel.tunnel_type, "IPSEC");
429 assert_eq!(tunnel.peer_address.as_deref(), Some("203.0.113.20"));
430 assert_eq!(
431 tunnel.local_subnets,
432 vec!["10.0.0.0/24".to_string(), "10.0.1.0/24".to_string()]
433 );
434 assert_eq!(
435 tunnel.remote_subnets,
436 vec!["10.10.0.0/24".to_string(), "10.10.1.0/24".to_string()]
437 );
438 assert!(tunnel.has_psk);
439 assert_eq!(tunnel.ike_version.as_deref(), Some("2"));
440 assert_eq!(tunnel.extra.len(), fields.len());
441 }
442
443 #[test]
444 fn field_string_list_accepts_scalar_and_array_values() {
445 let fields: std::collections::HashMap<String, serde_json::Value> =
446 serde_json::from_value(serde_json::json!({
447 "localNetworks": "10.0.0.0/24",
448 "localSubnets": ["10.0.1.0/24", 42]
449 }))
450 .expect("hash map");
451
452 let values = field_string_list(&fields, &["localNetworks", "localSubnets"]);
453
454 assert_eq!(
455 values,
456 vec![
457 "10.0.0.0/24".to_string(),
458 "10.0.1.0/24".to_string(),
459 "42".to_string()
460 ]
461 );
462 }
463}