diff --git a/src/WinSW.Core/Native/ConsoleApis.cs b/src/WinSW.Core/Native/ConsoleApis.cs index b3b0bb7..c687e0f 100644 --- a/src/WinSW.Core/Native/ConsoleApis.cs +++ b/src/WinSW.Core/Native/ConsoleApis.cs @@ -1,6 +1,8 @@ #pragma warning disable SA1310 // Field names should not contain underscore +using System; using System.Runtime.InteropServices; +using static WinSW.Native.Libraries; namespace WinSW.Native { @@ -10,24 +12,41 @@ namespace WinSW.Native internal const uint CP_UTF8 = 65001; - [DllImport(Libraries.Kernel32, SetLastError = true)] + internal const uint ENABLE_PROCESSED_INPUT = 0x0001; + internal const uint ENABLE_LINE_INPUT = 0x0002; + internal const uint ENABLE_ECHO_INPUT = 0x0004; + internal const uint ENABLE_MOUSE_INPUT = 0x0010; + + [DllImport(Kernel32, SetLastError = true)] internal static extern bool AllocConsole(); - [DllImport(Libraries.Kernel32, SetLastError = true)] + [DllImport(Kernel32, SetLastError = true)] internal static extern bool AttachConsole(int processId); - [DllImport(Libraries.Kernel32)] + [DllImport(Kernel32)] internal static extern bool FreeConsole(); - [DllImport(Libraries.Kernel32)] + [DllImport(Kernel32)] internal static extern bool GenerateConsoleCtrlEvent(CtrlEvents ctrlEvent, uint processGroupId); - [DllImport(Libraries.Kernel32)] + [DllImport(Kernel32, SetLastError = true)] + internal static extern bool GetConsoleMode(IntPtr consoleHandle, out uint mode); + + [DllImport(Kernel32, SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool ReadConsoleW(IntPtr consoleInput, out char buffer, int numberOfCharsToRead, out int numberOfCharsRead, IntPtr inputControl); + + [DllImport(Kernel32)] internal static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandlerRoutine? handlerRoutine, bool add); - [DllImport(Libraries.Kernel32)] + [DllImport(Kernel32, SetLastError = true)] + internal static extern bool SetConsoleMode(IntPtr consoleHandle, uint mode); + + [DllImport(Kernel32)] internal static extern bool SetConsoleOutputCP(uint codePageID); + [DllImport(Kernel32, SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool WriteConsoleW(IntPtr consoleOutput, string buffer, int numberOfCharsToWrite, out int numberOfCharsWritten, IntPtr reserved); + internal delegate bool ConsoleCtrlHandlerRoutine(CtrlEvents ctrlType); internal enum CtrlEvents : uint diff --git a/src/WinSW.Core/Native/ConsoleEx.cs b/src/WinSW.Core/Native/ConsoleEx.cs new file mode 100644 index 0000000..b67f3f3 --- /dev/null +++ b/src/WinSW.Core/Native/ConsoleEx.cs @@ -0,0 +1,117 @@ +using System; +using System.IO; +using System.Text; +using static WinSW.Native.ConsoleApis; + +namespace WinSW.Native +{ + internal static class ConsoleEx + { + internal static Handle OpenConsoleInput() + { + return FileApis.CreateFileW( + "CONIN$", + FileApis.GenericAccess.Read | FileApis.GenericAccess.Write, + FileShare.Read | FileShare.Write, + IntPtr.Zero, + FileMode.Open, + 0, + IntPtr.Zero); + } + + internal static Handle OpenConsoleOutput() + { + return FileApis.CreateFileW( + "CONOUT$", + FileApis.GenericAccess.Write, + FileShare.Write, + IntPtr.Zero, + FileMode.Open, + 0, + IntPtr.Zero); + } + + /// + internal static string ReadPassword() + { + using var consoleInput = OpenConsoleInput(); + using var consoleOutput = OpenConsoleOutput(); + + if (!GetConsoleMode(consoleInput, out uint mode)) + { + Throw.Command.Win32Exception("Failed to get console mode."); + } + + uint newMode = mode; + newMode &= ~ENABLE_PROCESSED_INPUT; + newMode &= ~ENABLE_LINE_INPUT; + newMode &= ~ENABLE_ECHO_INPUT; + newMode &= ~ENABLE_MOUSE_INPUT; + + if (newMode != mode) + { + if (!SetConsoleMode(consoleInput, newMode)) + { + Throw.Command.Win32Exception("Failed to set console mode."); + } + } + + try + { + var buffer = new StringBuilder(); + + while (true) + { + if (!ReadConsoleW(consoleInput, out char key, 1, out _, IntPtr.Zero)) + { + Throw.Command.Win32Exception("Failed to read console."); + } + + if (key == (char)3) + { + // Ctrl+C + Write(consoleOutput, Environment.NewLine); + Throw.Command.Win32Exception(Errors.ERROR_CANCELLED); + } + else if (key == '\r') + { + break; + } + else if (key == '\b') + { + if (buffer.Length > 0) + { + buffer.Remove(buffer.Length - 1, 1); + Write(consoleOutput, "\b \b"); + } + } + else + { + buffer.Append(key); + Write(consoleOutput, "*"); + } + } + + return buffer.ToString(); + } + finally + { + if (newMode != mode) + { + if (!SetConsoleMode(consoleInput, mode)) + { + Throw.Command.Win32Exception("Failed to set console mode."); + } + } + } + } + + internal static void Write(Handle consoleOutput, string value) + { + if (!WriteConsoleW(consoleOutput, value, value.Length, out _, IntPtr.Zero)) + { + Throw.Command.Win32Exception("Failed to write console."); + } + } + } +} diff --git a/src/WinSW.Core/Native/Credentials.cs b/src/WinSW.Core/Native/Credentials.cs index 2537634..fd2fe94 100644 --- a/src/WinSW.Core/Native/Credentials.cs +++ b/src/WinSW.Core/Native/Credentials.cs @@ -7,7 +7,24 @@ namespace WinSW.Native { internal static class Credentials { - internal static void PropmtForCredentialsDialog(ref string? userName, ref string? password, string caption, string message) + internal static void PromptForCredentialsConsole(ref string? userName, ref string? password) + { + using var consoleOutput = ConsoleEx.OpenConsoleOutput(); + + if (userName is null) + { + ConsoleEx.Write(consoleOutput, "Username: "); + userName = Console.ReadLine()!; + } + + if (password is null && !Security.IsSpecialAccount(userName)) + { + ConsoleEx.Write(consoleOutput, "Password: "); + password = ConsoleEx.ReadPassword(); + } + } + + internal static void PromptForCredentialsDialog(ref string? userName, ref string? password, string caption, string message) { userName ??= string.Empty; password ??= string.Empty; diff --git a/src/WinSW.Core/Native/FileApis.cs b/src/WinSW.Core/Native/FileApis.cs new file mode 100644 index 0000000..4a97d2d --- /dev/null +++ b/src/WinSW.Core/Native/FileApis.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace WinSW.Native +{ + internal static class FileApis + { + [DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern Handle CreateFileW( + string fileName, + GenericAccess desiredAccess, + FileShare shareMode, + IntPtr securityAttributes, + FileMode creationDisposition, + uint flagsAndAttributes, + IntPtr templateFile); + + internal enum GenericAccess : uint + { + Read = 0x80000000, + Write = 0x40000000, + Execute = 0x20000000, + All = 0x10000000, + } + } +} diff --git a/src/WinSW.Core/Native/Handle.cs b/src/WinSW.Core/Native/Handle.cs new file mode 100644 index 0000000..1a8ce60 --- /dev/null +++ b/src/WinSW.Core/Native/Handle.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; +using static WinSW.Native.HandleApis; + +namespace WinSW.Native +{ + [StructLayout(LayoutKind.Sequential)] + internal readonly ref struct Handle + { + private readonly IntPtr handle; + + internal Handle(IntPtr handle) => this.handle = handle; + + public void Dispose() => CloseHandle(this.handle); + + public static implicit operator IntPtr(Handle value) => value.handle; + } +} diff --git a/src/WinSW.Core/Native/ProcessApis.cs b/src/WinSW.Core/Native/ProcessApis.cs index 15b5af1..e54fba5 100644 --- a/src/WinSW.Core/Native/ProcessApis.cs +++ b/src/WinSW.Core/Native/ProcessApis.cs @@ -38,7 +38,7 @@ namespace WinSW.Native internal static extern bool OpenProcessToken( IntPtr processHandle, TokenAccessLevels desiredAccess, - out IntPtr tokenHandle); + out Handle tokenHandle); internal enum PROCESSINFOCLASS { diff --git a/src/WinSW.Core/Native/Security.cs b/src/WinSW.Core/Native/Security.cs index 503ae45..f7ca5c8 100644 --- a/src/WinSW.Core/Native/Security.cs +++ b/src/WinSW.Core/Native/Security.cs @@ -90,5 +90,15 @@ namespace WinSW.Native _ = LsaClose(policyHandle); } } + + internal static bool IsSpecialAccount(string accountName) => accountName switch + { + @"LocalSystem" => true, + @".\LocalSystem" => true, + @"NT AUTHORITY\LocalService" => true, + @"NT AUTHORITY\NetworkService" => true, + string name when name == $@"{Environment.MachineName}\LocalSystem" => true, + _ => false + }; } } diff --git a/src/WinSW.Tasks/Trim.cs b/src/WinSW.Tasks/Trim.cs index b8a1b90..1895189 100644 --- a/src/WinSW.Tasks/Trim.cs +++ b/src/WinSW.Tasks/Trim.cs @@ -15,7 +15,7 @@ namespace WinSW.Tasks public override bool Execute() { - using var module = ModuleDefinition.ReadModule(this.Path, new ReaderParameters { ReadWrite = true }); + using var module = ModuleDefinition.ReadModule(this.Path, new() { ReadWrite = true, ReadSymbols = true }); this.WalkType(module.EntryPoint.DeclaringType); @@ -46,7 +46,7 @@ namespace WinSW.Tasks types.RemoveAt(i); } - module.Write(); + module.Write(new WriterParameters { WriteSymbols = true }); return true; } diff --git a/src/WinSW/Program.cs b/src/WinSW/Program.cs index 5cc7b06..5086f26 100644 --- a/src/WinSW/Program.cs +++ b/src/WinSW/Program.cs @@ -13,7 +13,6 @@ using System.Reflection; using System.Security.AccessControl; using System.Security.Principal; using System.ServiceProcess; -using System.Text; using System.Threading; using log4net; using log4net.Appender; @@ -440,12 +439,12 @@ namespace WinSW username = config.ServiceAccountUserName ?? username; password = config.ServiceAccountPassword ?? password; - if (username is null || password is null && !IsSpecialAccount(username)) + if (username is null || password is null && !Security.IsSpecialAccount(username)) { switch (config.ServiceAccountPrompt) { case "dialog": - Credentials.PropmtForCredentialsDialog( + Credentials.PromptForCredentialsDialog( ref username, ref password, "Windows Service Wrapper", @@ -453,13 +452,13 @@ namespace WinSW break; case "console": - PromptForCredentialsConsole(); + Credentials.PromptForCredentialsConsole(ref username, ref password); break; } } } - if (username != null && !IsSpecialAccount(username)) + if (username != null && !Security.IsSpecialAccount(username)) { Security.AddServiceLogonRight(ref username); } @@ -509,34 +508,7 @@ namespace WinSW EventLog.CreateEventSource(eventLogSource, "Application"); } - Log.Info($"Service '{config.Format()}' was installed successfully."); - - void PromptForCredentialsConsole() - { - if (username is null) - { - Console.Write("Username: "); - username = Console.ReadLine()!; - } - - if (password is null && !IsSpecialAccount(username)) - { - Console.Write("Password: "); - password = ReadPassword(); - } - - Console.WriteLine(); - } - - static bool IsSpecialAccount(string accountName) => accountName switch - { - @"LocalSystem" => true, - @".\LocalSystem" => true, - @"NT AUTHORITY\LocalService" => true, - @"NT AUTHORITY\NetworkService" => true, - string name when name == $@"{Environment.MachineName}\LocalSystem" => true, - _ => false - }; + Log.Info($"Service '{config.Format()}' was installed successfully."); } void Uninstall(string? pathToConfig, bool noElevate) @@ -1228,7 +1200,7 @@ namespace WinSW Throw.Command.Win32Exception("Failed to open process token."); } - try + using (token) { unsafe { @@ -1245,33 +1217,6 @@ namespace WinSW return elevation.TokenIsElevated != 0; } } - finally - { - _ = HandleApis.CloseHandle(token); - } - } - - private static string ReadPassword() - { - var buf = new StringBuilder(); - while (true) - { - var key = Console.ReadKey(true); - if (key.Key == ConsoleKey.Enter) - { - return buf.ToString(); - } - else if (key.Key == ConsoleKey.Backspace) - { - _ = buf.Remove(buf.Length - 1, 1); - Console.Write("\b \b"); - } - else - { - Console.Write('*'); - _ = buf.Append(key.KeyChar); - } - } } } } diff --git a/src/WinSW/WinSW.csproj b/src/WinSW/WinSW.csproj index 422b070..c895380 100644 --- a/src/WinSW/WinSW.csproj +++ b/src/WinSW/WinSW.csproj @@ -39,9 +39,7 @@ - - false - +