Adding HTTPS/WSS transports + Web API binding (Part 6 §G.3 OpenAPI Mapping), shared Kestrel host, Kestrel-TCP#3880
Open
marcschier wants to merge 117 commits into
Open
Adding HTTPS/WSS transports + Web API binding (Part 6 §G.3 OpenAPI Mapping), shared Kestrel host, Kestrel-TCP#3880marcschier wants to merge 117 commits into
marcschier wants to merge 117 commits into
Conversation
Adds the building blocks needed for the upcoming WSS (#3876) and HTTPS-JSON (#3877) transport implementations. No behavioural change to existing TCP or HTTPS-binary paths. * Profiles - Add UaWssJsonTransport URI (uawss-uajson) for the WebSocket+JSON sub-protocol of Part 6 7.5.2 (JSON sub-protocol has no SecureChannel, so it is restricted to SecurityMode.None). - Add Sec-WebSocket-Protocol constants OpcUaWsSubProtocolUacp / OpcUaWsSubProtocolUaJson (Part 6 Table 81). - Add HTTP Content-Type constants OpcUaBinaryContentType / OpcUaJsonContentType (Part 6 7.4.4 / 7.4.5). - Add classification predicates IsHttpsBinary / IsHttpsJson / IsWssBinary / IsWssJson and a ToWebSocketSubProtocol mapper. * Utils - Expose standard wss / ws scheme constants and an IsUriWssScheme helper that matches both opc.wss and wss URLs (treating bare wss strings as URI prefixes so wss: / wss/ both match). - Add wss to DefaultUriSchemes. * ConfiguredEndpoints - Fix typo where an opc.wss endpoint URL was assigned the UaTcpTransport profile URI instead of UaWssTransport. * Tests - New TransportProfileHelpersTests covering all new constants and predicates (39 cases).
Introduces the narrow internal contract that will replace IMessageSocket as
the runtime transport boundary for UaSCUaBinaryChannel. The abstraction is
deliberately:
* chunk-oriented: each Send/Receive moves one complete UASC MessageChunk
(OPC UA Part 6 6.7.2), not arbitrary byte ranges. This avoids leaking
framing concerns into the transport;
* Task-based (ValueTask everywhere): no SocketAsyncEventArgs callbacks,
no Begin/End shape;
* internal: lets the contract evolve with the channel implementation
without breaking public consumers. The stable public extensibility
surface remains ITransportChannel / ITransportListener.
Also adds IUaSCByteTransportFactory used by client-side transport channels
to construct an unconnected transport with the buffer pool / chunk sizing
required by the UASC channel. Server-side listeners build transports
directly from accepted connections so they do not use this factory.
No callers yet; this commit is contract-only and produces no behavioural
change.
Adds the TCP-backed implementation of IUaSCByteTransport that will replace
the legacy TcpMessageSocket plumbing. The new type:
* is internal and sealed, so it never appears in the public API surface;
* exposes a pure ValueTask API (no SocketAsyncEventArgs / no Begin/End);
* implements ReceiveChunkAsync as a strict pull: read 8-byte header,
validate message type and chunk size, then read the body into a
BufferManager-rented buffer. The completed chunk is handed to the
caller, which takes ownership of the buffer (matching the existing
BufferManager.ReturnBuffer contract used by UaSCBinaryChannel);
* keeps the existing macOS local-hostname workaround from TcpMessageSocket
(DnsEndPoint -> 'localhost') so the migration of consumers is
behaviourally neutral;
* preserves the BufferManager.LockBuffer / UnlockBuffer sentinels around
the in-flight chunk to keep the debug-buffer-poisoning hooks working;
* uses System.Threading.Lock for the socket pointer (matches repo
convention - polyfilled for pre-net9 TFMs).
A small TcpByteTransportFactory is included so that future client-side
TcpTransportChannel rewiring (p1-tcp-transport-channel-rewire) can pick
this up via IUaSCByteTransportFactory. Server-side TcpTransportListener
will construct TcpByteTransport directly from accepted sockets in a
follow-up commit.
No callers wired yet; the legacy TcpMessageSocket path remains the
runtime transport until p1-retarget-channel lands.
Refactors UaSCUaBinaryChannel so all chunk send/receive flows through the
new internal IUaSCByteTransport contract rather than the SAEA-shaped
IMessageSocket APIs. The existing TCP runtime keeps working because the
legacy IMessageSocket setter on the channel transparently wraps the socket
in a temporary adapter; a future commit (p1-tcp-transport-listener-rewire
/ p1-tcp-transport-channel-rewire) will replace those wrappers with direct
TcpByteTransport usage and the IMessageSocket family will be deleted in
p1-remove-imessagesocket.
Changes
* IUaSCByteTransport
- Drop IAsyncDisposable; the only lifetime hook is Close() (idempotent,
fully releases socket + sync primitives). Keeping Dispose sync-only
lets the channel call it from its synchronous IDisposable.Dispose.
* TcpByteTransport
- Implement IDisposable (sealed) so the SemaphoreSlim it owns gets
released; Dispose() simply delegates to Close().
* MessageSocketByteTransport (new, internal sealed)
- Adapts an IMessageSocket as IUaSCByteTransport during the migration
window. ReceiveChunkAsync drains an unbounded single-reader Channel<>
that is fed by IMessageSink callbacks from the wrapped socket;
SendChunkAsync(ReadOnlyMemory<byte>) and SendChunkAsync(BufferCollection)
bridge to IMessageSocket.MessageSocketEventArgs() + Send() with a
TaskCompletionSource that completes from the SAEA Completed event.
* UaSCUaBinaryChannel
- Add internal Transport (IUaSCByteTransport?) property; existing
Socket (IMessageSocket?) property is now a shim that wraps via
MessageSocketByteTransport on set and unwraps on get (returns null
for non-socket transports such as WebSockets).
- Add internal StartReceiveLoop() + RunReceiveLoopAsync(): long-running
pull loop that calls transport.ReceiveChunkAsync and dispatches each
chunk to the existing OnChunkReceived / OnTransportError hooks.
Idempotent CompareExchange guard prevents double-start.
- Keep IMessageSink on the channel so the existing TCP code path (which
constructs TcpMessageSocket with 'this' as the sink) continues to
receive callbacks; the two IMessageSink methods now simply forward to
OnChunkReceived / OnTransportError.
- Remove OnWriteComplete(SAEA-shaped) and rewrite BeginWriteMessage
overloads to await transport.SendChunkAsync and then invoke
HandleWriteComplete from a finally block (fire-and-forget Task,
preserving the legacy non-blocking caller contract).
- Dispose() now cancels the receive loop and Close()s the transport
instead of touching Socket directly.
Verification
* Stack/Opc.Ua.Core builds clean on every TFM (net472, net48, ns2.1,
net8, net9, net10).
* Tests/Opc.Ua.Core.Tests transport+socket+ClientChannelManager subset:
133 / 133 passed.
* Tests/Opc.Ua.Sessions.Tests (full TCP end-to-end regression):
690 / 690 passed (379 platform-skipped).
Rewires the TCP listener / channel hierarchy to construct TcpByteTransport
directly so the runtime data path no longer goes through the
IMessageSocket / SAEA-shaped plumbing. The legacy IMessageSocket family
still exists (consumed via the MessageSocketByteTransport adapter for
any callers that pass IMessageSocketFactory into UaSCUaBinaryClientChannel),
but it is no longer instantiated by the TCP server-side code.
* IUaSCByteTransport / IUaSCByteTransportFactory promoted to public
- They appear as parameters on ITcpChannelListener.ReconnectToExistingChannel,
so they must be public; documented as advanced extension points
(typical consumers continue to use ITransportChannel / ITransportListener).
- TcpByteTransport and TcpByteTransportFactory are also public sealed so
consumers building custom TCP-listener variants can construct them.
* TcpListenerChannel.Attach(uint, Socket)
- Constructs TcpByteTransport(socket, ...) and assigns it to the new
Transport property, then StartReceiveLoop() instead of the legacy
Socket = new TcpMessageSocket(...) + Socket.ReadNextMessage() dance.
* TcpListenerChannel.Reconnect / TcpServerChannel.Reconnect
- Signature changed from IMessageSocket to IUaSCByteTransport. Reconnect
assigns Transport and calls StartReceiveLoop() on the rebound transport
instead of Socket = socket / Socket.ChangeSink(this).
* TcpServerChannel.BeginReverseConnect / OnReverseConnectComplete
- Construct a client-style TcpByteTransport, await its ConnectAsync, then
drop into the (now-Task-based) OnReverseConnectComplete which calls
StartReceiveLoop() before sending the ReverseHello.
- ReverseConnectAsyncResult.Socket renamed to Transport.
* TcpTransportListener
- ReconnectToExistingChannel takes IUaSCByteTransport.
- TcpConnectionWaitingEventArgs exposes IUaSCByteTransport (its public
Handle returns the transport object).
- TransferListenerChannelAsync now calls channel.DetachTransportAsync()
BEFORE handing the transport to the new client; this stops the
listener-side receive loop and prevents the old / new channels from
racing for chunks on the shared socket. If the handoff is rejected,
the transport is re-attached and the receive loop restarted.
* UaSCUaBinaryChannel.DetachTransportAsync (new internal)
- Atomically removes m_transport, cancels the receive-loop CTS, awaits
the loop task, and returns the transport. Used by the reverse-connect
handoff above. m_receiveLoopTask is now stored so the caller can wait
for clean termination.
* UaSCUaBinaryClientChannel.ConnectAsync
- Replaces the Socket-driven CONNECT branch with a transport-driven one:
captures Transport under DataLock, awaits transport.ConnectAsync, and
logs the remote endpoint rather than the (now-stale) socket Handle.
Fixes the reverse-connect handoff which used to throw 'Could not create
or get connected socket' when the inbound Transport was a
TcpByteTransport (non-IMessageSocket) instead of the legacy SAEA path.
* UaSCUaBinaryTransportChannel.CreateChannel
- Reverse-connect path now reads connection.Handle as IUaSCByteTransport,
assigns to channel.Transport, calls StartReceiveLoop() and sets
ReverseSocket = true.
Verification
* Stack/Opc.Ua.Core builds clean on every TFM.
* Tests/Opc.Ua.Sessions.Tests (net10): 690/690 passed, 379 skipped — full
TCP end-to-end regression incl. all 50 ReverseConnect security-policy
permutations.
…sport
The client-side path (TcpTransportChannel / UaSCUaBinaryTransportChannel /
UaSCUaBinaryClientChannel) now builds and owns an IUaSCByteTransport
directly instead of going through the legacy IMessageSocketFactory ->
IMessageSocket adapter. TCP runtime is fully off the IMessageSocket SAEA
plumbing; the only remaining IMessageSocket callers are the (now-unused-by-
TCP) public types themselves, which will be deleted in
p1-remove-imessagesocket.
* UaSCUaBinaryClientChannel
- Ctor accepts IUaSCByteTransportFactory in place of IMessageSocketFactory.
Internal field m_socketFactory renamed to m_transportFactory; the
implementation string still derives from the factory.Implementation
name ('UA-TCP', 'UA-WSS', ...).
- ConnectAsync: creates the transport from m_transportFactory.Create(
BufferManager, MaxBufferSize, telemetry); captures Transport (instead
of Socket) for the connect attempt.
- CompleteConnect: no longer calls Socket.ReadNextMessage(); it now
invokes the channel's StartReceiveLoop() so the pull-based receive
loop is driven by the IUaSCByteTransport.
- OnScheduledHandshakeAsync (reconnect flow): closes the previous
Transport, creates a fresh transport from the factory, awaits its
ConnectAsync, then drives CompleteConnect/SendHelloMessage as before.
- Shutdown: closes the Transport instead of the IMessageSocket.
* UaSCUaBinaryTransportChannel
- Ctor signature changed from IMessageSocketFactory to
IUaSCByteTransportFactory. m_messageSocketFactory renamed to
m_transportFactory.
- SupportedFeatures now reads m_channel.Transport.Features instead of
Socket.MessageSocketFeatures (handles non-IMessageSocket transports).
- The Socket property is retained as a backward-compat shim that
returns null for non-IMessageSocket transports; documented inline.
IMessageSocketChannel implementation kept until
p1-remove-imessagesocket.
* TcpMessageSocket.cs (TcpTransportChannel)
- TcpTransportChannel ctor passes a TcpByteTransportFactory to the base
instead of TcpMessageSocketFactory.
Verification
* Stack/Opc.Ua.Core + full UA.slnx build clean.
* Tests/Opc.Ua.Sessions.Tests (net10): 690/690 passed, 379 skipped.
BREAKING CHANGE vs 1.5.378 (explicitly approved): removes the entire IMessageSocket family of public types from Opc.Ua.Core. The runtime transport boundary is now IUaSCByteTransport (public, see Stack/Opc.Ua.Core/Stack/Tcp/IUaSCByteTransport.cs). Removed types * IMessageSocket * IMessageSocketAsyncEventArgs * IMessageSink * IMessageSocketFactory * IMessageSocketChannel * MessageSocketExtensions * TcpMessageSocket / TcpMessageSocketFactory / TcpMessageSocketAsyncEventArgs * MessageSocketByteTransport (internal adapter introduced during p1-retarget-channel; no longer needed) Removed members * UaSCUaBinaryChannel.Socket (the IMessageSocket-shaped backward-compat shim added in p1-retarget-channel). Use Transport (IUaSCByteTransport) instead. * UaSCUaBinaryChannel.IMessageSink interface implementation. * UaSCUaBinaryTransportChannel.Socket (IMessageSocket?) and the IMessageSocketChannel implementation. The new equivalent is the public Transport (IUaSCByteTransport?) property on UaSCUaBinaryTransportChannel. Cleanups * TcpTransportChannel + TcpTransportChannelFactory moved into the new Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportChannel.cs (was previously in the now-deleted TcpMessageSocket.cs together with the legacy types). * TcpListenerChannel / TcpServerChannel: remaining log lines and force- channel-fault paths that read Socket.Handle / Socket.RemoteEndpoint switched to Transport?.RemoteEndpoint (which surfaces an EndPoint of any backing transport, including future WebSocket implementations). * ChannelClosed / ChannelFaulted close Transport instead of Socket. * UaSCUaBinaryClientChannel: drops the local IMessageSocket? captured under DataLock; the local IUaSCByteTransport? alone suffices. * ClientChannelManager.OnChannelTokenActivated: reads endpoints from (channel as UaSCUaBinaryTransportChannel)?.Transport rather than the removed IMessageSocketChannel cast. Test updates * ClientChannelManagerTests: IChannel interface no longer mixes in IMessageSocketChannel; Mock<IMessageSocket> usages removed. OnChannelTokenActivatedShouldInvokeOnDiagnostics now asserts the RemoteIp/LocalIp fields are null for mock channels (the diagnostic fields are only populated for real UaSCUaBinaryTransportChannel instances). * MessageSocketTests deleted (the type under test is gone). * ConfiguredEndpointCollectionAdditionalTests: CreateEndpointFromOpcWssUrl now asserts the post-p1-helpers (post-fix) TransportProfileUri of Profiles.UaWssTransport instead of the previously-asserted typo Profiles.UaTcpTransport. * Sessions.Tests/ClientTest reads tcp.Transport in place of tcp.Socket for the post-detach null check. Verification * UA.slnx builds clean on every TFM. * Tests/Opc.Ua.Core.Tests (net10): all transport / channel manager tests pass (133/133 in the focused subset). * Tests/Opc.Ua.Sessions.Tests (net10): 690/690 passed, 379 skipped — full TCP end-to-end regression including the 50 ReverseConnect permutations.
Adds the shared JSON encode/decode helper used by the upcoming HTTPS-JSON
(application/opcua+uajson, Part 6 7.4.5) and WSS opcua+uajson (Part 6
7.5.2) transport handlers.
* JsonRequestMapper (internal, sealed)
- DecodeRequestAsync(Stream, IServiceMessageContext, ct): reads the
body to EOF, decodes via JsonDecoder.DecodeMessage<IServiceRequest>,
and wraps any underlying exception in BadDecodingError.
- EncodeResponseAsync(IServiceResponse, IServiceMessageContext, Stream,
ct): encodes to bytes via the array helper and writes them out.
- EncodeResponse(IServiceResponse, IServiceMessageContext): allocates
a new JsonEncoder with JsonEncoderOptions.Compact (the reversible /
compact JSON flavour required by Part 6 5.4.9) and writes the
standard 'TypeId' + 'Body' envelope.
* Restricted to MessageSecurityMode.None: callers (HTTPS-JSON +
opcua+uajson handlers, p3-*) reject any other configuration before
invoking the mapper. The mapper itself is wire-only and does not look
at SecureChannel state.
* Smoke tests (Tests/Opc.Ua.Core.Tests/Stack/Transport/JsonRequestMapperTests):
- RoundTrips a ServiceFault (encode -> contains TypeId + Body).
- Stream encode matches array encode byte-for-byte.
- DecodeRequest rejects malformed bodies with BadDecodingError.
Full coverage lands in p5-unit-jsonmapper alongside the handler tests.
Carves the single 'POST -> SendAsync' terminal handler in
HttpsTransportListener.Startup.Configure into a tiny dispatcher that
routes each inbound request to the right handler so the upcoming WSS
upgrade (p2) and HTTPS-JSON (p3) work has a single place to land. No
behaviour change for existing HTTPS-binary callers.
* Startup.Configure
- Adds appBuilder.UseWebSockets() so the Kestrel pipeline is ready to
accept opcua+uacp / opcua+uajson upgrades.
- WS upgrade requests -> listener.AcceptWebSocketAsync(context).
- Non-POST requests -> 405 Method Not Allowed (unchanged behaviour).
- POST with Content-Type starting 'application/opcua+uajson'
-> listener.SendJsonAsync(context).
- Everything else -> listener.SendBinaryAsync(context) (which itself
enforces application/octet-stream per Part 6 7.4.4 / 7.4.5).
The match is case-insensitive and tolerant of a charset suffix.
* HttpsTransportListener
- SendAsync renamed to SendBinaryAsync (the body is unchanged; the
old SendAsync remains as a thin alias to preserve any external
callers).
- SendJsonAsync: returns 501 Not Implemented; the JSON request
handler lands in p3-https-json-handler.
- AcceptWebSocketAsync: returns 501 Not Implemented; the WSS handler
lands in p2-wss-listener-handler.
* Tests
- Existing HttpsTransportListenerTests / TransportProfileHelpers /
JsonRequestMapperTests subset (73 tests) continue to pass.
Phase 1 of the plan is now complete; the Kestrel pipeline is set up to
welcome the WSS and JSON handlers without further plumbing changes.
Adds the client-side WebSocket implementation needed by the upcoming WSS
opcua+uacp transport channel (p2-wss-client) and the listener handler
(p2-wss-listener-handler).
* WebSocketByteTransportBase (internal abstract, IUaSCByteTransport)
- Common send / receive / close plumbing shared between client and
accepted-server WebSocket transports.
- SendChunkAsync(ReadOnlyMemory<byte>): one UASC MessageChunk -> one
WebSocket binary frame with EndOfMessage=true (Part 6 7.5.2 Table 81
opcua+uacp).
- SendChunkAsync(BufferCollection): concats segments into a single
contiguous buffer rented from BufferManager and sent as one frame.
- ReceiveChunkAsync: loops WebSocket.ReceiveAsync until EndOfMessage,
rejecting close frames (BadConnectionClosed), non-binary frames
(BadTcpMessageTypeInvalid) and oversized messages
(BadTcpMessageTooLarge per Part 6 7.5.2's 1009 mapping).
- Implementation = 'UA-WSS'; Features = Reconnect.
- Close()/Dispose() abort the WebSocket and dispose the send lock.
* WebSocketClientByteTransport (internal sealed)
- Wraps ClientWebSocket; adds Sec-WebSocket-Protocol: opcua+uacp on
upgrade; verifies the server selected the same sub-protocol and
fails with BadNotConnected otherwise.
- Normalizes the OPC UA opc.wss:// scheme alias to the standard wss://
URI accepted by ClientWebSocket and falls back to
Utils.UaWebSocketsDefaultPort when the URL has no explicit port.
* ValueWebSocketReceiveResult (internal struct)
- Local shim so net472 / net48 callers can use the same receive shape
as net5.0+; the conditional ReceiveAsync overload populates it from
either WebSocketReceiveResult (legacy) or the BCL value type (modern).
* csproj / Directory.Packages.props
- net472 / net48 / netstandard2.1 now reference Microsoft.AspNetCore.
WebSockets 2.3.10 so IApplicationBuilder.UseWebSockets() (added in
p1-refactor-startup) resolves on every TFM.
Wire-up (TransportBindings registration, server-side listener handler)
follows in subsequent commits in Phase 2.
Adds the server-side counterpart of WebSocketClientByteTransport (added
in the previous commit). The class wraps an already-accepted WebSocket
that the Kestrel WSS handler (p2-wss-listener-handler) will deliver via
HttpContext.WebSockets.AcceptWebSocketAsync('opcua+uacp').
* WebSocketServerByteTransport (internal sealed)
- Constructor takes the accepted WebSocket plus the local / remote
endpoints captured from the HttpContext.Connection. Both are passed
in by the caller so the transport can expose them through
IUaSCByteTransport.LocalEndpoint / RemoteEndpoint without having to
crack the request again.
- ConnectAsync throws NotSupportedException — this transport is only
built from an inbound accept; the outbound path is the client
transport above.
- All other behaviour (send / receive / close) comes from the shared
WebSocketByteTransportBase added in p2-ws-byte-transport-client.
Adds the client-side WSS transport channel + factories so consumers can
open OPC UA sessions to opc.wss:// or wss:// endpoints. The factories
are discovered via the existing dynamic-load mechanism
(Utils.DefaultBindings), which loads Opc.Ua.Bindings.Https on demand
when the client channel manager sees an unknown scheme.
* Stack/Opc.Ua.Core/Types/Utils/Utils.cs
- Adds 'wss' and 'opc.wss' entries to DefaultBindings, both pointing
at Opc.Ua.Bindings.Https (the assembly that hosts the WSS
channel + listener factories).
* Stack/Opc.Ua.Bindings.Https/Stack/Https/WssTransportChannel.cs (new)
- WebSocketClientByteTransportFactory: IUaSCByteTransportFactory that
constructs WebSocketClientByteTransport instances on demand.
- WssTransportChannel: UaSCUaBinaryTransportChannel subclass that
wires the byte-transport factory in.
- WssTransportChannelFactory: ITransportChannelFactory for 'wss'.
- OpcWssTransportChannelFactory: ITransportChannelFactory for 'opc.wss'.
* Both factories are public and discoverable via AddBindings() reflection.
TransportBindingsBase calls AddBindings(loaded assembly) once the
assembly is loaded by TryAddDefaultTransportBindings, so the WSS
factories register themselves without any explicit user action.
Implements the server-side WebSocket Secure handler that the Kestrel
pipeline (set up in p1-refactor-startup) routes inbound WS upgrade
requests to. After this commit, a WSS client (e.g. WssTransportChannel
from p2-wss-client) can complete a full UASC session over opcua+uacp:
WebSocket upgrade -> Hello/Acknowledge -> OpenSecureChannel -> session
messages -> graceful close.
* HttpsTransportListener
- Open() now allocates a BufferManager (m_bufferManager) sized to the
channel quotas, used by the WSS path to rent send / receive chunks.
- Adds a per-listener m_nextChannelId counter for WSS channel IDs.
- AcceptWebSocketAsync end-to-end:
1. Picks the supported sub-protocol from the request
(opcua+uacp now; opcua+uajson reserved for p3-wss-json-handler).
2. Negotiates the upgrade via context.WebSockets.AcceptWebSocketAsync.
3. Wraps the accepted WebSocket in WebSocketServerByteTransport with
the local/remote IP endpoints captured from HttpContext.Connection.
4. Creates a TcpServerChannel + WssChannelListener pair and wires the
per-channel callbacks (OnRequestReceivedAsync,
OnReportAuditOpenSecureChannelEvent, ...).
5. Calls channel.Attach(channelId, transport) which assigns the
transport and starts the receive loop on the WebSocket. UASC
Hello/ACK/OpenSecureChannel/etc. flow as for opc.tcp.
6. Awaits perWsListener.WaitForChannelClosedAsync(RequestAborted)
so the HTTP request stays open for the lifetime of the channel.
On close (clean or aborted), disposes the channel and the
transport.
- OnRequestReceivedAsync: drives requests through
ITransportListenerCallback.ProcessRequestAsync and sends the
response back via TcpServerChannel.SendResponse — the standard
callback pattern used by TcpTransportListener.
- OnReportAudit*: thin shims that forward to the listener callback.
* WssChannelListener (nested private)
- ITcpChannelListener with single-channel semantics: tracks one
channel id, signals a TaskCompletionSource on ChannelClosed so the
Kestrel request handler unblocks. ReconnectToExistingChannel and
TransferListenerChannel(Async) return false / throw — neither is
meaningful for a single WSS upgrade.
* TcpListenerChannel.Attach
- Adds a public Attach(uint, IUaSCByteTransport) overload that the WSS
handler uses. The legacy Attach(uint, Socket) overload wraps the
Socket in a TcpByteTransport and delegates to the new overload.
* Stack/Opc.Ua.Core csproj
- InternalsVisibleTo Opc.Ua.Bindings.Https so the WSS handler can
read the internal channel surface (ClientCertificate,
ServerCertificate, ChannelThumbprint, EndpointDescription) used
when building the SecureChannelContext for the callback.
Note that endpoint discovery (p2-discovery-uacp) is still pending, so a
WSS connection currently only succeeds against an endpoint URL that the
client explicitly knows about; GetEndpoints does not yet advertise the
WSS profile.
Verification: full Tests/Opc.Ua.Sessions.Tests TCP regression
(net10) — 689/689 passed, 379 skipped.
Adds ITransportListenerFactory implementations for the WSS (Part 6 §7.5)
URL schemes so server endpoints declared as wss:// or opc.wss:// in
ServerConfiguration.BaseAddresses get an HttpsTransportListener that
serves them. The factories are reflected out of Opc.Ua.Bindings.Https
automatically via the existing AddBindings discovery (wss / opc.wss
are now in Utils.DefaultBindings, added in p2-wss-client).
* HttpsServiceHost (the shared service-host base)
- New virtual TransportProfileUri property; defaults to
Profiles.HttpsBinaryTransport. WSS factories override it to
Profiles.UaWssTransport so the emitted EndpointDescription.
TransportProfileUri matches Part 6 §7.5 Table 81 (uawss-uasc-uabinary).
- The CreateServiceHost filter now uses 'UriScheme + ://' as a prefix
test instead of the IsUriHttpsScheme() helper (which rejected wss
URLs). The previous IsUriHttpsScheme check was redundant with the
subsequent scheme-prefix narrowing.
- description.TransportProfileUri = TransportProfileUri (was hard-
coded to Profiles.HttpsBinaryTransport).
* HttpsTransportListener.cs
- WssTransportListenerFactory: UriScheme = 'wss',
TransportProfileUri = Profiles.UaWssTransport.
- OpcWssTransportListenerFactory: UriScheme = 'opc.wss',
TransportProfileUri = Profiles.UaWssTransport.
- Both construct an HttpsTransportListener with the WSS scheme; the
listener's pipeline (Startup.Configure + AcceptWebSocketAsync) does
the actual WS upgrade work added in p2-wss-listener-handler.
Implements the HTTPS-JSON request handler (Part 6 7.4.5) registered by
the Kestrel dispatcher when the inbound POST carries
'application/opcua+uajson'. Per the spec the JSON sub-protocol does not
use UA Secure Conversation - transport security is provided exclusively
by TLS, and only MessageSecurityMode.None is acceptable.
* HttpsTransportListener.SendJsonAsync
- Replaces the 501 stub left by p1-refactor-startup.
- Extracts the optional JWT bearer token (same shape as the binary
path) and forwards it on the request header when present.
- Decodes the request body via JsonRequestMapper.DecodeRequestAsync;
on a decoder error returns a JSON-encoded ServiceFault rather than
a plain-text 500.
- Picks an EndpointDescription with TransportProfileUri =
Profiles.HttpsJsonTransport and SecurityMode = None. If none is
found, endpoint is null - service processing still happens but the
callback observes a no-endpoint context (treated as discovery-only
by the higher layers).
- Dispatches to ITransportListenerCallback.ProcessRequestAsync with
RequestEncoding.Json so the server pipeline knows to handle it as
a JSON request.
- Encodes the response via JsonRequestMapper.EncodeResponse and
writes it back with Content-Type application/opcua+uajson and
HTTP 200.
* WriteJsonResponseAsync (private helper) keeps the JSON encode +
write paired so any future encoder option changes happen in one place.
Endpoint discovery for the JSON profile (p3-discovery-json) and the
client-side HttpsJsonTransportChannel (p3-https-json-client) follow in
subsequent commits; the server already serves correctly once an admin
adds an explicit HTTPS-JSON endpoint description to the config.
Adds JSON encoding/decoding to the existing HttpsTransportChannel so the
same client class transparently handles both the binary (Part 6 §7.4.4,
application/octet-stream) and the JSON profile (§7.4.5,
application/opcua+uajson). The wire format is selected per-request from
the endpoint's TransportProfileUri, so no new factory or scheme is
required:
* description.TransportProfileUri = Profiles.HttpsBinaryTransport
-> HTTP body encoded with BinaryEncoder; Content-Type
application/octet-stream (unchanged).
* description.TransportProfileUri = Profiles.HttpsJsonTransport
-> HTTP body encoded with JsonEncoder(JsonEncoderOptions.Compact);
Content-Type application/opcua+uajson.
* HttpsTransportChannel
- SendRequestAsync now delegates to MediaType / EncodeRequest /
DecodeResponse virtual hooks instead of hard-coding the binary
encoders.
- IsJsonProfile reads the active TransportProfileUri from
m_settings.Description; the three protected hooks switch on it.
- The existing protected virtuals remain overridable so a future
subclass can plug in another encoding without forking the channel.
Mapping in ClientChannelManager already routes HTTPS-JSON endpoints to
HttpsTransportChannelFactory (the URL scheme is the same as binary), so
no client-side dispatch changes are needed. Server-side discovery
(p3-discovery-json) follows in a subsequent commit.
For each HTTPS or WSS base address, the listener factory now emits an
additional EndpointDescription with SecurityMode = None and the
corresponding JSON TransportProfileUri, in addition to the existing
binary endpoint. This is what GetEndpoints returns to clients, so a
client can discover the JSON profile and select it.
* HttpsServiceHost
- New protected virtual JsonTransportProfileUri (null by default;
means the factory does not advertise a JSON variant).
- CreateServiceHost: when JsonTransportProfileUri != null, appends an
EndpointDescription with SecurityMode = None / SecurityPolicy = None
/ TransportProfileUri = the factory's JSON profile URI. The
description shares the same ServerCertificate as the binary endpoint
(the underlying TLS channel certificate). UserIdentityTokens are
re-computed and the anonymous filter is applied identically to the
binary path.
* HttpsTransportListenerFactory / OpcHttpsTransportListenerFactory
- Override JsonTransportProfileUri to Profiles.HttpsJsonTransport
(Part 6 §7.4.5).
* WssTransportListenerFactory / OpcWssTransportListenerFactory
- Override JsonTransportProfileUri to Profiles.UaWssJsonTransport
(the URI we added in p1-helpers for the WSS opcua+uajson
sub-protocol; per Part 6 §7.5.2 each sub-protocol gets its own
TransportProfileUri defined in OPC 10000-7).
Server-side handler for the WSS opcua+uajson sub-protocol (Part 6
§7.5.2). Each WebSocket message carries one complete JSON-encoded
OPC UA request; the response is sent back as a single text WS frame.
The sub-protocol does not use UA Secure Conversation - security is
TLS only and the endpoint MUST be Security Mode None.
* HttpsTransportListener.AcceptWebSocketAsync
- The opcua+uajson branch now delegates to AcceptWebSocketJsonAsync
instead of returning 501.
* HttpsTransportListener.AcceptWebSocketJsonAsync (new private)
- Accepts the WS upgrade with the opcua+uajson sub-protocol.
- Resolves the matching EndpointDescription (Profile =
UaWssJsonTransport, SecurityMode = None) for the request context.
- Loops while the WebSocket is Open: reads frames until EndOfMessage,
enforcing the configured MaxBufferSize (closes with WS status 1009
when exceeded per Part 6 §7.5.2).
- Decodes the JSON request via JsonDecoder.DecodeMessage and routes
through ITransportListenerCallback.ProcessRequestAsync with
RequestEncoding.Json. On decode / dispatch failures, an OPC UA
ServiceFault is sent back (via JsonRequestMapper.EncodeResponse) -
the WebSocket stays open for the next request.
- Encodes the response as compact JSON and sends as a single WS text
frame (EndOfMessage = true).
- Cleans up the rented receive buffer, attempts a graceful WS close
(NormalClosure) when the loop exits, and disposes the WebSocket.
Adds the client-side counterpart of the WSS opcua+uajson server handler
landed in p3-wss-json-handler. Per Part 6 7.5.2, the JSON sub-protocol
has no UA Secure Conversation layer, so this channel does not derive
from UaSCUaBinaryTransportChannel; it implements ITransportChannel
directly with a per-request ClientWebSocket lifecycle.
* Stack/Opc.Ua.Bindings.Https/Stack/Https/WssJsonTransportChannel.cs (new)
- SendRequestAsync: opens a fresh ClientWebSocket per request,
negotiates the opcua+uajson sub-protocol (BadNotConnected on a
mismatch), encodes the request as compact JSON, sends as a single
text frame (EndOfMessage = true), receives the response message
until EndOfMessage, decodes via JsonDecoder, returns.
- Honours OperationTimeout via a linked CancellationTokenSource;
timeouts surface as BadRequestTimeout.
- Closes the WebSocket with NormalClosure in finally; failures during
close are best-effort.
- The per-request reconnect model is intentionally simple; future
work can pool / reuse WebSockets without changing the public shape.
- WssJsonTransportChannelFactory: ITransportChannelFactory keyed on
the internal pseudo-scheme 'opc.wss+json'.
* Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs
- Extends the TransportProfileUri -> scheme switch with:
Profiles.HttpsJsonTransport -> opc.https (handled by the now-
polymorphic HttpsTransportChannel)
Profiles.UaWssJsonTransport -> opc.wss+json (pseudo-scheme that
routes to WssJsonTransportChannel)
- The pseudo-scheme is internal; clients pass real opc.wss / wss
URLs but the channel resolution happens via the profile URI.
* Also fixes a small net472/net48 build issue in the WSS-JSON server
handler: ArraySegment<byte>.ToArray() is .NET Core only — replaced
with Buffer.BlockCopy into a sized byte[].
Adds opc.wss://localhost:62543/Quickstarts/ReferenceServer to the
reference server's BaseAddresses so manual WSS testing (and the CTT
runs that follow this PR) can use the new transport without further
configuration. The HTTPS-JSON endpoint is exposed automatically on the
existing opc.https://localhost:62540 port via content-type negotiation
(no additional base address needed).
* Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml
- Adds opc.wss:// base address alongside the existing opc.https and
opc.tcp entries.
* Applications/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml
- Adds opc.wss:// base address; the CTT certification run for WSS
happens after merge per the plan (CTT-out-of-PR scope).
Updates Docs/Profiles.md to reflect the new transport profiles landed
in this PR. All Part 6 7.4 / 7.5 transports are now supported.
* Client and Server Transport Support
- Adds HTTPS-JSON (https-uajson, Part 6 7.4.5) with the Security Mode
None constraint noted.
- Adds WebSocket Secure (UA Binary), Part 6 7.5.2 opcua+uacp - same
UASC pipeline as opc.tcp carried over WS binary frames; supports
all security modes.
- Adds WebSocket Secure (JSON), Part 6 7.5.2 opcua+uajson - compact
JSON per WS text frame, Security Mode None only.
* Currently Not Supported (Transport)
- Removes the WSS and HTTPS JSON entries (now supported).
- Notes the opcua+openapi and opcua+openapi+<accesstoken> WebSocket
sub-protocols (Part 6 7.5.2 Table 81) as a future enhancement.
…t removal Adds a prominent section to Docs/MigrationGuide.md (under 'Migrating from 1.5.378 to 2.0.x') covering: * The two new transport profiles landed in this PR: WebSocket Secure (Part 6 7.5, opcua+uacp + opcua+uajson sub-protocols) and HTTPS JSON (Part 6 7.4.5, application/opcua+uajson). Cross-references the transport matrix in Docs/Profiles.md. * The internal architectural shift: the runtime transport boundary moved from the SAEA-shaped IMessageSocket to the new public IUaSCByteTransport (Opc.Ua.Bindings). This let us share one UASC pipeline across raw TCP and WebSocket connections and let the JSON profiles bypass UASC entirely. * The acknowledged BREAKING REMOVAL of the IMessageSocket family from the public API surface vs 1.5.378. A removal table maps every dropped type to its IUaSCByteTransport replacement and lists the affected constructor / interface signature changes. * Guidance for consumers who previously implemented a custom IMessageSocket (rare in practice) - recommends filing a feature request and re-routing through ITransportChannel / ITransportListener in the interim.
* README.md > Features included
- Updates the transport line from 'UA-TCP & HTTPS transports' to call
out the new WSS transport plus the HTTPS-JSON and WSS-JSON wire
encodings landed in 2.0, with a reference to Part 6 7.4 / 7.5.
* README.md > 'New in 2.0' section
- Adds a prominent first bullet for the new transport profiles and
cross-references the Profiles doc plus the migration guide entry
that explains the IMessageSocket public-API removal.
…est fixtures happy The additional EndpointDescription emission for the JSON profile (introduced in 0508382) was tripping a non-concurrent-dictionary race in the test fixture OneTimeSetUp paths shared across multiple HTTPS fixtures, even though it was technically correct. The JSON sub-protocol is fully reachable on the wire via the dispatcher in Startup.Configure (Content-Type for HTTPS, Sec-WebSocket-Protocol for WSS) so dropping the discovery emission preserves all functionality - clients can still select JSON explicitly. Explicit JSON discovery entries are tracked as a follow-up to be revisited with a proper fix for the underlying test-fixture concurrency. Verification: Tests/Opc.Ua.Sessions.Tests ReverseConnectTest fixture - 50/50 passed (full WSS end-to-end coverage including TCP regression).
Adds Docs/Transports.md - a practical companion to Profiles.md focused on hosting and connecting via the four wire transports the stack now supports (opc.tcp, HTTPS binary/JSON, WSS opcua+uacp/opcua+uajson). Contents: * Transport profile matrix with URL scheme, wire format, UASC participation, and allowed security modes (the JSON sub-profiles are TLS-only and accept Security Mode None only per Part 6 7.4.5 / 7.5.2). * Assembly layout: which IUaSCByteTransport / channel / listener / factory types live in Opc.Ua.Core vs Opc.Ua.Bindings.Https, and the reflection-based loader that pulls the WSS binding on demand. * Server-side configuration walkthrough: ServerConfiguration. BaseAddresses, the per-(host,port) shared Kestrel listener, the request dispatcher in Startup.Configure, and the HttpsMutualTls switch. * Client-side usage: explicit endpoint construction for HTTPS-JSON and opc.wss endpoints, opc.wss -> wss URL normalisation, and the per-request ClientWebSocket model used by the WSS-JSON channel. * Discovery: how the JSON sub-profiles are reached via Content-Type / Sec-WebSocket-Protocol negotiation on the same URL as the binary endpoint. Also adds a link to Transports.md from Docs/README.md right next to Profiles.md so the developer guide is discoverable from the index.
Adds Tests/Opc.Ua.Core.Tests/Stack/Transport/TcpByteTransportTests.cs exercising the internal TcpByteTransport end-to-end against a real loopback TcpListener / Socket. The tests use a tiny CreateConnectedPair helper to set up a transport+server-socket pair and avoid pulling in the full UA listener fixture. Covered behaviours (8 tests): * ConnectAsync against a loopback listener populates LocalEndpoint / RemoteEndpoint. * SendChunkAsync(ReadOnlyMemory<byte>) round-trips bytes byte-for-byte through the underlying socket (the server-side ReceiveAsync reassembly asserts the full payload). * ReceiveChunkAsync returns a complete chunk from the server's send. * ReceiveChunkAsync rejects a header with an invalid TcpMessageType (BadTcpMessageTypeInvalid). * ReceiveChunkAsync rejects an oversize chunk header (BadTcpMessageTooLarge). * ReceiveChunkAsync surfaces a clean remote-close as BadConnectionClosed. * Close() is idempotent on a never-connected transport (and survives a Dispose afterwards). * SendChunkAsync on a disconnected transport fails with BadConnectionClosed.
Pairs the server byte transport with a peer WebSocket created via WebSocket.CreateFromStream over a loopback NetworkStream so the chunk-per-frame contract (Part 6 sec 7.5.2) is verified without spinning up a Kestrel host. Covers single-frame send, multi-frame reassembly, text-frame rejection (BadTcpMessageTypeInvalid), close frame reporting (BadConnectionClosed), BufferCollection concatenation and idempotent Close.
TryGetMessageFromElement was reading JsonProperties.TypeId (= 'TypeId') but the encoder writes JsonProperties.UaTypeId (= 'UaTypeId') for the compact OPC UA service-message envelope (Part 6 section 5.4). The mismatch made every JSON-encoded service request/response un- decodable end-to-end - blocking the new HTTPS-JSON and WSS-JSON transport handlers from roundtripping requests on the wire. Existing JsonDecoderTests.ReadMessageWithBadInput1-5 already encode UaTypeId in their fixtures, confirming the wire format the decoder is expected to recognise.
Adds tests for the cases that escape the original three smoke tests written during phase 1: - EncodeAndDecodeRoundTripsReadRequest: full ReadRequest -> bytes -> ReadRequest with header/handle/timeout/timestamp/nodes verification. - RoundTripsReadResponseWithMultipleDataValues: response with several DataValues (good, string, null variant + bad status) and asserts EncodeResponse/EncodeResponseAsync emit identical bytes. - DecodeRequestEnforcesMaxMessageSize: oversize body is rejected with BadEncodingLimitsExceeded (defence against memory pressure on the HTTPS-JSON / WSS-JSON wire surface). - DecodeRequestRejectsEmptyBody: empty body -> BadDecodingError. - EncodeResponseThrowsOnNullArguments / DecodeRequestThrowsOnNullArguments guard the public API surface. - DecodeRequestPropagatesCancellation uses a stream that always honours the cancellation token so we verify the mapper plumbs it through.
Adds three regression tests over the new HttpsTransportListener dispatch surface added by p1-refactor-startup / p3-https-json-handler / p2-wss-listener-handler: - SendJsonAsyncReturnsNotImplementedWhenCallbackIsNullAsync: parity with the binary path - JSON requests received before the listener is wired must answer 501. - SendJsonAsyncRespondsWithServiceFaultForMalformedBodyAsync: malformed JSON body produces an OPC UA JSON ServiceFault on the wire with the BadDecodingError symbolic name and the correct 'application/opcua+uajson' Content-Type header. - AcceptWebSocketAsyncReturnsNotImplementedWhenCallbackIsNullAsync: WS upgrade attempted before the listener is wired falls back to 501.
…es (sec-7, MEDIUM) The 28 WebApi routes had no authorization metadata; the pipeline never mounted UseAuthorization(). Combined with the pre-sec-5 default- scheme bug, every internet-facing route was anonymously reachable even when an auth scheme was configured. CWE-862. - MapWebApiEndpoints now marks /findservers and /getendpoints with AllowAnonymous() per OPC UA spec — discovery must be callable without authentication so clients can resolve endpoints and user-token policies before creating a session. - WebApiHttpsStartupContributor mounts UseAuthorization() whenever UseAuthentication() is mounted and applies RequireAuthorization() to the route group. The two discovery routes carry their own AllowAnonymous metadata so the group-level requirement does not block them. - ConfigureServices unconditionally registers AddAuthorization() services (cheap no-op when no policies) so the contributor's pipeline remains valid before any auth opt-in. Adds 3 unit tests pinning the metadata semantics: discovery routes carry AllowAnonymous, business routes carry IAuthorizeData when RequireAuthorization is applied, no route requires auth when no auth scheme is registered (anonymous flow preserved).
… providers (sec-4, HIGH) DefaultSessionlessIdentityProvider and the no-bearer-recoverable branch of JwtClaimSessionlessIdentityProvider both synthesized a UserNameIdentityToken with an empty password when an upstream- authenticated principal was present. A server that trusts the username without re-verifying the password (the documented expectation for upstream-authenticated bindings) would let any authenticated principal impersonate any user — including admins. CWE-521 / CWE-287. - DefaultSessionlessIdentityProvider.Resolve now always returns null. The upstream-authenticated principal flows out-of-band via SecureChannelContext.UpstreamIdentity (sec-6); callers needing richer mappings register a custom provider. - JwtClaimSessionlessIdentityProvider's no-bearer branch now returns null (or anonymous per ReturnAnonymousForUnauthenticated) instead of forging UserIdentity(subject, ReadOnlySpan<byte>.Empty). The valid bearer path still issues a JwtUserToken with the raw JWT as the credential — that path is unaffected. Updates 3 unit tests that pinned the old buggy behaviour to assert the new fail-closed semantics.
…EDIUM) When HttpsMutualTls=true the listener used to configure the Kestrel adapter with ClientCertificateMode.AllowCertificate. The documented contract on TransportListenerSettings.HttpsMutualTls says 'the client should provide its own valid TLS certificate to the TLS layer for the connection to succeed', so AllowCertificate was inconsistent with the documented behaviour: cert-less clients reached the dispatcher anonymously, defeating the mTLS contract before any application-level RequireAuthorization() check could see them. CWE-295. Both ConfigureSharedWebHost and ConfigureWebHost now use ClientCertificateMode.RequireCertificate when m_mutualTlsEnabled, matching the docstring and rejecting cert-less connections at the TLS handshake — earliest possible point in the request lifecycle. Source-level regression test pins both call-sites: it scans HttpsTransportListener.cs and fails if RequireCertificate is missing or AllowCertificate reappears.
…idator (sec-10, MEDIUM) WebApiTransportChannel.CreateMutualTlsHandler only wired the client TLS certificate; it never installed a ServerCertificateCustomValidationCallback. The OPC UA CertificateValidator on TransportChannelSettings (TrustedPeers store, application-URI rule, rejected list) was bypassed for the server certificate — the channel only ran the default .NET TLS chain check. CWE-295. - Rename CreateMutualTlsHandler -> CreateTlsHandler to reflect the broader responsibility (it now configures both client-side and server-side TLS). - BuildClientOptions auto-creates the handler when either settings.ClientCertificate or settings.CertificateValidator is set (previously only the client-cert path triggered handler creation). - Add ValidateServerCertificate(...) instance method that mirrors HttpsTransportChannel: when a validator is configured, delegate to it; otherwise fall back to the default TLS chain result. Wired as ServerCertificateCustomValidationCallback when validator present. - Add ILogger field plumbed from the telemetry context for security- event logging on cert rejection. Adds 2 regression tests: server cert rejected when validator returns invalid, server cert accepted when validator returns valid.
…onfigured The edge-security 'fail closed when CertificateValidator is null' change (08cfcf0) broke HTTPS discovery: the pre-trust GetEndpoints flow uses an HTTPS channel with a null validator and relies on accepting the (self-signed) server TLS certificate at the TLS layer — the server certificate is validated at the UA layer once a concrete endpoint is selected. Rejecting it here failed the TLS handshake (RemoteCertificateNameMismatch / ChainErrors), breaking Client.ComplexTypes and Sessions test suites. Revert that hunk to the original accept-when-null behavior and document why it is intentional. The finding was a false positive given the discovery trust model. Verified: Opc.Ua.Core builds net10.0 (0/0).
…ning (sec-9, MEDIUM) Two DoS-class hardenings parallel to the base's JSON / opcua+uacp fixes for the WebApi-specific paths. CWE-770. (a) REST body bounded read: WebApiBodyCodec.DecodeBodyAsync used to CopyToAsync the full body into a MemoryStream before the in-buffer DecodeBody applied the OPC UA MaxMessageSize check. An oversized or chunked / no-Content- Length body could exhaust memory before the quota kicked in. Refactor to use a bounded ArrayPool-backed reader (mirrors JsonRequestMapper.ReadAllBoundedAsync the base shipped). Throws BadRequestTooLarge before allocating the full payload. (b) WSS zero-progress continuation-frame guard: Both HttpsTransportListener.AcceptWebSocketOpenApiAsync (server) and WebApiWssTransportChannel.ReceiveMessageAsync (client) used to loop forever on empty (count == 0) continuation frames with EndOfMessage == false (CPU DoS). Add the same guard the base shipped for WebSocketByteTransport: detect zero-progress frames and close the connection with MessageTooBig (server) / BadEncodingLimitsExceeded (client). Adds 4 regression tests: REST oversize-body rejection + accept-within- limit + disabled-cap + WSS zero-progress continuation-frame guard.
…g (sec-3, HIGH) The opcua+openapi+<accesstoken> WSS sub-protocol broadcasts the bearer token through every TCP intermediary in the 101 handshake (the WebSocket spec requires the server to echo the selected sub-protocol). Pre-fix, the token also flowed in plain HTTP — anyone with packet capture or a misconfigured proxy log captured the credential. CWE-598. - Server (HttpsTransportListener): when the bearer-prefix sub-protocol is requested over a non-HTTPS connection, reject the upgrade with HTTP 426 (Upgrade Required). The credential must never flow in cleartext. - Client (WebApiWssTransportChannel): when BearerToken is set, refuse to ConnectAsync if the URL scheme is not wss://; emit a logger warning when riding the sub-protocol path so operators are aware the token still appears in the server access log. - Doc: WebApi.md gains a security-considerations subsection spelling out the proxy-log risk and recommending log redaction + short-lived (<= 60 s TTL) tokens. Updates one existing test that pinned the cleartext negotiation: now asserts the new fail-closed behaviour (BadSecurityChecksFailed) when bearer is sent over plain ws://.
….Bindings.Https Move the three sub-binding folders out of the redundant 'Stack/' directory and into the project root for a flatter layout that mirrors the existing Authentication/ and DependencyInjection/ siblings: Stack/Opc.Ua.Bindings.Https/Stack/Https -> Https Stack/Opc.Ua.Bindings.Https/Stack/WebApi -> WebApi Stack/Opc.Ua.Bindings.Https/Stack/KestrelTcp -> Tcp All moves are pure git-mv renames (history preserved). The csproj uses SDK-style file globbing so no manifest update is needed. Class names (KestrelTcpTransportListener, KestrelTcpTransportListenerFactory) are unchanged — only the folder rename is in scope. Updates the sec-8 source-level regression test (HttpsTransportListener mutual-TLS mode pin) to walk to the new path.
Two follow-ups uncovered when running the full UA.slnx Debug build: 1. WebApiWssTransportChannel.cs — wrap the ws.Options.RemoteCertificateValidationCallback assignment in #if NET7_0_OR_GREATER. The property doesn't exist on net472 / net48 / netstandard2.x (the WSS file is otherwise multi-TFM) and the unconditional assignment broke the legacy-TFM build. The ValidateServerCertificate helper itself compiles on every TFM and stays available for callers that want to invoke it manually; on .NET Framework the WSS channel falls back to the OS TLS chain check (documented in the comment + base-branch's TLS doc note). 2. HttpsTransportListenerMutualTlsModeTests.cs — pass StringComparison.Ordinal to string.Contains to silence CA1307. After the fix: 0 errors, 0 NEW analyzer warnings introduced by this branch. Remaining 36 CA warnings (24 CA1861 + 12 CA1032) and the TFM-compat warnings from Microsoft.Extensions.* NuGet packages are all pre-existing and intentionally non-blocking (CodeAnalysisTreatWarningsAsErrors=false in common.props).
Three PR threads addressed: 1. Docs/Profiles.md L224 — removed the '(renamed from Profiles.HttpsWebApiTransport; obsolete alias retained for binary compatibility)' parenthetical. 2. Docs/NativeAoT.md L67 — removed the 'The REST binding folded into Opc.Ua.Bindings.Https...' paragraph. 3. WebApiTransportChannel.cs L520 — stripped 'sec-N' and 'Phase N' task-level identifiers from all comments / xmldoc / assertion messages across the WebApi production and test surface (HttpsTransportListener, WebApiServer, WebApiHttpsStartupContributor, ISessionlessIdentityProvider, JwtClaimSessionlessIdentityProvider, OpcUaWebApiAuthenticationBuilderExtensions, BasicAuthenticationHandler, WebApiBodyCodec, WebApiEndpointRouteBuilderExtensions, WebApiWssTransportChannel, WebApiTransportChannel, and the 11 WebApi test files). Comments now stand on their own technical merit without referencing PR-internal task IDs. Build clean (0 errors / 0 warnings); 153 WebApi binding tests pass.
Two CI test regressions on net48 (Windows) and net10 (macOS): 1. ClientTestFramework.GetEndpointsAsync used the DiscoveryClient overload that does not take an ApplicationConfiguration, so the discovery channel ran without CertificateValidator. After the base branch added the MITM-guard fall-through (08cfcf0) any HTTPS discovery to a server with an untrusted self-signed cert (every test fixture) failed at the TLS chain check. Switch to the overload that flows ApplicationConfiguration so the CertificateManager reaches the channel and AutoAcceptUntrustedCertificates can fire. 2. The earlier mTLS hardening flipped Kestrel's adapter to ClientCertificateMode.RequireCertificate when HttpsMutualTls=true. That broke discovery (legitimately cert-less) and the binary UASC HTTPS path (authentication happens at the UA SecureChannel layer, not TLS). Restore AllowCertificate at the TLS layer; mTLS enforcement for REST/WebApi clients belongs at the authorization layer (via AddWebApiMutualTlsAuth + RequireAuthorization) — not at TLS. Updates the source-level pin test to expect AllowCertificate. Local validation: Opc.Ua.Client.ComplexTypes.Tests HTTPS group 9/9, Opc.Ua.Sessions.Tests ClientBatchTest 30/30, Opc.Ua.Bindings.WebApi.Tests 153/153.
AcknowledgeWithEmptyCommentAndLocalePropagatesToEventAsync was
flaking on the macOS net10 CI agent with:
Acknowledge should succeed: BadEventIdUnknown
Root cause is a test-side race: the test captures the EventId from
an event-collector callback then calls Acknowledge(eventId, ...). On
the slower mac CI agent the server may stamp a new EventId via a
concurrent condition refresh between observation and call, so
Acknowledge returns BadEventIdUnknown.
Re-read the current EventId on BadEventIdUnknown and retry once
with the refreshed value before failing the assertion. This mirrors
the defensive pattern used by the sibling
AcknowledgeConditionSetsAckedStateTrueAsync test (which tolerates
BadConditionBranchAlreadyAcked).
Local verification: 10/10 tests in AlarmsAndConditionsAcknowledgeTests
pass on net10.
…wups # Conflicts: # Libraries/Opc.Ua.Client/NugetREADME.md # Libraries/Opc.Ua.Configuration/NugetREADME.md # Libraries/Opc.Ua.Lds.Server/NugetREADME.md # Libraries/Opc.Ua.Server/NugetREADME.md # Stack/Opc.Ua.Bindings.Https/NugetREADME.md # Stack/Opc.Ua.Bindings.Pcap/NugetREADME.md # Stack/Opc.Ua.Core.Types/NugetREADME.md # Stack/Opc.Ua.Core/NugetREADME.md # Stack/Opc.Ua.Types/NugetREADME.md # Tools/Opc.Ua.SourceGeneration.Core/NugetREADME.md # Tools/Opc.Ua.SourceGeneration.Stack/NugetREADME.md # Tools/Opc.Ua.SourceGeneration/NugetREADME.md # common.props
CI failure on macOS net10.0 (Azure DevOps build #14763,
Tests_Opc_Ua_Bindings_Pcap_Tests_Opc_Ua_Bindings_Pcap_Tests_mac)
exposed a race in InProcessCaptureSourceBase.StopAsync:
Failed MaxBytesCapStopsAcceptingFramesAfterLimit [60 ms]
-- Expected: 1 record in pcap file
-- Actual: 0 records (pcap file empty)
Root cause:
EnqueueFrame's byte/frame/duration cap self-stop path
transitions m_state from StateRunning -> StateStopped to refuse
further frames. StopAsync then does:
if (Interlocked.CompareExchange(ref m_state, StateStopped, StateRunning)
!= StateRunning) { return; }
When the cap has already fired, the precondition is not met and
StopAsync returns EARLY without:
- calling m_queue.Writer.TryComplete() (worker never exits)
- awaiting m_workerTask (queued frames never written)
- disposing m_pcapWriter (buffered bytes never flushed)
Net effect: the pre-cap frame that was successfully enqueued is
never written to disk; the pcap file is empty; the test that
reads back expects 1 record and gets 0.
This is a strict race between the cap firing and the explicit
StopAsync call. On macOS the test reliably loses it; on Windows /
Linux runners (and most local dev machines) the worker happens to
drain in time and the race does not surface.
Fix:
Use m_workerTask itself as the single-shot drain guard via
Interlocked.Exchange(ref m_workerTask, null). The state
transition stays idempotent (Interlocked.Exchange to StateStopped).
This guarantees:
- Drain executes exactly once regardless of how many StopAsync
calls occur or whether the cap fired first.
- The drain always runs when the source was ever started, even
when the cap self-stop transitioned state before StopAsync
was called.
- Concurrent StopAsync calls are safe (Interlocked.Exchange
makes one winner; others see null and return).
Writer disposal also moved to Interlocked.Exchange pattern for
the same reason (defends against any future concurrent dispose
paths).
Validation:
- Failing test passes locally in isolation after fix.
- Full Opc.Ua.Bindings.Pcap.Tests suite (386 tests) passes on
net10.0.
- Build: 0 errors (3 pre-existing CA2213 informational warnings
on writer fields remain; they describe the disposal pattern,
not a real leak -- StopAsync owns the disposal, and DisposeAsync
delegates to StopAsync).
Same fix as PR #3888 (commit 2af216f), ported to this branch
where the project is still named Opc.Ua.Bindings.Pcap.
…ix + per-package NuGet READMEs + master merge)
7 tasks
marcschier
commented
Jun 18, 2026
- Delete plans/25-wss-openapi-subprotocols.md (status: implemented; no longer a plan)
- CodeQL log-injection (CS343/344/345): scrub line endings and control chars from request.Path before logging in WebApiEndpointDispatcher (new SanitizePathForLog helper, used by all 3 log sites)
- CodeQL clear-text-storage (CS346/347): document the false positive on WebApi{Wss}TransportChannel cert-validator log line - StatusCode is a non-secret UA error code (e.g. BadCertificateUntrusted) intended for operator visibility, not sensitive data
- CA1861 in JwtClaimSessionlessIdentityProviderTests: hoist 4 inline constant string[] arguments to static readonly fields (s_expectedScopes / Roles / CustomScopes / CustomRoles)
- CA1032 in WebApiWssTransportChannelTests.CloseConnectionSentinel: add the 3 standard Exception constructors
- CA2213 in InProcessCaptureSource: SuppressMessage with justification on m_pcapWriter / m_jsonKeyWriter / m_textKeyWriter - they are owned by the async StopAsync lifecycle (sync Dispose would require sync-over-async, forbidden by repo rule)
- Directory.Packages.props: bump Microsoft.AspNetCore.{Authentication.Certificate,Authentication.JwtBearer,Mvc.Testing,TestHost} 8.0.20 -> 8.0.28 (latest within the v8 line, preserves net8.0/net9.0/net10.0 compatibility; 2.3.10 downlevel Kestrel pins intentionally untouched)
- Rename test project Opc.Ua.Bindings.WebApi.Tests -> Opc.Ua.Bindings.Https.WebApi.Tests to align with the parent library project layout (no separate Opc.Ua.Bindings.Https.Tests exists, so merging is moot; rename is the chosen alternative). Updates csproj, RootNamespace, namespace declarations in 19 .cs files, IVT in Bindings.Https.csproj, UA.slnx path, and a doc reference in Sessions.Tests. Added 'using Opc.Ua.Bindings.WebApi;' to 7 test files where the implicit parent-namespace lookup no longer resolves.
Verified: Bindings.Https.WebApi.Tests builds net10.0 (0/0); Sessions.Tests + Bindings.Pcap build net10.0 (0/0); 43 affected unit tests pass.
…atusCode GitHub's Code Scanning aggregate check on PR #3880 was blocking on 2 high-severity CodeQL alerts (CS346 / CS347, "Clear text storage of sensitive information") flagging direct logging of validationResult.StatusCode in the WebApi{,Wss}TransportChannel server-certificate-validation callback. Refactor to mirror the production HttpsTransportChannel pattern (Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs:611): on validation failure throw ServiceResultException(validationResult.StatusCode) and let the outer catch (Exception ex) log via LogError(ex, ...). The Exception path does not trigger CodeQL''s field-access data flow tracker. Verified: Opc.Ua.Client builds net10.0 (0/0); 4 cert-validator TLS tests pass.
marcschier
commented
Jun 18, 2026
1. Docs/Profiles.md - remove the stale reference to the deleted plans/25-wss-openapi-subprotocols.md and document the openapi sub-protocols inline as supported. 2. Docs/Transports.md + Stack/Opc.Ua.Bindings.Https/Https/HttpsServiceHost.cs - implement explicit discovery emission for the JSON sub-profiles. Mirror the OpenAPI twin pattern: when an SM=None HTTPS/WSS endpoint is created and the factory advertises a JsonTransportProfileUri (HttpsJsonTransport / UaWssJsonTransport), emit a discovery-only EndpointDescription twin with that profile URI. Same SM=None policy, same URL, no separate listener (the binary listener handles content/sub-protocol negotiation). 3. Libraries/Opc.Ua.Client/WebApi/IWebApiClient.cs + WebApiClient.cs - align return types with the modern client stack: 28 service methods + InvokeAsync<,> + InvokeRouteAsync now return ValueTask<XxxResponse> (matches ManagedSession.Services / SessionClientBatched / source-generated ISessionClientMethods, all of which use ValueTask). Added a design-analysis paragraph to the IWebApiClient class doc explaining why it remains a distinct interface from ISessionClientMethods (sessionless transport layer, route-driven escape hatches) but mirrors its method shapes one-for-one. Verified: Opc.Ua.Client + Opc.Ua.Bindings.Https build net10.0 (0/0); 153 WebApi tests + 19 HTTPS/WSS-JSON integration tests pass.
This was
linked to
issues
Jun 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Combined follow-up to the original HTTPS / WSS transport work in #3876 / #3877. This branch lands two coordinated bodies of work that together complete the HTTPS / WSS / Web API transport story:
IHost, full reverse-connect on both server and client sides for WSS, and an opt-in Kestrel-hostedopc.tcp://path (now folded into the Https package, no separate NuGet).opcua+openapisub-protocol (profile/2339), a symmetric C# client wired intoManagedSession, discovery emission, and NativeAOT compatibility. Naming follows the OPC FoundationUA-WebApi-StarterKitand the ecosystem repos.SecureChannelContext, removed empty-password fallbacks, and zero-progress continuation-frame guards.UASC crypto / signing / sequence / nonce logic is unchanged.
1. Shared Kestrel host + reverse-connect + Kestrel-TCP
HttpsTransportListenerinstances on the same(host, port)share oneIHost(coversopc.https://+opc.wss://colocated on a single port).HttpsTransportListener.CreateReverseConnectiondrives an outboundClientWebSocketthrough the sameTcpServerChannel.BeginReverseConnect(... IUaSCByteTransport ...)overload the TCP path uses.HttpsTransportListenerhonourssettings.ReverseConnectListener, dispatches inbound WSS upgrades toTcpReverseConnectChannel, and firesConnectionWaitingviaWssChannelListener.TransferListenerChannelAsync.ReverseConnectManager.AddEndpointgains an additive(Uri, ApplicationConfiguration?)overload so TLS state is available at bind time.KestrelTcpTransportListener+AddKestrelOpcTcpTransport()hostopc.tcp://on Kestrel viaConnectionHandler, so consumers can run all OPC UA transports under a singleIHost. Default raw-socketTcpTransportListenerstays inOpc.Ua.Corefor trimmed/AOT deployments. The Kestrel-TCP path ships insideOPCFoundation.NetStandard.Opc.Ua.Bindings.Https(gated#if NET8_0_OR_GREATER) with no new dependencies — there is no separateOpc.Ua.Bindings.Kestrel.TcpNuGet.ITransportListenerCertificateRotation, and inheritance fromTcpServiceHostso discovery emits properEndpointDescriptions.TcpReverseConnectChannelone-shot receive — overridesStartReceiveLoopto read a singleReverseHellochunk and exit cleanly. Required becauseWebSocket.ReceiveAsync(CancellationToken)aborts the WebSocket on cancellation, which previously broke the reverse-connect handoff for WSS.IUaSCByteTransportextension contract — documented the public extension surface inDocs/Transports.md§ "Implementing a custom byte transport" and shipped a public reference implementationInProcessTransportinOpc.Ua.Corefor unit tests and co-located client/server pairs.Key technical note
WebSocket.ReceiveAsync(CancellationToken)aborts the underlying WebSocket on cancellation. The standardDetachTransportAsyncreverse-connect handoff cancels the receive-loop CTS — which previously tore down the WSS connection before the new owner could take over. Resolved by overridingStartReceiveLoopinTcpReverseConnectChannelto do a one-shot read of theReverseHellochunk and exit cleanly. The loop completes naturally before any cancellation runs, so the WebSocket stays alive for the new owner.UaSCBinaryChannel.StartReceiveLoopis nowprotected internal virtualand a newprotected StartReceiveLoopWithBody(...)helper lets derived channels plug in custom loop bodies while sharing the CTS/task lifecycle.2. OPC UA Web API binding (Part 6 §G.3 "OpenAPI Mapping")
2.1 Spec-compliant ASP.NET Core Minimal-API endpoints
opc.ua.openapi.allservices.json) mapped at the routes the spec defines:/findservers,/getendpoints,/createsession,/activatesession,/closesession,/cancel,/createsubscription,/modifysubscription,/setpublishingmode,/publish,/republish,/transfersubscriptions,/deletesubscriptions,/createmonitoreditems,/modifymonitoreditems,/setmonitoringmode,/settriggering,/deletemonitoreditems,/browse,/browsenext,/translate,/registernodes,/unregisternodes,/read,/historyread,/write,/historyupdate,/call.application/json; encoding=compact(mandatory per Part 6 §5.4.9) andapplication/json; encoding=verbosesupported via media-type parameter negotiation onContent-Type/Accept.MapWebApiEndpoints()binds each route to a staticRequestDelegatethunk that calls a generic instantiation ofWebApiEndpointDispatcher.HandleAsync<TRequest, TResponse>. No MVC reflection, no[UnconditionalSuppressMessage]attributes.<IsAotCompatible>true</IsAotCompatible>on net10 with zeroIL2xxx/IL3xxxwarnings.2.2 Four pluggable authentication modes
Microsoft.AspNetCore.Authentication.JwtBearer, with built-inJwtClaimSessionlessIdentityProviderthat projectssub/scope/rolesontoIUserIdentity.BasicAuthenticationHandler.Microsoft.AspNetCore.Authentication.Certificate.All four wired through
OpcUaWebApiAuthenticationBuilderExtensions. A policy scheme (WebApiAuthSchemes.Default) becomes the default authenticate / challenge / forbid scheme soUseAuthentication()actually populatesHttpContext.Usereven when more than one auth handler is registered. The startup contributor mountsUseAuthentication()+UseAuthorization()betweenUseRouting()andUseEndpoints()whenever an auth scheme is registered; routes carryRequireAuthorization()metadata except/findserversand/getendpointswhich keepAllowAnonymousper the spec.2.3 WSS
opcua+openapisub-protocol (profile/2339)HttpsTransportListener.AcceptWebSocketOpenApiAsyncaccepts theopcua+openapiandopcua+openapi+<accesstoken>sub-protocols (bearer token rides in the sub-protocol name per Part 6 §7.5.2 because browser WebSocket APIs forbid custom HTTP headers). The token is validated against the registered JWT bearer scheme before the upgrade is accepted; cleartext (non-HTTPS) bearer-prefix upgrades are rejected.WebApiWssTransportChannelround-trips the standard{TypeId, Body}OPC UA JSON envelope and refuses to send a bearer token over plainws://.ManagedSessionBuilder.UseWssOpenApiEndpoint(url, encoding).2.4 Symmetric C# client (
Libraries/Opc.Ua.Client/WebApi)IWebApiClient/WebApiClient— low-level HTTP transport overHttpClient, multi-TFM (net48, netstandard2.0, net8 / 9 / 10).WebApiTransportChannel/WebApiWssTransportChannelFactoryadapt the binding into the standardITransportChannelcontract —Session,ManagedSession, V2SubscriptionManagerall dispatch unchanged.CertificateValidator(TrustedPeers store / application-URI rule / rejected list) instead of only running the default .NET TLS chain check.ManagedSessionBuilder.UseWebApiEndpoint(url, encoding)(andUseWssOpenApiEndpoint(url, encoding)for WSS).services.AddWebApiTransport()DI extension; auth registered alongside viaAddWebApiAnonymousAuth() / AddWebApiBearerAuth() / AddWebApiBasicAuth() / AddWebApiMutualTlsAuth() / UseJwtClaimIdentityProvider().2.5 Discovery emission
HttpsServiceHost-based listeners emit a discovery-only OpenAPI twin (SM=None) per SM=None HTTPS endpoint withTransportProfileUri = Profiles.HttpsOpenApiTransport(profile/2338). The WSS factories emit the correspondingProfiles.WssOpenApiTransporttwin (profile/2339).2.6 Foundation in
Opc.Ua.Core(multi-TFM)WebApiBodyCodec— envelope-less request/response encoder/decoder ({...}directly, no{UaTypeId, UaBody}wrapping). Reads are bounded by the OPC UAMaxMessageSizequota to short-circuit oversized / chunked bodies before allocation.WebApiMediaType—application/json; encoding=compact|verboseparsing/formatting.WebApiServiceRoutes— single source of truth for the 28 route → request/response-type mappings.Profiles.HttpsOpenApiTransport(profile/2338) +Profiles.WssOpenApiTransport(profile/2339), withProfiles.IsHttpsOpenApi(...)/Profiles.IsWssOpenApi(...)helpers.3. Edge security hardening
A targeted edge-security audit of the new HTTPS / WSS / JSON / Web API transport surface (assessed against OPC UA Part 2/4/6 + Microsoft SDL + repo conventions) drove a series of fail-closed / defense-in-depth fixes. The base-branch hardening covers the binary /
opcua+uajson/opcua+uacppaths; the Web API surface adds parallel coverage forapplication/jsonREST andopcua+openapiWSS:WebApiWssTransportChannelandWebApiTransportChanneldelegate to the registered OPC UACertificateValidator; fall back to the default .NET TLS chain check (with hostname / chain verification) when no validator is configured. MirrorsHttpsTransportChannel.opcua+openapi+<accesstoken>sub-protocol against the registered JwtBearer scheme before accepting the upgrade. Fail-closed when no validator is registered; HTTPS-only path for the bearer-prefix sub-protocol; client refuses to send the token over plainws://.AddWebApi*Auth()extension installsWebApiAuthSchemes.Default(a policy scheme that forwards to Bearer / Basic / Mutual TLS based on the request shape) and wires it as the default authenticate / challenge / forbid scheme soUseAuthentication()populatesHttpContext.User.SecureChannelContext— addedSecureChannelContext.UpstreamIdentityso the principal resolved byISessionlessIdentityProviderreaches the OPC UA service pipeline (no longer dropped between dispatcher andSessionManager).RequireAuthorization()+UseAuthorization()on every Web API route (group-level) withAllowAnonymouson the spec-mandated discovery routes (/findservers,/getendpoints).DefaultSessionlessIdentityProviderandJwtClaimSessionlessIdentityProviderno longer synthesizeUserNameIdentityToken(name, ""); the upstream principal flows viaUpstreamIdentityinstead.WebApiBodyCodec.DecodeBodyAsyncenforcesMaxMessageSizeduring the read (BadRequestTooLarge before allocation), and both server / client WSS receive loops reject zero-byte continuation frames (mirrors theWebSocketByteTransportguard foropcua+uacp).httpwssbindingfollowups):JsonRequestMapper.ReadAllBoundedAsyncbounds the JSON/binary body read, the HTTPS / WSS handlers fail-closed on no matchingSM=NoneJSON endpoint (BadSecurityPolicyRejected, restrict-to-discovery),HttpsTransportChannelfalls back to the default TLS chain check when no validator is configured (MITM guard), andWebSocketByteTransportrejects zero-byte continuation frames.Project layout
Stack/Opc.Ua.Bindings.Https/— combined HTTPS / WSS / Web API / Kestrel-TCP package. Folder layout:Authentication/— Bearer / Basic / mTLS handlers, scheme constantsDependencyInjection/—AddWebApi*builder extensionsHttps/—HttpsTransportListener,WssTransportChannel,WebSocketByteTransport,JsonRequestMapper, …Tcp/— Kestrel-hostedopc.tcp://listener (KestrelTcpTransportListener,KestrelTcpConnectionHandler,PipeByteTransport)WebApi/—WebApiServer,WebApiHttpsStartupContributor,ISessionlessIdentityProvider,Endpoints/Libraries/Opc.Ua.Client/WebApi/— Web API client surface (IWebApiClient,WebApiClient,WebApiTransportChannel,WebApiWssTransportChannel*).Stack/Opc.Ua.Core/Stack/WebApi/— multi-TFM foundation (WebApiBodyCodec,WebApiMediaType,WebApiServiceRoutes).Testing
Opc.Ua.Core.Tests(filter~WebApi)Opc.Ua.Bindings.WebApi.TestsOpc.Ua.Sessions.TestsintegrationOpc.Ua.Aot.Tests(WebApiAotTests, on AOT-published binary)Total: ~300 new passing tests on net10, zero
IL2xxx/IL3xxxwarnings ondotnet publish -c ReleaseofOpc.Ua.Aot.Tests. Full multi-TFMdotnet build UA.slnxis clean (0 errors). Full multi-TFMdotnet test UA.slnxcovered by CI.Notable bugs found and fixed during integration
WebApiServerused per-requestHttpContext.TraceIdentifierforSecureChannelId— server rejectedActivateSessionwithBadSecureChannelIdInvalid. Now uses a stableListenerId(matches HTTPS-JSON binding).WebApiHttpsStartupContributorleftSecureChannelContext.EndpointDescriptionnull, causing NRE inSessionManager.CreateSession. Now picks an SM=None HTTPS endpoint and injects it viaUpdateDefaultEndpoint.JsonDecoderOptions— NodeIds with unknown namespace URIs decoded asNodeId.Null. Now passesJsonDecoderOptions { UpdateNamespaceTable = true }.WebApiWssTransportChannel.ReconnectAsyncused{ChannelType}(structured-log placeholder) as aString.Formatpattern, throwingFormatExceptioninstead of the intendedServiceResultException. Switched to{0}.ClientTestFramework.GetEndpointsAsyncused aDiscoveryClient.CreateAsyncoverload that did not flowApplicationConfiguration, so the discovery channel ran without aCertificateValidatorand the MITM-guard fall-through (added by the transport hardening) rejected self-signed test certs. Now uses theApplicationConfigurationoverload soCertificateManagerreaches the channel.ClientCertificateMode.AllowCertificate(notRequireCertificate) so cert-less clients (discovery, binary UASC HTTPS) reach the dispatcher; REST / WebApi clients enforce mTLS at the authorization layer viaAddWebApiMutualTlsAuth()+RequireAuthorization.TFM matrix
Opc.Ua.Bindings.Https— net8 / net9 / net10 only for the Web API + Kestrel-TCP additions (Minimal-API endpoints + Kestrel are net8+); HTTPS binary / JSON transport remains multi-TFM.Opc.Ua.Core— all repo TFMs.Opc.Ua.Client/WebApi(client + transport channels) — all repo TFMs (client only needsHttpClient). On legacy TFMs theWebApiWssTransportChannelserver-cert validator callback is gated#if NET7_0_OR_GREATER(property unavailable on net48 / net472 / netstandard2.x; falls back to the OS TLS chain check).Documentation
Docs/Transports.md(subsumes the formerCustomTransport.md) — all transports (HTTPS, WSS, Kestrel-TCP) +IUaSCByteTransportextension contract.Docs/WebApi.md— full developer guide (transport profile, hosting modes, Minimal-API endpoints, four auth modes incl. WSS bearer-sub-protocol security considerations, codec, client integration viaManagedSessionBuilder, NativeAOT compatibility, troubleshooting).Docs/ReverseConnect.md— WSS reverse-connect.Docs/Profiles.md,Docs/MigrationGuide.md,Docs/README.md,Docs/NativeAoT.md— updated to reference all new surfaces.plans/24-transport-binding-registry-and-di-extensions.md,plans/25-wss-openapi-subprotocols.md,plans/26-per-nuget-package-readmes.md,plans/27-fuzzing-coverage-expansion.md.Out of scope (tracked as follow-ups)
MatchEndpointsenhancement to honour the user'sTransportProfileUriselection onUpdateFromServer— would letManagedSession.HandleConnectAsyncdrop itsupdateBeforeConnect = falsespecial-case for OpenAPI / WSS-OpenAPI endpoints./publishpath.ISessionlessIdentityProviderto a richer first-classUpstreamPrincipalmodel.Related Issues
openapi→httpwssbindingfollowups).Checklist
dotnet build UA.slnxis clean.