Skip to content

Commit dc12a4b

Browse files
authored
[Client] Fix closing of shared Transport Channel in Recreate Scenario / Add missing event Handler to Session Constructor / Call RenewUserIdentity on Session.ReCreate (#3055)
* Extend Session.Open Method with new overload allowing to specify if the used channel is closed when the Session.Open fails Add Missing Event Handler m_RenewUserIdentity om Session constructor taking a template * Implement Traceable Session Create Async open overload * keep channel open on ReCreateAsync with provided channel * Add Test executing Recreate using existing channel * fix test * Add RenewUserIdentity Handler in case of a RecreateSession * Add Test, fix Recreate to use new Token * Improve code coverage with async test overload * fix Parameter checkDomain in Open Session overload
1 parent d1f9066 commit dc12a4b

File tree

5 files changed

+187
-10
lines changed

5 files changed

+187
-10
lines changed

Libraries/Opc.Ua.Client/Session/ISession.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,17 @@ Task<Dictionary<NodeId, DataDictionary>> LoadDataTypeSystem(
607607
/// <param name="checkDomain">If set to <c>true</c> then the domain in the certificate must match the endpoint used.</param>
608608
void Open(string sessionName, uint sessionTimeout, IUserIdentity identity, IList<string> preferredLocales, bool checkDomain);
609609

610+
/// <summary>
611+
/// Establishes a session with the server.
612+
/// </summary>
613+
/// <param name="sessionName">The name to assign to the session.</param>
614+
/// <param name="sessionTimeout">The session timeout.</param>
615+
/// <param name="identity">The user identity.</param>
616+
/// <param name="preferredLocales">The list of preferred locales.</param>
617+
/// <param name="checkDomain">If set to <c>true</c> then the domain in the certificate must match the endpoint used.</param>
618+
/// <param name="closeChannel">If set to <c>true</c> then the channel is closed when the Open fails.</param>
619+
void Open(string sessionName, uint sessionTimeout, IUserIdentity identity, IList<string> preferredLocales, bool checkDomain, bool closeChannel);
620+
610621
/// <summary>
611622
/// Updates the preferred locales used for the session.
612623
/// </summary>
@@ -669,6 +680,18 @@ Task<Dictionary<NodeId, DataDictionary>> LoadDataTypeSystem(
669680
/// <param name="ct">The cancellation token.</param>
670681
Task OpenAsync(string sessionName, uint sessionTimeout, IUserIdentity identity, IList<string> preferredLocales, bool checkDomain, CancellationToken ct);
671682

683+
/// <summary>
684+
/// Establishes a session with the server.
685+
/// </summary>
686+
/// <param name="sessionName">The name to assign to the session.</param>
687+
/// <param name="sessionTimeout">The session timeout.</param>
688+
/// <param name="identity">The user identity.</param>
689+
/// <param name="preferredLocales">The list of preferred locales.</param>
690+
/// <param name="checkDomain">If set to <c>true</c> then the domain in the certificate must match the endpoint used.</param>
691+
/// <param name="closeChannel">If set to <c>true</c> then the channel is closed when the Open fails.</param>
692+
/// <param name="ct">The cancellation token.</param>
693+
Task OpenAsync(string sessionName, uint sessionTimeout, IUserIdentity identity, IList<string> preferredLocales, bool checkDomain, bool closeChannel, CancellationToken ct);
694+
672695
/// <summary>
673696
/// Reads the values for the node attributes and returns a node object collection.
674697
/// </summary>

Libraries/Opc.Ua.Client/Session/Session.cs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ public Session(ITransportChannel channel, Session template, bool copyEventHandle
153153
m_SubscriptionsChanged = template.m_SubscriptionsChanged;
154154
m_SessionClosing = template.m_SessionClosing;
155155
m_SessionConfigurationChanged = template.m_SessionConfigurationChanged;
156+
m_RenewUserIdentity = template.m_RenewUserIdentity;
156157
}
157158

158159
foreach (Subscription subscription in template.Subscriptions)
@@ -1268,11 +1269,12 @@ public static Session Recreate(Session template)
12681269

12691270
try
12701271
{
1272+
session.RecreateRenewUserIdentity();
12711273
// open the session.
12721274
session.Open(
12731275
template.SessionName,
12741276
(uint)template.SessionTimeout,
1275-
template.Identity,
1277+
session.Identity,
12761278
template.PreferredLocales,
12771279
template.m_checkDomain);
12781280

@@ -1314,11 +1316,12 @@ public static Session Recreate(Session template, ITransportWaitingConnection con
13141316

13151317
try
13161318
{
1319+
session.RecreateRenewUserIdentity();
13171320
// open the session.
13181321
session.Open(
13191322
template.m_sessionName,
13201323
(uint)template.m_sessionTimeout,
1321-
template.m_identity,
1324+
session.Identity,
13221325
template.m_preferredLocales,
13231326
template.m_checkDomain);
13241327

@@ -1349,13 +1352,15 @@ public static Session Recreate(Session template, ITransportChannel transportChan
13491352

13501353
try
13511354
{
1355+
session.RecreateRenewUserIdentity();
13521356
// open the session.
13531357
session.Open(
13541358
template.m_sessionName,
13551359
(uint)template.m_sessionTimeout,
1356-
template.m_identity,
1360+
session.Identity,
13571361
template.m_preferredLocales,
1358-
template.m_checkDomain);
1362+
template.m_checkDomain,
1363+
false);
13591364

13601365
// create the subscriptions.
13611366
foreach (Subscription subscription in session.Subscriptions)
@@ -2288,6 +2293,16 @@ public void Open(
22882293
{
22892294
Open(sessionName, sessionTimeout, identity, preferredLocales, true);
22902295
}
2296+
/// <inheritdoc/>
2297+
public void Open(
2298+
string sessionName,
2299+
uint sessionTimeout,
2300+
IUserIdentity identity,
2301+
IList<string> preferredLocales,
2302+
bool checkDomain)
2303+
{
2304+
Open(sessionName, sessionTimeout, identity, preferredLocales, checkDomain, true);
2305+
}
22912306

22922307
/// <inheritdoc/>
22932308
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
@@ -2296,7 +2311,8 @@ public void Open(
22962311
uint sessionTimeout,
22972312
IUserIdentity identity,
22982313
IList<string> preferredLocales,
2299-
bool checkDomain)
2314+
bool checkDomain,
2315+
bool closeChannel)
23002316
{
23012317
OpenValidateIdentity(ref identity, out var identityToken, out var identityPolicy, out string securityPolicyUri, out bool requireEncryption);
23022318

@@ -2555,7 +2571,7 @@ public void Open(
25552571
{
25562572
try
25572573
{
2558-
Close(true);
2574+
Close(closeChannel);
25592575
}
25602576
catch (Exception e)
25612577
{
@@ -5346,6 +5362,17 @@ public bool ResendData(IEnumerable<Subscription> subscriptions, out IList<Servic
53465362

53475363
return false;
53485364
}
5365+
5366+
/// <summary>
5367+
/// Helper to refresh the identity (reprompt for password, refresh token) in case of a Recreate of the Session.
5368+
/// </summary>
5369+
public virtual void RecreateRenewUserIdentity()
5370+
{
5371+
if (m_RenewUserIdentity != null)
5372+
{
5373+
m_identity = m_RenewUserIdentity(this, m_identity);
5374+
}
5375+
}
53495376
#endregion
53505377

53515378
#region Private Methods

Libraries/Opc.Ua.Client/Session/SessionAsync.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,26 @@ public Task OpenAsync(
6767
return OpenAsync(sessionName, sessionTimeout, identity, preferredLocales, true, ct);
6868
}
6969

70+
/// <inheritdoc/>
71+
public Task OpenAsync(
72+
string sessionName,
73+
uint sessionTimeout,
74+
IUserIdentity identity,
75+
IList<string> preferredLocales,
76+
bool checkDomain,
77+
CancellationToken ct)
78+
{
79+
return OpenAsync(sessionName, sessionTimeout, identity, preferredLocales, checkDomain, true, ct);
80+
}
81+
7082
/// <inheritdoc/>
7183
public async Task OpenAsync(
7284
string sessionName,
7385
uint sessionTimeout,
7486
IUserIdentity identity,
7587
IList<string> preferredLocales,
7688
bool checkDomain,
89+
bool closeChannel,
7790
CancellationToken ct)
7891
{
7992
OpenValidateIdentity(ref identity, out var identityToken, out var identityPolicy, out string securityPolicyUri, out bool requireEncryption);
@@ -322,7 +335,10 @@ public async Task OpenAsync(
322335
try
323336
{
324337
await base.CloseSessionAsync(null, false, CancellationToken.None).ConfigureAwait(false);
325-
await CloseChannelAsync(CancellationToken.None).ConfigureAwait(false);
338+
if (closeChannel)
339+
{
340+
await CloseChannelAsync(CancellationToken.None).ConfigureAwait(false);
341+
}
326342
}
327343
catch (Exception e)
328344
{
@@ -1514,11 +1530,12 @@ public static async Task<Session> RecreateAsync(Session sessionTemplate, Cancell
15141530

15151531
try
15161532
{
1533+
session.RecreateRenewUserIdentity();
15171534
// open the session.
15181535
await session.OpenAsync(
15191536
sessionTemplate.SessionName,
15201537
(uint)sessionTemplate.SessionTimeout,
1521-
sessionTemplate.Identity,
1538+
session.Identity,
15221539
sessionTemplate.PreferredLocales,
15231540
sessionTemplate.m_checkDomain,
15241541
ct).ConfigureAwait(false);
@@ -1562,11 +1579,12 @@ public static async Task<Session> RecreateAsync(Session sessionTemplate, ITransp
15621579

15631580
try
15641581
{
1582+
session.RecreateRenewUserIdentity();
15651583
// open the session.
15661584
await session.OpenAsync(
15671585
sessionTemplate.m_sessionName,
15681586
(uint)sessionTemplate.m_sessionTimeout,
1569-
sessionTemplate.m_identity,
1587+
session.Identity,
15701588
sessionTemplate.m_preferredLocales,
15711589
sessionTemplate.m_checkDomain,
15721590
ct).ConfigureAwait(false);
@@ -1604,13 +1622,15 @@ public static async Task<Session> RecreateAsync(Session sessionTemplate, ITransp
16041622

16051623
try
16061624
{
1625+
session.RecreateRenewUserIdentity();
16071626
// open the session.
16081627
await session.OpenAsync(
16091628
sessionTemplate.m_sessionName,
16101629
(uint)sessionTemplate.m_sessionTimeout,
1611-
sessionTemplate.m_identity,
1630+
session.Identity,
16121631
sessionTemplate.m_preferredLocales,
16131632
sessionTemplate.m_checkDomain,
1633+
false,
16141634
ct).ConfigureAwait(false);
16151635

16161636
// create the subscriptions.

Libraries/Opc.Ua.Client/Session/TraceableSession.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,15 @@ public void Open(string sessionName, uint sessionTimeout, IUserIdentity identity
626626
}
627627
}
628628

629+
/// <inheritdoc/>
630+
public void Open(string sessionName, uint sessionTimeout, IUserIdentity identity, IList<string> preferredLocales, bool checkDomain, bool closeChannel)
631+
{
632+
using (Activity activity = ActivitySource.StartActivity())
633+
{
634+
m_session.Open(sessionName, sessionTimeout, identity, preferredLocales, checkDomain, closeChannel);
635+
}
636+
}
637+
629638
/// <inheritdoc/>
630639
public void ChangePreferredLocales(StringCollection preferredLocales)
631640
{
@@ -707,6 +716,15 @@ public async Task OpenAsync(string sessionName, uint sessionTimeout, IUserIdenti
707716
}
708717
}
709718

719+
/// <inheritdoc/>
720+
public async Task OpenAsync(string sessionName, uint sessionTimeout, IUserIdentity identity, IList<string> preferredLocales, bool checkDomain, bool closeChannel, CancellationToken ct)
721+
{
722+
using (Activity activity = ActivitySource.StartActivity())
723+
{
724+
await m_session.OpenAsync(sessionName, sessionTimeout, identity, preferredLocales, checkDomain, closeChannel, ct).ConfigureAwait(false);
725+
}
726+
}
727+
710728

711729
/// <inheritdoc/>
712730
public async Task FetchNamespaceTablesAsync(CancellationToken ct = default)

Tests/Opc.Ua.Client.Tests/ClientTest.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,12 @@ public async Task ConnectMultipleSessionsAsync()
625625
session2.DetachChannel();
626626
session2.Dispose();
627627

628+
629+
//Recreate session using same channel
630+
var session3 = await ClientFixture.SessionFactory.RecreateAsync(session1, channel).ConfigureAwait(false);
631+
632+
_ = session3.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType));
633+
628634
channel.Dispose();
629635
}
630636

@@ -851,6 +857,89 @@ public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecrets(stri
851857
Utils.SilentDispose(session2);
852858
}
853859

860+
/// <summary>
861+
/// Open a session on a channel, then recreate using the session as a template, verify the renewUserIdentityHandler is brought to the new session and called before Session.Open
862+
/// /// </summary>
863+
[Test, Order(270)]
864+
[TestCase(false)]
865+
[TestCase(true)]
866+
public async Task RecreateSessionWithRenewUserIdentity(bool async)
867+
{
868+
IUserIdentity userIdentityAnonymous = new UserIdentity() ;
869+
IUserIdentity userIdentityPW = new UserIdentity("user1", "password");
870+
871+
// the first channel determines the endpoint
872+
ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(ServerUrl, SecurityPolicies.Basic256Sha256, Endpoints).ConfigureAwait(false);
873+
Assert.NotNull(endpoint);
874+
875+
UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy(userIdentityAnonymous.TokenType,
876+
userIdentityAnonymous.IssuedTokenType,
877+
endpoint.Description.SecurityPolicyUri);
878+
if (identityPolicy == null)
879+
{
880+
Assert.Ignore($"No UserTokenPolicy found for {userIdentityAnonymous.TokenType} / {userIdentityAnonymous.IssuedTokenType}");
881+
}
882+
883+
// the active channel
884+
ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentityAnonymous).ConfigureAwait(false);
885+
Assert.NotNull(session1);
886+
887+
ServerStatusDataType value1 = (ServerStatusDataType)session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType));
888+
Assert.NotNull(value1);
889+
890+
// hook callback to renew the user identity
891+
session1.RenewUserIdentity += (session, identity) => {
892+
return userIdentityPW;
893+
};
894+
895+
Client.Session session2;
896+
if (async)
897+
{
898+
session2 = await Client.Session.RecreateAsync((Session)((TraceableSession)session1).Session).ConfigureAwait(false);
899+
}
900+
else
901+
{
902+
session2 = Client.Session.Recreate((Session)((TraceableSession)session1).Session);
903+
}
904+
905+
// create new channel
906+
ITransportChannel channel2 = await ClientFixture.CreateChannelAsync(session1.ConfiguredEndpoint, false).ConfigureAwait(false);
907+
Assert.NotNull(channel2);
908+
909+
Client.Session session3;
910+
if (async)
911+
{
912+
session3 = await Client.Session.RecreateAsync((Session)((TraceableSession)session1).Session, channel2).ConfigureAwait(false);
913+
914+
}
915+
else
916+
{
917+
session3 = Client.Session.Recreate((Session)((TraceableSession)session1).Session, channel2);
918+
}
919+
920+
//validate new Session Ids are used and also UserName PW identity token is injected as renewed token
921+
Assert.AreNotEqual(session1.SessionId, session2.SessionId);
922+
Assert.AreEqual(userIdentityPW.TokenType, session2.Identity.TokenType);
923+
Assert.AreNotEqual(session1.SessionId, session3.SessionId);
924+
Assert.AreEqual(userIdentityPW.TokenType, session3.Identity.TokenType);
925+
926+
ServerStatusDataType value2 = (ServerStatusDataType)session2.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType));
927+
Assert.NotNull(value2);
928+
929+
930+
session1.DeleteSubscriptionsOnClose = true;
931+
session1.Close(1000);
932+
Utils.SilentDispose(session1);
933+
934+
session2.DeleteSubscriptionsOnClose = true;
935+
session2.Close(1000);
936+
Utils.SilentDispose(session2);
937+
938+
session3.DeleteSubscriptionsOnClose = true;
939+
session3.Close(1000);
940+
Utils.SilentDispose(session3);
941+
}
942+
854943
[Test, Order(300)]
855944
public new void GetOperationLimits()
856945
{

0 commit comments

Comments
 (0)