/* Copyright (c) Cloud Software Group, Inc. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ using System; using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; namespace XenCenterLib { public class NamedPipes { [Flags] public enum FileAccess : uint { GenericRead = 0x80000000, GenericWrite = 0x40000000, GenericExecute = 0x20000000, GenericAll = 0x10000000 } [Flags] public enum FileShare : uint { None = 0x00000000, /// /// Enables subsequent open operations on an object to request read access. /// Otherwise, other processes cannot open the object if they request read access. /// If this flag is not specified, but the object has been opened for read access, the function fails. /// Read = 0x00000001, /// /// Enables subsequent open operations on an object to request write access. /// Otherwise, other processes cannot open the object if they request write access. /// If this flag is not specified, but the object has been opened for write access, the function fails. /// Write = 0x00000002, /// /// Enables subsequent open operations on an object to request delete access. /// Otherwise, other processes cannot open the object if they request delete access. /// If this flag is not specified, but the object has been opened for delete access, the function fails. /// Delete = 0x00000004 } public enum FileMode : uint { /// /// Creates a new file. The function fails if a specified file exists. /// CreateNew = 1, /// /// Creates a new file, always. /// If a file exists, the function overwrites the file, clears the existing attributes, combines the specified file attributes, /// and flags with FILE_ATTRIBUTE_ARCHIVE, but does not set the security descriptor that the SECURITY_ATTRIBUTES structure specifies. /// CreateAlways = 2, /// /// Opens a file. The function fails if the file does not exist. /// OpenExisting = 3, /// /// Opens a file, always. /// If a file does not exist, the function creates a file as if dwCreationDisposition is CREATE_NEW. /// OpenAlways = 4, /// /// Opens a file and truncates it so that its size is 0 (zero) bytes. The function fails if the file does not exist. /// The calling process must open the file with the GENERIC_WRITE access right. /// TruncateExisting = 5 } [Flags] public enum FileAttributes : uint { Readonly = 0x00000001, Hidden = 0x00000002, System = 0x00000004, Directory = 0x00000010, Archive = 0x00000020, Device = 0x00000040, Normal = 0x00000080, Temporary = 0x00000100, SparseFile = 0x00000200, ReparsePoint = 0x00000400, Compressed = 0x00000800, Offline = 0x00001000, NotContentIndexed = 0x00002000, Encrypted = 0x00004000, Write_Through = 0x80000000, Overlapped = 0x40000000, NoBuffering = 0x20000000, RandomAccess = 0x10000000, SequentialScan = 0x08000000, DeleteOnClose = 0x04000000, BackupSemantics = 0x02000000, PosixSemantics = 0x01000000, OpenReparsePoint = 0x00200000, OpenNoRecall = 0x00100000, FirstPipeInstance = 0x00080000 } /// /// Duplex pipe access. /// private const uint PIPE_ACCESS_DUPLEX = 0x00000003; /// /// Pipe blocking mode. /// private const uint PIPE_WAIT = 0x00000000; /// /// Pipe read mode of type Message. /// private const uint PIPE_READMODE_MESSAGE = 0x00000002; /// /// Message pipe type. /// private const uint PIPE_TYPE_MESSAGE = 0x00000004; /// /// Unlimited server pipe instances. /// private const uint PIPE_UNLIMITED_INSTANCES = 255; /// /// Waits indefinitely when connecting to a pipe. /// private const uint NMPWAIT_WAIT_FOREVER = 0xffffffff; /// /// Invalid operating system handle. /// private const int INVALID_HANDLE_VALUE = -1; /// /// More data is available. /// private const int ERROR_MORE_DATA = 234; /// /// There is a process on other end of the pipe. /// private const int ERROR_PIPE_CONNECTED = 535; [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr CreateNamedPipe(String pipeName, uint openMode, uint pipeMode, uint maxInstances, uint outputBufferSize, uint inputBufferSize, uint timeoutInterval, IntPtr pipeSecurityDescriptor); /// /// Enables a named pipe server process to wait for a client /// process to connect to an instance of a named pipe. /// /// Handle to the server end of a named pipe instance. /// Pointer to an /// Overlapped object. /// If the function succeeds, the return value is nonzero. [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool ConnectNamedPipe(IntPtr namedPipeHandle, ref NativeOverlapped overlapped); /// /// Disconnects the server end of a named pipe instance from a client process. /// /// Handle to an instance of a named pipe. /// If the function succeeds, the return value is nonzero. [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool DisconnectNamedPipe(IntPtr hHandle); /// /// Reads data from a file, starting at the position indicated by the file pointer. /// /// Handle to the file to be read. /// Pointer to the buffer that receives the data read from the file. /// Number of bytes to be read from the file. /// Pointer to the variable that receives the number of bytes read. /// Pointer to an /// Overlapped object. /// The ReadFile function returns when one of the following /// conditions is met: a write operation completes on the write end of /// the pipe, the number of bytes requested has been read, or an error occurs. [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool ReadFile(IntPtr fileHandle, [Out] byte[] dataBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, [In] ref NativeOverlapped overlapped); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool WriteFile(IntPtr fileHandle, [Out] byte[] dataBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, [In] ref NativeOverlapped overlapped); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool PeekNamedPipe(IntPtr handle, [Out] byte[] buffer, uint nBufferSize, out uint bytesRead, out uint bytesAvail, out uint BytesLeftThisMessage); [DllImport("kernel32.dll")] private static extern bool CallNamedPipe(string lpNamedPipeName, byte[] lpInBuffer, uint nInBufferSize, [Out] byte[] lpOutBuffer, uint nOutBufferSize, out uint lpBytesRead, uint nTimeOut); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CloseHandle(IntPtr hObject); private const string STOP_LISTENING_MSG = "stop-listening-on-pipe"; private const uint PIPE_CALL_TIMEOUT = 30 * 1000; //30sec public class Pipe { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); private readonly IntPtr Handle; private Thread pipeThread; private readonly string pipePath; private volatile bool run; public event Action Read; public Pipe(string path) { pipePath = path; Handle = CreateNamedPipe(pipePath, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 512, 512, NMPWAIT_WAIT_FOREVER, IntPtr.Zero); if (Handle.ToInt32() == INVALID_HANDLE_VALUE) log.Error($"Creating named pipe {path} failed.", new Win32Exception()); else log.Info($"Successfully created named pipe '{path}'."); } public static bool ExistsPipe(string pipePath) { log.Debug($"Checking {pipePath} exists"); // CA-382850: We iterate manually in order to catch ArgumentExceptions // when listing files. Pipes can be created with invalid characters in // their names. This throws exception when those files are accessed. // Other processes might create these pipes and inadvertently prevent // XenCenter from starting var e = Directory.EnumerateFiles(@"\\.\pipe\"); using (var enumerator = e.GetEnumerator()) { while (true) { try { if (!enumerator.MoveNext()) { break; } if (enumerator.Current != null && enumerator.Current.Contains(pipePath)) { return true; } } catch (ArgumentException) { // ignore, the pipe's name contains invalid characters } } } return false; } /// /// Send a message to the pipe with the given path. May block for up to PIPE_CALL_TIMEOUT milliseconds. /// public static void SendMessageToPipe(string pipePath, string message) { byte[] msg = Encoding.Unicode.GetBytes(message); byte[] rcv = Array.Empty(); var success = CallNamedPipe(pipePath, msg, (uint)msg.Length, rcv, (uint)rcv.Length, out _, PIPE_CALL_TIMEOUT); if (success) log.Debug($"Message successfully forwarded to {pipePath})"); else log.Error($"Failed to forward message to {pipePath} with native error code {new Win32Exception().NativeErrorCode}"); } /// /// Starts reading from this pipe on a new background thread. This call returns immediately. /// Whenever data is read from the pipe, the Read event is fired. /// public void BeginRead() { if (pipeThread == null) { run = true; pipeThread = new Thread(BackgroundPipeThread) { Name = "Named pipe thread", IsBackground = true }; pipeThread.Start(); } } /// /// Stops reading from this pipe. May block for up to PIPE_CALL_TIMEOUT milliseconds. /// public void EndRead() { SendMessageToPipe(pipePath, STOP_LISTENING_MSG); } private void BackgroundPipeThread() { while (run) { try { NativeOverlapped overlapped = new NativeOverlapped(); if (!ConnectNamedPipe(Handle, ref overlapped)) { Win32Exception exn = new Win32Exception(); if (exn.NativeErrorCode == ERROR_PIPE_CONNECTED) log.Debug("A remote process has already connected to the named pipe since we called CreateNamedPipe."); else throw new Win32Exception("ConnectNamedPipe failed", exn); } ProcessPipeMessage(); if (!DisconnectNamedPipe(Handle)) throw new Win32Exception("DisconnectNamedPipe failed", new Win32Exception()); } catch (Exception exn) { log.Error("Error in named pipe thread.", exn); // Sanity: pause after errors to prevent massive log spamming/CPU drain // in the event of an infinite error loop Thread.Sleep(1000); } } // Close pipe handle to destroy it if (CloseHandle(Handle)) log.Debug("Closed NamedPipe handle"); else log.Warn("CloseHandle() in NamedPipes failed", new Win32Exception()); log.Debug("NamedPipe thread exited"); } private void ProcessPipeMessage() { NativeOverlapped overlapped = new NativeOverlapped(); byte[] readMessage; using (MemoryStream ms = new MemoryStream()) { while (true) { // Loop as follows: // Peek at the pipe to see how much data is available // Read the data and append it to the buffer // If we get ERROR_MORE_DATA, repeat // First peek into the pipe to see how much data is waiting. byte[] peekBuf = Array.Empty(); if (!PeekNamedPipe(Handle, peekBuf, (uint)peekBuf.Length, out var bytesRead, out var bytesAvailable, out var bytesLeft)) { throw new Win32Exception( string.Format("PeekNamedPipe failed. bytesRead={0} bytesAvailable={1} bytesLeft={2}", bytesRead, bytesAvailable, bytesLeft), new Win32Exception()); } // Sanity check: throw away message if it is > 1 MB if (ms.Length + bytesLeft > 1024 * 1024) { throw new Exception("Indecently large message sent into named pipe: rejecting."); } // Now allocate a buffer of the correct size and read in the message. byte[] readBuf = new byte[bytesAvailable]; if (!ReadFile(Handle, readBuf, (uint)readBuf.Length, out bytesRead, ref overlapped)) { Win32Exception exn = new Win32Exception(); if (exn.NativeErrorCode == ERROR_MORE_DATA) { // The peek may have looked into the pipe before the write operation // had completed, and so reported a message size before the whole // message was sent. In this case we need to go round the loop again // with a larger buffer. ms.Write(readBuf, 0, (int)bytesRead); continue; } throw new Win32Exception($"ReadFile failed. readBuf.Length={readBuf.Length} bytesRead={bytesRead}", exn); } ms.Write(readBuf, 0, (int)bytesRead); break; } readMessage = ms.ToArray(); } // Now perform a zero-byte write. This causes the CallNamedPipe call to // return successfully in the splash screen. byte[] toWrite = Array.Empty(); if (!WriteFile(Handle, toWrite, (uint)toWrite.Length, out var bytesWritten, ref overlapped)) { throw new Win32Exception($"WriteFile failed. toWrite.Length={toWrite.Length} bytesWritten={bytesWritten}", new Win32Exception()); } var message = Encoding.Unicode.GetString(readMessage); if (message == STOP_LISTENING_MSG) { log.Debug("NamedPipe thread was told to stop listening"); run = false; return; } // Now inform any listeners of the received data. Read?.Invoke(message); } } } }