Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.6 #14

Merged
merged 7 commits into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ Just prepend `gsudo` (or the `sudo` alias) to your command and it will run eleva
- Those **streaming StdIn/Out/Err** to the non-elevated console.
This allows to capture or redirect StdIn/Out/Err but has limited user experience: Elevated processes can only append plain text to the console, so text formatting, full screen console apps, progress bars, tab-key auto-complete, does not work.

**`gsudo` implements all three methods**, and automatically uses the one that best fits your scenario, so you get the best user experience everytime.
**`gsudo` combines all three methods**, and automatically uses the one that best fits your scenario, so you get the best user experience everytime.

## Features

- Elevated commands are shown in the user-level console. (Unless you specify `-n` which opens a new window.)
- Credentials cache: If `gsudo` is invoked several times within minutes it only shows the UAC pop-up once.
- CMD commands: `gsudo md folder` (no need to use the longer form `gsudo cmd.exe /c md folder`)
- Supports [PowerShell/PowerShell Core commands](#Usage-from-PowerShell).
- Supports [PowerShell/PowerShell Core commands](#usage-from-powershell--powershell-core).
- Supports being used on scripts:
- `gsudo` can be used on scripts that requires to elevate one or more commands. (the UAC popup will appear once).
- Outputs of the elevated commands can be interpreted: E.g. StdOut/StdErr can be piped or captured (`gsudo dir | findstr /c:"bytes free" > FreeSpace.txt`) and exit codes too ('%errorlevel%)). If `gsudo` fails to elevate, the exit code will be 999.
Expand Down Expand Up @@ -71,6 +71,7 @@ Most relevant **`[options]`**:

- **`-n | --new`** Starts the command in a **new** console with elevated rights (and returns immediately).
- **`-w | --wait`** Force wait for the process to end (and return the exitcode).
- **`-s | --system`** Run As Local System account ("NT AUTHORITY\SYSTEM").
- **`--copyev `** Copy all environment variables to the elevated session before executing.
- **`--copyns `** Reconnect current connected network shares on the elevated session. Warning! This is verbose, affects the elevated user system-wide (other processes), and can prompt for credentials interactively.
- **`--debug `** Debug mode (verbose).
Expand Down
4 changes: 2 additions & 2 deletions src/gsudo.Tests/ArgumentParsingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void Arguments_QuotedTests()
var input = "\"my exe name\" \"my params\" OtherParam1 OtherParam2 OtherParam3";
var expected = new string[] { "\"my exe name\"", "\"my params\"", "OtherParam1", "OtherParam2", "OtherParam3" };

var actual = ArgumentsHelper.SplitArgs(input);
var actual = ArgumentsHelper.SplitArgs(input).ToArray();

Assert.AreEqual(expected.Length, actual.Length);

Expand All @@ -31,7 +31,7 @@ public void Arguments_NoQuotesTests()
var input = "HEllo I Am my params OtherParam1 OtherParam2 OtherParam3";
var expected = new string[] { "HEllo", "I", "Am", "my", "params", "OtherParam1", "OtherParam2", "OtherParam3" };

var actual = ArgumentsHelper.SplitArgs(input);
var actual = ArgumentsHelper.SplitArgs(input).ToArray();

Assert.AreEqual(expected.Length, actual.Length);

Expand Down
13 changes: 7 additions & 6 deletions src/gsudo/Commands/HelpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ internal static void ShowHelp()
Console.WriteLine("gsudo [-v | --version] \t Shows gsudo version");
Console.WriteLine();
Console.WriteLine("Valid options:");
Console.WriteLine(" --loglevel {val} Only show logs where level is at least the value specified. Valid values are: All, Debug, Info, Warning, Error, None");
Console.WriteLine(" --debug Enable debug mode. (makes gsudo service window visible)");
Console.WriteLine(" -n | --new Starts the command in a new console with elevated rights and returns immediately.");
Console.WriteLine(" -w | --wait Force wait for the process to end.");
Console.WriteLine(" --raw Force use of a reduced terminal.");
Console.WriteLine(" --vt Force use of full VT100 terminal emulator (experimental).");
Console.WriteLine(" -n | --new Starts the command in a new console (and returns immediately).");
Console.WriteLine(" -w | --wait Force wait for the command to end.");
Console.WriteLine(" -s | --system Run As Local System account (\"NT AUTHORITY\\SYSTEM\").");
Console.WriteLine(" --copyev Copy environment variables to the elevated process before executing.");
Console.WriteLine(" --copyns Connect current network drives to the elevated user. Warning! This is verbose, affects the elevated user system-wide, and can prompt for credentials interactively.");
Console.WriteLine(" --raw Force use of a reduced terminal.");
Console.WriteLine(" --vt Force use of full VT100 terminal emulator (experimental).");
Console.WriteLine(" --loglevel {val} Only show logs where level is at least the value specified. Valid values are: All, Debug, Info, Warning, Error, None");
Console.WriteLine(" --debug Enable debug mode. (makes gsudo service window visible)");

return;
}
Expand Down
57 changes: 44 additions & 13 deletions src/gsudo/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public async Task<int> Execute()
bool isWindowsApp = ProcessFactory.IsWindowsApp(CommandToRun.FirstOrDefault());
var consoleMode = GetConsoleMode(isWindowsApp);

if (!ProcessExtensions.IsAdministrator())
if (!RunningAsDesiredUser())
{
CommandToRun = AddCopyEnvironment(CommandToRun);
}
Expand All @@ -47,7 +47,7 @@ public async Task<int> Execute()
Wait = (!isWindowsApp && !GlobalSettings.NewWindow) || GlobalSettings.Wait,
Mode = consoleMode,
ConsoleProcessId = currentProcess.Id,
Prompt = consoleMode != ElevationRequest.ConsoleMode.Raw || GlobalSettings.NewWindow ? GlobalSettings.Prompt : GlobalSettings.RawPrompt
Prompt = consoleMode != ElevationRequest.ConsoleMode.Raw || GlobalSettings.NewWindow ? GlobalSettings.Prompt : GlobalSettings.RawPrompt
};

Logger.Instance.Log($"Command to run: {elevationRequest.FileName} {elevationRequest.Arguments}", LogLevel.Debug);
Expand All @@ -61,9 +61,9 @@ public async Task<int> Execute()
elevationRequest.ConsoleWidth--; // weird ConEmu/Cmder fix
}

