Skip to main content

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}