Skip to content
Merged
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
117 changes: 81 additions & 36 deletions src/Opc.Ua.Client/Session/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4761,43 +4761,13 @@ private void ValidateServerEndpoints(ArrayOf<EndpointDescription> serverEndpoint
"Server did not return a number of ServerEndpoints that matches the one from GetEndpoints.");
}

for (int ii = 0; ii < expectedServerEndpoints.Count; ii++)
if (!HaveEquivalentServerEndpoints(
expectedServerEndpoints,
m_discoveryServerEndpoints))
{
EndpointDescription serverEndpoint = expectedServerEndpoints[ii];
EndpointDescription expectedServerEndpoint = m_discoveryServerEndpoints[ii];

if (serverEndpoint.SecurityMode != expectedServerEndpoint.SecurityMode ||
serverEndpoint.SecurityPolicyUri != expectedServerEndpoint
.SecurityPolicyUri ||
serverEndpoint.TransportProfileUri != expectedServerEndpoint
.TransportProfileUri ||
serverEndpoint.SecurityLevel != expectedServerEndpoint.SecurityLevel)
{
throw ServiceResultException.Create(
StatusCodes.BadSecurityChecksFailed,
"The list of ServerEndpoints returned at CreateSession does not match the list from GetEndpoints.");
}

if (serverEndpoint.UserIdentityTokens.Count != expectedServerEndpoint
.UserIdentityTokens
.Count)
{
throw ServiceResultException.Create(
StatusCodes.BadSecurityChecksFailed,
"The list of ServerEndpoints returned at CreateSession does not match the one from GetEndpoints.");
}

for (int jj = 0; jj < serverEndpoint.UserIdentityTokens.Count; jj++)
{
if (!serverEndpoint
.UserIdentityTokens[jj]
.IsEqual(expectedServerEndpoint.UserIdentityTokens[jj]))
{
throw ServiceResultException.Create(
StatusCodes.BadSecurityChecksFailed,
"The list of ServerEndpoints returned at CreateSession does not match the one from GetEndpoints.");
}
}
throw ServiceResultException.Create(
StatusCodes.BadSecurityChecksFailed,
"The list of ServerEndpoints returned at CreateSession does not match the list from GetEndpoints.");
}
}

Expand Down Expand Up @@ -4837,6 +4807,81 @@ private void ValidateServerEndpoints(ArrayOf<EndpointDescription> serverEndpoint
}
}

private static bool HaveEquivalentServerEndpoints(
ArrayOf<EndpointDescription> serverEndpoints,
ArrayOf<EndpointDescription> discoveryEndpoints)
{
if (serverEndpoints.Count != discoveryEndpoints.Count)
{
return false;
}

var unmatchedDiscoveryEndpoints = discoveryEndpoints.ToList();

foreach (EndpointDescription serverEndpoint in serverEndpoints)
{
int matchIndex = unmatchedDiscoveryEndpoints.FindIndex(
discoveryEndpoint => AreEquivalentServerEndpoints(
serverEndpoint,
discoveryEndpoint));

if (matchIndex < 0)
{
return false;
}

unmatchedDiscoveryEndpoints.RemoveAt(matchIndex);
}

return unmatchedDiscoveryEndpoints.Count == 0;
}

private static bool AreEquivalentServerEndpoints(
EndpointDescription serverEndpoint,
EndpointDescription discoveryEndpoint)
{
return serverEndpoint.SecurityMode == discoveryEndpoint.SecurityMode &&
string.Equals(
serverEndpoint.SecurityPolicyUri,
discoveryEndpoint.SecurityPolicyUri,
StringComparison.Ordinal) &&
string.Equals(
serverEndpoint.TransportProfileUri,
discoveryEndpoint.TransportProfileUri,
StringComparison.Ordinal) &&
serverEndpoint.SecurityLevel == discoveryEndpoint.SecurityLevel &&
HaveEquivalentUserIdentityTokens(
serverEndpoint.UserIdentityTokens,
discoveryEndpoint.UserIdentityTokens);
}

