Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions Stack/Opc.Ua.Core/Stack/Client/DiscoveryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,27 @@ DateTime lastCounterResetTime
return (response.Servers, response.LastCounterResetTime);
}

/// <summary>
/// Gets a normalized endpoint URL from a Uri that preserves IPv6 scope IDs.
/// </summary>
/// <param name="uri">The URI to normalize.</param>
/// <returns>A normalized endpoint URL string.</returns>
internal static string GetNormalizedEndpointUrl(Uri uri)
{
// Manually reconstruct the URL to normalize it (e.g., add trailing slashes)
// while preserving IPv6 scope IDs using DnsSafeHost.
string host = uri.DnsSafeHost;

// For IPv6 addresses, wrap in brackets
if (uri.HostNameType == UriHostNameType.IPv6)
{
host = $"[{host}]";
}

// Reconstruct the URL
return $"{uri.Scheme}://{host}:{uri.Port}{uri.AbsolutePath}";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are "normal" URIs that do not contain any IPv6 scope id also treated differently? In this case uri.ToString() should suffice.

}

/// <summary>
/// Creates a new transport channel that supports the ISessionChannel service contract.
/// </summary>
Expand All @@ -498,7 +519,7 @@ internal static ValueTask<ITransportChannel> CreateChannelAsync(
// create a default description.
var endpoint = new EndpointDescription
{
EndpointUrl = discoveryUrl.OriginalString,
EndpointUrl = GetNormalizedEndpointUrl(discoveryUrl),
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None
};
Expand Down Expand Up @@ -530,7 +551,7 @@ internal static ValueTask<ITransportChannel> CreateChannelAsync(
// create a default description.
var endpoint = new EndpointDescription
{
EndpointUrl = connection.EndpointUrl.OriginalString,
EndpointUrl = GetNormalizedEndpointUrl(connection.EndpointUrl),
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None
};
Expand Down Expand Up @@ -563,7 +584,7 @@ internal static ValueTask<ITransportChannel> CreateChannelAsync(
// create a default description.
var endpoint = new EndpointDescription
{
EndpointUrl = discoveryUrl.OriginalString,
EndpointUrl = GetNormalizedEndpointUrl(discoveryUrl),
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None
};
Expand Down
35 changes: 35 additions & 0 deletions Tests/Opc.Ua.Core.Tests/Stack/Client/ClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,41 @@ public void DiscoveryEndPointUrls(string urlString)
Assert.AreEqual(uri.OriginalString, uriBuilder.Uri.OriginalString);
}

/// <summary>
/// Ensure that URIs with and without trailing slashes are normalized to be identical,
/// while preserving IPv6 scope IDs.
/// </summary>
[Test]
[TestCase("opc.tcp://hostname:4840/", "opc.tcp://hostname:4840", "opc.tcp://hostname:4840/")]
[TestCase("opc.tcp://hostname:4840/path", "opc.tcp://hostname:4840/path",
"opc.tcp://hostname:4840/path")]
[TestCase("opc.tcp://[fe80::280:deff:fa02:c63e%eth0]:4840/",
"opc.tcp://[fe80::280:deff:fa02:c63e%eth0]:4840",
"opc.tcp://[fe80::280:deff:fa02:c63e%eth0]:4840/")]
[TestCase("opc.tcp://[fe80::de39:6fff:feae:c78%12]:4840/Endpoint1",
"opc.tcp://[fe80::de39:6fff:feae:c78%12]:4840/Endpoint1",
"opc.tcp://[fe80::de39:6fff:feae:c78%12]:4840/Endpoint1")]
public void DiscoveryEndPointUrlNormalization(string url1, string url2, string expectedNormalized)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method signature is confusing. Why test 2 inputs at the same time?

Refactor to one input and the expected compare value.

{
var uri1 = new Uri(url1);
var uri2 = new Uri(url2);

// Call the internal GetNormalizedEndpointUrl method
var normalized1 = DiscoveryClient.GetNormalizedEndpointUrl(uri1);
var normalized2 = DiscoveryClient.GetNormalizedEndpointUrl(uri2);

// Both URIs should normalize to the same value
Assert.AreEqual(expectedNormalized, normalized1, "First URI should normalize correctly");
Assert.AreEqual(expectedNormalized, normalized2, "Second URI should normalize correctly");
Assert.AreEqual(normalized1, normalized2, "Both URIs should normalize to the same value");

// Verify IPv6 scope IDs are preserved
if (url1.Contains("%"))
{
Assert.IsTrue(normalized1.Contains("%"), "IPv6 scope ID should be preserved");
}
}

[Test]
public void ValidateAppConfigWithoutAppCert()
{
Expand Down
Loading