Skip to content

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
masterfrom
httpwssbindingfollowups
Open

Adding HTTPS/WSS transports + Web API binding (Part 6 §G.3 OpenAPI Mapping), shared Kestrel host, Kestrel-TCP#3880
marcschier wants to merge 117 commits into
masterfrom
httpwssbindingfollowups

Conversation

@marcschier

@marcschier marcschier commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

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:

  1. Shared Kestrel host + WSS reverse-connect + Kestrel-TCP listener — multiple listeners colocated on one IHost, full reverse-connect on both server and client sides for WSS, and an opt-in Kestrel-hosted opc.tcp:// path (now folded into the Https package, no separate NuGet).
  2. OPC UA Web API binding (Part 6 §G.3 "OpenAPI Mapping") — spec-compliant ASP.NET Core Minimal-API endpoints for all 28 services, four pluggable authentication modes (Anonymous / Bearer JWT / Basic / Mutual TLS), the WSS opcua+openapi sub-protocol (profile/2339), a symmetric C# client wired into ManagedSession, discovery emission, and NativeAOT compatibility. Naming follows the OPC Foundation UA-WebApi-StarterKit and the ecosystem repos.
  3. Edge security hardening across all new HTTPS / WSS / JSON / Web API surfaces — bounded request bodies, fail-closed auth, validated bearer tokens in WSS sub-protocols, server-cert validation in the Web API client, default auth scheme + policy forwarder, upstream-identity plumbing through 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

  • Shared Kestrel host — multiple HttpsTransportListener instances on the same (host, port) share one IHost (covers opc.https:// + opc.wss:// colocated on a single port).
  • WSS reverse-connect, server sideHttpsTransportListener.CreateReverseConnection drives an outbound ClientWebSocket through the same TcpServerChannel.BeginReverseConnect(... IUaSCByteTransport ...) overload the TCP path uses.
  • WSS reverse-connect, client sideHttpsTransportListener honours settings.ReverseConnectListener, dispatches inbound WSS upgrades to TcpReverseConnectChannel, and fires ConnectionWaiting via WssChannelListener.TransferListenerChannelAsync. ReverseConnectManager.AddEndpoint gains an additive (Uri, ApplicationConfiguration?) overload so TLS state is available at bind time.
  • Kestrel-TCP listenerKestrelTcpTransportListener + AddKestrelOpcTcpTransport() host opc.tcp:// on Kestrel via ConnectionHandler, so consumers can run all OPC UA transports under a single IHost. Default raw-socket TcpTransportListener stays in Opc.Ua.Core for trimmed/AOT deployments. The Kestrel-TCP path ships inside OPCFoundation.NetStandard.Opc.Ua.Bindings.Https (gated #if NET8_0_OR_GREATER) with no new dependencies — there is no separate Opc.Ua.Bindings.Kestrel.Tcp NuGet.
  • Kestrel-TCP parity — reverse-connect listener mode, TLS cert hot-update via ITransportListenerCertificateRotation, and inheritance from TcpServiceHost so discovery emits proper EndpointDescriptions.
  • TcpReverseConnectChannel one-shot receive — overrides StartReceiveLoop to read a single ReverseHello chunk and exit cleanly. Required because WebSocket.ReceiveAsync(CancellationToken) aborts the WebSocket on cancellation, which previously broke the reverse-connect handoff for WSS.
  • IUaSCByteTransport extension contract — documented the public extension surface in Docs/Transports.md § "Implementing a custom byte transport" and shipped a public reference implementation InProcessTransport in Opc.Ua.Core for unit tests and co-located client/server pairs.

Key technical note

WebSocket.ReceiveAsync(CancellationToken) aborts the underlying WebSocket on cancellation. The standard DetachTransportAsync reverse-connect handoff cancels the receive-loop CTS — which previously tore down the WSS connection before the new owner could take over. Resolved by overriding StartReceiveLoop in TcpReverseConnectChannel to do a one-shot read of the ReverseHello chunk and exit cleanly. The loop completes naturally before any cancellation runs, so the WebSocket stays alive for the new owner. UaSCBinaryChannel.StartReceiveLoop is now protected internal virtual and a new protected 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

  • All 28 services from the spec (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.
  • Both application/json; encoding=compact (mandatory per Part 6 §5.4.9) and application/json; encoding=verbose supported via media-type parameter negotiation on Content-Type / Accept.
  • NativeAOT-compatible. MapWebApiEndpoints() binds each route to a static RequestDelegate thunk that calls a generic instantiation of WebApiEndpointDispatcher.HandleAsync<TRequest, TResponse>. No MVC reflection, no [UnconditionalSuppressMessage] attributes. <IsAotCompatible>true</IsAotCompatible> on net10 with zero IL2xxx / IL3xxx warnings.
  • NodeManagement (§5.8) and Query (§5.10) are deliberately omitted per the spec doc.

2.2 Four pluggable authentication modes

  • Anonymous — no token validation.
  • Bearer (OIDC/JWT)Microsoft.AspNetCore.Authentication.JwtBearer, with built-in JwtClaimSessionlessIdentityProvider that projects sub / scope / roles onto IUserIdentity.
  • Basic — in-package BasicAuthenticationHandler.
  • Mutual TLSMicrosoft.AspNetCore.Authentication.Certificate.

All four wired through OpcUaWebApiAuthenticationBuilderExtensions. A policy scheme (WebApiAuthSchemes.Default) becomes the default authenticate / challenge / forbid scheme so UseAuthentication() actually populates HttpContext.User even when more than one auth handler is registered. The startup contributor mounts UseAuthentication() + UseAuthorization() between UseRouting() and UseEndpoints() whenever an auth scheme is registered; routes carry RequireAuthorization() metadata except /findservers and /getendpoints which keep AllowAnonymous per the spec.

2.3 WSS opcua+openapi sub-protocol (profile/2339)

  • Server-side HttpsTransportListener.AcceptWebSocketOpenApiAsync accepts the opcua+openapi and opcua+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.
  • Client-side WebApiWssTransportChannel round-trips the standard {TypeId, Body} OPC UA JSON envelope and refuses to send a bearer token over plain ws://.
  • Fluent shortcut ManagedSessionBuilder.UseWssOpenApiEndpoint(url, encoding).

2.4 Symmetric C# client (Libraries/Opc.Ua.Client/WebApi)

  • IWebApiClient / WebApiClient — low-level HTTP transport over HttpClient, multi-TFM (net48, netstandard2.0, net8 / 9 / 10).
  • WebApiTransportChannel / WebApiWssTransportChannelFactory adapt the binding into the standard ITransportChannel contract — Session, ManagedSession, V2 SubscriptionManager all dispatch unchanged.
  • Server-certificate validation in the HTTPS Web API channel delegates to the OPC UA CertificateValidator (TrustedPeers store / application-URI rule / rejected list) instead of only running the default .NET TLS chain check.
  • Fluent shortcut ManagedSessionBuilder.UseWebApiEndpoint(url, encoding) (and UseWssOpenApiEndpoint(url, encoding) for WSS).
  • services.AddWebApiTransport() DI extension; auth registered alongside via AddWebApiAnonymousAuth() / AddWebApiBearerAuth() / AddWebApiBasicAuth() / AddWebApiMutualTlsAuth() / UseJwtClaimIdentityProvider().

2.5 Discovery emission

  • HttpsServiceHost-based listeners emit a discovery-only OpenAPI twin (SM=None) per SM=None HTTPS endpoint with TransportProfileUri = Profiles.HttpsOpenApiTransport (profile/2338). The WSS factories emit the corresponding Profiles.WssOpenApiTransport twin (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 UA MaxMessageSize quota to short-circuit oversized / chunked bodies before allocation.
  • WebApiMediaTypeapplication/json; encoding=compact|verbose parsing/formatting.
  • WebApiServiceRoutes — single source of truth for the 28 route → request/response-type mappings.
  • New profile URI constants Profiles.HttpsOpenApiTransport (profile/2338) + Profiles.WssOpenApiTransport (profile/2339), with Profiles.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+uacp paths; the Web API surface adds parallel coverage for application/json REST and opcua+openapi WSS:

  • TLS server-cert validationWebApiWssTransportChannel and WebApiTransportChannel delegate to the registered OPC UA CertificateValidator; fall back to the default .NET TLS chain check (with hostname / chain verification) when no validator is configured. Mirrors HttpsTransportChannel.
  • WSS bearer-token validation — the server validates the access token from the 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 plain ws://.
  • Default auth scheme + policy forwarder — every AddWebApi*Auth() extension installs WebApiAuthSchemes.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 so UseAuthentication() populates HttpContext.User.
  • Upstream identity → SecureChannelContext — added SecureChannelContext.UpstreamIdentity so the principal resolved by ISessionlessIdentityProvider reaches the OPC UA service pipeline (no longer dropped between dispatcher and SessionManager).
  • RequireAuthorization() + UseAuthorization() on every Web API route (group-level) with AllowAnonymous on the spec-mandated discovery routes (/findservers, /getendpoints).
  • Removed empty-password fallbacksDefaultSessionlessIdentityProvider and JwtClaimSessionlessIdentityProvider no longer synthesize UserNameIdentityToken(name, ""); the upstream principal flows via UpstreamIdentity instead.
  • Body-size + zero-progress hardeningWebApiBodyCodec.DecodeBodyAsync enforces MaxMessageSize during the read (BadRequestTooLarge before allocation), and both server / client WSS receive loops reject zero-byte continuation frames (mirrors the WebSocketByteTransport guard for opcua+uacp).
  • Base-branch transport hardening (already on httpwssbindingfollowups): JsonRequestMapper.ReadAllBoundedAsync bounds the JSON/binary body read, the HTTPS / WSS handlers fail-closed on no matching SM=None JSON endpoint (BadSecurityPolicyRejected, restrict-to-discovery), HttpsTransportChannel falls back to the default TLS chain check when no validator is configured (MITM guard), and WebSocketByteTransport rejects 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 constants
    • DependencyInjection/AddWebApi* builder extensions
    • Https/HttpsTransportListener, WssTransportChannel, WebSocketByteTransport, JsonRequestMapper, …
    • Tcp/ — Kestrel-hosted opc.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

Project / area Count Notes
Opc.Ua.Core.Tests (filter ~WebApi) 107 codec, media type, route table, profile constants — multi-TFM
Opc.Ua.Bindings.WebApi.Tests 153 endpoint dispatcher, auth handlers, contributor hook, real Kestrel listener, channel adapter, WSS channel, TLS regression, mTLS source-pin, body-size, zero-progress, identity plumbing, default-scheme installation, route metadata, bearer validation, sessionless providers
Opc.Ua.Sessions.Tests integration 29 reference-server smoke tests for HTTPS WebApi, WSS OpenAPI, WSS reverse-connect, Kestrel-TCP, shared host
Opc.Ua.Aot.Tests (WebApiAotTests, on AOT-published binary) 7 Read / Browse / CreateSession / CreateSubscription / Publish + anonymous + Basic-auth round-trips on the natively-compiled executable

Total: ~300 new passing tests on net10, zero IL2xxx / IL3xxx warnings on dotnet publish -c Release of Opc.Ua.Aot.Tests. Full multi-TFM dotnet build UA.slnx is clean (0 errors). Full multi-TFM dotnet test UA.slnx covered by CI.

Notable bugs found and fixed during integration

  1. WebApiServer used per-request HttpContext.TraceIdentifier for SecureChannelId — server rejected ActivateSession with BadSecureChannelIdInvalid. Now uses a stable ListenerId (matches HTTPS-JSON binding).
  2. WebApiHttpsStartupContributor left SecureChannelContext.EndpointDescription null, causing NRE in SessionManager.CreateSession. Now picks an SM=None HTTPS endpoint and injects it via UpdateDefaultEndpoint.
  3. Client decoder used default JsonDecoderOptions — NodeIds with unknown namespace URIs decoded as NodeId.Null. Now passes JsonDecoderOptions { UpdateNamespaceTable = true }.
  4. WebApiWssTransportChannel.ReconnectAsync used {ChannelType} (structured-log placeholder) as a String.Format pattern, throwing FormatException instead of the intended ServiceResultException. Switched to {0}.
  5. ClientTestFramework.GetEndpointsAsync used a DiscoveryClient.CreateAsync overload that did not flow ApplicationConfiguration, so the discovery channel ran without a CertificateValidator and the MITM-guard fall-through (added by the transport hardening) rejected self-signed test certs. Now uses the ApplicationConfiguration overload so CertificateManager reaches the channel.
  6. Kestrel mTLS adapter — ClientCertificateMode.AllowCertificate (not RequireCertificate) so cert-less clients (discovery, binary UASC HTTPS) reach the dispatcher; REST / WebApi clients enforce mTLS at the authorization layer via AddWebApiMutualTlsAuth() + 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.
  • Foundation in Opc.Ua.Core — all repo TFMs.
  • Opc.Ua.Client/WebApi (client + transport channels) — all repo TFMs (client only needs HttpClient). On legacy TFMs the WebApiWssTransportChannel server-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).
  • NativeAOT on net10.

Documentation

  • Docs/Transports.md (subsumes the former CustomTransport.md) — all transports (HTTPS, WSS, Kestrel-TCP) + IUaSCByteTransport extension 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 via ManagedSessionBuilder, 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.
  • Forward-looking plans tracked under 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)

  • Own-listener hosting mode for the Web API binding — explicitly descoped.
  • Source generation of extended complex types from the spec yaml that aren't exercised by the 28 services — separate PR.
  • Load-testing harness — Tier-3 follow-up.
  • MatchEndpoints enhancement to honour the user's TransportProfileUri selection on UpdateFromServer — would let ManagedSession.HandleConnectAsync drop its updateBeforeConnect = false special-case for OpenAPI / WSS-OpenAPI endpoints.
  • Rate limiting / per-IP quotas on the long-poll /publish path.
  • Refactoring ISessionlessIdentityProvider to a richer first-class UpstreamPrincipal model.

Related Issues

Checklist

marcschier and others added 30 commits June 12, 2026 21:00
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).
@marcschier marcschier marked this pull request as ready for review June 17, 2026 10:43
@marcschier marcschier changed the title Adding Ws/Wss incl. reverse-connect, shared Kestrel host and Kestrel based TCP alternative Adding Ws/Wss, incl. reverse-connect, OpenAPI transports, shared Kestrel host and Kestrel based TCP alternative Jun 17, 2026
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)
@marcschier marcschier changed the title Adding Ws/Wss, incl. reverse-connect, OpenAPI transports, shared Kestrel host and Kestrel based TCP alternative Adding HTTPS/WSS transports + Web API binding (Part 6 §G.3 OpenAPI Mapping), shared Kestrel host, Kestrel-TCP Jun 18, 2026
Comment thread Libraries/Opc.Ua.Client/WebApi/WebApiTransportChannel.cs Fixed
Comment thread Libraries/Opc.Ua.Client/WebApi/WebApiWssTransportChannel.cs Fixed
Comment thread plans/25-wss-openapi-subprotocols.md Outdated
Comment thread Directory.Packages.props Outdated
- 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.
Comment thread Docs/Profiles.md
Comment thread Docs/Transports.md Outdated
Comment thread Libraries/Opc.Ua.Client/WebApi/IWebApiClient.cs
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement HTTPS JSON UA transport Implement WebSocket Secure (WSS) UA transport

3 participants