Skip to main content

opcua_client/
config.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2024 Adam Lock
4
5//! Client configuration data.
6
7use std::{
8    self,
9    collections::BTreeMap,
10    path::{Path, PathBuf},
11    str::FromStr,
12    time::Duration,
13};
14
15use chrono::TimeDelta;
16use serde::{Deserialize, Serialize};
17use tracing::warn;
18
19use opcua_core::config::Config;
20use opcua_crypto::SecurityPolicy;
21use opcua_types::{
22    ApplicationType, EndpointDescription, Error, MessageSecurityMode, StatusCode, UAString,
23};
24
25use crate::{Client, IdentityToken, SessionRetryPolicy};
26
27/// Token ID of the anonymous user token.
28pub const ANONYMOUS_USER_TOKEN_ID: &str = "ANONYMOUS";
29
30#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
31/// User token in client configuration.
32pub struct ClientUserToken {
33    /// Username
34    pub user: String,
35    /// Password
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub password: Option<String>,
38    /// Certificate path for x509 authentication.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub cert_path: Option<String>,
41    /// Private key path for x509 authentication.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub private_key_path: Option<String>,
44}
45
46impl ClientUserToken {
47    /// Constructs a client token which holds a username and password.
48    pub fn user_pass<S, T>(user: S, password: T) -> Self
49    where
50        S: Into<String>,
51        T: Into<String>,
52    {
53        ClientUserToken {
54            user: user.into(),
55            password: Some(password.into()),
56            cert_path: None,
57            private_key_path: None,
58        }
59    }
60
61    /// Constructs a client token which holds a username and paths to X509 certificate and private key.
62    pub fn x509<S>(user: S, cert_path: &Path, private_key_path: &Path) -> Self
63    where
64        S: Into<String>,
65    {
66        // Apparently on Windows, a PathBuf can hold weird non-UTF chars but they will not
67        // be stored in a config file properly in any event, so this code will lossily strip them out.
68        ClientUserToken {
69            user: user.into(),
70            password: None,
71            cert_path: Some(cert_path.to_string_lossy().to_string()),
72            private_key_path: Some(private_key_path.to_string_lossy().to_string()),
73        }
74    }
75
76    /// Test if the token, i.e. that it has a name, and either a password OR a cert path and key path.
77    /// The paths are not validated.
78    pub fn validate(&self) -> Result<(), Vec<String>> {
79        let mut errors = Vec::new();
80        if self.user.is_empty() {
81            errors.push("User token has an empty name.".to_owned());
82        }
83        // A token must properly represent one kind of token or it is not valid
84        if self.password.is_some() {
85            if self.cert_path.is_some() || self.private_key_path.is_some() {
86                errors.push(format!(
87                    "User token {} holds a password and certificate info - it cannot be both.",
88                    self.user
89                ));
90            }
91        } else if self.cert_path.is_none() && self.private_key_path.is_none() {
92            errors.push(format!(
93                "User token {} fails to provide a password or certificate info.",
94                self.user
95            ));
96        } else if self.cert_path.is_none() || self.private_key_path.is_none() {
97            errors.push(format!(
98                "User token {} fails to provide both a certificate path and a private key path.",
99                self.user
100            ));
101        }
102        if errors.is_empty() {
103            Ok(())
104        } else {
105            Err(errors)
106        }
107    }
108}
109
110/// Describes an endpoint, it's url security policy, mode and user token
111#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
112pub struct ClientEndpoint {
113    /// Endpoint path
114    pub url: String,
115    /// Security policy
116    pub security_policy: String,
117    /// Security mode
118    pub security_mode: String,
119    /// User id to use with the endpoint
120    #[serde(default = "ClientEndpoint::anonymous_id")]
121    pub user_token_id: String,
122}
123
124impl ClientEndpoint {
125    /// Makes a client endpoint
126    pub fn new<T>(url: T) -> Self
127    where
128        T: Into<String>,
129    {
130        ClientEndpoint {
131            url: url.into(),
132            security_policy: SecurityPolicy::None.to_str().into(),
133            security_mode: MessageSecurityMode::None.into(),
134            user_token_id: Self::anonymous_id(),
135        }
136    }
137
138    fn anonymous_id() -> String {
139        ANONYMOUS_USER_TOKEN_ID.to_string()
140    }
141
142    /// Returns the security policy for this endpoint.
143    pub fn security_policy(&self) -> SecurityPolicy {
144        SecurityPolicy::from_str(&self.security_policy).unwrap_or(SecurityPolicy::Unknown)
145    }
146}
147
148#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
149pub(crate) struct DecodingOptions {
150    /// Maximum size of a message chunk in bytes. 0 means no limit
151    #[serde(default = "defaults::max_message_size")]
152    pub(crate) max_message_size: usize,
153    /// Maximum number of chunks in a message. 0 means no limit
154    #[serde(default = "defaults::max_chunk_count")]
155    pub(crate) max_chunk_count: usize,
156    /// Maximum size of each individual sent message chunk.
157    #[serde(default = "defaults::max_chunk_size")]
158    pub(crate) max_chunk_size: usize,
159    /// Maximum size of each received chunk.
160    #[serde(default = "defaults::max_incoming_chunk_size")]
161    pub(crate) max_incoming_chunk_size: usize,
162    /// Maximum length in bytes (not chars!) of a string. 0 actually means 0, i.e. no string permitted
163    #[serde(default = "defaults::max_string_length")]
164    pub(crate) max_string_length: usize,
165    /// Maximum length in bytes of a byte string. 0 actually means 0, i.e. no byte string permitted
166    #[serde(default = "defaults::max_byte_string_length")]
167    pub(crate) max_byte_string_length: usize,
168    /// Maximum number of array elements. 0 actually means 0, i.e. no array permitted
169    #[serde(default = "defaults::max_array_length")]
170    pub(crate) max_array_length: usize,
171}
172
173impl DecodingOptions {
174    pub(crate) fn as_comms_decoding_options(&self) -> opcua_types::DecodingOptions {
175        opcua_types::DecodingOptions {
176            max_chunk_count: self.max_chunk_count,
177            max_message_size: self.max_message_size,
178            max_string_length: self.max_string_length,
179            max_byte_string_length: self.max_byte_string_length,
180            max_array_length: self.max_array_length,
181            client_offset: TimeDelta::zero(),
182            ..Default::default()
183        }
184    }
185}
186
187impl Default for DecodingOptions {
188    fn default() -> Self {
189        Self {
190            max_message_size: defaults::max_message_size(),
191            max_chunk_count: defaults::max_chunk_count(),
192            max_chunk_size: defaults::max_chunk_size(),
193            max_incoming_chunk_size: defaults::max_incoming_chunk_size(),
194            max_string_length: defaults::max_string_length(),
195            max_byte_string_length: defaults::max_byte_string_length(),
196            max_array_length: defaults::max_array_length(),
197        }
198    }
199}
200
201#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
202pub(crate) struct Performance {
203    /// Ignore clock skew allows the client to make a successful connection to the server, even
204    /// when the client and server clocks are out of sync.
205    #[serde(default)]
206    pub(crate) ignore_clock_skew: bool,
207    /// Maximum number of monitored items per request when recreating subscriptions on session recreation.
208    #[serde(default = "defaults::recreate_monitored_items_chunk")]
209    pub(crate) recreate_monitored_items_chunk: usize,
210}
211
212impl Default for Performance {
213    fn default() -> Self {
214        Self {
215            ignore_clock_skew: false,
216            recreate_monitored_items_chunk: defaults::recreate_monitored_items_chunk(),
217        }
218    }
219}
220
221/// Client OPC UA configuration
222#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
223pub struct ClientConfig {
224    /// Name of the application that the client presents itself as to the server
225    pub(crate) application_name: String,
226    /// The application uri
227    pub(crate) application_uri: String,
228    /// Product uri
229    pub(crate) product_uri: String,
230    /// Autocreates public / private keypair if they don't exist. For testing/samples only
231    /// since you do not have control of the values
232    pub(crate) create_sample_keypair: bool,
233    /// Custom certificate path, to be used instead of the default .der certificate path
234    pub(crate) certificate_path: Option<PathBuf>,
235    /// Custom private key path, to be used instead of the default private key path
236    pub(crate) private_key_path: Option<PathBuf>,
237    /// Auto trusts server certificates. For testing/samples only unless you're sure what you're
238    /// doing.
239    pub(crate) trust_server_certs: bool,
240    /// Verify server certificates. For testing/samples only unless you're sure what you're
241    /// doing.
242    pub(crate) verify_server_certs: bool,
243    /// PKI folder, either absolute or relative to executable
244    pub(crate) pki_dir: PathBuf,
245    /// Preferred locales
246    pub(crate) preferred_locales: Vec<String>,
247    /// Identifier of the default endpoint
248    pub(crate) default_endpoint: String,
249    /// List of end points
250    pub(crate) endpoints: BTreeMap<String, ClientEndpoint>,
251    /// User tokens
252    pub(crate) user_tokens: BTreeMap<String, ClientUserToken>,
253    /// Length of the nonce generated for CreateSession requests.
254    #[serde(default = "defaults::session_nonce_length")]
255    pub(crate) session_nonce_length: usize,
256    /// Requested channel lifetime in milliseconds.
257    #[serde(default = "defaults::channel_lifetime")]
258    pub(crate) channel_lifetime: u32,
259    /// Decoding options used for serialization / deserialization
260    #[serde(default)]
261    pub(crate) decoding_options: DecodingOptions,
262    /// Maximum number of times to attempt to reconnect to the server before giving up.
263    /// -1 retries forever
264    #[serde(default = "defaults::session_retry_limit")]
265    pub(crate) session_retry_limit: i32,
266
267    /// Initial delay for exponential backoff when reconnecting to the server.
268    #[serde(default = "defaults::session_retry_initial")]
269    pub(crate) session_retry_initial: Duration,
270    /// Max delay between retry attempts.
271    #[serde(default = "defaults::session_retry_max")]
272    pub(crate) session_retry_max: Duration,
273    /// Interval between each keep-alive request sent to the server.
274    #[serde(default = "defaults::keep_alive_interval")]
275    pub(crate) keep_alive_interval: Duration,
276    /// Maximum number of failed keep alives before the client will be closed.
277    /// Note that this should not actually needed if the server is compliant,
278    /// only if the connection ends up in a bad state and needs to be
279    /// forcibly reset.
280    #[serde(default = "defaults::max_failed_keep_alive_count")]
281    pub(crate) max_failed_keep_alive_count: u64,
282
283    /// Timeout for each request sent to the server.
284    #[serde(default = "defaults::request_timeout")]
285    pub(crate) request_timeout: Duration,
286    /// Timeout for publish requests, separate from normal timeout since
287    /// subscriptions are often more time sensitive.
288    #[serde(default = "defaults::publish_timeout")]
289    pub(crate) publish_timeout: Duration,
290    /// Minimum publish interval. Setting this higher will make sure that subscriptions
291    /// publish together, which may reduce the number of publish requests if you have a lot of subscriptions.
292    #[serde(default = "defaults::min_publish_interval")]
293    pub(crate) min_publish_interval: Duration,
294
295    /// Client performance settings
296    #[serde(default)]
297    pub(crate) performance: Performance,
298    /// Automatically recreate subscriptions on reconnect, by first calling
299    /// `transfer_subscriptions`, then attempting to recreate subscriptions if that fails.
300    #[serde(default = "defaults::recreate_subscriptions")]
301    pub(crate) recreate_subscriptions: bool,
302    /// Session name
303    pub(crate) session_name: String,
304    /// Requested session timeout in milliseconds
305    #[serde(default = "defaults::session_timeout")]
306    pub(crate) session_timeout: u32,
307}
308
309impl Config for ClientConfig {
310    /// Test if the config is valid, which requires at the least that
311    fn validate(&self) -> Result<(), Vec<String>> {
312        let mut errors = Vec::new();
313
314        if self.application_name.is_empty() {
315            errors.push("Application name is empty".to_owned());
316        }
317        if self.application_uri.is_empty() {
318            errors.push("Application uri is empty".to_owned());
319        }
320        if self.user_tokens.contains_key(ANONYMOUS_USER_TOKEN_ID) {
321            errors.push(format!(
322                "User tokens contains the reserved \"{ANONYMOUS_USER_TOKEN_ID}\" id"
323            ));
324        }
325        if self.user_tokens.contains_key("") {
326            errors.push("User tokens contains an endpoint with an empty id".to_owned());
327        }
328        self.user_tokens.iter().for_each(|(k, token)| {
329            if let Err(e) = token.validate() {
330                errors.push(format!("Token {k} failed to validate: {}", e.join(", ")))
331            }
332        });
333        if self.endpoints.is_empty() {
334            warn!("Endpoint config contains no endpoints");
335        } else {
336            // Check for invalid ids in endpoints
337            if self.endpoints.contains_key("") {
338                errors.push("Endpoints contains an endpoint with an empty id".to_owned());
339            }
340            if !self.default_endpoint.is_empty()
341                && !self.endpoints.contains_key(&self.default_endpoint)
342            {
343                errors.push(format!(
344                    "Default endpoint id {} does not exist in list of endpoints",
345                    self.default_endpoint
346                ));
347            }
348            // Check for invalid security policy and modes in endpoints
349            self.endpoints.iter().for_each(|(id, e)| {
350                if SecurityPolicy::from_str(&e.security_policy).unwrap_or(SecurityPolicy::Unknown)
351                    != SecurityPolicy::Unknown
352                {
353                    if MessageSecurityMode::Invalid
354                        == MessageSecurityMode::from(e.security_mode.as_ref())
355                    {
356                        errors.push(format!(
357                            "Endpoint {} security mode {} is invalid",
358                            id, e.security_mode
359                        ));
360                    }
361                } else {
362                    errors.push(format!(
363                        "Endpoint {} security policy {} is invalid",
364                        id, e.security_policy
365                    ));
366                }
367            });
368        }
369        if self.session_retry_limit < 0 && self.session_retry_limit != -1 {
370            errors.push(format!("Session retry limit of {} is invalid - must be -1 (infinite), 0 (never) or a positive value", self.session_retry_limit));
371        }
372        if errors.is_empty() {
373            Ok(())
374        } else {
375            Err(errors)
376        }
377    }
378
379    fn application_name(&self) -> UAString {
380        UAString::from(&self.application_name)
381    }
382
383    fn application_uri(&self) -> UAString {
384        UAString::from(&self.application_uri)
385    }
386
387    fn product_uri(&self) -> UAString {
388        UAString::from(&self.product_uri)
389    }
390
391    fn application_type(&self) -> ApplicationType {
392        ApplicationType::Client
393    }
394}
395
396impl ClientConfig {
397    /// Get the configured session retry policy.
398    pub fn session_retry_policy(&self) -> SessionRetryPolicy {
399        SessionRetryPolicy::new(
400            self.session_retry_max,
401            if self.session_retry_limit < 0 {
402                None
403            } else {
404                Some(self.session_retry_limit as u32)
405            },
406            self.session_retry_initial,
407        )
408    }
409
410    /// Returns an identity token corresponding to the matching user in the configuration. Or None
411    /// if there is no matching token.
412    pub fn client_identity_token(
413        &self,
414        user_token_id: impl Into<String>,
415    ) -> Result<IdentityToken, Error> {
416        let user_token_id = user_token_id.into();
417        if user_token_id == ANONYMOUS_USER_TOKEN_ID {
418            Ok(IdentityToken::Anonymous)
419        } else {
420            let Some(token) = self.user_tokens.get(&user_token_id) else {
421                return Err(Error::new(
422                    StatusCode::BadInvalidArgument,
423                    format!("Requested user token: {user_token_id} not found in config",),
424                ));
425            };
426
427            if let Some(ref password) = token.password {
428                Ok(IdentityToken::UserName(token.user.clone(), password.into()))
429            } else if let Some(ref cert_path) = token.cert_path {
430                let Some(private_key_path) = &token.private_key_path else {
431                    return Err(Error::new(
432                        StatusCode::BadInvalidArgument,
433                        "Client identity token with certificate does not have a private key",
434                    ));
435                };
436                IdentityToken::new_x509_path(cert_path, private_key_path)
437            } else {
438                Err(Error::new(
439                    StatusCode::BadInvalidArgument,
440                    "Non-anonymous client identity token with neither password nor certificate",
441                ))
442            }
443        }
444    }
445
446    /// Creates a [`EndpointDescription`](EndpointDescription) information from the supplied client endpoint.
447    pub(super) fn endpoint_description_for_client_endpoint(
448        &self,
449        client_endpoint: &ClientEndpoint,
450        endpoints: &[EndpointDescription],
451    ) -> Result<EndpointDescription, Error> {
452        let security_policy =
453            SecurityPolicy::from_str(&client_endpoint.security_policy).map_err(|_| {
454                Error::new(
455                    StatusCode::BadSecurityPolicyRejected,
456                    format!(
457                        "Endpoint {} security policy {} is invalid",
458                        client_endpoint.url, client_endpoint.security_policy
459                    ),
460                )
461            })?;
462        let security_mode = MessageSecurityMode::from(client_endpoint.security_mode.as_ref());
463        if security_mode == MessageSecurityMode::Invalid {
464            return Err(Error::new(
465                StatusCode::BadSecurityModeRejected,
466                format!(
467                    "Endpoint {} security mode {} is invalid",
468                    client_endpoint.url, client_endpoint.security_mode
469                ),
470            ));
471        }
472        let endpoint_url = client_endpoint.url.clone();
473        let endpoint = Client::find_matching_endpoint(
474            endpoints,
475            &endpoint_url,
476            security_policy,
477            security_mode,
478        )
479        .ok_or_else(|| {
480            Error::new(
481                StatusCode::BadTcpEndpointUrlInvalid,
482                format!(
483                    "Endpoint {endpoint_url}, {security_policy:?} / {security_mode:?} does not match any supplied by the server"
484                ),
485            )
486        })?;
487
488        Ok(endpoint)
489    }
490}
491
492impl Default for ClientConfig {
493    fn default() -> Self {
494        Self::new("", "")
495    }
496}
497
498mod defaults {
499    use std::time::Duration;
500
501    use crate::retry::SessionRetryPolicy;
502
503    pub(super) fn verify_server_certs() -> bool {
504        true
505    }
506
507    pub(super) fn channel_lifetime() -> u32 {
508        60_000
509    }
510
511    pub(super) fn session_retry_limit() -> i32 {
512        SessionRetryPolicy::DEFAULT_RETRY_LIMIT as i32
513    }
514
515    pub(super) fn session_retry_initial() -> Duration {
516        Duration::from_secs(1)
517    }
518
519    pub(super) fn session_retry_max() -> Duration {
520        Duration::from_secs(30)
521    }
522
523    pub(super) fn keep_alive_interval() -> Duration {
524        Duration::from_secs(10)
525    }
526
527    pub(super) fn max_array_length() -> usize {
528        opcua_types::constants::MAX_ARRAY_LENGTH
529    }
530
531    pub(super) fn max_byte_string_length() -> usize {
532        opcua_types::constants::MAX_BYTE_STRING_LENGTH
533    }
534
535    pub(super) fn max_chunk_count() -> usize {
536        opcua_types::constants::MAX_CHUNK_COUNT
537    }
538
539    pub(super) fn max_chunk_size() -> usize {
540        65535
541    }
542
543    pub(super) fn max_failed_keep_alive_count() -> u64 {
544        0
545    }
546
547    pub(super) fn max_incoming_chunk_size() -> usize {
548        65535
549    }
550
551    pub(super) fn max_message_size() -> usize {
552        opcua_types::constants::MAX_MESSAGE_SIZE
553    }
554
555    pub(super) fn max_string_length() -> usize {
556        opcua_types::constants::MAX_STRING_LENGTH
557    }
558
559    pub(super) fn request_timeout() -> Duration {
560        Duration::from_secs(60)
561    }
562
563    pub(super) fn publish_timeout() -> Duration {
564        Duration::from_secs(60)
565    }
566
567    pub(super) fn min_publish_interval() -> Duration {
568        Duration::from_millis(100)
569    }
570
571    pub(super) fn recreate_monitored_items_chunk() -> usize {
572        1000
573    }
574
575    pub(super) fn recreate_subscriptions() -> bool {
576        true
577    }
578
579    pub(super) fn session_timeout() -> u32 {
580        60_000
581    }
582
583    pub(super) fn session_nonce_length() -> usize {
584        32
585    }
586}
587
588impl ClientConfig {
589    /// The default PKI directory
590    pub const PKI_DIR: &'static str = "pki";
591
592    /// Create a new default client config.
593    pub fn new(application_name: impl Into<String>, application_uri: impl Into<String>) -> Self {
594        let mut pki_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
595        pki_dir.push(Self::PKI_DIR);
596
597        ClientConfig {
598            application_name: application_name.into(),
599            application_uri: application_uri.into(),
600            product_uri: String::new(),
601            create_sample_keypair: false,
602            certificate_path: None,
603            private_key_path: None,
604            trust_server_certs: false,
605            verify_server_certs: defaults::verify_server_certs(),
606            pki_dir,
607            preferred_locales: Vec::new(),
608            default_endpoint: String::new(),
609            endpoints: BTreeMap::new(),
610            user_tokens: BTreeMap::new(),
611            channel_lifetime: defaults::channel_lifetime(),
612            decoding_options: DecodingOptions::default(),
613            session_retry_limit: defaults::session_retry_limit(),
614            session_retry_initial: defaults::session_retry_initial(),
615            session_retry_max: defaults::session_retry_max(),
616            keep_alive_interval: defaults::keep_alive_interval(),
617            max_failed_keep_alive_count: defaults::max_failed_keep_alive_count(),
618            request_timeout: defaults::request_timeout(),
619            publish_timeout: defaults::publish_timeout(),
620            min_publish_interval: defaults::min_publish_interval(),
621            performance: Performance::default(),
622            recreate_subscriptions: defaults::recreate_subscriptions(),
623            session_name: "Rust OPC UA Client".into(),
624            session_timeout: defaults::session_timeout(),
625            session_nonce_length: defaults::session_nonce_length(),
626        }
627    }
628}
629
630#[cfg(test)]
631mod tests {
632    use std::{self, collections::BTreeMap, path::PathBuf};
633
634    use crate::ClientBuilder;
635    use opcua_core::config::Config;
636    use opcua_crypto::SecurityPolicy;
637    use opcua_types::MessageSecurityMode;
638
639    use super::{ClientConfig, ClientEndpoint, ClientUserToken, ANONYMOUS_USER_TOKEN_ID};
640
641    fn make_test_file(filename: &str) -> PathBuf {
642        let mut path = std::env::temp_dir();
643        path.push(filename);
644        path
645    }
646
647    fn sample_builder() -> ClientBuilder {
648        ClientBuilder::new()
649            .application_name("OPC UA Sample Client")
650            .application_uri("urn:SampleClient")
651            .create_sample_keypair(true)
652            .certificate_path("own/cert.der")
653            .private_key_path("private/private.pem")
654            .trust_server_certs(true)
655            .pki_dir("./pki")
656            .endpoints(vec![
657                (
658                    "sample_none",
659                    ClientEndpoint {
660                        url: String::from("opc.tcp://127.0.0.1:4855/"),
661                        security_policy: String::from(SecurityPolicy::None.to_str()),
662                        security_mode: String::from(MessageSecurityMode::None),
663                        user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
664                    },
665                ),
666                (
667                    "sample_basic128rsa15",
668                    ClientEndpoint {
669                        url: String::from("opc.tcp://127.0.0.1:4855/"),
670                        security_policy: String::from(SecurityPolicy::Basic128Rsa15.to_str()),
671                        security_mode: String::from(MessageSecurityMode::SignAndEncrypt),
672                        user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
673                    },
674                ),
675                (
676                    "sample_basic256",
677                    ClientEndpoint {
678                        url: String::from("opc.tcp://127.0.0.1:4855/"),
679                        security_policy: String::from(SecurityPolicy::Basic256.to_str()),
680                        security_mode: String::from(MessageSecurityMode::SignAndEncrypt),
681                        user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
682                    },
683                ),
684                (
685                    "sample_basic256sha256",
686                    ClientEndpoint {
687                        url: String::from("opc.tcp://127.0.0.1:4855/"),
688                        security_policy: String::from(SecurityPolicy::Basic256Sha256.to_str()),
689                        security_mode: String::from(MessageSecurityMode::SignAndEncrypt),
690                        user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
691                    },
692                ),
693            ])
694            .default_endpoint("sample_none")
695            .user_token(
696                "sample_user",
697                ClientUserToken::user_pass("sample1", "sample1pwd"),
698            )
699            .user_token(
700                "sample_user2",
701                ClientUserToken::user_pass("sample2", "sample2pwd"),
702            )
703    }
704
705    fn default_sample_config() -> ClientConfig {
706        sample_builder().into_config()
707    }
708
709    #[test]
710    fn client_sample_config() {
711        // This test exists to create the samples/client.conf file
712        // This test only exists to dump a sample config
713        let config = default_sample_config();
714        let mut path = std::env::current_dir().unwrap();
715        path.push("..");
716        path.push("samples");
717        path.push("client.conf");
718        println!("Path is {path:?}");
719
720        let saved = config.save(&path);
721        println!("Saved = {saved:?}");
722        assert!(saved.is_ok());
723        config.validate().unwrap();
724    }
725
726    #[test]
727    fn client_config() {
728        let path = make_test_file("client_config.yaml");
729        println!("Client path = {path:?}");
730        let config = default_sample_config();
731        let saved = config.save(&path);
732        println!("Saved = {saved:?}");
733        assert!(config.save(&path).is_ok());
734        if let Ok(config2) = ClientConfig::load(&path) {
735            assert_eq!(config, config2);
736        } else {
737            panic!("Cannot load config from file");
738        }
739    }
740
741    #[test]
742    fn client_invalid_security_policy_config() {
743        let mut config = default_sample_config();
744        // Security policy is wrong
745        config.endpoints = BTreeMap::new();
746        config.endpoints.insert(
747            String::from("sample_none"),
748            ClientEndpoint {
749                url: String::from("opc.tcp://127.0.0.1:4855"),
750                security_policy: String::from("http://blah"),
751                security_mode: String::from(MessageSecurityMode::None),
752                user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
753            },
754        );
755        assert_eq!(
756            config.validate().unwrap_err().join(", "),
757            "Endpoint sample_none security policy http://blah is invalid"
758        );
759    }
760
761    #[test]
762    fn client_invalid_security_mode_config() {
763        let mut config = default_sample_config();
764        // Message security mode is wrong
765        config.endpoints = BTreeMap::new();
766        config.endpoints.insert(
767            String::from("sample_none"),
768            ClientEndpoint {
769                url: String::from("opc.tcp://127.0.0.1:4855"),
770                security_policy: String::from(SecurityPolicy::Basic128Rsa15.to_uri()),
771                security_mode: String::from("SingAndEncrypt"),
772                user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
773            },
774        );
775        assert_eq!(
776            config.validate().unwrap_err().join(", "),
777            "Endpoint sample_none security mode SingAndEncrypt is invalid"
778        );
779    }
780
781    #[test]
782    fn client_anonymous_user_tokens_id() {
783        let mut config = default_sample_config();
784        // id anonymous is reserved
785        config.user_tokens = BTreeMap::new();
786        config.user_tokens.insert(
787            String::from("ANONYMOUS"),
788            ClientUserToken {
789                user: String::new(),
790                password: Some(String::new()),
791                cert_path: None,
792                private_key_path: None,
793            },
794        );
795        assert_eq!(
796            config.validate().unwrap_err().join(", "),
797            "User tokens contains the reserved \"ANONYMOUS\" id, Token ANONYMOUS failed to validate: User token has an empty name."
798        );
799    }
800}