mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
Secure discovery requests and transport discovery announcements
Review feedback: - A secured connection dropped its own discovery requests: the fail-closed check refused every UadpDiscoveryRequestMessage, so a secured subscriber could not ask a publisher for metadata, writer configuration, or endpoints. Requests are now secured like responses on connections that use the UADP encoding. - The ApplicationInformation, PublisherEndpoints, and PubSubConnection announcements that UDP and Ethernet send to their transport-specific discovery destination were still encoded in plaintext. SendDiscoveryResponseAsync now uses the same encode-and-secure step as SendNetworkMessageAsync (EncodeOutboundAsync), which also refuses to send a message that a secured connection cannot protect. The MQTT last-will status message is unchanged: it is registered with the broker when the connection opens and published later, so it cannot carry a current SecurityHeader. docs/PubSub.md describes the secured discovery and this exception. Tests: - PubSubDiscoveryTests.SecuredSubscriberDiscoversSecuredPublisherWithoutPlaintextFramesAsync connects a secured publisher and a secured subscriber through an in-memory transport that also sends announcements to a separate destination. Metadata, writer configuration, and endpoints are discovered, and no frame without a SecurityHeader is sent. Before the fix the metadata request was dropped. - UadpDiscoveryTests.EncodeWithSecurityBoundaryRejectsAMessageItCannotSecure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1222,6 +1222,11 @@ subscribers that need to find publishers and bind to metadata at runtime.
|
||||
duplicate probes are suppressed, and identical responses are throttled.
|
||||
- MQTT publishes retained discovery messages on the standard status, connection,
|
||||
application, endpoint, and metadata topics.
|
||||
- A UADP connection configured for message security signs and encrypts its
|
||||
discovery requests, responses, and announcements with the keys of its
|
||||
SecurityGroup, so only applications that hold those keys can discover each
|
||||
other. The MQTT last-will status message is the exception: the broker
|
||||
publishes it later, so it is sent unsecured.
|
||||
|
||||
```csharp
|
||||
PubSubDiscoveryResult result = await application.RequestDiscoveryAsync(
|
||||
|
||||
@@ -1873,11 +1873,21 @@ namespace Opc.Ua.PubSub.Connections
|
||||
out IPubSubDiscoveryAnnouncementTransport? announcementTransport) &&
|
||||
encoder is not null)
|
||||
{
|
||||
ReadOnlyMemory<byte> payload = await EncodeNetworkMessageAsync(
|
||||
var context = new PubSubNetworkMessageContext(
|
||||
ServiceMessageContext.CreateEmpty(m_telemetry),
|
||||
m_metaDataRegistry,
|
||||
m_diagnostics,
|
||||
m_timeProvider);
|
||||
ReadOnlyMemory<byte>? payload = await EncodeOutboundAsync(
|
||||
networkMessage,
|
||||
encoder,
|
||||
context,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
await announcementTransport!.SendDiscoveryAnnouncementAsync(payload, cancellationToken)
|
||||
if (payload is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await announcementTransport!.SendDiscoveryAnnouncementAsync(payload.Value, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
@@ -2498,36 +2508,16 @@ namespace Opc.Ua.PubSub.Connections
|
||||
m_diagnostics,
|
||||
m_timeProvider);
|
||||
|
||||
ReadOnlyMemory<byte> payload;
|
||||
if (m_securityWrapper is not null &&
|
||||
networkMessage is UadpNetworkMessage or UadpDiscoveryResponseMessage)
|
||||
ReadOnlyMemory<byte>? encoded = await EncodeOutboundAsync(
|
||||
networkMessage,
|
||||
encoder,
|
||||
context,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (encoded is null)
|
||||
{
|
||||
payload = await EncodeAndWrapUadpAsync(networkMessage, context, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (RequiresInboundSecurity ||
|
||||
(m_securityWrapper is not null &&
|
||||
m_requiredSecurityMode is MessageSecurityMode.Sign
|
||||
or MessageSecurityMode.SignAndEncrypt))
|
||||
{
|
||||
// Fail-closed: never emit plaintext for a secured group.
|
||||
// This path is only reachable for non-UADP messages, which
|
||||
// the UADP security wrapper cannot protect.
|
||||
m_diagnostics.Increment(PubSubDiagnosticsCounterKind.EncryptionErrors);
|
||||
m_diagnostics.RecordError(
|
||||
StatusCodes.BadSecurityModeRejected,
|
||||
"Refusing to publish an unsecured NetworkMessage on a connection " +
|
||||
"configured for message security.");
|
||||
m_logger.DroppingOutboundMessage(Name, m_requiredSecurityMode);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
payload = await encoder.EncodeAsync(
|
||||
networkMessage,
|
||||
context,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
ReadOnlyMemory<byte> payload = encoded.Value;
|
||||
|
||||
if (m_maxNetworkMessageSize > 0 &&
|
||||
payload.Length > m_maxNetworkMessageSize &&
|
||||
@@ -2542,6 +2532,57 @@ namespace Opc.Ua.PubSub.Connections
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a NetworkMessage for sending and applies the message security of
|
||||
/// the connection. Returns <see langword="null"/>, and the message must not be
|
||||
/// sent, when the connection requires message security that the message
|
||||
/// cannot carry.
|
||||
/// </summary>
|
||||
private async ValueTask<ReadOnlyMemory<byte>?> EncodeOutboundAsync(
|
||||
PubSubNetworkMessage networkMessage,
|
||||
INetworkMessageEncoder encoder,
|
||||
PubSubNetworkMessageContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (m_securityWrapper is not null && IsSecurableUadpMessage(networkMessage))
|
||||
{
|
||||
return await EncodeAndWrapUadpAsync(networkMessage, context, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
if (RequiresInboundSecurity ||
|
||||
(m_securityWrapper is not null &&
|
||||
m_requiredSecurityMode is MessageSecurityMode.Sign
|
||||
or MessageSecurityMode.SignAndEncrypt))
|
||||
{
|
||||
// Fail-closed: never emit plaintext for a secured group. This path is
|
||||
// reached for messages that the UADP security wrapper cannot protect.
|
||||
m_diagnostics.Increment(PubSubDiagnosticsCounterKind.EncryptionErrors);
|
||||
m_diagnostics.RecordError(
|
||||
StatusCodes.BadSecurityModeRejected,
|
||||
"Refusing to publish an unsecured NetworkMessage on a connection " +
|
||||
"configured for message security.");
|
||||
m_logger.DroppingOutboundMessage(Name, m_requiredSecurityMode);
|
||||
return null;
|
||||
}
|
||||
return await encoder.EncodeAsync(networkMessage, context, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true for the NetworkMessages that the UADP security wrapper
|
||||
/// protects: UADP data messages, and the discovery requests and responses
|
||||
/// of a connection that uses the UADP encoding.
|
||||
/// </summary>
|
||||
private bool IsSecurableUadpMessage(PubSubNetworkMessage networkMessage)
|
||||
{
|
||||
if (networkMessage is UadpNetworkMessage)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return networkMessage is UadpDiscoveryRequestMessage or UadpDiscoveryResponseMessage &&
|
||||
TransportProfileFamily(TransportProfileUri) == "Uadp";
|
||||
}
|
||||
|
||||
private ValueTask<ReadOnlyMemory<byte>> EncodeNetworkMessageAsync(
|
||||
PubSubNetworkMessage networkMessage,
|
||||
INetworkMessageEncoder encoder,
|
||||
|
||||
@@ -39,6 +39,9 @@ using Opc.Ua.PubSub.DataSets;
|
||||
using Opc.Ua.PubSub.Diagnostics;
|
||||
using Opc.Ua.PubSub.Encoding;
|
||||
using Opc.Ua.PubSub.Encoding.Uadp;
|
||||
using Opc.Ua.PubSub.Security;
|
||||
using Opc.Ua.PubSub.Security.Policies;
|
||||
using Opc.Ua.PubSub.Tests.Security;
|
||||
using Opc.Ua.PubSub.Transports;
|
||||
using Opc.Ua.PubSub.Udp;
|
||||
using Opc.Ua.Tests;
|
||||
@@ -56,6 +59,7 @@ namespace Opc.Ua.PubSub.Tests.Application
|
||||
private const ushort WriterGroupIdValue = 7;
|
||||
private const ushort DataSetWriterIdValue = 42;
|
||||
private const string PublishedDataSetName = "pds-1";
|
||||
private const string SecurityGroupIdValue = "discovery-group";
|
||||
|
||||
[Test]
|
||||
public async Task RequestDiscoveryAsyncEncodesRequestAndCollectsResponse()
|
||||
@@ -176,6 +180,71 @@ namespace Opc.Ua.PubSub.Tests.Application
|
||||
Assert.That(endpoints.PublisherEndpoints[0].EndpointUrl, Is.EqualTo(url));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A subscriber and a publisher whose connections are secured with the same
|
||||
/// SecurityGroup exchange discovery requests, responses, and the publisher's
|
||||
/// transport-specific announcements, and no frame leaves either connection
|
||||
/// without a SecurityHeader.
|
||||
/// </summary>
|
||||
[Test]
|
||||
[TestSpec("7.2.4.6.3")]
|
||||
public async Task SecuredSubscriberDiscoversSecuredPublisherWithoutPlaintextFramesAsync()
|
||||
{
|
||||
const string url = "opc.udp://239.0.0.1:4840";
|
||||
using PubSubSecurityKeyRing publisherRing = NewKeyRing();
|
||||
using PubSubSecurityKeyRing subscriberRing = NewKeyRing();
|
||||
var bus = new LoopbackBus();
|
||||
await using IPubSubApplication publisher = BuildPublisherApp(
|
||||
url,
|
||||
new LoopbackTransportFactory(bus),
|
||||
new StaticSecurityKeyProvider(SecurityGroupIdValue, publisherRing));
|
||||
await using IPubSubApplication subscriber = BuildSubscriberApp(
|
||||
url,
|
||||
new LoopbackTransportFactory(bus),
|
||||
new StaticSecurityKeyProvider(SecurityGroupIdValue, subscriberRing));
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
await publisher.StartAsync(cts.Token).ConfigureAwait(false);
|
||||
await subscriber.StartAsync(cts.Token).ConfigureAwait(false);
|
||||
|
||||
// Each request collects responses for its whole timeout, so the endpoints
|
||||
// request is sent after the publisher's startup announcement of its
|
||||
// endpoints has left the 500 ms duplicate-response window.
|
||||
PubSubDiscoveryResult metaData = await subscriber.RequestDiscoveryAsync(
|
||||
new PubSubDiscoveryRequest
|
||||
{
|
||||
DiscoveryType = UadpDiscoveryType.DataSetMetaData,
|
||||
DataSetWriterIds = [DataSetWriterIdValue]
|
||||
},
|
||||
TimeSpan.FromMilliseconds(600),
|
||||
cts.Token).ConfigureAwait(false);
|
||||
PubSubDiscoveryResult writerConfiguration = await subscriber.RequestDiscoveryAsync(
|
||||
new PubSubDiscoveryRequest
|
||||
{
|
||||
DiscoveryType = UadpDiscoveryType.DataSetWriterConfiguration,
|
||||
DataSetWriterIds = [DataSetWriterIdValue]
|
||||
},
|
||||
TimeSpan.FromMilliseconds(600),
|
||||
cts.Token).ConfigureAwait(false);
|
||||
PubSubDiscoveryResult endpoints = await subscriber.RequestDiscoveryAsync(
|
||||
new PubSubDiscoveryRequest
|
||||
{
|
||||
DiscoveryType = UadpDiscoveryType.PublisherEndpoints
|
||||
},
|
||||
TimeSpan.FromMilliseconds(600),
|
||||
cts.Token).ConfigureAwait(false);
|
||||
|
||||
Assert.That(metaData.DataSetMetaDataEntries, Has.Count.EqualTo(1));
|
||||
Assert.That(
|
||||
metaData.DataSetMetaDataEntries[0].DataSetMetaData?.Name,
|
||||
Is.EqualTo(PublishedDataSetName));
|
||||
Assert.That(writerConfiguration.WriterConfigurations, Has.Count.EqualTo(1));
|
||||
Assert.That(endpoints.PublisherEndpoints, Is.Not.Empty);
|
||||
Assert.That(bus.AnnouncementCount, Is.GreaterThan(0),
|
||||
"The publisher sent no transport-specific discovery announcement.");
|
||||
Assert.That(bus.PlaintextFrames, Is.Empty,
|
||||
"Every frame of a secured connection must carry a SecurityHeader.");
|
||||
}
|
||||
|
||||
private static IPubSubApplication BuildDiscoveryOnlyApp(IPubSubTransportFactory factory)
|
||||
{
|
||||
return new PubSubApplicationBuilder(NUnitTelemetryContext.Create())
|
||||
@@ -203,10 +272,35 @@ namespace Opc.Ua.PubSub.Tests.Application
|
||||
|
||||
private static IPubSubApplication BuildPublisherApp(
|
||||
string url,
|
||||
IPubSubTransportFactory factory)
|
||||
IPubSubTransportFactory factory,
|
||||
IPubSubSecurityKeyProvider? securityKeyProvider = null)
|
||||
{
|
||||
DataSetMetaDataType metaData = NewMetaData();
|
||||
return new PubSubApplicationBuilder(NUnitTelemetryContext.Create())
|
||||
var writerGroup = new WriterGroupDataType
|
||||
{
|
||||
Name = "writer-group",
|
||||
WriterGroupId = WriterGroupIdValue,
|
||||
PublishingInterval = 600_000,
|
||||
DataSetWriters =
|
||||
[
|
||||
new DataSetWriterDataType
|
||||
{
|
||||
Name = "writer",
|
||||
DataSetWriterId = DataSetWriterIdValue,
|
||||
DataSetName = PublishedDataSetName
|
||||
}
|
||||
]
|
||||
};
|
||||
var readerGroup = new ReaderGroupDataType
|
||||
{
|
||||
Name = "discovery-listener"
|
||||
};
|
||||
if (securityKeyProvider is not null)
|
||||
{
|
||||
SecureGroup(writerGroup);
|
||||
SecureGroup(readerGroup);
|
||||
}
|
||||
PubSubApplicationBuilder builder = new PubSubApplicationBuilder(NUnitTelemetryContext.Create())
|
||||
.WithApplicationId("discovery-publisher")
|
||||
.UseConfiguration(new PubSubConfigurationDataType
|
||||
{
|
||||
@@ -221,31 +315,8 @@ namespace Opc.Ua.PubSub.Tests.Application
|
||||
{
|
||||
Url = url
|
||||
}),
|
||||
WriterGroups =
|
||||
[
|
||||
new WriterGroupDataType
|
||||
{
|
||||
Name = "writer-group",
|
||||
WriterGroupId = WriterGroupIdValue,
|
||||
PublishingInterval = 600_000,
|
||||
DataSetWriters =
|
||||
[
|
||||
new DataSetWriterDataType
|
||||
{
|
||||
Name = "writer",
|
||||
DataSetWriterId = DataSetWriterIdValue,
|
||||
DataSetName = PublishedDataSetName
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
ReaderGroups =
|
||||
[
|
||||
new ReaderGroupDataType
|
||||
{
|
||||
Name = "discovery-listener"
|
||||
}
|
||||
]
|
||||
WriterGroups = [writerGroup],
|
||||
ReaderGroups = [readerGroup]
|
||||
}
|
||||
],
|
||||
PublishedDataSets =
|
||||
@@ -259,35 +330,71 @@ namespace Opc.Ua.PubSub.Tests.Application
|
||||
})
|
||||
.AddDataSetSource(PublishedDataSetName, new MetaDataOnlySource(metaData))
|
||||
.UseAllStandardEncoders()
|
||||
.AddTransportFactory(factory)
|
||||
.Build();
|
||||
.AddTransportFactory(factory);
|
||||
if (securityKeyProvider is not null)
|
||||
{
|
||||
builder.AddSecurityKeyProvider(securityKeyProvider);
|
||||
}
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private static IPubSubApplication BuildSubscriberApp(
|
||||
string url,
|
||||
IPubSubTransportFactory factory)
|
||||
IPubSubTransportFactory factory,
|
||||
IPubSubSecurityKeyProvider? securityKeyProvider = null)
|
||||
{
|
||||
return new PubSubApplicationBuilder(NUnitTelemetryContext.Create())
|
||||
var connection = new PubSubConnectionDataType
|
||||
{
|
||||
Name = "subscriber",
|
||||
TransportProfileUri = Profiles.PubSubUdpUadpTransport,
|
||||
Address = new ExtensionObject(new NetworkAddressUrlDataType
|
||||
{
|
||||
Url = url
|
||||
})
|
||||
};
|
||||
if (securityKeyProvider is not null)
|
||||
{
|
||||
var readerGroup = new ReaderGroupDataType
|
||||
{
|
||||
Name = "secured-listener"
|
||||
};
|
||||
SecureGroup(readerGroup);
|
||||
connection.ReaderGroups = [readerGroup];
|
||||
}
|
||||
PubSubApplicationBuilder builder = new PubSubApplicationBuilder(NUnitTelemetryContext.Create())
|
||||
.WithApplicationId("discovery-subscriber")
|
||||
.UseConfiguration(new PubSubConfigurationDataType
|
||||
{
|
||||
Connections =
|
||||
[
|
||||
new PubSubConnectionDataType
|
||||
{
|
||||
Name = "subscriber",
|
||||
TransportProfileUri = Profiles.PubSubUdpUadpTransport,
|
||||
Address = new ExtensionObject(new NetworkAddressUrlDataType
|
||||
{
|
||||
Url = url
|
||||
})
|
||||
}
|
||||
],
|
||||
Connections = [connection],
|
||||
PublishedDataSets = []
|
||||
})
|
||||
.UseAllStandardEncoders()
|
||||
.AddTransportFactory(factory)
|
||||
.Build();
|
||||
.AddTransportFactory(factory);
|
||||
if (securityKeyProvider is not null)
|
||||
{
|
||||
builder.AddSecurityKeyProvider(securityKeyProvider);
|
||||
}
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private static void SecureGroup(PubSubGroupDataType group)
|
||||
{
|
||||
group.SecurityMode = MessageSecurityMode.SignAndEncrypt;
|
||||
group.SecurityGroupId = SecurityGroupIdValue;
|
||||
group.SecurityKeyServices = new ArrayOf<EndpointDescription>(new[]
|
||||
{
|
||||
new EndpointDescription { EndpointUrl = "opc.tcp://localhost:4840/SecurityKeyService" }
|
||||
});
|
||||
}
|
||||
|
||||
private static PubSubSecurityKeyRing NewKeyRing()
|
||||
{
|
||||
var ring = new PubSubSecurityKeyRing(SecurityGroupIdValue);
|
||||
ring.SetCurrent(TestSecurityKeyFactory.Create(
|
||||
1,
|
||||
PubSubAes256CtrPolicy.Instance.SigningKeyLength,
|
||||
PubSubAes256CtrPolicy.Instance.EncryptingKeyLength));
|
||||
return ring;
|
||||
}
|
||||
|
||||
private static DataSetMetaDataType NewMetaData()
|
||||
@@ -437,6 +544,201 @@ namespace Opc.Ua.PubSub.Tests.Application
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delivers every frame that one transport sends to all other transports of
|
||||
/// the bus, and records the frames that carry no SecurityHeader.
|
||||
/// </summary>
|
||||
private sealed class LoopbackBus
|
||||
{
|
||||
private readonly List<LoopbackTransport> m_transports = [];
|
||||
private readonly List<string> m_plaintextFrames = [];
|
||||
private readonly Lock m_gate = new();
|
||||
private int m_announcementCount;
|
||||
|
||||
public int AnnouncementCount => Volatile.Read(ref m_announcementCount);
|
||||
|
||||
public IReadOnlyList<string> PlaintextFrames
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (m_gate)
|
||||
{
|
||||
return [.. m_plaintextFrames];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Attach(LoopbackTransport transport)
|
||||
{
|
||||
lock (m_gate)
|
||||
{
|
||||
m_transports.Add(transport);
|
||||
}
|
||||
}
|
||||
|
||||
public void Deliver(LoopbackTransport sender, ReadOnlyMemory<byte> payload, bool isAnnouncement)
|
||||
{
|
||||
if (isAnnouncement)
|
||||
{
|
||||
Interlocked.Increment(ref m_announcementCount);
|
||||
}
|
||||
var receivers = new List<LoopbackTransport>();
|
||||
lock (m_gate)
|
||||
{
|
||||
if (!UadpDecoder.TryReadOuterPrefix(payload, out _, out bool securityEnabled, out _, out _) ||
|
||||
!securityEnabled)
|
||||
{
|
||||
byte[] head = payload.Slice(0, Math.Min(8, payload.Length)).ToArray();
|
||||
m_plaintextFrames.Add(BitConverter.ToString(head));
|
||||
}
|
||||
foreach (LoopbackTransport transport in m_transports)
|
||||
{
|
||||
if (!ReferenceEquals(transport, sender))
|
||||
{
|
||||
receivers.Add(transport);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (LoopbackTransport receiver in receivers)
|
||||
{
|
||||
receiver.Enqueue(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoopbackTransportFactory : IPubSubTransportFactory
|
||||
{
|
||||
private readonly LoopbackBus m_bus;
|
||||
|
||||
public LoopbackTransportFactory(LoopbackBus bus)
|
||||
{
|
||||
m_bus = bus;
|
||||
}
|
||||
|
||||
public string TransportProfileUri => Profiles.PubSubUdpUadpTransport;
|
||||
|
||||
public IPubSubTransport Create(
|
||||
PubSubConnectionDataType connection,
|
||||
ITelemetryContext telemetry,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_ = connection;
|
||||
_ = telemetry;
|
||||
_ = timeProvider;
|
||||
var transport = new LoopbackTransport(m_bus);
|
||||
m_bus.Attach(transport);
|
||||
return transport;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An in-memory datagram transport that, like the UDP transport, sends
|
||||
/// transport-specific discovery announcements to a separate destination.
|
||||
/// </summary>
|
||||
private sealed class LoopbackTransport : IPubSubTransport, IPubSubDiscoveryAnnouncementTransport
|
||||
{
|
||||
private readonly LoopbackBus m_bus;
|
||||
private readonly Queue<PubSubTransportFrame> m_frames = new();
|
||||
private readonly SemaphoreSlim m_signal = new(0, int.MaxValue);
|
||||
private readonly Lock m_gate = new();
|
||||
private bool m_disposed;
|
||||
|
||||
public LoopbackTransport(LoopbackBus bus)
|
||||
{
|
||||
m_bus = bus;
|
||||
}
|
||||
|
||||
public string TransportProfileUri => Profiles.PubSubUdpUadpTransport;
|
||||
|
||||
public PubSubTransportDirection Direction => PubSubTransportDirection.SendReceive;
|
||||
|
||||
public bool IsConnected { get; private set; }
|
||||
|
||||
public uint DiscoveryAnnounceRate => 0;
|
||||
|
||||
public event EventHandler<PubSubTransportStateChangedEventArgs>? StateChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public ValueTask OpenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
IsConnected = true;
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask CloseAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
IsConnected = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask SendAsync(
|
||||
ReadOnlyMemory<byte> payload,
|
||||
string? topic = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = topic;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
m_bus.Deliver(this, payload, isAnnouncement: false);
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask SendDiscoveryAnnouncementAsync(
|
||||
ReadOnlyMemory<byte> payload,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
m_bus.Deliver(this, payload, isAnnouncement: true);
|
||||
return default;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<PubSubTransportFrame> ReceiveAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await m_signal.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
PubSubTransportFrame frame;
|
||||
lock (m_gate)
|
||||
{
|
||||
frame = m_frames.Dequeue();
|
||||
}
|
||||
yield return frame;
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
lock (m_gate)
|
||||
{
|
||||
m_disposed = true;
|
||||
IsConnected = false;
|
||||
}
|
||||
m_signal.Dispose();
|
||||
return default;
|
||||
}
|
||||
|
||||
public void Enqueue(ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
lock (m_gate)
|
||||
{
|
||||
if (m_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_frames.Enqueue(new PubSubTransportFrame(
|
||||
payload.ToArray(),
|
||||
topic: null,
|
||||
DateTimeUtc.From(DateTimeOffset.UtcNow)));
|
||||
m_signal.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MetaDataOnlySource : IPublishedDataSetSource
|
||||
{
|
||||
private readonly DataSetMetaDataType m_metaData;
|
||||
|
||||
@@ -102,6 +102,21 @@ namespace Opc.Ua.PubSub.Tests.Encoding.Uadp
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The security boundary is defined for UADP data, action, and discovery
|
||||
/// messages only; any other message cannot be secured.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void EncodeWithSecurityBoundaryRejectsAMessageItCannotSecure()
|
||||
{
|
||||
PubSubNetworkMessageContext context = UadpTestUtilities.NewContext();
|
||||
var message = new Opc.Ua.PubSub.Encoding.Json.JsonNetworkMessage { MessageId = "json" };
|
||||
|
||||
Assert.That(
|
||||
() => UadpEncoder.EncodeWithSecurityBoundary(message, context, out _),
|
||||
Throws.ArgumentException);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DiscoveryRequest_DataSetMetaData_RoundTrips()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user