6888b8c55a
Executes docs/plans/tcp-connection-validation.md. Full read-only SDK
surface now works against a remote AVEVA Historian over Net.TCP with
Windows transport authentication. 124/124 tests pass; the +10 new live
integration tests in RemoteTcpIntegrationTests.cs are gated by
HISTORIAN_REMOTE_TCP_HOST + HISTORIAN_REMOTE_TCP_TAG.
Two SDK bugs found while executing the plan:
1. Historian2020ProtocolDialect.ReadRawAsync / ReadAggregateAsync /
ReadAtTimeAsync / ReadEventsAsync had explicit
`if (_options.Transport != HistorianTransport.LocalPipe) return Missing<T>`
guards. These were a guardrail from before the orchestrators handled
TCP; the orchestrators have always used CreateBindingPair(options)
which dispatches on transport correctly. Gates removed.
2. HistorianWcfStatusClient and HistorianWcfEventOrchestrator hardcoded
HistorianWcfBindingFactory.CreatePipeEndpointAddress for the auxiliary
services (Stat, Trx, Retr). Worked for LocalPipe; for TCP it produced
an EndpointAddress with scheme net.pipe attached to a TCP binding
(channel factory rejected the URI). Worse, when only the endpoint was
transport-aware, the binding still requested a Windows-transport-
security upgrade that the Stat endpoint over TCP doesn't support
(auxiliaries don't repeat the auth — the Hist session is already
authenticated). Added two helpers:
- HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(options, name)
-> net.pipe for LocalPipe, net.tcp for remote
- HistorianWcfBindingFactory.CreateAuxiliaryBinding(options)
-> NamedPipe for LocalPipe, plain MdasNetTcpBinding for remote
Both call sites updated.
Live verification against the remote (probed previously in prior
sessions; reachability re-confirmed today):
- ProbeAsync over RemoteTcpIntegrated and RemoteTcpCertificate
- ReadRawAsync (8 samples returned for SysTimeSec)
- ReadAggregateAsync (TimeWeightedAverage, 1-min cycle, 10-min window)
- ReadAtTimeAsync (3 timestamps)
- BrowseTagNamesAsync (finds the test tag)
- GetTagMetadataAsync (full metadata populated)
- ReadEventsAsync (chain runs without throwing)
- GetConnectionStatusAsync (ConnectedToServer=true)
- GetSystemParameterAsync (HistorianVersion="20,0,000,000")
The default 'NT SERVICE\aahClientAccessPoint' SPN turned out to work
for the remote too — discovery workstream A (SPN-finding) was not
needed in practice.
README and the TCP plan doc updated to reflect the executed status.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
206 lines
8.3 KiB
C#
206 lines
8.3 KiB
C#
using System.Net.Security;
|
|
using System.Runtime.Versioning;
|
|
using System.ServiceModel;
|
|
using System.ServiceModel.Channels;
|
|
|
|
namespace AVEVA.Historian.Client.Wcf;
|
|
|
|
internal static class HistorianWcfBindingFactory
|
|
{
|
|
public const string Scheme = "net.tcp";
|
|
public const int DefaultPort = 32568;
|
|
|
|
public static Binding CreateMdasNetTcpBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024)
|
|
{
|
|
var encoding = new MdasMessageEncodingBindingElement(
|
|
new BinaryMessageEncodingBindingElement
|
|
{
|
|
MessageVersion = MessageVersion.Soap12WSAddressing10
|
|
});
|
|
|
|
var transport = new TcpTransportBindingElement
|
|
{
|
|
MaxReceivedMessageSize = maxReceivedMessageSize,
|
|
TransferMode = TransferMode.Buffered
|
|
};
|
|
|
|
return new CustomBinding(encoding, transport)
|
|
{
|
|
CloseTimeout = timeout,
|
|
OpenTimeout = timeout,
|
|
ReceiveTimeout = timeout,
|
|
SendTimeout = timeout
|
|
};
|
|
}
|
|
|
|
public static Binding CreateMdasNetTcpWindowsBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024)
|
|
{
|
|
NetTcpBinding nativeShape = new(SecurityMode.Transport)
|
|
{
|
|
MaxReceivedMessageSize = maxReceivedMessageSize,
|
|
MaxBufferSize = checked((int)Math.Min(maxReceivedMessageSize, int.MaxValue))
|
|
};
|
|
nativeShape.ReaderQuotas.MaxArrayLength = nativeShape.MaxBufferSize;
|
|
nativeShape.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
|
|
nativeShape.Security.Transport.ProtectionLevel = ProtectionLevel.None;
|
|
|
|
BindingElementCollection elements = nativeShape.CreateBindingElements();
|
|
for (int i = 0; i < elements.Count; i++)
|
|
{
|
|
if (elements[i] is MessageEncodingBindingElement encoding)
|
|
{
|
|
elements[i] = new MdasMessageEncodingBindingElement(encoding);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return new CustomBinding(elements)
|
|
{
|
|
CloseTimeout = timeout,
|
|
OpenTimeout = timeout,
|
|
ReceiveTimeout = timeout,
|
|
SendTimeout = timeout
|
|
};
|
|
}
|
|
|
|
public static Binding CreateMdasNetTcpCertificateBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024)
|
|
{
|
|
NetTcpBinding nativeShape = new(SecurityMode.Transport)
|
|
{
|
|
MaxReceivedMessageSize = maxReceivedMessageSize,
|
|
MaxBufferSize = checked((int)Math.Min(maxReceivedMessageSize, int.MaxValue))
|
|
};
|
|
nativeShape.ReaderQuotas.MaxArrayLength = nativeShape.MaxBufferSize;
|
|
nativeShape.Security.Transport.ClientCredentialType = TcpClientCredentialType.None;
|
|
|
|
BindingElementCollection elements = nativeShape.CreateBindingElements();
|
|
for (int i = 0; i < elements.Count; i++)
|
|
{
|
|
if (elements[i] is MessageEncodingBindingElement encoding)
|
|
{
|
|
elements[i] = new MdasMessageEncodingBindingElement(encoding);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return new CustomBinding(elements)
|
|
{
|
|
CloseTimeout = timeout,
|
|
OpenTimeout = timeout,
|
|
ReceiveTimeout = timeout,
|
|
SendTimeout = timeout
|
|
};
|
|
}
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
public static Binding CreateMdasNetNamedPipeBinding(TimeSpan timeout, int maxBufferSize = 64 * 1024 * 1024)
|
|
{
|
|
NetNamedPipeBinding nativeShape = new()
|
|
{
|
|
MaxBufferSize = maxBufferSize,
|
|
MaxReceivedMessageSize = maxBufferSize
|
|
};
|
|
nativeShape.Security.Mode = NetNamedPipeSecurityMode.None;
|
|
nativeShape.ReaderQuotas.MaxArrayLength = maxBufferSize;
|
|
|
|
BindingElementCollection elements = nativeShape.CreateBindingElements();
|
|
for (int i = 0; i < elements.Count; i++)
|
|
{
|
|
if (elements[i] is MessageEncodingBindingElement encoding)
|
|
{
|
|
elements[i] = new MdasMessageEncodingBindingElement(encoding);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return new CustomBinding(elements)
|
|
{
|
|
CloseTimeout = timeout,
|
|
OpenTimeout = timeout,
|
|
ReceiveTimeout = timeout,
|
|
SendTimeout = timeout
|
|
};
|
|
}
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
public static (Binding HistoryBinding, EndpointAddress HistoryEndpoint, Binding RetrievalBinding, EndpointAddress RetrievalEndpoint) CreateBindingPair(
|
|
HistorianClientOptions options)
|
|
{
|
|
TimeSpan timeout = options.RequestTimeout;
|
|
|
|
return options.Transport switch
|
|
{
|
|
HistorianTransport.LocalPipe => (
|
|
CreateMdasNetNamedPipeBinding(timeout),
|
|
CreatePipeEndpointAddress(options.Host, HistorianWcfServiceNames.History),
|
|
CreateMdasNetNamedPipeBinding(timeout),
|
|
CreatePipeEndpointAddress(options.Host, HistorianWcfServiceNames.Retrieval)),
|
|
HistorianTransport.RemoteTcpIntegrated => (
|
|
CreateMdasNetTcpWindowsBinding(timeout),
|
|
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryIntegrated),
|
|
CreateMdasNetTcpBinding(timeout),
|
|
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)),
|
|
HistorianTransport.RemoteTcpCertificate => (
|
|
CreateMdasNetTcpCertificateBinding(timeout),
|
|
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryCertificate),
|
|
CreateMdasNetTcpBinding(timeout),
|
|
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)),
|
|
_ => throw new NotSupportedException($"Transport {options.Transport} is not supported.")
|
|
};
|
|
}
|
|
|
|
public static EndpointAddress CreateEndpointAddress(string host, int port, string serviceName)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(host);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(serviceName);
|
|
|
|
return new EndpointAddress($"{Scheme}://{host}:{port}/{serviceName}");
|
|
}
|
|
|
|
public static EndpointAddress CreatePipeEndpointAddress(string host, string serviceName)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(host);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(serviceName);
|
|
|
|
return new EndpointAddress($"net.pipe://{host}/{serviceName}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the appropriate endpoint address for an auxiliary service (Stat, Trx, etc.)
|
|
/// based on the transport — net.pipe for LocalPipe, net.tcp for the remote variants.
|
|
/// Use this rather than <see cref="CreatePipeEndpointAddress"/> directly when the calling
|
|
/// code may run under any transport.
|
|
/// </summary>
|
|
public static EndpointAddress CreateAuxiliaryEndpointAddress(HistorianClientOptions options, string serviceName)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(serviceName);
|
|
|
|
return options.Transport == HistorianTransport.LocalPipe
|
|
? CreatePipeEndpointAddress(options.Host, serviceName)
|
|
: CreateEndpointAddress(options.Host, options.Port, serviceName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the appropriate binding for an auxiliary service (Stat, Trx, etc.) given the
|
|
/// transport. For LocalPipe, same NamedPipe binding as History. For remote TCP variants,
|
|
/// plain <see cref="CreateMdasNetTcpBinding"/> — auxiliaries don't repeat the Windows-
|
|
/// transport-security upgrade that the History service negotiates; the established session
|
|
/// authenticates the client already.
|
|
/// </summary>
|
|
[SupportedOSPlatform("windows")]
|
|
public static Binding CreateAuxiliaryBinding(HistorianClientOptions options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
|
|
TimeSpan timeout = options.RequestTimeout;
|
|
return options.Transport switch
|
|
{
|
|
HistorianTransport.LocalPipe => CreateMdasNetNamedPipeBinding(timeout),
|
|
HistorianTransport.RemoteTcpIntegrated => CreateMdasNetTcpBinding(timeout),
|
|
HistorianTransport.RemoteTcpCertificate => CreateMdasNetTcpBinding(timeout),
|
|
_ => throw new NotSupportedException($"Transport {options.Transport} is not supported.")
|
|
};
|
|
}
|
|
}
|