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()
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() != SecurityPolicy::Unknown
351                {
352                    if MessageSecurityMode::Invalid
353                        == MessageSecurityMode::from(e.security_mode.as_ref())
354                    {
355                        errors.push(format!(
356                            "Endpoint {} security mode {} is invalid",
357                            id, e.security_mode
358                        ));
359                    }
360                } else {
361                    errors.push(format!(
362                        "Endpoint {} security policy {} is invalid",
363                        id, e.security_policy
364                    ));
365                }
366            });
367        }
368        if self.session_retry_limit < 0 && self.session_retry_limit != -1 {
369            errors.push(format!("Session retry limit of {} is invalid - must be -1 (infinite), 0 (never) or a positive value", self.session_retry_limit));
370        }
371        if errors.is_empty() {
372            Ok(())
373        } else {
374            Err(errors)
375        }
376    }
377
378    fn application_name(&self) -> UAString {
379        UAString::from(&self.application_name)
380    }
381
382    fn application_uri(&self) -> UAString {
383        UAString::from(&self.application_uri)
384    }
385
386    fn product_uri(&self) -> UAString {
387        UAString::from(&self.product_uri)
388    }
389
390    fn application_type(&self) -> ApplicationType {
391        ApplicationType::Client
392    }
393}
394
395impl ClientConfig {
396    /// Get the configured session retry policy.
397    pub fn session_retry_policy(&self) -> SessionRetryPolicy {
398        SessionRetryPolicy::new(
399            self.session_retry_max,
400            if self.session_retry_limit < 0 {
401                None
402            } else {
403                Some(self.session_retry_limit as u32)
404            },
405            self.session_retry_initial,
406        )
407    }
408
409    /// Returns an identity token corresponding to the matching user in the configuration. Or None
410    /// if there is no matching token.
411    pub fn client_identity_token(
412        &self,
413        user_token_id: impl Into<String>,
414    ) -> Result<IdentityToken, Error> {
415        let user_token_id = user_token_id.into();
416        if user_token_id == ANONYMOUS_USER_TOKEN_ID {
417            Ok(IdentityToken::Anonymous)
418        } else {
419            let Some(token) = self.user_tokens.get(&user_token_id) else {
420                return Err(Error::new(
421                    StatusCode::BadInvalidArgument,
422                    format!("Requested user token: {user_token_id} not found in config",),
423                ));
424            };
425
426            if let Some(ref password) = token.password {
427                Ok(IdentityToken::UserName(token.user.clone(), password.into()))
428            } else if let Some(ref cert_path) = token.cert_path {
429                let Some(private_key_path) = &token.private_key_path else {
430                    return Err(Error::new(
431                        StatusCode::BadInvalidArgument,
432                        "Client identity token with certificate does not have a private key",
433                    ));
434                };
435                IdentityToken::new_x509_path(cert_path, private_key_path)
436            } else {
437                Err(Error::new(
438                    StatusCode::BadInvalidArgument,
439                    "Non-anonymous client identity token with neither password nor certificate",
440                ))
441            }
442        }
443    }
444
445    /// Creates a [`EndpointDescription`](EndpointDescription) information from the supplied client endpoint.
446    pub(super) fn endpoint_description_for_client_endpoint(
447        &self,
448        client_endpoint: &ClientEndpoint,
449        endpoints: &[EndpointDescription],
450    ) -> Result<EndpointDescription, Error> {
451        let security_policy =
452            SecurityPolicy::from_str(&client_endpoint.security_policy).map_err(|_| {
453                Error::new(
454                    StatusCode::BadSecurityPolicyRejected,
455                    format!(
456                        "Endpoint {} security policy {} is invalid",
457                        client_endpoint.url, client_endpoint.security_policy
458                    ),
459                )
460            })?;
461        let security_mode = MessageSecurityMode::from(client_endpoint.security_mode.as_ref());
462        if security_mode == MessageSecurityMode::Invalid {
463            return Err(Error::new(
464                StatusCode::BadSecurityModeRejected,
465                format!(
466                    "Endpoint {} security mode {} is invalid",
467                    client_endpoint.url, client_endpoint.security_mode
468                ),
469            ));
470        }
471        let endpoint_url = client_endpoint.url.clone();
472        let endpoint = Client::find_matching_endpoint(
473            endpoints,
474            &endpoint_url,
475            security_policy,
476            security_mode,
477        )
478        .ok_or_else(|| {
479            Error::new(
480                StatusCode::BadTcpEndpointUrlInvalid,
481                format!(
482                    "Endpoint {endpoint_url}, {security_policy:?} / {security_mode:?} does not match any supplied by the server"
483                ),
484            )
485        })?;
486
487        Ok(endpoint)
488    }
489}
490
491impl Default for ClientConfig {
492    fn default() -> Self {
493        Self::new("", "")
494    }
495}
496
497mod defaults {
498    use std::time::Duration;
499
500    use crate::retry::SessionRetryPolicy;
501
502    pub(super) fn verify_server_certs() -> bool {
503        true
504    }
505
506    pub(super) fn channel_lifetime() -> u32 {
507        60_000
508    }
509
510    pub(super) fn session_retry_limit() -> i32 {
511        SessionRetryPolicy::DEFAULT_RETRY_LIMIT as i32
512    }
513
514    pub(super) fn session_retry_initial() -> Duration {
515        Duration::from_secs(1)
516    }
517
518    pub(super) fn session_retry_max() -> Duration {
519        Duration::from_secs(30)
520    }
521
522    pub(super) fn keep_alive_interval() -> Duration {
523        Duration::from_secs(10)
524    }
525
526    pub(super) fn max_array_length() -> usize {
527        opcua_types::constants::MAX_ARRAY_LENGTH
528    }
529
530    pub(super) fn max_byte_string_length() -> usize {
531        opcua_types::constants::MAX_BYTE_STRING_LENGTH
532    }
533
534    pub(super) fn max_chunk_count() -> usize {
535        opcua_types::constants::MAX_CHUNK_COUNT
536    }
537
538    pub(super) fn max_chunk_size() -> usize {
539        65535
540    }
541
542    pub(super) fn max_failed_keep_alive_count() -> u64 {
543        0
544    }
545
546    pub(super) fn max_incoming_chunk_size() -> usize {
547        65535
548    }
549
550    pub(super) fn max_message_size() -> usize {
551        opcua_types::constants::MAX_MESSAGE_SIZE
552    }
553
554    pub(super) fn max_string_length() -> usize {
555        opcua_types::constants::MAX_STRING_LENGTH
556    }
557
558    pub(super) fn request_timeout() -> Duration {
559        Duration::from_secs(60)
560    }
561
562    pub(super) fn publish_timeout() -> Duration {
563        Duration::from_secs(60)
564    }
565
566    pub(super) fn min_publish_interval() -> Duration {
567        Duration::from_millis(100)
568    }
569
570    pub(super) fn recreate_monitored_items_chunk() -> usize {
571        1000
572    }
573
574    pub(super) fn recreate_subscriptions() -> bool {
575        true
576    }
577
578    pub(super) fn session_timeout() -> u32 {
579        60_000
580    }
581
582    pub(super) fn session_nonce_length() -> usize {
583        32
584    }
585}
586
587impl ClientConfig {
588    /// The default PKI directory
589    pub const PKI_DIR: &'static str = "pki";
590
591    /// Create a new default client config.
592    pub fn new(application_name: impl Into<String>, application_uri: impl Into<String>) -> Self {
593        let mut pki_dir = std::env::current_dir().unwrap();
594        pki_dir.push(Self::PKI_DIR);
595
596        ClientConfig {
597            application_name: application_name.into(),
598            application_uri: application_uri.into(),
599            product_uri: String::new(),
600            create_sample_keypair: false,
601            certificate_path: None,
602            private_key_path: None,
603            trust_server_certs: false,
604            verify_server_certs: defaults::verify_server_certs(),
605            pki_dir,
606            preferred_locales: Vec::new(),
607            default_endpoint: String::new(),
608            endpoints: BTreeMap::new(),
609            user_tokens: BTreeMap::new(),
610            channel_lifetime: defaults::channel_lifetime(),
611            decoding_options: DecodingOptions::default(),
612            session_retry_limit: defaults::session_retry_limit(),
613            session_retry_initial: defaults::session_retry_initial(),
614            session_retry_max: defaults::session_retry_max(),
615            keep_alive_interval: defaults::keep_alive_interval(),
616            max_failed_keep_alive_count: defaults::max_failed_keep_alive_count(),
617            request_timeout: defaults::request_timeout(),
618            publish_timeout: defaults::publish_timeout(),
619            min_publish_interval: defaults::min_publish_interval(),
620            performance: Performance::default(),
621            recreate_subscriptions: defaults::recreate_subscriptions(),
622            session_name: "Rust OPC UA Client".into(),
623            session_timeout: defaults::session_timeout(),
624            session_nonce_length: defaults::session_nonce_length(),
625        }
626    }
627}
628
629#[cfg(test)]
630mod tests {
631    use std::{self, collections::BTreeMap, path::PathBuf};
632
633    use crate::ClientBuilder;
634    use opcua_core::config::Config;
635    use opcua_crypto::SecurityPolicy;
636    use opcua_types::MessageSecurityMode;
637
638    use super::{ClientConfig, ClientEndpoint, ClientUserToken, ANONYMOUS_USER_TOKEN_ID};
639
640    fn make_test_file(filename: &str) -> PathBuf {
641        let mut path = std::env::temp_dir();
642        path.push(filename);
643        path
644    }
645
646    fn sample_builder() -> ClientBuilder {
647        ClientBuilder::new()
648            .application_name("OPC UA Sample Client")
649            .application_uri("urn:SampleClient")
650            .create_sample_keypair(true)
651            .certificate_path("own/cert.der")
652            .private_key_path("private/private.pem")
653            .trust_server_certs(true)
654            .pki_dir("./pki")
655            .endpoints(vec![
656                (
657                    "sample_none",
658                    ClientEndpoint {
659                        url: String::from("opc.tcp://127.0.0.1:4855/"),
660                        security_policy: String::from(SecurityPolicy::None.to_str()),
661                        security_mode: String::from(MessageSecurityMode::None),
662                        user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
663                    },
664                ),
665                (
666                    "sample_basic128rsa15",
667                    ClientEndpoint {
668                        url: String::from("opc.tcp://127.0.0.1:4855/"),
669                        security_policy: String::from(SecurityPolicy::Basic128Rsa15.to_str()),
670                        security_mode: String::from(MessageSecurityMode::SignAndEncrypt),
671                        user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
672                    },
673                ),
674                (
675                    "sample_basic256",
676                    ClientEndpoint {
677                        url: String::from("opc.tcp://127.0.0.1:4855/"),
678                        security_policy: String::from(SecurityPolicy::Basic256.to_str()),
679                        security_mode: String::from(MessageSecurityMode::SignAndEncrypt),
680                        user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
681                    },
682                ),
683                (
684                    "sample_basic256sha256",
685                    ClientEndpoint {
686                        url: String::from("opc.tcp://127.0.0.1:4855/"),
687                        security_policy: String::from(SecurityPolicy::Basic256Sha256.to_str()),
688                        security_mode: String::from(MessageSecurityMode::SignAndEncrypt),
689                        user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
690                    },
691                ),
692            ])
693            .default_endpoint("sample_none")
694            .user_token(
695                "sample_user",
696                ClientUserToken::user_pass("sample1", "sample1pwd"),
697            )
698            .user_token(
699                "sample_user2",
700                ClientUserToken::user_pass("sample2", "sample2pwd"),
701            )
702    }
703
704    fn default_sample_config() -> ClientConfig {
705        sample_builder().into_config()
706    }
707
708    #[test]
709    fn client_sample_config() {
710        // This test exists to create the samples/client.conf file
711        // This test only exists to dump a sample config
712        let config = default_sample_config();
713        let mut path = std::env::current_dir().unwrap();
714        path.push("..");
715        path.push("samples");
716        path.push("client.conf");
717        println!("Path is {path:?}");
718
719        let saved = config.save(&path);
720        println!("Saved = {saved:?}");
721        assert!(saved.is_ok());
722        config.validate().unwrap();
723    }
724
725    #[test]
726    fn client_config() {
727        let path = make_test_file("client_config.yaml");
728        println!("Client path = {path:?}");
729        let config = default_sample_config();
730        let saved = config.save(&path);
731        println!("Saved = {saved:?}");
732        assert!(config.save(&path).is_ok());
733        if let Ok(config2) = ClientConfig::load(&path) {
734            assert_eq!(config, config2);
735        } else {
736            panic!("Cannot load config from file");
737        }
738    }
739
740    #[test]
741    fn client_invalid_security_policy_config() {
742        let mut config = default_sample_config();
743        // Security policy is wrong
744        config.endpoints = BTreeMap::new();
745        config.endpoints.insert(
746            String::from("sample_none"),
747            ClientEndpoint {
748                url: String::from("opc.tcp://127.0.0.1:4855"),
749                security_policy: String::from("http://blah"),
750                security_mode: String::from(MessageSecurityMode::None),
751                user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
752            },
753        );
754        assert_eq!(
755            config.validate().unwrap_err().join(", "),
756            "Endpoint sample_none security policy http://blah is invalid"
757        );
758    }
759
760    #[test]
761    fn client_invalid_security_mode_config() {
762        let mut config = default_sample_config();
763        // Message security mode is wrong
764        config.endpoints = BTreeMap::new();
765        config.endpoints.insert(
766            String::from("sample_none"),
767            ClientEndpoint {
768                url: String::from("opc.tcp://127.0.0.1:4855"),
769                security_policy: String::from(SecurityPolicy::Basic128Rsa15.to_uri()),
770                security_mode: String::from("SingAndEncrypt"),
771                user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
772            },
773        );
774        assert_eq!(
775            config.validate().unwrap_err().join(", "),
776            "Endpoint sample_none security mode SingAndEncrypt is invalid"
777        );
778    }
779
780    #[test]
781    fn client_anonymous_user_tokens_id() {
782        let mut config = default_sample_config();
783        // id anonymous is reserved
784        config.user_tokens = BTreeMap::new();
785        config.user_tokens.insert(
786            String::from("ANONYMOUS"),
787            ClientUserToken {
788                user: String::new(),
789                password: Some(String::new()),
790                cert_path: None,
791                private_key_path: None,
792            },
793        );
794        assert_eq!(
795            config.validate().unwrap_err().join(", "),
796            "User tokens contains the reserved \"ANONYMOUS\" id, Token ANONYMOUS failed to validate: User token has an empty name."
797        );
798    }
799}