1use 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
27pub const ANONYMOUS_USER_TOKEN_ID: &str = "ANONYMOUS";
29
30#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
31pub struct ClientUserToken {
33 pub user: String,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub password: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub cert_path: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub private_key_path: Option<String>,
44}
45
46impl ClientUserToken {
47 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 pub fn x509<S>(user: S, cert_path: &Path, private_key_path: &Path) -> Self
63 where
64 S: Into<String>,
65 {
66 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 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 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#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
112pub struct ClientEndpoint {
113 pub url: String,
115 pub security_policy: String,
117 pub security_mode: String,
119 #[serde(default = "ClientEndpoint::anonymous_id")]
121 pub user_token_id: String,
122}
123
124impl ClientEndpoint {
125 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 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 #[serde(default = "defaults::max_message_size")]
152 pub(crate) max_message_size: usize,
153 #[serde(default = "defaults::max_chunk_count")]
155 pub(crate) max_chunk_count: usize,
156 #[serde(default = "defaults::max_chunk_size")]
158 pub(crate) max_chunk_size: usize,
159 #[serde(default = "defaults::max_incoming_chunk_size")]
161 pub(crate) max_incoming_chunk_size: usize,
162 #[serde(default = "defaults::max_string_length")]
164 pub(crate) max_string_length: usize,
165 #[serde(default = "defaults::max_byte_string_length")]
167 pub(crate) max_byte_string_length: usize,
168 #[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 #[serde(default)]
206 pub(crate) ignore_clock_skew: bool,
207 #[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#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
223pub struct ClientConfig {
224 pub(crate) application_name: String,
226 pub(crate) application_uri: String,
228 pub(crate) product_uri: String,
230 pub(crate) create_sample_keypair: bool,
233 pub(crate) certificate_path: Option<PathBuf>,
235 pub(crate) private_key_path: Option<PathBuf>,
237 pub(crate) trust_server_certs: bool,
240 pub(crate) verify_server_certs: bool,
243 pub(crate) pki_dir: PathBuf,
245 pub(crate) preferred_locales: Vec<String>,
247 pub(crate) default_endpoint: String,
249 pub(crate) endpoints: BTreeMap<String, ClientEndpoint>,
251 pub(crate) user_tokens: BTreeMap<String, ClientUserToken>,
253 #[serde(default = "defaults::session_nonce_length")]
255 pub(crate) session_nonce_length: usize,
256 #[serde(default = "defaults::channel_lifetime")]
258 pub(crate) channel_lifetime: u32,
259 #[serde(default)]
261 pub(crate) decoding_options: DecodingOptions,
262 #[serde(default = "defaults::session_retry_limit")]
265 pub(crate) session_retry_limit: i32,
266
267 #[serde(default = "defaults::session_retry_initial")]
269 pub(crate) session_retry_initial: Duration,
270 #[serde(default = "defaults::session_retry_max")]
272 pub(crate) session_retry_max: Duration,
273 #[serde(default = "defaults::keep_alive_interval")]
275 pub(crate) keep_alive_interval: Duration,
276 #[serde(default = "defaults::max_failed_keep_alive_count")]
281 pub(crate) max_failed_keep_alive_count: u64,
282
283 #[serde(default = "defaults::request_timeout")]
285 pub(crate) request_timeout: Duration,
286 #[serde(default = "defaults::publish_timeout")]
289 pub(crate) publish_timeout: Duration,
290 #[serde(default = "defaults::min_publish_interval")]
293 pub(crate) min_publish_interval: Duration,
294
295 #[serde(default)]
297 pub(crate) performance: Performance,
298 #[serde(default = "defaults::recreate_subscriptions")]
301 pub(crate) recreate_subscriptions: bool,
302 pub(crate) session_name: String,
304 #[serde(default = "defaults::session_timeout")]
306 pub(crate) session_timeout: u32,
307}
308
309impl Config for ClientConfig {
310 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 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 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 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 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 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 pub const PKI_DIR: &'static str = "pki";
590
591 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 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 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 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 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}