1use std::str::FromStr;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use anyhow::{Result, anyhow};
6use opcua::client::custom_types::DataTypeTreeBuilder;
7use opcua::client::{ClientBuilder, IdentityToken, Session};
8use opcua::crypto::SecurityPolicy;
9use opcua::types::custom::{DynamicStructure, DynamicTypeLoader};
10use opcua::types::json::{JsonEncodable, JsonStreamWriter, JsonWriter};
11use opcua::types::{
12 Argument, Array, AttributeId, BrowseDescription, BrowseDescriptionResultMask, BrowseDirection,
13 CallMethodRequest, DataTypeId, DataValue, EndpointDescription, ExpandedNodeId, Guid,
14 Identifier, MessageSecurityMode, NodeClass, NodeClassMask, NodeId, QualifiedName, ReadValueId,
15 ReferenceDescription, ReferenceTypeId, StatusCode, TimestampsToReturn, TryFromVariant,
16 TypeLoader, UAString, UserTokenType, Variant, VariantScalarTypeId,
17};
18use tokio::sync::Mutex;
19use tokio::task::JoinHandle;
20
21use crate::types::{
22 AuthMode, AuthSpec, EndpointInfo, MethodArgument, MethodCallOutcome, MethodSignature,
23 NodeAttribute, NodeSummary, ReferenceRow, SecurityMode, TreeChild, ValueTree,
24};
25
26struct Connected {
27 session: Arc<Session>,
28 event_loop: JoinHandle<StatusCode>,
29}
30
31enum State {
32 Disconnected,
33 Connected(Connected),
34}
35
36pub struct UaClient {
37 state: Mutex<State>,
38 verify_certificate_metadata: AtomicBool,
45}
46
47impl Default for UaClient {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl UaClient {
54 pub fn set_verify_cert_metadata(&self, on: bool) {
55 self.verify_certificate_metadata
56 .store(on, Ordering::Relaxed);
57 }
58
59 pub fn new() -> Self {
60 warn_insecure_default();
61 Self {
62 state: Mutex::new(State::Disconnected),
63 verify_certificate_metadata: AtomicBool::new(false),
64 }
65 }
66
67 pub async fn connect(
68 &self,
69 endpoint_url: &str,
70 endpoint: Option<&EndpointInfo>,
71 auth: &AuthSpec,
72 ) -> Result<()> {
73 let mut guard = self.state.lock().await;
74 if matches!(*guard, State::Connected(_)) {
75 return Err(anyhow!("already connected"));
76 }
77
78 let mut client = build_client(self.verify_certificate_metadata.load(Ordering::Relaxed))?;
79
80 let (policy_uri, mode) = match endpoint {
81 Some(ep) => (
82 ep.security_policy_uri.clone(),
83 security_mode_to_message_mode(ep.security_mode),
84 ),
85 None => (
86 SecurityPolicy::None.to_uri().to_string(),
87 MessageSecurityMode::None,
88 ),
89 };
90 let identity = build_identity_token(auth)?;
91 if mode != MessageSecurityMode::None {
92 log_client_cert_hint();
93 }
94
95 let descriptions = client
103 .get_server_endpoints_from_url(endpoint_url)
104 .await
105 .map_err(|e| anyhow!("get_server_endpoints failed: {e}"))?;
106 let mut matched = descriptions
107 .into_iter()
108 .find(|d| d.security_policy_uri.as_ref() == policy_uri && d.security_mode == mode)
109 .ok_or_else(|| {
110 anyhow!(
111 "server has no endpoint with policy '{}' and mode {:?}",
112 policy_uri,
113 mode
114 )
115 })?;
116 let reported_url = matched.endpoint_url.as_ref().to_string();
117 if !reported_url.is_empty() && reported_url != endpoint_url {
118 tracing::info!(
119 "server endpoint URL is {reported_url}; forcing transport to typed URL {endpoint_url}"
120 );
121 }
122 matched.endpoint_url = endpoint_url.into();
123
124 let (session, event_loop) = client
125 .connect_to_endpoint_directly(matched, identity)
126 .map_err(|e| {
127 let msg = e.to_string();
128 let lower = msg.to_lowercase();
129 if lower.contains("uriinvalid") {
130 tracing::error!(
131 "certificate URI mismatch (BadCertificateUriInvalid). \
132 Delete the pki/ folder and reconnect to regenerate the cert with the current application URI \"{}\".",
133 APPLICATION_URI
134 );
135 } else if looks_like_cert_trust_error(&lower) {
136 tracing::error!(
137 "server rejected the client certificate. \
138 Mark pki/own/cert.der as trusted in the server's PKI store and try again."
139 );
140 }
141 anyhow!("connect_to_endpoint_directly failed: {e}")
142 })?;
143
144 let mut handle = event_loop.spawn();
145 let session_for_wait = session.clone();
146 let connected = tokio::select! {
147 res = &mut handle => {
148 return Err(anyhow!(
149 "session ended before connection was established: {res:?}"
150 ));
151 }
152 c = session_for_wait.wait_for_connection() => c,
153 };
154 if !connected {
155 handle.abort();
156 return Err(anyhow!("failed to establish connection"));
157 }
158
159 if let Err(e) = register_dynamic_type_loader(&session).await {
160 tracing::warn!("dynamic type loader setup failed: {e}");
161 }
162
163 *guard = State::Connected(Connected {
164 session,
165 event_loop: handle,
166 });
167 Ok(())
168 }
169
170 pub async fn browse_path(&self, node_id: &NodeId) -> Result<String> {
173 const MAX_DEPTH: usize = 64;
174 let session = self.session().await?;
175 let root = NodeId::new(0, opcua::types::ObjectId::RootFolder as u32);
176
177 let mut segments: Vec<String> = Vec::new();
178 let mut current = node_id.clone();
179 for _ in 0..MAX_DEPTH {
180 if current == root {
181 break;
182 }
183 let bn = read_browse_name(&session, ¤t).await?;
184 segments.push(bn);
185 match read_inverse_parent(&session, ¤t).await? {
186 Some(p) => current = p,
187 None => break,
188 }
189 }
190 segments.reverse();
191 Ok(if segments.is_empty() {
192 "/".to_string()
193 } else {
194 format!("/{}", segments.join("/"))
195 })
196 }
197
198 pub async fn node_path(&self, node_id: &NodeId) -> Result<Vec<NodeId>> {
201 const MAX_DEPTH: usize = 64;
202 let session = self.session().await?;
203 let root = NodeId::new(0, opcua::types::ObjectId::RootFolder as u32);
204
205 let mut path = vec![node_id.clone()];
206 let mut current = node_id.clone();
207 for _ in 0..MAX_DEPTH {
208 if current == root {
209 break;
210 }
211 match read_inverse_parent(&session, ¤t).await? {
212 Some(parent) => {
213 path.push(parent.clone());
214 current = parent;
215 }
216 None => break,
217 }
218 }
219 path.reverse();
220 Ok(path)
221 }
222
223 pub async fn resolve_browse_path(&self, text: &str) -> Result<NodeId> {
228 let session = self.session().await?;
229 let root = NodeId::new(0, opcua::types::ObjectId::RootFolder as u32);
230
231 let mut segments: Vec<&str> = text.split('/').filter(|s| !s.is_empty()).collect();
232 if segments
233 .first()
234 .is_some_and(|s| s.eq_ignore_ascii_case("Root"))
235 {
236 segments.remove(0);
237 }
238 if segments.is_empty() {
239 return Ok(root);
240 }
241
242 let mut current = root;
243 let mut walked = String::new();
244 for seg in &segments {
245 let target = parse_qualified_name(seg);
246 match find_child_by_browse_name(&session, ¤t, &target).await? {
247 Some(next) => {
248 walked.push('/');
249 walked.push_str(seg);
250 current = next;
251 }
252 None => {
253 return Err(anyhow!(
254 "no child '{seg}' under {current} (resolved {walked} so far)"
255 ));
256 }
257 }
258 }
259 Ok(current)
260 }
261
262 pub async fn discover_endpoints(&self, endpoint_url: &str) -> Result<Vec<EndpointInfo>> {
263 let client = build_client(self.verify_certificate_metadata.load(Ordering::Relaxed))?;
264 let descriptions = client
265 .get_server_endpoints_from_url(endpoint_url)
266 .await
267 .map_err(|e| anyhow!("get_server_endpoints failed: {e}"))?;
268 Ok(descriptions
269 .into_iter()
270 .map(endpoint_description_to_info)
271 .collect())
272 }
273
274 pub async fn disconnect(&self) -> Result<()> {
275 let mut guard = self.state.lock().await;
276 let connected = match std::mem::replace(&mut *guard, State::Disconnected) {
277 State::Connected(c) => c,
278 State::Disconnected => return Ok(()),
279 };
280 let _ = connected.session.disconnect().await;
281 let _ = connected.event_loop.await;
282 Ok(())
283 }
284
285 async fn session(&self) -> Result<Arc<Session>> {
286 let guard = self.state.lock().await;
287 match &*guard {
288 State::Connected(c) => Ok(c.session.clone()),
289 State::Disconnected => Err(anyhow!("not connected")),
290 }
291 }
292
293 pub async fn browse_children(&self, node_id: &NodeId) -> Result<Vec<TreeChild>> {
294 let session = self.session().await?;
295 let desc = browse_hierarchical(node_id.clone());
296 let mut results = session
297 .browse(&[desc], 0, None)
298 .await
299 .map_err(|s| anyhow!("browse failed: {s}"))?;
300 let result = results
301 .pop()
302 .ok_or_else(|| anyhow!("empty browse result"))?;
303 let refs = result.references.unwrap_or_default();
304
305 let mut seen: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
306 let mut children: Vec<TreeChild> = Vec::with_capacity(refs.len());
307 for r in &refs {
308 if is_excluded_tree_reference(&r.reference_type_id) {
309 continue;
310 }
311 let child = reference_to_tree_child(r);
312 if seen.insert(child.node_id.clone()) {
313 children.push(child);
314 }
315 }
316 let target_ids: Vec<NodeId> = children.iter().map(|c| c.node_id.clone()).collect();
317 let has_kids = has_children_batch(&session, &target_ids).await;
318 for (child, hk) in children.iter_mut().zip(has_kids.into_iter()) {
319 child.has_children = hk;
320 }
321 Ok(children)
322 }
323
324 pub async fn read_node_summary(&self, node_id: &NodeId) -> Result<NodeSummary> {
325 let session = self.session().await?;
326 let to_read: Vec<ReadValueId> = ALL_ATTRIBUTES
327 .iter()
328 .map(|(a, _)| ReadValueId::new(node_id.clone(), *a))
329 .collect();
330 let values = session
331 .read(&to_read, TimestampsToReturn::Both, 0.0)
332 .await
333 .map_err(|s| anyhow!("read failed: {s}"))?;
334
335 let mut attributes: Vec<NodeAttribute> = Vec::new();
336 for ((attr_id, name), dv) in ALL_ATTRIBUTES.iter().zip(values.iter()) {
337 if !attribute_status_ok(dv) {
338 continue;
339 }
340 let Some(v) = dv.value.as_ref() else { continue };
341 let tree = format_attribute_value(*attr_id, v, &session);
342 attributes.push(NodeAttribute {
343 name: name.to_string(),
344 value: tree,
345 });
346 if matches!(attr_id, AttributeId::Value) {
347 if let Some(s) = dv.status.map(|s| s.to_string()) {
348 attributes.push(NodeAttribute {
349 name: "StatusCode".to_string(),
350 value: ValueTree::Leaf(s),
351 });
352 }
353 if let Some(t) = dv.source_timestamp.as_ref() {
354 attributes.push(NodeAttribute {
355 name: "SourceTimestamp".to_string(),
356 value: ValueTree::Leaf(t.to_string()),
357 });
358 }
359 if let Some(t) = dv.server_timestamp.as_ref() {
360 attributes.push(NodeAttribute {
361 name: "ServerTimestamp".to_string(),
362 value: ValueTree::Leaf(t.to_string()),
363 });
364 }
365 }
366 }
367 attributes.sort_by(|a, b| {
368 let rank = |n: &str| if n == "Value" { 0 } else { 1 };
369 rank(&a.name).cmp(&rank(&b.name)).then_with(|| a.name.cmp(&b.name))
370 });
371
372 Ok(NodeSummary {
373 node_id: node_id.clone(),
374 attributes,
375 })
376 }
377
378 pub async fn read_method_signature(&self, method_node_id: &NodeId) -> Result<MethodSignature> {
379 let session = self.session().await?;
380 let node_class = read_node_class(&session, method_node_id).await?;
381 if node_class != NodeClass::Method {
382 return Err(anyhow!("node {method_node_id} is not a Method ({node_class:?})"));
383 }
384 let parent_object = read_inverse_parent(&session, method_node_id)
385 .await?
386 .ok_or_else(|| anyhow!("method has no parent object"))?;
387 let method_display_name = read_display_name(&session, method_node_id)
388 .await
389 .unwrap_or_else(|_| method_node_id.to_string());
390
391 let (inputs_node, outputs_node) = find_argument_properties(&session, method_node_id).await?;
392 let inputs = match inputs_node {
393 Some(n) => read_argument_list(&session, &n).await?,
394 None => Vec::new(),
395 };
396 let outputs = match outputs_node {
397 Some(n) => read_argument_list(&session, &n).await?,
398 None => Vec::new(),
399 };
400
401 let mut input_args = Vec::with_capacity(inputs.len());
402 for a in inputs {
403 input_args.push(argument_to_method_argument(&session, a).await);
404 }
405 let mut output_args = Vec::with_capacity(outputs.len());
406 for a in outputs {
407 output_args.push(argument_to_method_argument(&session, a).await);
408 }
409
410 Ok(MethodSignature {
411 parent_object,
412 method_node: method_node_id.clone(),
413 method_display_name,
414 inputs: input_args,
415 outputs: output_args,
416 })
417 }
418
419 pub async fn call_method(
420 &self,
421 parent_object: &NodeId,
422 method_node_id: &NodeId,
423 inputs: Vec<Variant>,
424 ) -> Result<MethodCallOutcome> {
425 let session = self.session().await?;
426 let request = CallMethodRequest {
427 object_id: parent_object.clone(),
428 method_id: method_node_id.clone(),
429 input_arguments: Some(inputs),
430 };
431 let r = session
432 .call_one(request)
433 .await
434 .map_err(|s| anyhow!("call failed: {s}"))?;
435 let status = r.status_code.to_string();
436 let outputs = r
437 .output_arguments
438 .unwrap_or_default()
439 .iter()
440 .map(|v| variant_to_tree(&session, v))
441 .collect();
442 let input_arg_errors = r
443 .input_argument_results
444 .unwrap_or_default()
445 .into_iter()
446 .map(|s| if s.is_good() { None } else { Some(s.to_string()) })
447 .collect();
448 Ok(MethodCallOutcome {
449 status,
450 outputs,
451 input_arg_errors,
452 })
453 }
454
455 pub async fn browse_references(&self, node_id: &NodeId) -> Result<Vec<ReferenceRow>> {
456 let session = self.session().await?;
457 let desc = BrowseDescription {
458 node_id: node_id.clone(),
459 browse_direction: BrowseDirection::Both,
460 reference_type_id: NodeId::new(0, ReferenceTypeId::References as u32),
461 include_subtypes: true,
462 node_class_mask: NodeClassMask::all().bits(),
463 result_mask: BrowseDescriptionResultMask::all().bits(),
464 };
465 let mut results = session
466 .browse(&[desc], 0, None)
467 .await
468 .map_err(|s| anyhow!("browse failed: {s}"))?;
469 let result = results
470 .pop()
471 .ok_or_else(|| anyhow!("empty browse result"))?;
472 let refs = result.references.unwrap_or_default();
473
474 let mut rows = Vec::with_capacity(refs.len());
475 for r in refs {
476 rows.push(reference_to_row(&session, r).await);
477 }
478 Ok(rows)
479 }
480}
481
482async fn read_node_class(session: &Session, node_id: &NodeId) -> Result<NodeClass> {
483 let to_read = vec![ReadValueId::new(node_id.clone(), AttributeId::NodeClass)];
484 let values = session
485 .read(&to_read, TimestampsToReturn::Neither, 0.0)
486 .await
487 .map_err(|s| anyhow!("read NodeClass failed: {s}"))?;
488 let v = values
489 .into_iter()
490 .next()
491 .and_then(|v| v.value)
492 .ok_or_else(|| anyhow!("NodeClass attribute missing for {node_id}"))?;
493 match v {
494 Variant::Int32(i) => NodeClass::try_from(i)
495 .map_err(|_| anyhow!("invalid NodeClass {i} for {node_id}")),
496 other => Err(anyhow!("unexpected NodeClass variant: {other:?}")),
497 }
498}
499
500async fn read_display_name(session: &Session, node_id: &NodeId) -> Result<String> {
501 let to_read = vec![ReadValueId::new(node_id.clone(), AttributeId::DisplayName)];
502 let values = session
503 .read(&to_read, TimestampsToReturn::Neither, 0.0)
504 .await
505 .map_err(|s| anyhow!("read DisplayName failed: {s}"))?;
506 let text = values
507 .into_iter()
508 .next()
509 .and_then(|v| v.value)
510 .and_then(|v| match v {
511 Variant::LocalizedText(t) => Some(t.text.to_string()),
512 _ => None,
513 });
514 Ok(text.unwrap_or_else(|| node_id.to_string()))
515}
516
517async fn find_argument_properties(
518 session: &Session,
519 method_node_id: &NodeId,
520) -> Result<(Option<NodeId>, Option<NodeId>)> {
521 let desc = BrowseDescription {
522 node_id: method_node_id.clone(),
523 browse_direction: BrowseDirection::Forward,
524 reference_type_id: NodeId::new(0, ReferenceTypeId::HasProperty as u32),
525 include_subtypes: true,
526 node_class_mask: NodeClassMask::VARIABLE.bits(),
527 result_mask: BrowseDescriptionResultMask::all().bits(),
528 };
529 let mut results = session
530 .browse(&[desc], 0, None)
531 .await
532 .map_err(|s| anyhow!("browse properties failed: {s}"))?;
533 let refs = results
534 .pop()
535 .and_then(|r| r.references)
536 .unwrap_or_default();
537 let mut inputs = None;
538 let mut outputs = None;
539 for r in refs {
540 if r.browse_name.namespace_index != 0 {
541 continue;
542 }
543 match r.browse_name.name.as_ref() {
544 "InputArguments" => inputs = Some(r.node_id.node_id),
545 "OutputArguments" => outputs = Some(r.node_id.node_id),
546 _ => {}
547 }
548 }
549 Ok((inputs, outputs))
550}
551
552async fn read_argument_list(session: &Session, property_node: &NodeId) -> Result<Vec<Argument>> {
553 let to_read = vec![ReadValueId::new(property_node.clone(), AttributeId::Value)];
554 let values = session
555 .read(&to_read, TimestampsToReturn::Neither, 0.0)
556 .await
557 .map_err(|s| anyhow!("read {property_node} failed: {s}"))?;
558 let Some(variant) = values.into_iter().next().and_then(|v| v.value) else {
559 return Ok(Vec::new());
560 };
561 if matches!(variant, Variant::Empty) {
562 return Ok(Vec::new());
563 }
564 <Vec<Argument>>::try_from_variant(variant)
565 .map_err(|e| anyhow!("decode Argument array failed: {e}"))
566}
567
568async fn argument_to_method_argument(session: &Session, a: Argument) -> MethodArgument {
569 let type_label = data_type_label(session, &a.data_type, a.value_rank).await;
570 MethodArgument {
571 name: a.name.to_string(),
572 description: a.description.text.to_string(),
573 data_type: a.data_type,
574 value_rank: a.value_rank,
575 type_label,
576 }
577}
578
579async fn data_type_label(session: &Session, data_type: &NodeId, value_rank: i32) -> String {
580 let base = match builtin_data_type_label(data_type) {
581 Some(s) => s.to_string(),
582 None => read_display_name(session, data_type)
583 .await
584 .unwrap_or_else(|_| data_type.to_string()),
585 };
586 if value_rank >= 1 {
587 format!("{base}[]")
588 } else {
589 base
590 }
591}
592
593fn builtin_data_type_label(id: &NodeId) -> Option<&'static str> {
594 if id.namespace != 0 {
595 return None;
596 }
597 let Identifier::Numeric(n) = id.identifier else {
598 return None;
599 };
600 Some(match n {
601 x if x == DataTypeId::Boolean as u32 => "Boolean",
602 x if x == DataTypeId::SByte as u32 => "SByte",
603 x if x == DataTypeId::Byte as u32 => "Byte",
604 x if x == DataTypeId::Int16 as u32 => "Int16",
605 x if x == DataTypeId::UInt16 as u32 => "UInt16",
606 x if x == DataTypeId::Int32 as u32 => "Int32",
607 x if x == DataTypeId::UInt32 as u32 => "UInt32",
608 x if x == DataTypeId::Int64 as u32 => "Int64",
609 x if x == DataTypeId::UInt64 as u32 => "UInt64",
610 x if x == DataTypeId::Float as u32 => "Float",
611 x if x == DataTypeId::Double as u32 => "Double",
612 x if x == DataTypeId::String as u32 => "String",
613 x if x == DataTypeId::DateTime as u32 => "DateTime",
614 x if x == DataTypeId::Guid as u32 => "Guid",
615 x if x == DataTypeId::ByteString as u32 => "ByteString",
616 x if x == DataTypeId::NodeId as u32 => "NodeId",
617 x if x == DataTypeId::ExpandedNodeId as u32 => "ExpandedNodeId",
618 x if x == DataTypeId::StatusCode as u32 => "StatusCode",
619 x if x == DataTypeId::QualifiedName as u32 => "QualifiedName",
620 x if x == DataTypeId::LocalizedText as u32 => "LocalizedText",
621 _ => return None,
622 })
623}
624
625pub fn parse_variant(input: &str, data_type: &NodeId, value_rank: i32) -> Result<Variant, String> {
628 let is_array = value_rank >= 1;
629 let scalar_type = builtin_scalar_type(data_type)
630 .ok_or_else(|| format!("unsupported data type: {data_type}"))?;
631 if !is_array {
632 return parse_scalar(input.trim(), scalar_type);
633 }
634 let trimmed = input.trim().trim_start_matches('[').trim_end_matches(']');
635 let tokens: Vec<&str> = if trimmed.is_empty() {
636 Vec::new()
637 } else {
638 trimmed.split(',').map(|s| s.trim()).collect()
639 };
640 let mut variants = Vec::with_capacity(tokens.len());
641 for (i, t) in tokens.iter().enumerate() {
642 let v = parse_scalar(t, scalar_type).map_err(|e| format!("item {i}: {e}"))?;
643 variants.push(v);
644 }
645 let variant_type = scalar_type_to_variant_scalar(scalar_type);
646 let array = Array::new(variant_type, variants).map_err(|e| format!("array build: {e}"))?;
647 Ok(Variant::Array(Box::new(array)))
648}
649
650#[derive(Clone, Copy)]
651enum ScalarType {
652 Boolean,
653 SByte,
654 Byte,
655 Int16,
656 UInt16,
657 Int32,
658 UInt32,
659 Int64,
660 UInt64,
661 Float,
662 Double,
663 String,
664 NodeId,
665 Guid,
666}
667
668fn builtin_scalar_type(id: &NodeId) -> Option<ScalarType> {
669 if id.namespace != 0 {
670 return None;
671 }
672 let Identifier::Numeric(n) = id.identifier else {
673 return None;
674 };
675 Some(match n {
676 x if x == DataTypeId::Boolean as u32 => ScalarType::Boolean,
677 x if x == DataTypeId::SByte as u32 => ScalarType::SByte,
678 x if x == DataTypeId::Byte as u32 => ScalarType::Byte,
679 x if x == DataTypeId::Int16 as u32 => ScalarType::Int16,
680 x if x == DataTypeId::UInt16 as u32 => ScalarType::UInt16,
681 x if x == DataTypeId::Int32 as u32 => ScalarType::Int32,
682 x if x == DataTypeId::UInt32 as u32 => ScalarType::UInt32,
683 x if x == DataTypeId::Int64 as u32 => ScalarType::Int64,
684 x if x == DataTypeId::UInt64 as u32 => ScalarType::UInt64,
685 x if x == DataTypeId::Float as u32 => ScalarType::Float,
686 x if x == DataTypeId::Double as u32 => ScalarType::Double,
687 x if x == DataTypeId::String as u32 => ScalarType::String,
688 x if x == DataTypeId::NodeId as u32 => ScalarType::NodeId,
689 x if x == DataTypeId::Guid as u32 => ScalarType::Guid,
690 _ => return None,
691 })
692}
693
694fn scalar_type_to_variant_scalar(t: ScalarType) -> VariantScalarTypeId {
695 match t {
696 ScalarType::Boolean => VariantScalarTypeId::Boolean,
697 ScalarType::SByte => VariantScalarTypeId::SByte,
698 ScalarType::Byte => VariantScalarTypeId::Byte,
699 ScalarType::Int16 => VariantScalarTypeId::Int16,
700 ScalarType::UInt16 => VariantScalarTypeId::UInt16,
701 ScalarType::Int32 => VariantScalarTypeId::Int32,
702 ScalarType::UInt32 => VariantScalarTypeId::UInt32,
703 ScalarType::Int64 => VariantScalarTypeId::Int64,
704 ScalarType::UInt64 => VariantScalarTypeId::UInt64,
705 ScalarType::Float => VariantScalarTypeId::Float,
706 ScalarType::Double => VariantScalarTypeId::Double,
707 ScalarType::String => VariantScalarTypeId::String,
708 ScalarType::NodeId => VariantScalarTypeId::NodeId,
709 ScalarType::Guid => VariantScalarTypeId::Guid,
710 }
711}
712
713fn parse_scalar(s: &str, t: ScalarType) -> Result<Variant, String> {
714 if matches!(t, ScalarType::String) {
715 return Ok(Variant::String(UAString::from(s)));
716 }
717 if s.is_empty() {
718 return Err("value required".to_string());
719 }
720 Ok(match t {
721 ScalarType::Boolean => Variant::Boolean(
722 s.parse::<bool>().map_err(|e| format!("invalid Boolean: {e}"))?,
723 ),
724 ScalarType::SByte => {
725 Variant::SByte(s.parse::<i8>().map_err(|e| format!("invalid SByte: {e}"))?)
726 }
727 ScalarType::Byte => {
728 Variant::Byte(s.parse::<u8>().map_err(|e| format!("invalid Byte: {e}"))?)
729 }
730 ScalarType::Int16 => {
731 Variant::Int16(s.parse::<i16>().map_err(|e| format!("invalid Int16: {e}"))?)
732 }
733 ScalarType::UInt16 => Variant::UInt16(
734 s.parse::<u16>().map_err(|e| format!("invalid UInt16: {e}"))?,
735 ),
736 ScalarType::Int32 => {
737 Variant::Int32(s.parse::<i32>().map_err(|e| format!("invalid Int32: {e}"))?)
738 }
739 ScalarType::UInt32 => Variant::UInt32(
740 s.parse::<u32>().map_err(|e| format!("invalid UInt32: {e}"))?,
741 ),
742 ScalarType::Int64 => {
743 Variant::Int64(s.parse::<i64>().map_err(|e| format!("invalid Int64: {e}"))?)
744 }
745 ScalarType::UInt64 => Variant::UInt64(
746 s.parse::<u64>().map_err(|e| format!("invalid UInt64: {e}"))?,
747 ),
748 ScalarType::Float => {
749 Variant::Float(s.parse::<f32>().map_err(|e| format!("invalid Float: {e}"))?)
750 }
751 ScalarType::Double => Variant::Double(
752 s.parse::<f64>().map_err(|e| format!("invalid Double: {e}"))?,
753 ),
754 ScalarType::String => unreachable!(),
755 ScalarType::NodeId => Variant::NodeId(Box::new(
756 NodeId::from_str(s).map_err(|e| format!("invalid NodeId: {e}"))?,
757 )),
758 ScalarType::Guid => Variant::Guid(Box::new(
759 Guid::from_str(s).map_err(|e| format!("invalid Guid: {e:?}"))?,
760 )),
761 })
762}
763
764async fn read_browse_name(session: &Session, node_id: &NodeId) -> Result<String> {
765 let to_read = vec![ReadValueId::new(node_id.clone(), AttributeId::BrowseName)];
766 let values = session
767 .read(&to_read, TimestampsToReturn::Neither, 0.0)
768 .await
769 .map_err(|s| anyhow!("read BrowseName failed: {s}"))?;
770 let q = values
771 .into_iter()
772 .next()
773 .and_then(|v| v.value)
774 .and_then(|v| match v {
775 Variant::QualifiedName(q) => Some(*q),
776 _ => None,
777 });
778 Ok(match q {
779 Some(q) => format_path_segment(q.namespace_index, q.name.as_ref()),
780 None => node_id.to_string(),
781 })
782}
783
784async fn read_inverse_parent(session: &Session, node_id: &NodeId) -> Result<Option<NodeId>> {
785 let desc = BrowseDescription {
786 node_id: node_id.clone(),
787 browse_direction: BrowseDirection::Inverse,
788 reference_type_id: NodeId::new(0, ReferenceTypeId::HierarchicalReferences as u32),
789 include_subtypes: true,
790 node_class_mask: NodeClassMask::all().bits(),
791 result_mask: BrowseDescriptionResultMask::all().bits(),
792 };
793 let mut results = session
794 .browse(&[desc], 0, None)
795 .await
796 .map_err(|s| anyhow!("browse inverse failed: {s}"))?;
797 let parent = results
798 .pop()
799 .and_then(|r| r.references)
800 .and_then(|refs| {
801 refs.into_iter()
802 .find(|rd| !is_excluded_tree_reference(&rd.reference_type_id))
803 })
804 .map(|r| r.node_id.node_id);
805 Ok(parent)
806}
807
808fn format_path_segment(ns: u16, name: &str) -> String {
809 let escaped = escape_browse_name(name);
810 if ns == 0 {
811 escaped
812 } else {
813 format!("{ns}:{escaped}")
814 }
815}
816
817fn escape_browse_name(s: &str) -> String {
818 let mut out = String::with_capacity(s.len());
819 for c in s.chars() {
820 if matches!(c, '&' | '/' | '.' | '<' | '>' | ':' | '#' | '!' | ';') {
821 out.push('&');
822 }
823 out.push(c);
824 }
825 out
826}
827
828fn is_excluded_tree_reference(ref_type: &NodeId) -> bool {
829 if ref_type.namespace != 0 {
830 return false;
831 }
832 let id = match &ref_type.identifier {
833 opcua::types::Identifier::Numeric(n) => *n,
834 _ => return false,
835 };
836 matches!(
837 id,
838 x if x == ReferenceTypeId::HasEventSource as u32
839 || x == ReferenceTypeId::HasNotifier as u32
840 )
841}
842
843fn browse_hierarchical(node_id: NodeId) -> BrowseDescription {
844 BrowseDescription {
845 node_id,
846 browse_direction: BrowseDirection::Forward,
847 reference_type_id: NodeId::new(0, ReferenceTypeId::HierarchicalReferences as u32),
848 include_subtypes: true,
849 node_class_mask: NodeClassMask::all().bits(),
850 result_mask: BrowseDescriptionResultMask::all().bits(),
851 }
852}
853
854fn reference_to_tree_child(r: &ReferenceDescription) -> TreeChild {
855 TreeChild {
856 node_id: expanded_to_local(&r.node_id),
857 browse_name: r.browse_name.name.to_string(),
858 display_name: r.display_name.text.to_string(),
859 node_class: r.node_class,
860 has_children: false,
861 }
862}
863
864async fn reference_to_row(session: &Session, r: ReferenceDescription) -> ReferenceRow {
865 let reference_type = resolve_reference_type_name(session, &r.reference_type_id).await;
866 ReferenceRow {
867 reference_type,
868 is_forward: r.is_forward,
869 target_node_id: expanded_to_local(&r.node_id),
870 target_browse_name: r.browse_name.name.to_string(),
871 target_display_name: r.display_name.text.to_string(),
872 target_node_class: r.node_class,
873 }
874}
875
876async fn resolve_reference_type_name(session: &Session, ref_type: &NodeId) -> String {
877 let read = vec![ReadValueId::new(ref_type.clone(), AttributeId::DisplayName)];
878 match session.read(&read, TimestampsToReturn::Neither, 0.0).await {
879 Ok(vals) => vals
880 .into_iter()
881 .next()
882 .and_then(|v| v.value)
883 .and_then(|v| match v {
884 Variant::LocalizedText(t) => Some(t.text.to_string()),
885 _ => None,
886 })
887 .unwrap_or_else(|| ref_type.to_string()),
888 Err(_) => ref_type.to_string(),
889 }
890}
891
892async fn has_children_batch(session: &Session, ids: &[NodeId]) -> Vec<bool> {
893 if ids.is_empty() {
894 return Vec::new();
895 }
896 let descs: Vec<BrowseDescription> = ids
897 .iter()
898 .map(|id| BrowseDescription {
899 node_id: id.clone(),
900 browse_direction: BrowseDirection::Forward,
901 reference_type_id: NodeId::new(0, ReferenceTypeId::HierarchicalReferences as u32),
902 include_subtypes: true,
903 node_class_mask: NodeClassMask::all().bits(),
904 result_mask: BrowseDescriptionResultMask::RESULT_MASK_REFERENCE_TYPE.bits(),
905 })
906 .collect();
907 match session.browse(&descs, 0, None).await {
908 Ok(results) => results
909 .into_iter()
910 .map(|r| {
911 r.references
912 .map(|refs| {
913 refs.iter()
914 .any(|rd| !is_excluded_tree_reference(&rd.reference_type_id))
915 })
916 .unwrap_or(false)
917 })
918 .collect(),
919 Err(_) => vec![false; ids.len()],
920 }
921}
922
923fn expanded_to_local(eid: &ExpandedNodeId) -> NodeId {
924 eid.node_id.clone()
925}
926
927fn log_client_cert_hint() {
928 let path = std::env::current_dir()
929 .unwrap_or_default()
930 .join("pki/own/cert.der");
931 tracing::info!(
932 "encrypted connection as \"{}\" ({}); client certificate at {}",
933 APPLICATION_NAME,
934 APPLICATION_URI,
935 path.display()
936 );
937 tracing::info!(
938 "if the server rejects the connection, copy that file into the server's trusted certs folder"
939 );
940}
941
942fn looks_like_cert_trust_error(msg: &str) -> bool {
943 let lower = msg.to_lowercase();
944 lower.contains("badsecurity")
945 || lower.contains("badcertificate")
946 || lower.contains("certificatevalidation")
947 || lower.contains("untrusted")
948 || lower.contains("rejected")
949}
950
951fn build_identity_token(auth: &AuthSpec) -> Result<IdentityToken> {
952 match auth.mode {
953 AuthMode::Anonymous => Ok(IdentityToken::Anonymous),
954 AuthMode::UserName => {
955 if auth.username.is_empty() {
956 return Err(anyhow!("username required"));
957 }
958 Ok(IdentityToken::new_user_name(
959 auth.username.clone(),
960 auth.password.clone(),
961 ))
962 }
963 AuthMode::Certificate => {
964 if auth.cert_path.is_empty() || auth.key_path.is_empty() {
965 return Err(anyhow!("certificate and private-key paths required"));
966 }
967 IdentityToken::new_x509_path(&auth.cert_path, &auth.key_path)
968 .map_err(|e| anyhow!("failed to load certificate/key: {e}"))
969 }
970 }
971}
972
973const APPLICATION_NAME: &str = "Rust OPC UA Client from FreeOpcUa";
974const APPLICATION_URI: &str = "urn:FreeOpcUa:ua-client";
975
976fn warn_insecure_default() {
977 tracing::warn!(
978 "INSECURE DEFAULT: server-certificate checks (time, hostname, application-URI) are DISABLED — trusted networks only"
979 );
980}
981
982fn build_client(verify_cert_metadata: bool) -> Result<opcua::client::Client> {
983 ClientBuilder::new()
984 .application_name(APPLICATION_NAME)
985 .application_uri(APPLICATION_URI)
986 .product_uri(APPLICATION_URI)
987 .trust_server_certs(true)
988 .verify_server_certs(verify_cert_metadata)
989 .create_sample_keypair(true)
990 .session_retry_limit(0)
991 .client()
992 .map_err(|errs| anyhow!("failed to build OPC UA client: {errs:?}"))
993}
994
995fn security_mode_to_message_mode(m: SecurityMode) -> MessageSecurityMode {
996 match m {
997 SecurityMode::None => MessageSecurityMode::None,
998 SecurityMode::Sign => MessageSecurityMode::Sign,
999 SecurityMode::SignAndEncrypt => MessageSecurityMode::SignAndEncrypt,
1000 }
1001}
1002
1003fn message_mode_to_security_mode(m: MessageSecurityMode) -> SecurityMode {
1004 match m {
1005 MessageSecurityMode::Sign => SecurityMode::Sign,
1006 MessageSecurityMode::SignAndEncrypt => SecurityMode::SignAndEncrypt,
1007 _ => SecurityMode::None,
1008 }
1009}
1010
1011fn endpoint_description_to_info(ep: EndpointDescription) -> EndpointInfo {
1012 let policy_uri = ep.security_policy_uri.to_string();
1013 let policy_short = SecurityPolicy::from_str(&policy_uri)
1014 .map(|p| p.to_string())
1015 .unwrap_or_else(|_| policy_uri.clone());
1016 let tokens = ep.user_identity_tokens.unwrap_or_default();
1017 let supports_anonymous = tokens
1018 .iter()
1019 .any(|t| matches!(t.token_type, UserTokenType::Anonymous));
1020 let supports_username = tokens
1021 .iter()
1022 .any(|t| matches!(t.token_type, UserTokenType::UserName));
1023 let supports_certificate = tokens
1024 .iter()
1025 .any(|t| matches!(t.token_type, UserTokenType::Certificate));
1026
1027 EndpointInfo {
1028 endpoint_url: ep.endpoint_url.to_string(),
1029 security_policy: policy_short,
1030 security_policy_uri: policy_uri,
1031 security_mode: message_mode_to_security_mode(ep.security_mode),
1032 security_level: ep.security_level,
1033 supports_anonymous,
1034 supports_username,
1035 supports_certificate,
1036 }
1037}
1038
1039const ALL_ATTRIBUTES: &[(AttributeId, &str)] = &[
1040 (AttributeId::AccessLevel, "AccessLevel"),
1041 (AttributeId::AccessLevelEx, "AccessLevelEx"),
1042 (AttributeId::AccessRestrictions, "AccessRestrictions"),
1043 (AttributeId::ArrayDimensions, "ArrayDimensions"),
1044 (AttributeId::BrowseName, "BrowseName"),
1045 (AttributeId::ContainsNoLoops, "ContainsNoLoops"),
1046 (AttributeId::DataType, "DataType"),
1047 (AttributeId::DataTypeDefinition, "DataTypeDefinition"),
1048 (AttributeId::Description, "Description"),
1049 (AttributeId::DisplayName, "DisplayName"),
1050 (AttributeId::EventNotifier, "EventNotifier"),
1051 (AttributeId::Executable, "Executable"),
1052 (AttributeId::Historizing, "Historizing"),
1053 (AttributeId::InverseName, "InverseName"),
1054 (AttributeId::IsAbstract, "IsAbstract"),
1055 (
1056 AttributeId::MinimumSamplingInterval,
1057 "MinimumSamplingInterval",
1058 ),
1059 (AttributeId::NodeClass, "NodeClass"),
1060 (AttributeId::NodeId, "NodeId"),
1061 (AttributeId::RolePermissions, "RolePermissions"),
1062 (AttributeId::Symmetric, "Symmetric"),
1063 (AttributeId::UserAccessLevel, "UserAccessLevel"),
1064 (AttributeId::UserExecutable, "UserExecutable"),
1065 (AttributeId::UserRolePermissions, "UserRolePermissions"),
1066 (AttributeId::UserWriteMask, "UserWriteMask"),
1067 (AttributeId::Value, "Value"),
1068 (AttributeId::ValueRank, "ValueRank"),
1069 (AttributeId::WriteMask, "WriteMask"),
1070];
1071
1072fn attribute_status_ok(dv: &DataValue) -> bool {
1073 match dv.status {
1074 None => dv.value.is_some(),
1075 Some(s) => s.is_good(),
1076 }
1077}
1078
1079fn format_attribute_value(attr: AttributeId, v: &Variant, session: &Session) -> ValueTree {
1080 if matches!(attr, AttributeId::NodeClass)
1081 && let Variant::Int32(i) = v
1082 && let Ok(nc) = NodeClass::try_from(*i)
1083 {
1084 return ValueTree::Leaf(format!("{nc:?}"));
1085 }
1086 variant_to_tree(session, v)
1087}
1088
1089fn variant_to_tree(session: &Session, v: &Variant) -> ValueTree {
1090 match v {
1091 Variant::Empty => ValueTree::Null,
1092 Variant::Boolean(b) => ValueTree::Leaf(b.to_string()),
1093 Variant::SByte(n) => ValueTree::Leaf(n.to_string()),
1094 Variant::Byte(n) => ValueTree::Leaf(n.to_string()),
1095 Variant::Int16(n) => ValueTree::Leaf(n.to_string()),
1096 Variant::UInt16(n) => ValueTree::Leaf(n.to_string()),
1097 Variant::Int32(n) => ValueTree::Leaf(n.to_string()),
1098 Variant::UInt32(n) => ValueTree::Leaf(n.to_string()),
1099 Variant::Int64(n) => ValueTree::Leaf(n.to_string()),
1100 Variant::UInt64(n) => ValueTree::Leaf(n.to_string()),
1101 Variant::Float(n) => ValueTree::Leaf(n.to_string()),
1102 Variant::Double(n) => ValueTree::Leaf(n.to_string()),
1103 Variant::String(s) => ValueTree::Leaf(s.to_string()),
1104 Variant::DateTime(d) => ValueTree::Leaf(d.to_string()),
1105 Variant::Guid(g) => ValueTree::Leaf(format!("{g:?}")),
1106 Variant::StatusCode(s) => ValueTree::Leaf(s.to_string()),
1107 Variant::ByteString(b) => match b.value.as_ref() {
1108 Some(bytes) => ValueTree::Leaf(format!("<{} bytes>", bytes.len())),
1109 None => ValueTree::Null,
1110 },
1111 Variant::XmlElement(_) => ValueTree::Leaf("XmlElement(…)".to_string()),
1112 Variant::QualifiedName(q) => ValueTree::Leaf(q.name.to_string()),
1113 Variant::LocalizedText(t) => ValueTree::Leaf(t.text.to_string()),
1114 Variant::NodeId(n) => ValueTree::Leaf(n.to_string()),
1115 Variant::ExpandedNodeId(n) => ValueTree::Leaf(format!("{n}")),
1116 Variant::ExtensionObject(obj) => extension_object_to_tree(session, obj),
1117 Variant::Variant(inner) => variant_to_tree(session, inner),
1118 Variant::DataValue(_) => ValueTree::Leaf("DataValue(…)".to_string()),
1119 Variant::DiagnosticInfo(_) => ValueTree::Leaf("DiagnosticInfo(…)".to_string()),
1120 Variant::Array(arr) => ValueTree::Array(
1121 arr.values
1122 .iter()
1123 .map(|i| variant_to_tree(session, i))
1124 .collect(),
1125 ),
1126 }
1127}
1128
1129fn extension_object_to_tree(session: &Session, obj: &opcua::types::ExtensionObject) -> ValueTree {
1130 if obj.inner_as::<DynamicStructure>().is_none() {
1131 let label = obj
1132 .type_name()
1133 .map(|n| format!("ExtensionObject ({n})"))
1134 .unwrap_or_else(|| "ExtensionObject".to_string());
1135 return ValueTree::Leaf(label);
1136 }
1137 match dynamic_struct_to_tree(session, obj) {
1138 Some(tree) => tree,
1139 None => ValueTree::Leaf("ExtensionObject (decode failed)".to_string()),
1140 }
1141}
1142
1143fn dynamic_struct_to_tree(
1144 session: &Session,
1145 obj: &opcua::types::ExtensionObject,
1146) -> Option<ValueTree> {
1147 let ds = obj.inner_as::<DynamicStructure>()?;
1148 let ctx_owned = session.context();
1149 let ctx_guard = ctx_owned.read();
1150 let ctx = ctx_guard.context();
1151 let mut buf = Vec::new();
1152 {
1153 let writer_ref: &mut dyn std::io::Write = &mut buf;
1154 let mut writer = JsonStreamWriter::new(writer_ref);
1155 ds.encode(&mut writer, &ctx).ok()?;
1156 writer.finish_document().ok()?;
1157 }
1158 let json: serde_json::Value = serde_json::from_slice(&buf).ok()?;
1159 Some(json_to_tree(&json))
1160}
1161
1162fn json_to_tree(v: &serde_json::Value) -> ValueTree {
1163 match v {
1164 serde_json::Value::Null => ValueTree::Null,
1165 serde_json::Value::Bool(b) => ValueTree::Leaf(b.to_string()),
1166 serde_json::Value::Number(n) => ValueTree::Leaf(n.to_string()),
1167 serde_json::Value::String(s) => ValueTree::Leaf(s.clone()),
1168 serde_json::Value::Array(arr) => ValueTree::Array(arr.iter().map(json_to_tree).collect()),
1169 serde_json::Value::Object(map) => ValueTree::Object(
1170 map.iter()
1171 .map(|(k, v)| (k.clone(), json_to_tree(v)))
1172 .collect(),
1173 ),
1174 }
1175}
1176
1177async fn find_child_by_browse_name(
1178 session: &Session,
1179 parent: &NodeId,
1180 target: &QualifiedName,
1181) -> Result<Option<NodeId>> {
1182 let desc = browse_hierarchical(parent.clone());
1183 let mut results = session
1184 .browse(&[desc], 0, None)
1185 .await
1186 .map_err(|s| anyhow!("browse failed: {s}"))?;
1187 let refs = results.pop().and_then(|r| r.references).unwrap_or_default();
1188 for r in refs {
1189 if is_excluded_tree_reference(&r.reference_type_id) {
1190 continue;
1191 }
1192 if r.browse_name.namespace_index == target.namespace_index
1193 && r.browse_name.name.as_ref() == target.name.as_ref()
1194 {
1195 return Ok(Some(r.node_id.node_id));
1196 }
1197 }
1198 Ok(None)
1199}
1200
1201fn parse_qualified_name(segment: &str) -> QualifiedName {
1206 let body = segment.strip_prefix("ns=").unwrap_or(segment);
1207 if let Some((head, rest)) = body.split_once(':')
1208 && let Ok(ns) = head.parse::<u16>()
1209 {
1210 return QualifiedName::new(ns, rest);
1211 }
1212 QualifiedName::new(0, segment)
1213}
1214
1215async fn register_dynamic_type_loader(session: &Session) -> Result<()> {
1216 let type_tree = DataTypeTreeBuilder::new(|_| true)
1217 .build(session)
1218 .await
1219 .map_err(|e| anyhow!("DataTypeTreeBuilder failed: {e}"))?;
1220 let loader: Arc<dyn TypeLoader> = Arc::new(DynamicTypeLoader::new(Arc::new(type_tree)));
1221 session.add_type_loader(loader);
1222 Ok(())
1223}