private static bool HaveEquivalentUserIdentityTokens(
ArrayOf<UserTokenPolicy> serverTokens,
ArrayOf<UserTokenPolicy> discoveryTokens)
{
if (serverTokens.Count != discoveryTokens.Count)
{
return false;
}

var unmatchedDiscoveryTokens = discoveryTokens.ToList();

foreach (UserTokenPolicy serverToken in serverTokens)
{
int matchIndex = unmatchedDiscoveryTokens.FindIndex(
discoveryToken => serverToken.IsEqual(discoveryToken));

if (matchIndex < 0)
{
return false;
}

unmatchedDiscoveryTokens.RemoveAt(matchIndex);
}

return unmatchedDiscoveryTokens.Count == 0;
}

/// <summary>
/// Find and return matching application description
/// </summary>
Expand Down
60 changes: 47 additions & 13 deletions tests/Opc.Ua.Client.TestFramework/SessionMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,33 @@ public SessionMock(
Channel = channel;
}

public SessionMock(
Mock<ITransportChannel> channel,
ApplicationConfiguration configuration,
ConfiguredEndpoint endpoint,
ArrayOf<EndpointDescription> availableEndpoints,
ArrayOf<string> discoveryProfileUris = default)
: base(
channel.Object,
configuration,
endpoint,
clientCertificate: null,
clientCertificateChain: null,
availableEndpoints: availableEndpoints,
discoveryProfileUris: discoveryProfileUris,
engineFactory: null)
{
Channel = channel;
}

/// <summary>
/// Create default mock
/// </summary>
/// <returns></returns>
public static SessionMock Create(EndpointDescription endpoint = null)
public static SessionMock Create(
EndpointDescription endpoint = null,
ArrayOf<EndpointDescription> availableEndpoints = default,
ArrayOf<string> discoveryProfileUris = default)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
var channel = new Mock<ITransportChannel>();
Expand Down Expand Up @@ -105,18 +127,30 @@ public static SessionMock Create(EndpointDescription endpoint = null)
application.CheckApplicationInstanceCertificatesAsync(true).AsTask().GetAwaiter().GetResult();
}

return new SessionMock(channel, configuration,
new ConfiguredEndpoint(null, endpoint ??
new EndpointDescription
{
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None,
EndpointUrl = "opc.tcp://localhost:4840",
UserIdentityTokens =
[
new UserTokenPolicy()
]
}));
var configuredEndpoint = new ConfiguredEndpoint(
null,
endpoint ?? new EndpointDescription
{
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None,
EndpointUrl = "opc.tcp://localhost:4840",
UserIdentityTokens =
[
new UserTokenPolicy()
]
});

if (availableEndpoints.IsEmpty && discoveryProfileUris.IsEmpty)
{
return new SessionMock(channel, configuration, configuredEndpoint);
}

return new SessionMock(
channel,
configuration,
configuredEndpoint,
availableEndpoints,
discoveryProfileUris);
}

public void SetConnected()
Expand Down
116 changes: 116 additions & 0 deletions tests/Opc.Ua.Client.Tests/Session/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1582,6 +1582,78 @@ public async Task OpenAsyncShouldOpenSessionSuccessfullyAsync()
sut.Channel.Verify();
}

[Test]
public async Task OpenAsyncAcceptsServerEndpointsWithDifferentOrderingAsync()
{
EndpointDescription primaryEndpoint = CreateSessionEndpointDescription(
"opc.tcp://localhost:4840");
primaryEndpoint.SecurityLevel = 1;
primaryEndpoint.TransportProfileUri = Profiles.UaTcpTransport;

EndpointDescription secondaryEndpoint = CreateSessionEndpointDescription(
"opc.tcp://localhost:4841");
secondaryEndpoint.SecurityMode = MessageSecurityMode.Sign;
secondaryEndpoint.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
secondaryEndpoint.SecurityLevel = 2;
secondaryEndpoint.TransportProfileUri = Profiles.UaTcpTransport;

using var sut = SessionMock.Create(
primaryEndpoint,
[primaryEndpoint, secondaryEndpoint],
[Profiles.UaTcpTransport]);

ConfigureSuccessfulOpenResponses(
sut.Channel,
[secondaryEndpoint, primaryEndpoint],
ByteString.From([1, 2, 3, 4]),
NodeId.Parse("s=cookie"));

await sut.OpenAsync("test", new UserIdentity(), CancellationToken.None)
.ConfigureAwait(false);

Assert.That(sut.ServerNonce, Is.EqualTo(ByteString.From([1, 2, 3, 4])));
sut.Channel.Verify();
}

