lightstreamer_rs/connection/details.rs
1use crate::client::ClientListener;
2use crate::utils::LightstreamerError;
3use std::fmt::{self, Debug, Formatter};
4
5/// Used by `LightstreamerClient` to provide a basic connection properties data object.
6///
7/// Data object that contains the configuration settings needed to connect to a Lightstreamer Server.
8///
9/// An instance of this class is attached to every `LightstreamerClient` as `LightstreamerClient.connectionDetails`
10///
11/// See also `LightstreamerClient`
12#[derive(Default)]
13pub struct ConnectionDetails {
14 adapter_set: Option<String>,
15 client_ip: Option<String>,
16 server_address: Option<String>,
17 server_instance_address: Option<String>,
18 server_socket_name: Option<String>,
19 session_id: Option<String>,
20 user: Option<String>,
21 password: Option<String>,
22 listeners: Vec<Box<dyn ClientListener>>,
23}
24
25impl ConnectionDetails {
26 /// Inquiry method that gets the name of the Adapter Set (which defines the Metadata Adapter
27 /// and one or several Data Adapters) mounted on Lightstreamer Server that supply all the
28 /// items used in this application.
29 ///
30 /// # Returns
31 ///
32 /// The name of the Adapter Set; returns `None` if no name has been configured, that means
33 /// that the "DEFAULT" Adapter Set is used.
34 ///
35 /// See also `setAdapterSet()`
36 pub fn get_adapter_set(&self) -> Option<&String> {
37 self.adapter_set.as_ref()
38 }
39
40 /// Inquiry method that gets the IP address of this client as seen by the Server which is
41 /// serving the current session as the client remote address (note that it may not correspond
42 /// to the client host; for instance it may refer to an intermediate proxy). If, upon a new
43 /// session, this address changes, it may be a hint that the intermediary network nodes handling
44 /// the connection have changed, hence the network capabilities may be different. The library
45 /// uses this information to optimize the connection.
46 ///
47 /// Note that in case of polling or in case rebind requests are needed, subsequent requests
48 /// related to the same session may, in principle, expose a different IP address to the Server;
49 /// these changes would not be reported.
50 ///
51 /// If a session is not currently active, `None` is returned; soon after a session is established,
52 /// the value may become available; but it is possible that this information is not provided
53 /// by the Server and that it will never be available.
54 ///
55 /// A change to this setting will be notified through a call to `ClientListener.onPropertyChange()`
56 /// with argument "clientIp" on any `ClientListener` listening to the related `LightstreamerClient`.
57 ///
58 /// # Returns
59 ///
60 /// A canonical representation of an IP address (it can be either IPv4 or IPv6), or `None`.
61 pub fn get_client_ip(&self) -> Option<&String> {
62 self.client_ip.as_ref()
63 }
64
65 /// Retrieves a reference to the password, if set.
66 ///
67 /// This method is crucial for accessing sensitive information in a controlled manner. It returns
68 /// an immutable reference to the password, encapsulated within an `Option`. The use of `Option`
69 /// signifies that the password may or may not be present, thus providing flexibility in scenarios
70 /// where a password is optional. By returning a reference, we avoid unnecessary cloning of the
71 /// password data, which could have security implications and also incur a performance cost.
72 ///
73 /// # Returns
74 /// An `Option` containing a reference to the password `String` if it exists, or `None` if the
75 /// password has not been set. This allows calling code to handle the presence or absence of a
76 /// password appropriately without risking exposure of the password itself.
77 pub fn get_password(&self) -> Option<&String> {
78 self.password.as_ref()
79 }
80
81 /// Inquiry method that gets the configured address of Lightstreamer Server.
82 ///
83 /// # Returns
84 ///
85 /// The configured address of Lightstreamer Server.
86 pub fn get_server_address(&self) -> Option<&String> {
87 self.server_address.as_ref()
88 }
89
90 /// Inquiry method that gets the server address to be used to issue all requests related to
91 /// the current session. In fact, when a Server cluster is in place, the Server address specified
92 /// through `setServerAddress()` can identify various Server instances; in order to ensure that
93 /// all requests related to a session are issued to the same Server instance, the Server can
94 /// answer to the session opening request by providing an address which uniquely identifies
95 /// its own instance. When this is the case, this address is returned by the method; otherwise,
96 /// `None` is returned.
97 ///
98 /// Note that the addresses will always have the `http:` or `https:` scheme. In case WebSockets
99 /// are used, the specified scheme is internally converted to match the related WebSocket protocol
100 /// (i.e. `http` becomes `ws` while `https` becomes `wss`).
101 ///
102 /// Server Clustering is an optional feature, available depending on Edition and License Type.
103 /// To know what features are enabled by your license, please see the License tab of the Monitoring
104 /// Dashboard (by default, available at /dashboard).
105 ///
106 /// The method gives a meaningful answer only when a session is currently active.
107 ///
108 /// A change to this setting will be notified through a call to `ClientListener.onPropertyChange()`
109 /// with argument "serverInstanceAddress" on any `ClientListener` listening to the related
110 /// `LightstreamerClient`.
111 ///
112 /// # Returns
113 ///
114 /// Address used to issue all requests related to the current session.
115 pub fn get_server_instance_address(&self) -> Option<&String> {
116 self.server_instance_address.as_ref()
117 }
118
119 /// Inquiry method that gets the instance name of the Server which is serving the current session.
120 /// To be more precise, each answering port configured on a Server instance (through a `<http_server>`
121 /// or `<https_server>` element in the Server configuration file) can be given a different name;
122 /// the name related to the port to which the session opening request has been issued is returned.
123 ///
124 /// Note that each rebind to the same session can, potentially, reach the Server on a port different
125 /// than the one used for the previous request, depending on the behavior of intermediate nodes.
126 /// However, the only meaningful case is when a Server cluster is in place and it is configured
127 /// in such a way that the port used for all `bind_session` requests differs from the port used
128 /// for the initial `create_session` request.
129 ///
130 /// Server Clustering is an optional feature, available depending on Edition and License Type.
131 /// To know what features are enabled by your license, please see the License tab of the Monitoring
132 /// Dashboard (by default, available at /dashboard).
133 ///
134 /// If a session is not currently active, `None` is returned; soon after a session is established,
135 /// the value will become available.
136 ///
137 /// A change to this setting will be notified through a call to `ClientListener.onPropertyChange()`
138 /// with argument "serverSocketName" on any `ClientListener` listening to the related `LightstreamerClient`.
139 ///
140 /// # Returns
141 ///
142 /// Name configured for the Server instance which is managing the current session, or `None`.
143 pub fn get_server_socket_name(&self) -> Option<&String> {
144 self.server_socket_name.as_ref()
145 }
146
147 /// Inquiry method that gets the ID associated by the server to this client session.
148 ///
149 /// The method gives a meaningful answer only when a session is currently active.
150 ///
151 /// A change to this setting will be notified through a call to `ClientListener.onPropertyChange()`
152 /// with argument "sessionId" on any `ClientListener` listening to the related `LightstreamerClient`.
153 ///
154 /// # Returns
155 ///
156 /// ID assigned by the Server to this client session.
157 pub fn get_session_id(&self) -> Option<&String> {
158 self.session_id.as_ref()
159 }
160
161 /// Inquiry method that gets the username to be used for the authentication on Lightstreamer
162 /// Server when initiating the session.
163 ///
164 /// # Returns
165 ///
166 /// The username to be used for the authentication on Lightstreamer Server; returns `None`
167 /// if no user name has been configured.
168 pub fn get_user(&self) -> Option<&String> {
169 self.user.as_ref()
170 }
171
172 /// Creates a new ConnectionDetails object with default values.
173 ///
174 /// # Errors
175 ///
176 /// Returns `LightstreamerError::InvalidArgument` if the server address is invalid.
177 pub fn new(
178 server_address: Option<&str>,
179 adapter_set: Option<&str>,
180 user: Option<&str>,
181 password: Option<&str>,
182 ) -> Result<ConnectionDetails, LightstreamerError> {
183 let mut connection_details = ConnectionDetails::default();
184 connection_details.set_server_address(server_address.map(|s| s.to_string()))?;
185 connection_details.set_adapter_set(adapter_set.map(|s| s.to_string()));
186 connection_details.set_user(user.map(|s| s.to_string()));
187 connection_details.set_password(password.map(|s| s.to_string()));
188
189 Ok(connection_details)
190 }
191
192 /// Setter method that sets the name of the Adapter Set mounted on Lightstreamer Server to
193 /// be used to handle all requests in the session.
194 ///
195 /// An Adapter Set defines the Metadata Adapter and one or several Data Adapters. It is configured
196 /// on the server side through an "adapters.xml" file; the name is configured through the "id"
197 /// attribute in the `<adapters_conf>` element.
198 ///
199 /// The default Adapter Set, configured as "DEFAULT" on the Server.
200 ///
201 /// The Adapter Set name should be set on the `LightstreamerClient.connectionDetails` object
202 /// before calling the `LightstreamerClient.connect()` method. However, the value can be changed
203 /// at any time: the supplied value will be used for the next time a new session is requested
204 /// to the server.
205 ///
206 /// This setting can also be specified in the `LightstreamerClient` constructor.
207 ///
208 /// A change to this setting will be notified through a call to `ClientListener.onPropertyChange()`
209 /// with argument "adapterSet" on any `ClientListener` listening to the related `LightstreamerClient`.
210 ///
211 /// # Parameters
212 ///
213 /// * `adapter_set`: The name of the Adapter Set to be used. A `None` value is equivalent to
214 /// the "DEFAULT" name.
215 pub fn set_adapter_set(&mut self, adapter_set: Option<String>) {
216 self.adapter_set = Some(adapter_set.unwrap_or_else(|| "DEFAULT".to_string()));
217
218 // Notify listeners about the property change
219 for listener in &self.listeners {
220 listener.on_property_change("adapterSet");
221 }
222 }
223
224 /// Setter method that sets the password to be used for the authentication on Lightstreamer
225 /// Server when initiating the session. The Metadata Adapter is responsible for checking the
226 /// credentials (username and password).
227 ///
228 /// If no password is supplied, no password information will be sent at session initiation.
229 /// The Metadata Adapter, however, may still allow the session.
230 ///
231 /// The password should be set on the `LightstreamerClient.connectionDetails` object before
232 /// calling the `LightstreamerClient.connect()` method. However, the value can be changed at
233 /// any time: the supplied value will be used for the next time a new session is requested to
234 /// the server.
235 ///
236 /// NOTE: The password string will be stored in the current instance. That is necessary in order
237 /// to allow automatic reconnection/reauthentication for fail-over. For maximum security, avoid
238 /// using an actual private password to authenticate on Lightstreamer Server; rather use a session-id
239 /// originated by your web/application server, that can be checked by your Metadata Adapter.
240 ///
241 /// A change to this setting will be notified through a call to `ClientListener.onPropertyChange()`
242 /// with argument "password" on any `ClientListener` listening to the related `LightstreamerClient`.
243 ///
244 /// # Parameters
245 ///
246 /// * `password`: The password to be used for the authentication on Lightstreamer Server. The
247 /// password can be `None`.
248 ///
249 /// See also `setUser()`
250 pub fn set_password(&mut self, password: Option<String>) {
251 self.password = password;
252
253 // Notify listeners about the property change
254 for listener in &self.listeners {
255 listener.on_property_change("password");
256 }
257 }
258
259 /// Setter method that sets the address of Lightstreamer Server.
260 ///
261 /// Note that the addresses specified must always have the `http:` or `https:` scheme. In case
262 /// WebSockets are used, the specified scheme is internally converted to match the related WebSocket
263 /// protocol (i.e. `http` becomes `ws` while `https` becomes `wss`).
264 ///
265 /// WSS/HTTPS is an optional feature, available depending on Edition and License Type. To know
266 /// what features are enabled by your license, please see the License tab of the Monitoring
267 /// Dashboard (by default, available at /dashboard).
268 ///
269 /// If no server address is supplied the client will be unable to connect.
270 ///
271 /// This method can be called at any time. If called while connected, it will be applied when
272 /// the next session creation request is issued. This setting can also be specified in the
273 /// `LightstreamerClient` constructor.
274 ///
275 /// A change to this setting will be notified through a call to `ClientListener.onPropertyChange()`
276 /// with argument "serverAddress" on any `ClientListener` listening to the related `LightstreamerClient`.
277 ///
278 /// # Parameters
279 ///
280 /// * `server_address`: The full address of Lightstreamer Server. A `None` value can also be
281 /// used, to restore the default value.
282 ///
283 /// An IPv4 or IPv6 can also be used in place of a hostname. Some examples of valid values include:
284 ///
285 /// - `http://push.mycompany.com`
286 /// - `http://push.mycompany.com:8080`
287 /// - `http://79.125.7.252`
288 /// - `http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]`
289 /// - `http://[2001:0db8:85a3::8a2e:0370:7334]:8080`
290 ///
291 /// # Errors
292 ///
293 /// Returns `LightstreamerError::InvalidArgument` if the given address is not valid.
294 pub fn set_server_address(
295 &mut self,
296 server_address: Option<String>,
297 ) -> Result<(), LightstreamerError> {
298 // Validate the server address
299 if let Some(address) = &server_address
300 && !address.starts_with("http://")
301 && !address.starts_with("https://")
302 {
303 return Err(LightstreamerError::invalid_argument(
304 "Invalid server address: must start with http:// or https://",
305 ));
306 }
307
308 self.server_address = server_address;
309
310 // Notify listeners about the property change
311 for listener in &self.listeners {
312 listener.on_property_change("serverAddress");
313 }
314
315 Ok(())
316 }
317
318 /// Setter method that sets the username to be used for the authentication on Lightstreamer
319 /// Server when initiating the session. The Metadata Adapter is responsible for checking the
320 /// credentials (username and password).
321 ///
322 /// If no username is supplied, no user information will be sent at session initiation. The
323 /// Metadata Adapter, however, may still allow the session.
324 ///
325 /// The username should be set on the `LightstreamerClient.connectionDetails` object before
326 /// calling the `LightstreamerClient.connect()` method. However, the value can be changed at
327 /// any time: the supplied value will be used for the next time a new session is requested to
328 /// the server.
329 ///
330 /// A change to this setting will be notified through a call to `ClientListener.onPropertyChange()`
331 /// with argument "user" on any `ClientListener` listening to the related `LightstreamerClient`.
332 ///
333 /// # Parameters
334 ///
335 /// * `user`: The username to be used for the authentication on Lightstreamer Server. The username
336 /// can be `None`.
337 ///
338 /// See also `setPassword()`
339 pub fn set_user(&mut self, user: Option<String>) {
340 self.user = user;
341
342 // Notify listeners about the property change
343 for listener in &self.listeners {
344 listener.on_property_change("user");
345 }
346 }
347
348 /// Adds a listener that will receive events related to changes in the `ConnectionDetails`.
349 ///
350 /// The same listener can be added to multiple instances of `ConnectionDetails`.
351 ///
352 /// # Parameters
353 ///
354 /// * `listener`: An object that will receive the events as documented in the `ClientListener`
355 /// interface.
356 pub fn add_listener(&mut self, listener: Box<dyn ClientListener>) {
357 self.listeners.push(listener);
358 }
359
360 /// Removes a listener from the `ConnectionDetails` instance so that it will not receive events
361 /// anymore.
362 ///
363 /// # Parameters
364 ///
365 /// * `listener`: The listener to be removed.
366 pub fn remove_listener(&mut self, _listener: Box<dyn ClientListener>) {
367 unimplemented!("Implement mechanism to remove listener from ConnectionDetails.");
368 //self.listeners.remove(&listener);
369 }
370}
371
372impl Debug for ConnectionDetails {
373 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
374 f.debug_struct("ConnectionDetails")
375 .field("adapter_set", &self.adapter_set)
376 .field("client_ip", &self.client_ip)
377 .field("server_address", &self.server_address)
378 .field("server_instance_address", &self.server_instance_address)
379 .field("server_socket_name", &self.server_socket_name)
380 .field("session_id", &self.session_id)
381 .field("user", &self.user)
382 .field("password", &self.password)
383 .finish()
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use std::fmt::Debug;
391
392 #[derive(Debug)]
393 struct MockClientListener {
394 property_changes: std::cell::RefCell<Vec<String>>,
395 }
396
397 impl MockClientListener {
398 fn new() -> Self {
399 MockClientListener {
400 property_changes: std::cell::RefCell::new(Vec::new()),
401 }
402 }
403
404 fn get_property_changes(&self) -> Vec<String> {
405 self.property_changes.borrow().clone()
406 }
407 }
408
409 impl ClientListener for MockClientListener {
410 fn on_property_change(&self, property: &str) {
411 self.property_changes
412 .borrow_mut()
413 .push(property.to_string());
414 }
415 }
416
417 #[test]
418 fn test_new_connection_details() -> Result<(), LightstreamerError> {
419 // Test with all values provided
420 let details = ConnectionDetails::new(
421 Some("http://test.lightstreamer.com"),
422 Some("DEMO"),
423 Some("user1"),
424 Some("pass1"),
425 )?;
426 assert_eq!(
427 details.get_server_address(),
428 Some(&"http://test.lightstreamer.com".to_string())
429 );
430 assert_eq!(details.get_adapter_set(), Some(&"DEMO".to_string()));
431 assert_eq!(details.get_user(), Some(&"user1".to_string()));
432 assert_eq!(details.get_password(), Some(&"pass1".to_string()));
433
434 // Test with only mandatory values
435 let details =
436 ConnectionDetails::new(Some("http://test.lightstreamer.com"), None, None, None)?;
437 assert_eq!(
438 details.get_server_address(),
439 Some(&"http://test.lightstreamer.com".to_string())
440 );
441 assert_eq!(details.get_adapter_set(), Some(&"DEFAULT".to_string())); // Default value
442 assert_eq!(details.get_user(), None);
443 assert_eq!(details.get_password(), None);
444
445 // Test with invalid server address
446 let result = ConnectionDetails::new(Some("invalid-url"), None, None, None);
447 assert!(result.is_err());
448 Ok(())
449 }
450
451 #[test]
452 fn test_set_server_address() {
453 let mut details = ConnectionDetails::default();
454
455 // Test valid HTTP URL
456 assert!(
457 details
458 .set_server_address(Some("http://test.lightstreamer.com".to_string()))
459 .is_ok()
460 );
461 assert_eq!(
462 details.get_server_address(),
463 Some(&"http://test.lightstreamer.com".to_string())
464 );
465
466 // Test valid HTTPS URL
467 assert!(
468 details
469 .set_server_address(Some("https://test.lightstreamer.com".to_string()))
470 .is_ok()
471 );
472 assert_eq!(
473 details.get_server_address(),
474 Some(&"https://test.lightstreamer.com".to_string())
475 );
476
477 // Test with port
478 assert!(
479 details
480 .set_server_address(Some("https://test.lightstreamer.com:8080".to_string()))
481 .is_ok()
482 );
483 assert_eq!(
484 details.get_server_address(),
485 Some(&"https://test.lightstreamer.com:8080".to_string())
486 );
487
488 // Test invalid URL (missing http:// or https://)
489 assert!(
490 details
491 .set_server_address(Some("test.lightstreamer.com".to_string()))
492 .is_err()
493 );
494
495 // Test None value
496 assert!(details.set_server_address(None).is_ok());
497 assert_eq!(details.get_server_address(), None);
498 }
499
500 #[test]
501 fn test_set_adapter_set() {
502 let mut details = ConnectionDetails::default();
503
504 // Test setting adapter set
505 details.set_adapter_set(Some("TEST_ADAPTER".to_string()));
506 assert_eq!(details.get_adapter_set(), Some(&"TEST_ADAPTER".to_string()));
507
508 // Test setting None (should default to "DEFAULT")
509 details.set_adapter_set(None);
510 assert_eq!(details.get_adapter_set(), Some(&"DEFAULT".to_string()));
511 }
512
513 #[test]
514 fn test_set_user_and_password() {
515 let mut details = ConnectionDetails::default();
516
517 // Test setting user
518 details.set_user(Some("test_user".to_string()));
519 assert_eq!(details.get_user(), Some(&"test_user".to_string()));
520
521 // Test setting None for user
522 details.set_user(None);
523 assert_eq!(details.get_user(), None);
524
525 // Test setting password
526 details.set_password(Some("test_password".to_string()));
527 assert_eq!(details.get_password(), Some(&"test_password".to_string()));
528
529 // Test setting None for password
530 details.set_password(None);
531 assert_eq!(details.get_password(), None);
532 }
533
534 #[test]
535 fn test_property_change_notifications() {
536 let mut details = ConnectionDetails::default();
537 let listener = Box::new(MockClientListener::new());
538 let listener_ref = &*listener as &dyn ClientListener as *const _ as *mut MockClientListener;
539
540 details.add_listener(listener);
541
542 // Change server address and verify notification
543 assert!(
544 details
545 .set_server_address(Some("http://test.lightstreamer.com".to_string()))
546 .is_ok()
547 );
548
549 // Change adapter set and verify notification
550 details.set_adapter_set(Some("TEST_ADAPTER".to_string()));
551
552 // Change user and verify notification
553 details.set_user(Some("test_user".to_string()));
554
555 // Change password and verify notification
556 details.set_password(Some("test_password".to_string()));
557
558 // Get property changes from the listener
559 let changes = unsafe { &*listener_ref }.get_property_changes();
560
561 // Verify all property changes were notified
562 assert!(changes.contains(&"serverAddress".to_string()));
563 assert!(changes.contains(&"adapterSet".to_string()));
564 assert!(changes.contains(&"user".to_string()));
565 assert!(changes.contains(&"password".to_string()));
566 }
567
568 #[test]
569 fn test_default_connection_details() {
570 let details = ConnectionDetails::default();
571
572 assert_eq!(details.get_server_address(), None);
573 assert_eq!(details.get_adapter_set(), None);
574 assert_eq!(details.get_user(), None);
575 assert_eq!(details.get_password(), None);
576 assert_eq!(details.get_client_ip(), None);
577 assert_eq!(details.get_server_instance_address(), None);
578 assert_eq!(details.get_server_socket_name(), None);
579 assert_eq!(details.get_session_id(), None);
580 }
581}