Enhance console prompting

pull/760/head
NextTurn 2020-09-07 00:00:00 +08:00 committed by Next Turn
parent 6d5dcfa70c
commit 576cf6be55
10 changed files with 225 additions and 74 deletions

View File

@ -1,6 +1,8 @@
#pragma warning disable SA1310 // Field names should not contain underscore #pragma warning disable SA1310 // Field names should not contain underscore
using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using static WinSW.Native.Libraries;
namespace WinSW.Native namespace WinSW.Native
{ {
@ -10,24 +12,41 @@ namespace WinSW.Native
internal const uint CP_UTF8 = 65001; 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(); internal static extern bool AllocConsole();
[DllImport(Libraries.Kernel32, SetLastError = true)] [DllImport(Kernel32, SetLastError = true)]
internal static extern bool AttachConsole(int processId); internal static extern bool AttachConsole(int processId);
[DllImport(Libraries.Kernel32)] [DllImport(Kernel32)]
internal static extern bool FreeConsole(); internal static extern bool FreeConsole();
[DllImport(Libraries.Kernel32)] [DllImport(Kernel32)]
internal static extern bool GenerateConsoleCtrlEvent(CtrlEvents ctrlEvent, uint processGroupId); 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); 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); 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 delegate bool ConsoleCtrlHandlerRoutine(CtrlEvents ctrlType);
internal enum CtrlEvents : uint internal enum CtrlEvents : uint

View File

@ -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);
}
/// <exception cref="CommandException" />
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.");
}
}
}
}

View File

@ -7,7 +7,24 @@ namespace WinSW.Native
{ {
internal static class Credentials 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; userName ??= string.Empty;
password ??= string.Empty; password ??= string.Empty;

View File

@ -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,
}
}
}

View File

@ -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;
}
}

View File

@ -38,7 +38,7 @@ namespace WinSW.Native
internal static extern bool OpenProcessToken( internal static extern bool OpenProcessToken(
IntPtr processHandle, IntPtr processHandle,
TokenAccessLevels desiredAccess, TokenAccessLevels desiredAccess,
out IntPtr tokenHandle); out Handle tokenHandle);
internal enum PROCESSINFOCLASS internal enum PROCESSINFOCLASS
{ {

View File

@ -90,5 +90,15 @@ namespace WinSW.Native
_ = LsaClose(policyHandle); _ = 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
};
} }
} }

View File

@ -15,7 +15,7 @@ namespace WinSW.Tasks
public override bool Execute() 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); this.WalkType(module.EntryPoint.DeclaringType);
@ -46,7 +46,7 @@ namespace WinSW.Tasks
types.RemoveAt(i); types.RemoveAt(i);
} }
module.Write(); module.Write(new WriterParameters { WriteSymbols = true });
return true; return true;
} }

View File

@ -13,7 +13,6 @@ using System.Reflection;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal; using System.Security.Principal;
using System.ServiceProcess; using System.ServiceProcess;
using System.Text;
using System.Threading; using System.Threading;
using log4net; using log4net;
using log4net.Appender; using log4net.Appender;
@ -440,12 +439,12 @@ namespace WinSW
username = config.ServiceAccountUserName ?? username; username = config.ServiceAccountUserName ?? username;
password = config.ServiceAccountPassword ?? password; 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) switch (config.ServiceAccountPrompt)
{ {
case "dialog": case "dialog":
Credentials.PropmtForCredentialsDialog( Credentials.PromptForCredentialsDialog(
ref username, ref username,
ref password, ref password,
"Windows Service Wrapper", "Windows Service Wrapper",
@ -453,13 +452,13 @@ namespace WinSW
break; break;
case "console": case "console":
PromptForCredentialsConsole(); Credentials.PromptForCredentialsConsole(ref username, ref password);
break; break;
} }
} }
} }
if (username != null && !IsSpecialAccount(username)) if (username != null && !Security.IsSpecialAccount(username))
{ {
Security.AddServiceLogonRight(ref username); Security.AddServiceLogonRight(ref username);
} }
@ -509,34 +508,7 @@ namespace WinSW
EventLog.CreateEventSource(eventLogSource, "Application"); EventLog.CreateEventSource(eventLogSource, "Application");
} }
Log.Info($"Service '{config.Format()}' was installed successfully."); 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
};
} }
void Uninstall(string? pathToConfig, bool noElevate) void Uninstall(string? pathToConfig, bool noElevate)
@ -1228,7 +1200,7 @@ namespace WinSW
Throw.Command.Win32Exception("Failed to open process token."); Throw.Command.Win32Exception("Failed to open process token.");
} }
try using (token)
{ {
unsafe unsafe
{ {
@ -1245,33 +1217,6 @@ namespace WinSW
return elevation.TokenIsElevated != 0; 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);
}
}
} }
} }
} }

View File

@ -39,9 +39,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net5.0-windows'"> <ItemGroup Condition="'$(TargetFramework)' != 'net5.0-windows'">
<ProjectReference Include="..\WinSW.Tasks\WinSW.Tasks.csproj"> <ProjectReference Include="..\WinSW.Tasks\WinSW.Tasks.csproj" ReferenceOutputAssembly="false" />
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup> </ItemGroup>
<Target Name="PublishCoreZip" AfterTargets="Publish" Condition="'$(TargetFramework)' == 'net5.0-windows' and '$(IncludeNativeLibrariesForSelfExtract)' != 'true'"> <Target Name="PublishCoreZip" AfterTargets="Publish" Condition="'$(TargetFramework)' == 'net5.0-windows' and '$(IncludeNativeLibrariesForSelfExtract)' != 'true'">