if (ProcessExtensions.IsAdministrator())
if (RunningAsDesiredUser()) // already elevated or running as correct user. No service needed.
{
if (emptyArgs)
if (emptyArgs && !GlobalSettings.NewWindow)
{
Logger.Instance.Log("Already elevated (and no parameters specified). Exiting...", LogLevel.Error);
return Constants.GSUDO_ERROR_EXITCODE;
Expand Down Expand Up @@ -109,7 +109,7 @@ public async Task<int> Execute()
}
}
}
else // IsAdministrator() == false
else
{
Logger.Instance.Log($"Using Console mode {elevationRequest.Mode}", LogLevel.Debug);
var callingPid = GetCallingPid(currentProcess);
Expand Down Expand Up @@ -138,11 +138,8 @@ public async Task<int> Execute()
if (connection == null) // service is not running or listening.
{
// Start elevated service instance
var dbg = GlobalSettings.Debug ? "--debug " : string.Empty;
using (var process = ProcessFactory.StartElevatedDetached(currentProcess.MainModule.FileName, $"{dbg}gsudoservice {callingPid} {callingSid} {GlobalSettings.LogLevel}", !GlobalSettings.Debug))
{
Logger.Instance.Log("Elevated instance started.", LogLevel.Debug);
}
if (!StartElevatedService(currentProcess, callingPid, callingSid))
return Constants.GSUDO_ERROR_EXITCODE;

connection = await rpcClient.Connect(elevationRequest, callingPid, 5000).ConfigureAwait(false);
}
Expand All @@ -159,7 +156,7 @@ public async Task<int> Execute()

var renderer = GetRenderer(connection, elevationRequest);
var exitCode = await renderer.Start().ConfigureAwait(false);

if (!(elevationRequest.NewWindow && !elevationRequest.Wait))
Logger.Instance.Log($"Elevated process exited with code {exitCode}", exitCode == 0 ? LogLevel.Debug : LogLevel.Info);

Expand All @@ -172,9 +169,43 @@ public async Task<int> Execute()
}
}

private static bool StartElevatedService(Process currentProcess, int callingPid, string callingSid)
{
var dbg = GlobalSettings.Debug ? "--debug " : string.Empty;
Process process;
if (GlobalSettings.RunAsSystem && ProcessExtensions.IsAdministrator())
{
process = ProcessFactory.StartAsSystem(currentProcess.MainModule.FileName, $"{dbg}-s gsudoservice {callingPid} {callingSid} {GlobalSettings.LogLevel}", Environment.CurrentDirectory, !GlobalSettings.Debug);
}
else
{
var verb = GlobalSettings.RunAsSystem ? "gsudosystemservice" : "gsudoservice";
process = ProcessFactory.StartElevatedDetached(currentProcess.MainModule.FileName, $"{dbg}{verb} {callingPid} {callingSid} {GlobalSettings.LogLevel}", !GlobalSettings.Debug);
}

if (process == null)
{
Logger.Instance.Log("Failed to start elevated instance.", LogLevel.Error);
return false;
}

Logger.Instance.Log("Elevated instance started.", LogLevel.Debug);
return true;
}

