fix(client): Client.Shared EndpointSelector uses the shared OpcUaEndpointUrlRewriter (#812)
Client.Shared now project-references Driver.OpcUaClient.Contracts (Core.Abstractions only, no NuGet/SDK; pinned by ContractsIsDependencyLightTests) and delegates the host/port rewrite to OpcUaEndpointUrlRewriter, deleting its own copy. The two copies differed; reconciled toward the client so its pinned behaviour is unchanged (EndpointSelectorRewriteParityTests, written green against the old copy): - no-op rewrite (same host case-insensitively + same port) now returns the advertised string verbatim in the shared rewriter too (it re-serialised: added a trailing slash and case-folded a case-only host difference); - a real rewrite serialises via AbsoluteUri, not Uri.ToString(), which un-escaped the advertised path (/x%20y became /x y) in the driver + browser; - unusable URLs: the rewriter still falls back to the configured URL (driver/browser); the client keeps its loud failure as call-site validation (null -> InvalidOperation, unparseable -> UriFormatException).
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters;
|
||||
|
||||
@@ -36,8 +37,7 @@ internal static class EndpointSelector
|
||||
/// <summary>
|
||||
/// Picks the best endpoint from the discovery response that matches the requested
|
||||
/// security mode, preferring <c>Basic256Sha256</c>, and rewrites the endpoint URL
|
||||
/// host to match the user-supplied URL when the discovery response advertises a
|
||||
/// different hostname.
|
||||
/// host and port to the user-supplied ones via the shared <see cref="OpcUaEndpointUrlRewriter"/>.
|
||||
/// </summary>
|
||||
/// <param name="allEndpoints">Endpoints returned by the discovery query, in any order.</param>
|
||||
/// <param name="endpointUrl">The endpoint URL the operator supplied; supplies the hostname rewrite target.</param>
|
||||
@@ -85,22 +85,23 @@ internal static class EndpointSelector
|
||||
$"No endpoint found with security mode '{requestedMode}'. Available endpoints: {available}");
|
||||
}
|
||||
|
||||
// Rewrite the endpoint URL host AND port to the user-supplied ones (as the SDK's
|
||||
// CoreClientUtils.SelectEndpoint does). A server behind a port mapping or internal DNS
|
||||
// advertises its own host:port (docker-dev containers all say :4840); following that would
|
||||
// open the session on a different server and fail the secure-channel certificate check.
|
||||
// Only when the scheme matches: a different scheme (https vs opc.tcp) is a different transport.
|
||||
var serverUri = new Uri(best.EndpointUrl ?? throw new InvalidOperationException("Endpoint has no URL."));
|
||||
var requestedUri = new Uri(endpointUrl);
|
||||
// A URL typed without a port (opc.tcp has no registered default, so Port is -1) keeps the advertised port.
|
||||
var port = requestedUri.Port > 0 ? requestedUri.Port : serverUri.Port;
|
||||
if (string.Equals(serverUri.Scheme, requestedUri.Scheme, StringComparison.OrdinalIgnoreCase)
|
||||
&& (serverUri.Host != requestedUri.Host || serverUri.Port != port))
|
||||
// Rewrite the endpoint URL host AND port to the user-supplied ones, keeping the advertised path. A server
|
||||
// behind a port mapping or internal DNS advertises its own host:port (docker-dev containers all say :4840);
|
||||
// following that would open the session on a different server and fail the secure-channel certificate check.
|
||||
// The rule is the ONE shared with the OpcUaClient driver and the AdminUI browse picker (#812, #806, #676):
|
||||
// same scheme only, configured port only when it names one, the advertised string verbatim on a no-op.
|
||||
//
|
||||
// Validation stays here, not in the rewriter: the shared rewriter falls back to the configured URL on an
|
||||
// unusable advertised one (right for a deployed driver), while this interactive tool reports it — a null URL
|
||||
// throws InvalidOperationException and an unparseable one (either side) UriFormatException, as before #812.
|
||||
var advertisedUrl = best.EndpointUrl ?? throw new InvalidOperationException("Endpoint has no URL.");
|
||||
_ = new Uri(advertisedUrl);
|
||||
_ = new Uri(endpointUrl);
|
||||
var rewritten = OpcUaEndpointUrlRewriter.Rewrite(advertisedUrl, endpointUrl);
|
||||
if (!string.Equals(rewritten, advertisedUrl, StringComparison.Ordinal))
|
||||
{
|
||||
var builder = new UriBuilder(best.EndpointUrl) { Host = requestedUri.Host, Port = port };
|
||||
best.EndpointUrl = builder.ToString();
|
||||
Logger.Debug("Rewrote endpoint from {ServerHost}:{ServerPort} to {RequestedHost}:{RequestedPort}",
|
||||
serverUri.Host, serverUri.Port, requestedUri.Host, port);
|
||||
best.EndpointUrl = rewritten;
|
||||
Logger.Debug("Rewrote endpoint from {AdvertisedUrl} to {RewrittenUrl}", advertisedUrl, rewritten);
|
||||
}
|
||||
|
||||
return best;
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
<PackageReference Include="Serilog"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- #812: the endpoint-URL rewrite rule shared with the OpcUaClient driver + AdminUI browse picker
|
||||
(OpcUaEndpointUrlRewriter). Contracts is dependency-light by contract (Core.Abstractions only, no
|
||||
NuGet packages, no OPC UA SDK; pinned by ContractsIsDependencyLightTests), so this adds two small
|
||||
first-party assemblies and nothing else to the CLI/UI. -->
|
||||
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts\ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Client.Shared.Tests"/>
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user