Skip to content

Commit

Permalink
Merge pull request #14 from gerardog/dev
Browse files Browse the repository at this point in the history
Release 0.6
  • Loading branch information
gerardog authored Jan 28, 2020
2 parents 9f3d72b + 9fde61a commit ac057fb
Show file tree
Hide file tree
Showing 25 changed files with 444 additions and 143 deletions.
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

0 comments on commit ac057fb

Please sign in to comment.