[Test]
public async Task OpenAsyncAcceptsUserTokenPoliciesWithDifferentOrderingAsync()
{
EndpointDescription discoveryEndpoint = CreateSessionEndpointDescription(
"opc.tcp://localhost:4840");
discoveryEndpoint.TransportProfileUri = Profiles.UaTcpTransport;
discoveryEndpoint.UserIdentityTokens =
[
CreateUserTokenPolicy("anonymous", UserTokenType.Anonymous),
CreateUserTokenPolicy("username", UserTokenType.UserName)
];

EndpointDescription responseEndpoint = CreateSessionEndpointDescription(
"opc.tcp://localhost:4840");
responseEndpoint.TransportProfileUri = Profiles.UaTcpTransport;
responseEndpoint.UserIdentityTokens =
[
CreateUserTokenPolicy("username", UserTokenType.UserName),
CreateUserTokenPolicy("anonymous", UserTokenType.Anonymous)
];

using var sut = SessionMock.Create(
discoveryEndpoint,
[discoveryEndpoint],
[Profiles.UaTcpTransport]);

ConfigureSuccessfulOpenResponses(
sut.Channel,
[responseEndpoint],
ByteString.From([1, 2, 3, 4]),
NodeId.Parse("s=cookie"));

await sut.OpenAsync("test", new UserIdentity(), CancellationToken.None)
.ConfigureAwait(false);

Assert.That(sut.ServerNonce, Is.EqualTo(ByteString.From([1, 2, 3, 4])));
sut.Channel.Verify();
}

[Test]
public void OpenAsyncShouldHandleCreateSessionSuccessButActivationError()
{
Expand Down Expand Up @@ -2036,6 +2108,18 @@ private static EndpointDescription CreateSessionEndpointDescription(string endpo
};
}

private static UserTokenPolicy CreateUserTokenPolicy(
string policyId,
UserTokenType tokenType)
{
return new UserTokenPolicy
{
PolicyId = policyId,
TokenType = tokenType,
SecurityPolicyUri = SecurityPolicies.None
};
}

private static Mock<ITransportChannel> CreateReconnectChannelMock(
SessionMock session,
EndpointDescription endpointDescription)
Expand All @@ -2051,6 +2135,38 @@ private static Mock<ITransportChannel> CreateReconnectChannelMock(
return channel;
}

private static void ConfigureSuccessfulOpenResponses(
Mock<ITransportChannel> channel,
ArrayOf<EndpointDescription> serverEndpoints,
ByteString serverNonce,
NodeId authToken)
{
channel
.Setup(c => c.SendRequestAsync(
It.IsAny<CreateSessionRequest>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<IServiceResponse>(new CreateSessionResponse
{
ServerNonce = serverNonce,
SessionId = NodeId.Parse("s=connected"),
AuthenticationToken = authToken,
ServerEndpoints = serverEndpoints
}));

channel
.Setup(c => c.SendRequestAsync(
It.Is<ActivateSessionRequest>(r => r.RequestHeader.AuthenticationToken == authToken),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<IServiceResponse>(new ActivateSessionResponse
{
ServerNonce = serverNonce,
Results = [],
DiagnosticInfos = []
}));

ConfigureOpenAsyncReadResponses(channel);
}

private static void ConfigureOpenAsyncReadResponses(Mock<ITransportChannel> channel)
{
channel
Expand Down
Loading