Files
5edb512ab7 Stricter validation and error handling in GDS server & Server Method Call validation (#3746)
* Stricter validation and error handling in GDS server

- Allow empty DatabaseStorePath for in-memory operation
- Select database type dynamically in GdsNodeManagerFactory
- Enforce required fields and argument validation in ApplicationsDatabaseBase
- Prevent duplicate application URIs in LinqApplicationsDatabase
- Propagate argument errors as OPC UA service exceptions in ApplicationsNodeManager
- Remove obsolete attribute from ParsedNodeId
- Refactor MasterNodeManager write/call validation to async with improved permission checks and attribute caching
- Update UAReferenceServer.ctt.xml test data with non-empty credentials
- Improves robustness, security, error reporting, and test coverage

* Add method-state resolver API for call validation and tests

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/0caf8699-f0e0-4dd5-8dcf-368b08912205

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Refine call validation comments and test coverage naming

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/0caf8699-f0e0-4dd5-8dcf-368b08912205

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Move method resolver onto core NodeManager interfaces

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/0bb87dfd-f432-431f-a0fe-dd4d1ba3197f

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Align NodeManager interfaces and call paths for method resolver

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/0bb87dfd-f432-431f-a0fe-dd4d1ba3197f

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* fixes

* fix

* Fix GDS ClientTest expectations for stricter app validation

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/47d99514-94f9-4051-9e6d-8cb2b44555d0

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* fix tests

* Fix AOT GDS fixture for RSA secured endpoint

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/f6c8fba1-3dbf-41f0-b920-5e1766b9d6e1

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Update app registration logic and audit event sources

Refactor application registration to separate update and create logic, introducing UpdateApplication in database interfaces and implementations. Improve error handling and validation, ensure unique ApplicationUris, and enhance certificate revocation logging. Standardize audit event source names and add ToString override to BaseVariableState. Update server queries for correct pagination.

* fixes

* Add wildcard match coverage for ApplicationsDatabaseBase

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/a2c1a6b0-e867-4610-bead-7e621e3764ff

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Expand wildcard spec coverage in ApplicationsDatabaseBase tests

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/a2c1a6b0-e867-4610-bead-7e621e3764ff

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Align UA wildcard matching and add spec coverage tests

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/a2c1a6b0-e867-4610-bead-7e621e3764ff

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Fix GDS QueryServers test expectations for pagination semantics

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/efa1c64d-05c8-47ee-b308-c77e7d5a34c9

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Add focused tests for LinqApplicationsDatabase behaviors

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/7e4ec1ce-722f-43fb-99db-06b854c3fa7a

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Refine LinqApplicationsDatabase test method naming

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/7e4ec1ce-722f-43fb-99db-06b854c3fa7a

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Merge origin/master and resolve PR merge conflicts

Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/69970cd4-4196-4fb4-9e4c-6ad86104bb5f

Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
2026-05-17 18:47:50 +02:00

298 lines
8.9 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Text;
namespace Opc.Ua.Server
{
/// <summary>
/// Stores the elements of a NodeId after it is parsed.
/// </summary>
/// <remarks>
/// The NodeIds used by the samples are strings with an optional path appended.
/// The RootType identifies the type of Root Node. The RootId is the unique identifier
/// for the Root Node. The ComponentPath is constructed from the SymbolicNames
/// of one or more children of the Root Node.
/// </remarks>
public class ParsedNodeId
{
/// <summary>
/// The namespace index that qualified the NodeId.
/// </summary>
public ushort NamespaceIndex { get; set; }
/// <summary>
/// The identifier for the root of the NodeId.
/// </summary>
public string RootId { get; set; } = null!;
/// <summary>
/// The type of root node.
/// </summary>
public int RootType { get; set; }
/// <summary>
/// The relative path to the component identified by the NodeId.
/// </summary>
public string ComponentPath { get; set; } = null!;
/// <summary>
/// Parses the specified node identifier.
/// </summary>
/// <param name="nodeId">The node identifier.</param>
/// <returns>The parsed node identifier. Null if the identifier cannot be parsed.</returns>
public static ParsedNodeId? Parse(NodeId nodeId)
{
// can only parse non-null string node identifiers.
if (nodeId.IsNull)
{
return null;
}
if (!nodeId.TryGetValue(out string identifier) ||
string.IsNullOrEmpty(identifier))
{
return null;
}
var parsedNodeId = new ParsedNodeId
{
NamespaceIndex = nodeId.NamespaceIndex,
// extract the type of identifier.
RootType = 0
};
int start = 0;
for (int ii = 0; ii < identifier.Length; ii++)
{
if (!char.IsDigit(identifier[ii]))
{
start = ii;
break;
}
parsedNodeId.RootType *= 10;
parsedNodeId.RootType += (byte)(identifier[ii] - '0');
}
if (start >= identifier.Length || identifier[start] != ':')
{
return null;
}
// extract any component path.
var buffer = new StringBuilder();
int index = start + 1;
int end = identifier.Length;
bool escaped = false;
while (index < end)
{
char ch = identifier[index++];
// skip any escape character but keep the one after it.
if (ch == '&')
{
escaped = true;
continue;
}
if (!escaped && ch == '?')
{
end = index;
break;
}
buffer.Append(ch);
escaped = false;
}
// extract any component.
parsedNodeId.RootId = buffer.ToString();
parsedNodeId.ComponentPath = null!;
if (end < identifier.Length)
{
parsedNodeId.ComponentPath = identifier[end..];
}
return parsedNodeId;
}
/// <summary>
/// Constructs a node identifier from the component pieces.
/// </summary>
public static NodeId Construct(
int rootType,
string rootId,
ushort namespaceIndex,
params string[] componentNames)
{
var pnd = new ParsedNodeId
{
RootType = rootType,
RootId = rootId,
NamespaceIndex = namespaceIndex
};
if (componentNames != null)
{
var path = new StringBuilder();
for (int ii = 0; ii < componentNames.Length; ii++)
{
if (path.Length > 0)
{
path.Append('/');
}
path.Append(componentNames[ii]);
}
pnd.ComponentPath = path.ToString();
}
return pnd.Construct(null!);
}
/// <summary>
/// Constructs a node identifier.
/// </summary>
/// <returns>The node identifier.</returns>
public NodeId Construct()
{
return Construct(null!);
}
/// <summary>
/// Constructs a node identifier for a component with the specified name.
/// </summary>
/// <returns>The node identifier.</returns>
public NodeId Construct(string componentName)
{
var buffer = new StringBuilder();
// add the root type.
buffer.Append(RootType)
.Append(':');
// add the root identifier.
if (RootId != null)
{
for (int ii = 0; ii < RootId.Length; ii++)
{
char ch = RootId[ii];
// escape any special characters.
if (ch is '&' or '?')
{
buffer.Append('&');
}
buffer.Append(ch);
}
}
// add the component path.
if (!string.IsNullOrEmpty(ComponentPath))
{
buffer.Append('?')
.Append(ComponentPath);
}
// add the component name.
if (!string.IsNullOrEmpty(componentName))
{
if (string.IsNullOrEmpty(ComponentPath))
{
buffer.Append('?');
}
else
{
buffer.Append('/');
}
buffer.Append(componentName);
}
// construct the node id with the namespace index provided.
return new NodeId(buffer.ToString(), NamespaceIndex);
}
/// <summary>
/// Constructs the node identifier for a component.
/// </summary>
public static NodeId CreateIdForComponent(NodeState component, ushort namespaceIndex)
{
if (component == null)
{
return default;
}
// components must be instances with a parent.
if (component is not BaseInstanceState instance || instance.Parent == null)
{
return component.NodeId;
}
// parent must have a string identifier.
if (!instance.Parent.NodeId.TryGetValue(out string parentId))
{
return default;
}
var buffer = new StringBuilder();
buffer.Append(parentId);
// check if the parent is another component.
int index = parentId.IndexOf('?', StringComparison.Ordinal);
if (index < 0)
{
buffer.Append('?');
}
else
{
buffer.Append('/');
}
buffer.Append(component.SymbolicName);
// return the node identifier.
return new NodeId(buffer.ToString(), namespaceIndex);
}
}
}