From 2c86e14dec72ed30a68e0d0e39c76aec9576b23e Mon Sep 17 00:00:00 2001 From: Next Turn <45985406+nxtn@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:13:04 +0800 Subject: [PATCH] Enhance console prompting (#759) --- src/WinSW.Core/Native/ConsoleApis.cs | 18 +++++ src/WinSW.Core/Native/ConsoleEx.cs | 117 +++++++++++++++++++++++++++ src/WinSW.Core/Native/Credentials.cs | 24 ++++++ src/WinSW.Core/Native/FileApis.cs | 27 +++++++ src/WinSW.Core/Native/Handle.cs | 18 +++++ src/WinSW.Core/Native/ProcessApis.cs | 2 +- src/WinSW.Core/Native/Security.cs | 10 +++ src/WinSW/Program.cs | 30 +------ 8 files changed, 216 insertions(+), 30 deletions(-) create mode 100644 src/WinSW.Core/Native/ConsoleEx.cs create mode 100644 src/WinSW.Core/Native/Credentials.cs create mode 100644 src/WinSW.Core/Native/FileApis.cs create mode 100644 src/WinSW.Core/Native/Handle.cs diff --git a/src/WinSW.Core/Native/ConsoleApis.cs b/src/WinSW.Core/Native/ConsoleApis.cs index ea4656c..8bd1585 100644 --- a/src/WinSW.Core/Native/ConsoleApis.cs +++ b/src/WinSW.Core/Native/ConsoleApis.cs @@ -1,5 +1,6 @@ #pragma warning disable SA1310 // Field names should not contain underscore +using System; using System.Runtime.InteropServices; namespace WinSW.Native @@ -10,6 +11,11 @@ namespace WinSW.Native internal const uint CP_UTF8 = 65001; + 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(Libraries.Kernel32, SetLastError = true)] internal static extern bool AttachConsole(int processId); @@ -19,12 +25,24 @@ namespace WinSW.Native [DllImport(Libraries.Kernel32)] internal static extern bool GenerateConsoleCtrlEvent(CtrlEvents ctrlEvent, uint processGroupId); + [DllImport(Libraries.Kernel32, SetLastError = true)] + internal static extern bool GetConsoleMode(IntPtr consoleHandle, out uint mode); + + [DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool ReadConsoleW(IntPtr consoleInput, out char buffer, int numberOfCharsToRead, out int numberOfCharsRead, IntPtr inputControl); + [DllImport(Libraries.Kernel32)] internal static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandlerRoutine? handlerRoutine, bool add); + [DllImport(Libraries.Kernel32, SetLastError = true)] + internal static extern bool SetConsoleMode(IntPtr consoleHandle, uint mode); + [DllImport(Libraries.Kernel32)] internal static extern bool SetConsoleOutputCP(uint codePageID); + [DllImport(Libraries.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..d375ff9 --- /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') + { + Write(consoleOutput, Environment.NewLine); + 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 new file mode 100644 index 0000000..5d0e49f --- /dev/null +++ b/src/WinSW.Core/Native/Credentials.cs @@ -0,0 +1,24 @@ +using System; + +namespace WinSW.Native +{ + internal static class Credentials + { + 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(); + } + } + } +} 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 3f30055..51c61cb 100644 --- a/src/WinSW.Core/Native/Security.cs +++ b/src/WinSW.Core/Native/Security.cs @@ -80,5 +80,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/Program.cs b/src/WinSW/Program.cs index 99b2767..7328db3 100644 --- a/src/WinSW/Program.cs +++ b/src/WinSW/Program.cs @@ -10,7 +10,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; @@ -261,11 +260,7 @@ namespace WinSW bool allowServiceLogonRight = false; if (args.Count > 1 && args[1] == "/p") { - Console.Write("Username: "); - username = Console.ReadLine(); - Console.Write("Password: "); - password = ReadPassword(); - Console.WriteLine(); + Credentials.PromptForCredentialsConsole(ref username, ref password); Console.Write("Set Account rights to allow log on as a service (y/n)?: "); var keypressed = Console.ReadKey(); Console.WriteLine(); @@ -760,29 +755,6 @@ namespace WinSW } } - 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); - } - } - } - private static void PrintHelp() { Console.WriteLine("A wrapper binary that can be used to host executables as Windows services");