Skip to content

Commit

Permalink
wsl: detect host Windows session 0 and disable browser
Browse files Browse the repository at this point in the history
In order to detect if we have an interactive Windows desktop session
from inside WSL we need to 'punch out' from WSL to determine the session
ID and window station.

Strictly speaking, except for session 0 (from Vista onwards), any
Windows session can have exactly one interactive window station (always
called WinSta0). However, because we cannot easily determine the window
station name from a simple cmd/powershell script, we take a simplified
approach which isn't 100% accurate.

Instead, we only permit browser auth methods if we are NOT in Windows
session 0; any other Windows session we assume we are in WinSta0.

The default OpenSSH Server configuration (Windows 10+) has `sshd`
running as the built-in NT user NETWORK_SERVICE, which means it runs in
session 0 (the services session). This is most common scenario, other
than using WSL from a 'real', interactive Windows session that we're
likely to face.
  • Loading branch information
mjcheetham committed Mar 13, 2023
1 parent 336705f commit bfe4517
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 32 deletions.
46 changes: 26 additions & 20 deletions src/shared/Core/BrowserUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)

string url = uri.ToString();

ProcessStartInfo psi = null;
ProcessStartInfo psi;
if (PlatformUtils.IsLinux())
{
//
Expand All @@ -41,29 +41,17 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
// We try and use the same 'shell execute' utilities as the Framework does,
// searching for them in the same order until we find one.
//
// One additional 'shell execute' utility we also attempt to use is `wslview`
// that is commonly found on WSL (Windows Subsystem for Linux) distributions that
// opens the browser on the Windows host.
foreach (string shellExec in new[] {"xdg-open", "gnome-open", "kfmclient", "wslview"})
if (!TryGetLinuxShellExecuteHandler(environment, out string shellExecPath))
{
if (environment.TryLocateExecutable(shellExec, out string shellExecPath))
{
psi = new ProcessStartInfo(shellExecPath, url)
{
RedirectStandardOutput = true,
// Ok to redirect stderr for non-git-related processes
RedirectStandardError = true
};

// We found a way to open the URI; stop searching!
break;
}
throw new Exception("Failed to locate a utility to launch the default web browser.");
}

if (psi is null)
psi = new ProcessStartInfo(shellExecPath, url)
{
throw new Exception("Failed to locate a utility to launch the default web browser.");
}
RedirectStandardOutput = true,
// Ok to redirect stderr for non-git-related processes
RedirectStandardError = true
};
}
else
{
Expand All @@ -78,5 +66,23 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri)
// is no need to add the extra overhead associated with ChildProcess here.
Process.Start(psi);
}