private static bool RunningAsDesiredUser()
{
if (GlobalSettings.RunAsSystem)
{
return WindowsIdentity.GetCurrent().IsSystem;
}
return ProcessExtensions.IsAdministrator();
}

private static int GetCallingPid(Process currentProcess)
{
var parent = currentProcess.ParentProcess();
if (parent == null) return currentProcess.ParentProcessId();
while (parent.MainModule.FileName.In("sudo.exe", "gsudo.exe")) // naive shim detection
{
parent = parent.ParentProcess();
Expand Down Expand Up @@ -235,9 +266,9 @@ private IRpcClient GetClient(ElevationRequest elevationRequest)
private static IProcessRenderer GetRenderer(Connection connection, ElevationRequest elevationRequest)
{
if (elevationRequest.Mode == ElevationRequest.ConsoleMode.Attached)
return new AttachedConsoleRenderer(connection, elevationRequest);
return new AttachedConsoleRenderer(connection);
if (elevationRequest.Mode == ElevationRequest.ConsoleMode.Raw)
return new PipedClientRenderer(connection, elevationRequest);
return new PipedClientRenderer(connection);
else
return new VTClientRenderer(connection, elevationRequest);
}
Expand Down
3 changes: 1 addition & 2 deletions src/gsudo/Commands/ServiceCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.IO;
using gsudo.Rpc;
using gsudo.ProcessHosts;
using gsudo.Helpers;
using System.Runtime.Serialization.Formatters.Binary;

namespace gsudo.Commands
Expand Down Expand Up @@ -64,7 +63,7 @@ private async Task AcceptConnection(Connection connection)
private static IProcessHost CreateProcessHost(ElevationRequest request)
{
if (request.NewWindow || !request.Wait)
return new DetachedHostProcess();
return new NewWindowProcessHost();
if (request.Mode == ElevationRequest.ConsoleMode.Attached)
return new AttachedConsoleHost();
else if (request.Mode == ElevationRequest.ConsoleMode.VT)
Expand Down
44 changes: 44 additions & 0 deletions src/gsudo/Commands/SystemServiceCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Threading.Tasks;
using gsudo.Helpers;
using System.Diagnostics;

namespace gsudo.Commands
{
/// <summary>
/// We can only spawn a process as system account if we were elevated first.
/// So,
/// Non-elevated Gsudo client -(elevates)-> Gsudo SystemService -(runs as System)-> Gsudo Service.
/// Then..
/// Non-elevated Gsudo client connects to Gsudo Service running as system.
/// </summary>
class SystemServiceCommand : ICommand
{
public int allowedPid { get; set; }
public string allowedSid { get; set; }

public LogLevel? LogLvl { get; set; }

public Task<int> Execute()
{
// service mode
if (LogLvl.HasValue) GlobalSettings.LogLevel.Value = LogLvl.Value;

var dbg = GlobalSettings.Debug ? "--debug " : string.Empty;

if (ProcessExtensions.IsAdministrator())
{
var process = ProcessFactory.StartAsSystem(Process.GetCurrentProcess().MainModule.FileName, $"{dbg}-s gsudoservice {allowedPid} {allowedSid} {GlobalSettings.LogLevel}", Environment.CurrentDirectory, !GlobalSettings.Debug);
if (process == null)
{
Logger.Instance.Log("Failed to start elevated instance.", LogLevel.Error);
return Task.FromResult(Constants.GSUDO_ERROR_EXITCODE);
}

Logger.Instance.Log("Elevated instance started.", LogLevel.Debug);
}

return Task.FromResult(0);
}
}
}
7 changes: 3 additions & 4 deletions src/gsudo/GlobalSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class GlobalSettings
public static bool Debug { get; internal set; }
public static bool NewWindow { get; internal set; }
public static bool Wait { get; internal set; }
public static bool RunAsSystem { get; internal set; }

public static RegistrySetting<bool> ForceRawConsole { get; internal set; } = new RegistrySetting<bool>(nameof(ForceRawConsole), false, bool.Parse);
public static RegistrySetting<bool> ForceVTConsole { get; internal set; } = new RegistrySetting<bool>(nameof(ForceVTConsole), false, bool.Parse);
public static RegistrySetting<bool> CopyEnvironmentVariables { get; internal set; } = new RegistrySetting<bool>(nameof(CopyEnvironmentVariables), false, bool.Parse);
Expand All @@ -33,10 +35,7 @@ class GlobalSettings
ForceRawConsole,
ForceVTConsole,
CopyEnvironmentVariables,
CopyNetworkShares,
PowerShellArguments,
PowerShellCore6Arguments,
PowerShellCore7Arguments);
CopyNetworkShares);
}

static class Extension
Expand Down
Loading