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_or(SecurityPolicy::Unknown)
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_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 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 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 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 pub const PKI_DIR: &'static str = "pki";
591
592 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 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 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 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 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}