public static bool TryGetLinuxShellExecuteHandler(IEnvironment env, out string shellExecPath)
{
// One additional 'shell execute' utility we also attempt to use over the Framework
// is `wslview` that is commonly found on WSL (Windows Subsystem for Linux) distributions
// that opens the browser on the Windows host.
string[] shellHandlers = { "xdg-open", "gnome-open", "kfmclient", WslUtils.WslViewShellHandlerName };
foreach (string shellExec in shellHandlers)
{
if (env.TryLocateExecutable(shellExec, out shellExecPath))
{
return true;
}
}

shellExecPath = null;
return false;
}
}
}
6 changes: 3 additions & 3 deletions src/shared/Core/CommandContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ public CommandContext()
if (PlatformUtils.IsWindows())
{
FileSystem = new WindowsFileSystem();
SessionManager = new WindowsSessionManager();
Environment = new WindowsEnvironment(FileSystem);
SessionManager = new WindowsSessionManager(Environment, FileSystem);
ProcessManager = new ProcessManager(Trace2);
Terminal = new WindowsTerminal(Trace);
string gitPath = GetGitPath(Environment, FileSystem, Trace);
Expand All @@ -118,7 +118,7 @@ public CommandContext()
else if (PlatformUtils.IsMacOS())
{
FileSystem = new MacOSFileSystem();
SessionManager = new MacOSSessionManager();
SessionManager = new MacOSSessionManager(Environment, FileSystem);
Environment = new MacOSEnvironment(FileSystem);
ProcessManager = new WindowsProcessManager(Trace2);
Terminal = new MacOSTerminal(Trace);
Expand All @@ -134,7 +134,7 @@ public CommandContext()
else if (PlatformUtils.IsLinux())
{
FileSystem = new LinuxFileSystem();
SessionManager = new PosixSessionManager();
SessionManager = new LinuxSessionManager(Environment, FileSystem);
Environment = new PosixEnvironment(FileSystem);
ProcessManager = new ProcessManager(Trace2);
Terminal = new LinuxTerminal(Trace);
Expand Down
9 changes: 9 additions & 0 deletions src/shared/Core/ISessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ public interface ISessionManager

public abstract class SessionManager : ISessionManager
{
protected IEnvironment Environment { get; }
protected IFileSystem FileSystem { get; }

protected SessionManager(IEnvironment env, IFileSystem fs)
{
Environment = env;
FileSystem = fs;
}

public abstract bool IsDesktopSession { get; }

public virtual bool IsWebBrowserAvailable => IsDesktopSession;
Expand Down
64 changes: 64 additions & 0 deletions src/shared/Core/Interop/Linux/LinuxSessionManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using GitCredentialManager.Interop.Posix;

namespace GitCredentialManager.Interop.Linux;

public class LinuxSessionManager : PosixSessionManager
{
private bool? _isWebBrowserAvailable;

public LinuxSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsureLinux();
}

public override bool IsWebBrowserAvailable
{
get
{
return _isWebBrowserAvailable ??= GetWebBrowserAvailable();
}
}

private bool GetWebBrowserAvailable()
{
// If this is a Windows Subsystem for Linux distribution we may
// be able to launch the web browser of the host Windows OS.
if (WslUtils.IsWslDistribution(Environment, FileSystem, out _))
{
// We need a shell execute handler to be able to launch to browser
if (!BrowserUtils.TryGetLinuxShellExecuteHandler(Environment, out _))
{
return false;
}

//
// If we are in Windows logon session 0 then the user can never interact,
// even in the WinSta0 window station. This is typical when SSH-ing into a
// Windows 10+ machine using the default OpenSSH Server configuration,
// which runs in the 'services' session 0.
//
// If we're in any other session, and in the WinSta0 window station then
// the user can possibly interact. However, since it's hard to determine
// the window station from PowerShell cmdlets (we'd need to write P/Invoke
// code and that's just messy and too many levels of indirection quite
// frankly!) we just assume any non session 0 is interactive.
//
// This assumption doesn't hold true if the user has changed the user that
// the OpenSSH Server service runs as (not a built-in NT service) *AND*
// they've SSH-ed into the Windows host (and then started a WSL shell).
// This feels like a very small subset of users...
//
if (WslUtils.GetWindowsSessionId(FileSystem) == 0)
{
return false;
}

// If we are not in session 0, or we cannot get the Windows session ID,
// assume that we *CAN* launch the browser so that users are never blocked.
return true;
}

// We require an interactive desktop session to be able to launch a browser
return IsDesktopSession;
}
}
2 changes: 1 addition & 1 deletion src/shared/Core/Interop/MacOS/MacOSSessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace GitCredentialManager.Interop.MacOS
{
public class MacOSSessionManager : PosixSessionManager
{
public MacOSSessionManager()
public MacOSSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsureMacOS();
}
Expand Down
10 changes: 4 additions & 6 deletions src/shared/Core/Interop/Posix/PosixSessionManager.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
using System;

namespace GitCredentialManager.Interop.Posix
{
public class PosixSessionManager : SessionManager
public abstract class PosixSessionManager : SessionManager
{
public PosixSessionManager()
protected PosixSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsurePosix();
}

// Check if we have an X11 or Wayland display environment available
public override bool IsDesktopSession =>
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DISPLAY")) ||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));
!string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("DISPLAY")) ||
!string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"));
}
}
2 changes: 1 addition & 1 deletion src/shared/Core/Interop/Windows/WindowsSessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace GitCredentialManager.Interop.Windows
{
public class WindowsSessionManager : SessionManager
{
public WindowsSessionManager()
public WindowsSessionManager(IEnvironment env, IFileSystem fs) : base(env, fs)
{
PlatformUtils.EnsureWindows();
}
Expand Down
1 change: 0 additions & 1 deletion src/shared/Core/ProcessManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Threading.Tasks;

namespace GitCredentialManager;

Expand Down
36 changes: 36 additions & 0 deletions src/shared/Core/WslUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public static class WslUtils
private const string DefaultWslMountPrefix = "/mnt";
private const string DefaultWslSysDriveMountName = "c";

internal const string WslViewShellHandlerName = "wslview";

/// <summary>
/// Cached Windows host session ID.
/// </summary>
/// <remarks>A value less than 0 represents "unknown".</remarks>
private static int _windowsSessionId = -1;

/// <summary>
/// Cached WSL version.
/// </summary>
Expand Down Expand Up @@ -176,6 +184,34 @@ public static bool IsWslPath(string path)
return new Process { StartInfo = psi };
}

/// <summary>
/// Get the host Windows session ID.
/// </summary>
/// <returns>Windows session ID, or a negative value if it is not known.</returns>
public static int GetWindowsSessionId(IFileSystem fs)
{
if (_windowsSessionId < 0)
{
const string script = @"(Get-Process -ID $PID).SessionId";
using (Process proc = CreateWindowsShellProcess(fs, WindowsShell.PowerShell, script))
{
proc.Start();
proc.WaitForExit();

if (proc.ExitCode == 0)
{
string output = proc.StandardOutput.ReadToEnd().Trim();
if (int.TryParse(output, out int sessionId))
{
_windowsSessionId = sessionId;
}
}
}
}

return _windowsSessionId;
}

private static string GetSystemDriveMountPath(IFileSystem fs)
{
if (_sysDriveMountPath is null)
Expand Down

0 comments on commit bfe4517

Please sign in to comment.