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 _connections = new HashSet(); 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 active; Thread acceptThread; lock (_syncRoot) { _running = false; _listenerReady.Set(); if (_listener != null) { try { _listener.Dispose(); } catch (ObjectDisposedException) { } _listener = null; } active = new List(_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); } }