Skip to main content

opcua/client/
config.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2022 Adam Lock
4
5//! Client configuration data.
6
7use std::{
8    self,
9    collections::BTreeMap,
10    path::{Path, PathBuf},
11    str::FromStr,
12};
13
14use crate::{
15    core::config::Config,
16    crypto::SecurityPolicy,
17    types::{ApplicationType, MessageSecurityMode, UAString},
18};
19
20use super::session_retry_policy::SessionRetryPolicy;
21
22pub const ANONYMOUS_USER_TOKEN_ID: &str = "ANONYMOUS";
23
24#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
25pub struct ClientUserToken {
26    /// Username
27    pub user: String,
28    /// Password
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub password: Option<String>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub cert_path: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub private_key_path: Option<String>,
35}
36
37impl ClientUserToken {
38    /// Constructs a client token which holds a username and password.
39    pub fn user_pass<S, T>(user: S, password: T) -> Self
40    where
41        S: Into<String>,
42        T: Into<String>,
43    {
44        ClientUserToken {
45            user: user.into(),
46            password: Some(password.into()),
47            cert_path: None,
48            private_key_path: None,
49        }
50    }
51
52    /// Constructs a client token which holds a username and paths to X509 certificate and private key.
53    pub fn x509<S>(user: S, cert_path: &Path, private_key_path: &Path) -> Self
54    where
55        S: Into<String>,
56    {
57        // Apparently on Windows, a PathBuf can hold weird non-UTF chars but they will not
58        // be stored in a config file properly in any event, so this code will lossily strip them out.
59        ClientUserToken {
60            user: user.into(),
61            password: None,
62            cert_path: Some(cert_path.to_string_lossy().to_string()),
63            private_key_path: Some(private_key_path.to_string_lossy().to_string()),
64        }
65    }
66
67    /// Test if the token, i.e. that it has a name, and either a password OR a cert path and key path.
68    /// The paths are not validated.
69    pub fn is_valid(&self) -> bool {
70        let mut valid = true;
71        if self.user.is_empty() {
72            error!("User token has an empty name.");
73            valid = false;
74        }
75        // A token must properly represent one kind of token or it is not valid
76        if self.password.is_some() {
77            if self.cert_path.is_some() || self.private_key_path.is_some() {
78                error!(
79                    "User token {} holds a password and certificate info - it cannot be both.",
80                    self.user
81                );
82                valid = false;
83            }
84        } else {
85            if self.cert_path.is_none() && self.private_key_path.is_none() {
86                error!(
87                    "User token {} fails to provide a password or certificate info.",
88                    self.user
89                );
90                valid = false;
91            } else if self.cert_path.is_none() || self.private_key_path.is_none() {
92                error!("User token {} fails to provide both a certificate path and a private key path.", self.user);
93                valid = false;
94            }
95        }
96        valid
97    }
98}
99
100/// Describes an endpoint, it's url security policy, mode and user token
101#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
102pub struct ClientEndpoint {
103    /// Endpoint path
104    pub url: String,
105    /// Security policy
106    pub security_policy: String,
107    /// Security mode
108    pub security_mode: String,
109    /// User id to use with the endpoint
110    #[serde(default = "ClientEndpoint::anonymous_id")]
111    pub user_token_id: String,
112}
113
114impl ClientEndpoint {
115    /// Makes a client endpoint
116    pub fn new<T>(url: T) -> Self
117    where
118        T: Into<String>,
119    {
120        ClientEndpoint {
121            url: url.into(),
122            security_policy: SecurityPolicy::None.to_str().into(),
123            security_mode: MessageSecurityMode::None.into(),
124            user_token_id: Self::anonymous_id(),
125        }
126    }
127
128    fn anonymous_id() -> String {
129        ANONYMOUS_USER_TOKEN_ID.to_string()
130    }
131
132    // Returns the security policy
133    pub fn security_policy(&self) -> SecurityPolicy {
134        SecurityPolicy::from_str(&self.security_policy).unwrap()
135    }
136}
137
138#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
139pub struct DecodingOptions {
140    /// Maximum size of a message chunk in bytes. 0 means no limit
141    pub max_message_size: usize,
142    /// Maximum number of chunks in a message. 0 means no limit
143    pub max_chunk_count: usize,
144    /// Maximum length in bytes (not chars!) of a string. 0 actually means 0, i.e. no string permitted
145    pub max_string_length: usize,
146    /// Maximum length in bytes of a byte string. 0 actually means 0, i.e. no byte string permitted
147    pub max_byte_string_length: usize,
148    /// Maximum number of array elements. 0 actually means 0, i.e. no array permitted
149    pub max_array_length: usize,
150}
151
152#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
153pub struct Performance {
154    /// Ignore clock skew allows the client to make a successful connection to the server, even
155    /// when the client and server clocks are out of sync.
156    pub ignore_clock_skew: bool,
157    /// Use a single-threaded executor. The default executor uses a thread pool with a worker
158    /// thread for each CPU core available on the system.
159    pub single_threaded_executor: bool,
160}
161
162/// Client OPC UA configuration
163#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
164pub struct ClientConfig {
165    /// Name of the application that the client presents itself as to the server
166    pub application_name: String,
167    /// The application uri
168    pub application_uri: String,
169    /// Product uri
170    pub product_uri: String,
171    /// Autocreates public / private keypair if they don't exist. For testing/samples only
172    /// since you do not have control of the values
173    pub create_sample_keypair: bool,
174    /// Custom certificate path, to be used instead of the default .der certificate path
175    pub certificate_path: Option<PathBuf>,
176    /// Custom private key path, to be used instead of the default private key path
177    pub private_key_path: Option<PathBuf>,
178    /// Auto trusts server certificates. For testing/samples only unless you're sure what you're
179    /// doing.
180    pub trust_server_certs: bool,
181    /// Verify server certificates. For testing/samples only unless you're sure what you're
182    /// doing.
183    pub verify_server_certs: bool,
184    /// PKI folder, either absolute or relative to executable
185    pub pki_dir: PathBuf,
186    /// Preferred locales
187    pub preferred_locales: Vec<String>,
188    /// Identifier of the default endpoint
189    pub default_endpoint: String,
190    /// User tokens
191    pub user_tokens: BTreeMap<String, ClientUserToken>,
192    /// List of end points
193    pub endpoints: BTreeMap<String, ClientEndpoint>,
194    /// Decoding options used for serialization / deserialization
195    pub decoding_options: DecodingOptions,
196    /// Max retry limit -1, 0 or number
197    pub session_retry_limit: i32,
198    /// Retry interval in milliseconds
199    pub session_retry_interval: u32,
200    /// Session timeout period in milliseconds
201    pub session_timeout: u32,
202    /// Client performance settings
203    pub performance: Performance,
204    /// Session name
205    pub session_name: String,
206}
207
208impl Config for ClientConfig {
209    /// Test if the config is valid, which requires at the least that
210    fn is_valid(&self) -> bool {
211        let mut valid = true;
212
213        if self.application_name.is_empty() {
214            error!("Application name is empty");
215            valid = false;
216        }
217        if self.application_uri.is_empty() {
218            error!("Application uri is empty");
219            valid = false;
220        }
221        if self.user_tokens.contains_key(ANONYMOUS_USER_TOKEN_ID) {
222            error!(
223                "User tokens contains the reserved \"{}\" id",
224                ANONYMOUS_USER_TOKEN_ID
225            );
226            valid = false;
227        }
228        if self.user_tokens.contains_key("") {
229            error!("User tokens contains an endpoint with an empty id");
230            valid = false;
231        }
232        self.user_tokens.iter().for_each(|(_, token)| {
233            if !token.is_valid() {
234                valid = false;
235            }
236        });
237        if self.endpoints.is_empty() {
238            warn!("Endpoint config contains no endpoints");
239        } else {
240            // Check for invalid ids in endpoints
241            if self.endpoints.contains_key("") {
242                error!("Endpoints contains an endpoint with an empty id");
243                valid = false;
244            }
245            if !self.default_endpoint.is_empty()
246                && !self.endpoints.contains_key(&self.default_endpoint)
247            {
248                error!(
249                    "Default endpoint id {} does not exist in list of endpoints",
250                    self.default_endpoint
251                );
252                valid = false;
253            }
254            // Check for invalid security policy and modes in endpoints
255            self.endpoints.iter().for_each(|(id, e)| {
256                if SecurityPolicy::from_str(&e.security_policy).unwrap() != SecurityPolicy::Unknown
257                {
258                    if MessageSecurityMode::Invalid
259                        == MessageSecurityMode::from(e.security_mode.as_ref())
260                    {
261                        error!(
262                            "Endpoint {} security mode {} is invalid",
263                            id, e.security_mode
264                        );
265                        valid = false;
266                    }
267                } else {
268                    error!(
269                        "Endpoint {} security policy {} is invalid",
270                        id, e.security_policy
271                    );
272                    valid = false;
273                }
274            });
275        }
276        if self.session_retry_limit < 0 && self.session_retry_limit != -1 {
277            error!("Session retry limit of {} is invalid - must be -1 (infinite), 0 (never) or a positive value", self.session_retry_limit);
278            valid = false;
279        }
280        valid
281    }
282
283    fn application_name(&self) -> UAString {
284        UAString::from(&self.application_name)
285    }
286
287    fn application_uri(&self) -> UAString {
288        UAString::from(&self.application_uri)
289    }
290
291    fn product_uri(&self) -> UAString {
292        UAString::from(&self.product_uri)
293    }
294
295    fn application_type(&self) -> ApplicationType {
296        ApplicationType::Client
297    }
298}
299
300impl Default for ClientConfig {
301    fn default() -> Self {
302        Self::new("", "")
303    }
304}
305
306impl ClientConfig {
307    /// The default PKI directory
308    pub const PKI_DIR: &'static str = "pki";
309
310    pub fn new<T>(application_name: T, application_uri: T) -> Self
311    where
312        T: Into<String>,
313    {
314        let mut pki_dir = std::env::current_dir().unwrap();
315        pki_dir.push(Self::PKI_DIR);
316
317        let decoding_options = crate::types::DecodingOptions::default();
318        ClientConfig {
319            application_name: application_name.into(),
320            application_uri: application_uri.into(),
321            create_sample_keypair: false,
322            certificate_path: None,
323            private_key_path: None,
324            trust_server_certs: false,
325            verify_server_certs: true,
326            product_uri: String::new(),
327            pki_dir,
328            preferred_locales: Vec::new(),
329            default_endpoint: String::new(),
330            user_tokens: BTreeMap::new(),
331            endpoints: BTreeMap::new(),
332            session_retry_limit: SessionRetryPolicy::DEFAULT_RETRY_LIMIT as i32,
333            session_retry_interval: SessionRetryPolicy::DEFAULT_RETRY_INTERVAL_MS,
334            session_timeout: 0,
335            decoding_options: DecodingOptions {
336                max_array_length: decoding_options.max_array_length,
337                max_string_length: decoding_options.max_string_length,
338                max_byte_string_length: decoding_options.max_byte_string_length,
339                max_chunk_count: decoding_options.max_chunk_count,
340                max_message_size: decoding_options.max_message_size,
341            },
342            performance: Performance {
343                ignore_clock_skew: false,
344                single_threaded_executor: true,
345            },
346            session_name: "Rust OPC UA Client".into(),
347        }
348    }
349}