392 lines
15 KiB
C#
392 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.IO.Pipes;
|
|
using System.Security.AccessControl;
|
|
using System.Security.Principal;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace Lskj.AgentBridge
|
|
{
|
|
public sealed class NamedPipeAgentBridgeServer : IDisposable
|
|
{
|
|
internal const int MaximumMessageBytes = DeadlineFrameCodec.MaximumMessageBytes;
|
|
internal const int MaximumConcurrentConnections = 4;
|
|
internal const int ListenerStartupTimeoutMilliseconds = 5000;
|
|
internal const int ListenerStopTimeoutMilliseconds = 2000;
|
|
internal static readonly TimeSpan RequestReadTimeout = TimeSpan.FromSeconds(15);
|
|
internal static readonly TimeSpan ResponseWriteTimeout = TimeSpan.FromSeconds(15);
|
|
private static readonly Regex SafePipeName = new Regex(
|
|
"^[A-Za-z0-9_.-]{1,128}$",
|
|
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
|
|
|
private readonly string _pipeName;
|
|
private readonly IAgentBridgeRuntime _runtime;
|
|
private readonly SecurityIdentifier _userSid;
|
|
private readonly object _syncRoot = new object();
|
|
private readonly Semaphore _connectionSlots = new Semaphore(
|
|
MaximumConcurrentConnections,
|
|
MaximumConcurrentConnections);
|
|
private readonly HashSet<NamedPipeServerStream> _connections =
|
|
new HashSet<NamedPipeServerStream>();
|
|
private readonly ManualResetEvent _listenerReady =
|
|
new ManualResetEvent(false);
|
|
private volatile bool _running;
|
|
private bool _disposed;
|
|
private Exception _listenerStartupError;
|
|
private Thread _acceptThread;
|
|
private NamedPipeServerStream _listener;
|
|
|
|
public NamedPipeAgentBridgeServer(string pipeName, IAgentBridgeRuntime runtime)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(pipeName) || !SafePipeName.IsMatch(pipeName))
|
|
throw new ArgumentException("管道名称只允许 1-128 位字母、数字、点、下划线和连字符。", "pipeName");
|
|
if (runtime == null) throw new ArgumentNullException("runtime");
|
|
_pipeName = pipeName;
|
|
_runtime = runtime;
|
|
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
|
|
{
|
|
SecurityIdentifier user = identity == null ? null : identity.User;
|
|
if (user == null) throw new InvalidOperationException("无法识别当前 Windows 用户。");
|
|
// Pin the interactive ERP identity at construction time. Listener/worker
|
|
// thread token changes must never widen a later pipe instance ACL.
|
|
_userSid = new SecurityIdentifier(user.Value);
|
|
}
|
|
}
|
|
|
|
public string PipeName
|
|
{
|
|
get { return _pipeName; }
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
if (_disposed)
|
|
throw new ObjectDisposedException("NamedPipeAgentBridgeServer");
|
|
if (_running) return;
|
|
if (_acceptThread != null && _acceptThread.IsAlive)
|
|
throw new InvalidOperationException("上一次命名管道监听线程尚未结束。");
|
|
// Validate the Windows identity/ACL and pipe creation synchronously.
|
|
// The ERP bootstrap writes discovery only after Start returns.
|
|
using (NamedPipeServerStream validation = CreateServer()) { }
|
|
_listenerReady.Reset();
|
|
_listenerStartupError = null;
|
|
_running = true;
|
|
_acceptThread = new Thread(AcceptLoop);
|
|
_acceptThread.Name = "Lskj.AgentBridge.Accept";
|
|
_acceptThread.IsBackground = true;
|
|
try
|
|
{
|
|
_acceptThread.Start();
|
|
}
|
|
catch
|
|
{
|
|
_running = false;
|
|
_acceptThread = null;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
if (!_listenerReady.WaitOne(ListenerStartupTimeoutMilliseconds, false))
|
|
{
|
|
Stop();
|
|
throw new TimeoutException("命名管道监听线程未在安全期限内就绪。");
|
|
}
|
|
|
|
Exception startupError;
|
|
bool running;
|
|
bool disposed;
|
|
lock (_syncRoot)
|
|
{
|
|
startupError = _listenerStartupError;
|
|
running = _running;
|
|
disposed = _disposed;
|
|
}
|
|
if (disposed) throw new ObjectDisposedException("NamedPipeAgentBridgeServer");
|
|
if (startupError != null)
|
|
{
|
|
Stop();
|
|
throw new InvalidOperationException("命名管道监听线程启动失败。", startupError);
|
|
}
|
|
if (!running)
|
|
throw new InvalidOperationException("命名管道监听线程在启动期间停止。");
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
StopCore();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (_syncRoot) _disposed = true;
|
|
StopCore();
|
|
}
|
|
|
|
private void StopCore()
|
|
{
|
|
List<NamedPipeServerStream> active;
|
|
Thread acceptThread;
|
|
lock (_syncRoot)
|
|
{
|
|
_running = false;
|
|
_listenerReady.Set();
|
|
if (_listener != null)
|
|
{
|
|
try { _listener.Dispose(); }
|
|
catch (ObjectDisposedException) { }
|
|
_listener = null;
|
|
}
|
|
active = new List<NamedPipeServerStream>(_connections);
|
|
_connections.Clear();
|
|
acceptThread = _acceptThread;
|
|
}
|
|
foreach (NamedPipeServerStream connection in active)
|
|
{
|
|
try { connection.Dispose(); }
|
|
catch (ObjectDisposedException) { }
|
|
}
|
|
bool stopped = acceptThread == null
|
|
|| ReferenceEquals(acceptThread, Thread.CurrentThread)
|
|
|| acceptThread.Join(ListenerStopTimeoutMilliseconds);
|
|
if (stopped)
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
if (ReferenceEquals(_acceptThread, acceptThread))
|
|
_acceptThread = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AcceptLoop()
|
|
{
|
|
bool startupSignalled = false;
|
|
while (_running)
|
|
{
|
|
NamedPipeServerStream server = null;
|
|
bool slotAcquired = false;
|
|
try
|
|
{
|
|
while (_running && !_connectionSlots.WaitOne(250, false)) { }
|
|
if (!_running) return;
|
|
slotAcquired = true;
|
|
server = CreateServer();
|
|
lock (_syncRoot)
|
|
{
|
|
if (!_running)
|
|
{
|
|
server.Dispose();
|
|
return;
|
|
}
|
|
_listener = server;
|
|
}
|
|
_listenerReady.Set();
|
|
startupSignalled = true;
|
|
|
|
server.WaitForConnection();
|
|
lock (_syncRoot)
|
|
{
|
|
if (ReferenceEquals(_listener, server)) _listener = null;
|
|
if (!_running)
|
|
{
|
|
server.Dispose();
|
|
return;
|
|
}
|
|
_connections.Add(server);
|
|
}
|
|
Thread worker = new Thread(HandleConnection);
|
|
worker.Name = "Lskj.AgentBridge.Client";
|
|
worker.IsBackground = true;
|
|
try
|
|
{
|
|
worker.Start(server);
|
|
}
|
|
catch
|
|
{
|
|
lock (_syncRoot) _connections.Remove(server);
|
|
throw;
|
|
}
|
|
server = null;
|
|
slotAcquired = false;
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
if (!HandleAcceptFailure(ex, startupSignalled)) return;
|
|
}
|
|
catch (ObjectDisposedException ex)
|
|
{
|
|
if (!HandleAcceptFailure(ex, startupSignalled)) return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!HandleAcceptFailure(ex, startupSignalled)) return;
|
|
}
|
|
finally
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
if (ReferenceEquals(_listener, server)) _listener = null;
|
|
}
|
|
if (server != null) server.Dispose();
|
|
if (slotAcquired) _connectionSlots.Release();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool HandleAcceptFailure(Exception error, bool startupSignalled)
|
|
{
|
|
if (!_running) return false;
|
|
if (!startupSignalled)
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
_listenerStartupError = error;
|
|
_running = false;
|
|
}
|
|
_listenerReady.Set();
|
|
return false;
|
|
}
|
|
Thread.Sleep(250);
|
|
return _running;
|
|
}
|
|
|
|
private void HandleConnection(object state)
|
|
{
|
|
NamedPipeServerStream pipe = (NamedPipeServerStream)state;
|
|
try
|
|
{
|
|
using (pipe)
|
|
{
|
|
BridgeRequest request = null;
|
|
BridgeResponse response;
|
|
try
|
|
{
|
|
byte[] payload = DeadlineFrameCodec.ReadFrame(
|
|
pipe,
|
|
RequestReadTimeout);
|
|
request = BridgeRequestParser.Parse(BridgeUtf8Codec.Decode(payload));
|
|
uint clientProcessId;
|
|
bool clientProcessKnown;
|
|
try
|
|
{
|
|
clientProcessKnown = GetNamedPipeClientProcessId(
|
|
pipe.SafePipeHandle,
|
|
out clientProcessId);
|
|
}
|
|
catch (DllNotFoundException)
|
|
{
|
|
clientProcessKnown = false;
|
|
clientProcessId = 0;
|
|
}
|
|
catch (EntryPointNotFoundException)
|
|
{
|
|
clientProcessKnown = false;
|
|
clientProcessId = 0;
|
|
}
|
|
request.TransportClientProcessId = clientProcessKnown
|
|
&& clientProcessId <= int.MaxValue
|
|
? (int)clientProcessId : 0;
|
|
response = _runtime.Handle(request);
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
return;
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
return;
|
|
}
|
|
catch (DecoderFallbackException)
|
|
{
|
|
response = BridgeResponse.Error(
|
|
request,
|
|
"invalid_utf8",
|
|
"请求必须使用有效 UTF-8 编码。");
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
response = BridgeResponse.Error(request, "invalid_json", "请求不是有效 JSON。");
|
|
}
|
|
catch (InvalidDataException ex)
|
|
{
|
|
response = BridgeResponse.Error(request, "invalid_frame", ex.Message);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
response = BridgeResponse.Error(request, "bridge_transport_error", "命令桥传输失败。");
|
|
}
|
|
|
|
try
|
|
{
|
|
byte[] body = BridgeUtf8Codec.Encode(
|
|
JsonConvert.SerializeObject(response));
|
|
DeadlineFrameCodec.WriteFrame(
|
|
pipe,
|
|
body,
|
|
ResponseWriteTimeout);
|
|
}
|
|
catch (IOException) { }
|
|
catch (ObjectDisposedException) { }
|
|
catch (TimeoutException) { }
|
|
catch (Exception) { }
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lock (_syncRoot) _connections.Remove(pipe);
|
|
_connectionSlots.Release();
|
|
}
|
|
}
|
|
|
|
private NamedPipeServerStream CreateServer()
|
|
{
|
|
PipeSecurity security = new PipeSecurity();
|
|
security.SetAccessRuleProtection(true, false);
|
|
security.SetOwner(_userSid);
|
|
// A matching domain user SID is not sufficient to prove that a client is
|
|
// local: Windows named pipes can also be reached over the network. Explicit
|
|
// deny ACEs prevent SMB/anonymous clients before any protocol bytes are read.
|
|
security.AddAccessRule(new PipeAccessRule(
|
|
new SecurityIdentifier(WellKnownSidType.NetworkSid, null),
|
|
PipeAccessRights.FullControl,
|
|
AccessControlType.Deny));
|
|
security.AddAccessRule(new PipeAccessRule(
|
|
new SecurityIdentifier(WellKnownSidType.AnonymousSid, null),
|
|
PipeAccessRights.FullControl,
|
|
AccessControlType.Deny));
|
|
security.AddAccessRule(new PipeAccessRule(
|
|
_userSid,
|
|
PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance,
|
|
AccessControlType.Allow));
|
|
security.AddAccessRule(new PipeAccessRule(
|
|
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
|
|
PipeAccessRights.FullControl,
|
|
AccessControlType.Allow));
|
|
|
|
return new NamedPipeServerStream(
|
|
_pipeName,
|
|
PipeDirection.InOut,
|
|
MaximumConcurrentConnections,
|
|
PipeTransmissionMode.Byte,
|
|
PipeOptions.Asynchronous,
|
|
MaximumMessageBytes,
|
|
MaximumMessageBytes,
|
|
security);
|
|
}
|
|
|
|
[System.Runtime.InteropServices.DllImport(
|
|
"kernel32.dll",
|
|
SetLastError = true)]
|
|
private static extern bool GetNamedPipeClientProcessId(
|
|
Microsoft.Win32.SafeHandles.SafePipeHandle pipe,
|
|
out uint clientProcessId);
|
|
|
|
}
|
|
}
|