|
|
|
@@ -1,258 +0,0 @@
|
|
|
|
|
using System;
|
|
|
|
|
using System.IO.Pipes;
|
|
|
|
|
using System.Security.Principal;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using MessagePack;
|
|
|
|
|
using Serilog;
|
|
|
|
|
|
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Accepts one client connection at a time on a named pipe with the strict ACL from
|
|
|
|
|
/// <see cref="PipeAcl"/>. Verifies the peer SID and the per-process shared secret before
|
|
|
|
|
/// any frame is dispatched. Mirrors Driver.Galaxy.Host's PipeServer; the sidecar carries
|
|
|
|
|
/// its own copy so the deletion of Galaxy.Host in PR 7.2 leaves the sidecar self-contained.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class PipeServer : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly string _pipeName;
|
|
|
|
|
private readonly SecurityIdentifier _allowedSid;
|
|
|
|
|
private readonly string _sharedSecret;
|
|
|
|
|
private readonly ILogger _logger;
|
|
|
|
|
private readonly CancellationTokenSource _cts = new();
|
|
|
|
|
private readonly CallerVerifier _verifier;
|
|
|
|
|
private NamedPipeServerStream? _current;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Pluggable caller-verification seam. Default implementation calls
|
|
|
|
|
/// <see cref="VerifyCaller"/>; tests can substitute one that ignores the pipe ACL
|
|
|
|
|
/// to exercise the rejection paths.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="pipe">The named pipe server stream to verify.</param>
|
|
|
|
|
/// <param name="allowedSid">The allowed security identifier.</param>
|
|
|
|
|
/// <param name="reason">The rejection reason if verification fails.</param>
|
|
|
|
|
internal delegate bool CallerVerifier(NamedPipeServerStream pipe, SecurityIdentifier allowedSid, out string reason);
|
|
|
|
|
|
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="PipeServer"/> class.</summary>
|
|
|
|
|
/// <param name="pipeName">The name of the named pipe.</param>
|
|
|
|
|
/// <param name="allowedSid">The security identifier allowed to connect.</param>
|
|
|
|
|
/// <param name="sharedSecret">The shared secret for client authentication.</param>
|
|
|
|
|
/// <param name="logger">The logger for diagnostic messages.</param>
|
|
|
|
|
public PipeServer(string pipeName, SecurityIdentifier allowedSid, string sharedSecret, ILogger logger)
|
|
|
|
|
: this(pipeName, allowedSid, sharedSecret, logger, DefaultVerifier) { }
|
|
|
|
|
|
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="PipeServer"/> class with a custom verifier.</summary>
|
|
|
|
|
/// <param name="pipeName">The name of the named pipe.</param>
|
|
|
|
|
/// <param name="allowedSid">The security identifier allowed to connect.</param>
|
|
|
|
|
/// <param name="sharedSecret">The shared secret for client authentication.</param>
|
|
|
|
|
/// <param name="logger">The logger for diagnostic messages.</param>
|
|
|
|
|
/// <param name="verifier">The caller verification delegate.</param>
|
|
|
|
|
internal PipeServer(
|
|
|
|
|
string pipeName, SecurityIdentifier allowedSid, string sharedSecret, ILogger logger,
|
|
|
|
|
CallerVerifier verifier)
|
|
|
|
|
{
|
|
|
|
|
_pipeName = pipeName ?? throw new ArgumentNullException(nameof(pipeName));
|
|
|
|
|
_allowedSid = allowedSid ?? throw new ArgumentNullException(nameof(allowedSid));
|
|
|
|
|
_sharedSecret = sharedSecret ?? throw new ArgumentNullException(nameof(sharedSecret));
|
|
|
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
|
|
|
_verifier = verifier ?? throw new ArgumentNullException(nameof(verifier));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool DefaultVerifier(NamedPipeServerStream pipe, SecurityIdentifier allowedSid, out string reason)
|
|
|
|
|
=> VerifyCaller(pipe, allowedSid, out reason);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Accepts one connection, performs Hello handshake, then dispatches frames to
|
|
|
|
|
/// <paramref name="handler"/> until EOF or cancel. Returns when the client disconnects.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="handler">The frame handler to process frames.</param>
|
|
|
|
|
/// <param name="ct">Cancellation token for the operation.</param>
|
|
|
|
|
public async Task RunOneConnectionAsync(IFrameHandler handler, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
using var linked = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, ct);
|
|
|
|
|
var acl = PipeAcl.Create(_allowedSid);
|
|
|
|
|
|
|
|
|
|
_current = new NamedPipeServerStream(
|
|
|
|
|
_pipeName,
|
|
|
|
|
PipeDirection.InOut,
|
|
|
|
|
maxNumberOfServerInstances: 1,
|
|
|
|
|
PipeTransmissionMode.Byte,
|
|
|
|
|
PipeOptions.Asynchronous,
|
|
|
|
|
inBufferSize: 64 * 1024,
|
|
|
|
|
outBufferSize: 64 * 1024,
|
|
|
|
|
pipeSecurity: acl);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await _current.WaitForConnectionAsync(linked.Token).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
using var reader = new FrameReader(_current, leaveOpen: true);
|
|
|
|
|
using var writer = new FrameWriter(_current, leaveOpen: true);
|
|
|
|
|
|
|
|
|
|
// First frame must be Hello with the correct shared secret. Reading it before
|
|
|
|
|
// the caller-SID impersonation check satisfies Windows' ERROR_CANNOT_IMPERSONATE
|
|
|
|
|
// rule — ImpersonateNamedPipeClient fails until at least one frame has been read.
|
|
|
|
|
var first = await reader.ReadFrameAsync(linked.Token).ConfigureAwait(false);
|
|
|
|
|
if (first is null || first.Value.Kind != MessageKind.Hello)
|
|
|
|
|
{
|
|
|
|
|
_logger.Warning("Sidecar IPC first frame was not Hello; dropping");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!_verifier(_current, _allowedSid, out var reason))
|
|
|
|
|
{
|
|
|
|
|
// Driver.Historian.Wonderware-007: send a rejecting HelloAck so the client
|
|
|
|
|
// learns why instead of having to wait for its own read timeout. The reason
|
|
|
|
|
// tag "caller-sid-mismatch" is symmetric with the shared-secret-mismatch and
|
|
|
|
|
// major-version-mismatch acks the two other rejection paths emit below.
|
|
|
|
|
await writer.WriteAsync(MessageKind.HelloAck,
|
|
|
|
|
new HelloAck { Accepted = false, RejectReason = $"caller-sid-mismatch: {reason}" },
|
|
|
|
|
linked.Token).ConfigureAwait(false);
|
|
|
|
|
_logger.Warning("Sidecar IPC caller rejected: {Reason}", reason);
|
|
|
|
|
_current.Disconnect();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hello = MessagePackSerializer.Deserialize<Hello>(first.Value.Body);
|
|
|
|
|
if (!string.Equals(hello.SharedSecret, _sharedSecret, StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
await writer.WriteAsync(MessageKind.HelloAck,
|
|
|
|
|
new HelloAck { Accepted = false, RejectReason = "shared-secret-mismatch" },
|
|
|
|
|
linked.Token).ConfigureAwait(false);
|
|
|
|
|
_logger.Warning("Sidecar IPC Hello rejected: shared-secret-mismatch");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hello.ProtocolMajor != Hello.CurrentMajor)
|
|
|
|
|
{
|
|
|
|
|
await writer.WriteAsync(MessageKind.HelloAck,
|
|
|
|
|
new HelloAck { Accepted = false, RejectReason = $"major-version-mismatch-peer={hello.ProtocolMajor}-server={Hello.CurrentMajor}" },
|
|
|
|
|
linked.Token).ConfigureAwait(false);
|
|
|
|
|
_logger.Warning("Sidecar IPC Hello rejected: major mismatch peer={Peer} server={Server}",
|
|
|
|
|
hello.ProtocolMajor, Hello.CurrentMajor);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await writer.WriteAsync(MessageKind.HelloAck,
|
|
|
|
|
new HelloAck { Accepted = true, HostName = Environment.MachineName },
|
|
|
|
|
linked.Token).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
while (!linked.Token.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
var frame = await reader.ReadFrameAsync(linked.Token).ConfigureAwait(false);
|
|
|
|
|
if (frame is null) break;
|
|
|
|
|
|
|
|
|
|
await handler.HandleAsync(frame.Value.Kind, frame.Value.Body, writer, linked.Token).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_current.Dispose();
|
|
|
|
|
_current = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Backoff sequence for consecutive RunOneConnection failures: 250 ms → 500 ms →
|
|
|
|
|
// 1 000 ms → 2 000 ms → 4 000 ms → capped at 8 000 ms thereafter.
|
|
|
|
|
private static readonly TimeSpan[] BackoffSteps =
|
|
|
|
|
{
|
|
|
|
|
TimeSpan.FromMilliseconds(250),
|
|
|
|
|
TimeSpan.FromMilliseconds(500),
|
|
|
|
|
TimeSpan.FromSeconds(1),
|
|
|
|
|
TimeSpan.FromSeconds(2),
|
|
|
|
|
TimeSpan.FromSeconds(4),
|
|
|
|
|
TimeSpan.FromSeconds(8),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Maximum consecutive failures before the server gives up and lets the process exit
|
|
|
|
|
/// so the supervisor (NSSM / SCM) can restart the sidecar cleanly.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private const int MaxConsecutiveFailures = 20;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Runs the server continuously, handling one connection at a time. When a connection
|
|
|
|
|
/// ends (clean or error), waits with exponential backoff before accepting the next.
|
|
|
|
|
/// If <see cref="MaxConsecutiveFailures"/> consecutive failures occur the method
|
|
|
|
|
/// throws so the supervisor can restart the sidecar.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="handler">The frame handler to process frames.</param>
|
|
|
|
|
/// <param name="ct">Cancellation token for the operation.</param>
|
|
|
|
|
public async Task RunAsync(IFrameHandler handler, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
var consecutiveFailures = 0;
|
|
|
|
|
|
|
|
|
|
while (!ct.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await RunOneConnectionAsync(handler, ct).ConfigureAwait(false);
|
|
|
|
|
consecutiveFailures = 0; // a clean connection (even a short-lived one) resets the counter
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException) { break; }
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
consecutiveFailures++;
|
|
|
|
|
|
|
|
|
|
if (consecutiveFailures >= MaxConsecutiveFailures)
|
|
|
|
|
{
|
|
|
|
|
_logger.Fatal(ex,
|
|
|
|
|
"Sidecar IPC connection loop failed {Count} consecutive times — giving up so supervisor can restart",
|
|
|
|
|
consecutiveFailures);
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var delay = BackoffSteps[Math.Min(consecutiveFailures - 1, BackoffSteps.Length - 1)];
|
|
|
|
|
_logger.Error(ex,
|
|
|
|
|
"Sidecar IPC connection loop error (consecutive failure {Count}/{Max}) — retrying in {Delay}",
|
|
|
|
|
consecutiveFailures, MaxConsecutiveFailures, delay);
|
|
|
|
|
|
|
|
|
|
try { await Task.Delay(delay, ct).ConfigureAwait(false); }
|
|
|
|
|
catch (OperationCanceledException) { break; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool VerifyCaller(NamedPipeServerStream pipe, SecurityIdentifier allowedSid, out string reason)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
pipe.RunAsClient(() =>
|
|
|
|
|
{
|
|
|
|
|
using var wi = WindowsIdentity.GetCurrent();
|
|
|
|
|
if (wi.User is null)
|
|
|
|
|
throw new InvalidOperationException("GetCurrent().User is null — cannot verify caller");
|
|
|
|
|
if (wi.User != allowedSid)
|
|
|
|
|
throw new UnauthorizedAccessException(
|
|
|
|
|
$"caller SID {wi.User.Value} does not match allowed {allowedSid.Value}");
|
|
|
|
|
});
|
|
|
|
|
reason = string.Empty;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex) { reason = ex.Message; return false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Disposes the pipe server and cancels any pending operations.</summary>
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
_cts.Cancel();
|
|
|
|
|
_current?.Dispose();
|
|
|
|
|
_cts.Dispose();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Strategy for handling each post-Hello frame the pipe server reads. Implementations
|
|
|
|
|
/// deserialize the body per the <see cref="MessageKind"/>, dispatch to the historian, and
|
|
|
|
|
/// write the corresponding reply through the supplied <see cref="FrameWriter"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public interface IFrameHandler
|
|
|
|
|
{
|
|
|
|
|
/// <summary>Handles a frame from the pipe server.</summary>
|
|
|
|
|
/// <param name="kind">The type of message being handled.</param>
|
|
|
|
|
/// <param name="body">The serialized message body.</param>
|
|
|
|
|
/// <param name="writer">The frame writer to send responses.</param>
|
|
|
|
|
/// <param name="ct">Cancellation token for the operation.</param>
|
|
|
|
|
Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct);
|
|
|
|
|
}
|