7502575204
When the server cert's CN/SAN doesn't match the URL host (typical for installer-generated AVEVA Historian certs that claim DNS=localhost even when reached over a LAN IP), WCF rejects the channel with "Identity check failed for outgoing message". Set ServerDnsIdentity to whatever the cert claims (often "localhost") to satisfy the check. The endpoint address for the cert binding is constructed with a DnsEndpointIdentity when the option is non-null. Default null. Pairs with AllowUntrustedServerCertificate so a Linux client can talk to a self-signed dev Historian over RemoteTcpCertificate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
213 lines
8.9 KiB
C#
213 lines
8.9 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, options.ServerDnsIdentity),
|
|
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, string? dnsIdentity = null)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(host);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(serviceName);
|
|
|
|
Uri uri = new($"{Scheme}://{host}:{port}/{serviceName}");
|
|
return string.IsNullOrWhiteSpace(dnsIdentity)
|
|
? new EndpointAddress(uri)
|
|
: new EndpointAddress(uri, new DnsEndpointIdentity(dnsIdentity));
|
|
}
|
|
|
|
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.")
|
|
};
|
|
}
|
|
}
|