Files
histsdk/src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs
T
Joseph Doherty b8280a1465 Drop SupportedOSPlatform gates; SDK now runs on Linux
The dialect / orchestrators were defensively gated on Windows because
HistorianSspiClient previously P/Invoked InitializeSecurityContextW. With
that replaced by NegotiateAuthentication (cross-platform), the gates are
unnecessary. Removed them from:

- Historian2020ProtocolDialect (4 read paths + 3 status helpers)
- HistorianClient.EnsureTagAsync / DeleteTagAsync
- HistorianWcf{Auth,Read,Event,Status,TagWrite}Orchestrator/Helper
- HistorianWcf{HistAddressing,MessageCapture}Behavior
- HistorianWcfBindingFactory (with #pragma on the Named-Pipe builder
  which still requires Windows at the BCL level)

Runtime constraint: LocalPipe and RemoteTcpIntegrated transports still
require Windows because NetNamedPipeBinding and the Windows transport
security binding are Windows-only at the BCL level. RemoteTcpCertificate
is now usable from Linux, and ProbeAsync is verified working from a
Debian client (10.100.0.35) against the Windows Historian (10.100.0.48).

171/171 tests still pass on Windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:27:57 -04:00

210 lines
8.7 KiB
C#

using System.Net.Security;
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
};
}
// NetNamedPipeBinding is Windows-only at the BCL level; calling this on Linux
// throws PlatformNotSupportedException at runtime. Cross-platform callers should
// choose Transport = RemoteTcpCertificate (or RemoteTcpIntegrated on Windows).
#pragma warning disable CA1416 // Documented Windows-only entry point
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
};
}
#pragma warning restore CA1416
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>
// NetNamedPipeBinding / WindowsStreamSecurityBindingElement are Windows-only at the
// BCL level; calling this on Linux throws PlatformNotSupportedException at runtime.
// Cross-platform callers should choose Transport = RemoteTcpCertificate.
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.")
};
}
}