From 7d11a86c72ef430162b6dd68220a77e1d3f121fa Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 Oct 2022 15:58:50 -0700 Subject: [PATCH] wsl: detect host Windows session 0 and disable browser 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. --- src/shared/Core/BrowserUtils.cs | 46 +++++++------ src/shared/Core/CommandContext.cs | 6 +- src/shared/Core/ISessionManager.cs | 9 +++ .../Core/Interop/Linux/LinuxSessionManager.cs | 64 +++++++++++++++++++ .../Core/Interop/MacOS/MacOSSessionManager.cs | 2 +- .../Core/Interop/Posix/PosixSessionManager.cs | 10 ++- .../Interop/Windows/WindowsSessionManager.cs | 2 +- src/shared/Core/ProcessManager.cs | 1 - src/shared/Core/WslUtils.cs | 36 +++++++++++ 9 files changed, 144 insertions(+), 32 deletions(-) create mode 100644 src/shared/Core/Interop/Linux/LinuxSessionManager.cs diff --git a/src/shared/Core/BrowserUtils.cs b/src/shared/Core/BrowserUtils.cs index c34164ef0e..6e908d1fcc 100644 --- a/src/shared/Core/BrowserUtils.cs +++ b/src/shared/Core/BrowserUtils.cs @@ -25,7 +25,7 @@ public static void OpenDefaultBrowser(IEnvironment environment, Uri uri) string url = uri.ToString(); - ProcessStartInfo psi = null; + ProcessStartInfo psi; if (PlatformUtils.IsLinux()) { // @@ -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 { @@ -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; + } } } diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index d051b2ef2d..304bdb6833 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -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 WindowsProcessManager(Trace2); Terminal = new WindowsTerminal(Trace); string gitPath = GetGitPath(Environment, FileSystem, Trace); @@ -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 ProcessManager(Trace2); Terminal = new MacOSTerminal(Trace); @@ -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); diff --git a/src/shared/Core/ISessionManager.cs b/src/shared/Core/ISessionManager.cs index 165393153d..2569833f41 100644 --- a/src/shared/Core/ISessionManager.cs +++ b/src/shared/Core/ISessionManager.cs @@ -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; diff --git a/src/shared/Core/Interop/Linux/LinuxSessionManager.cs b/src/shared/Core/Interop/Linux/LinuxSessionManager.cs new file mode 100644 index 0000000000..2147289ac7 --- /dev/null +++ b/src/shared/Core/Interop/Linux/LinuxSessionManager.cs @@ -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; + } +} diff --git a/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs b/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs index febcd20c24..584965ca1b 100644 --- a/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs +++ b/src/shared/Core/Interop/MacOS/MacOSSessionManager.cs @@ -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(); } diff --git a/src/shared/Core/Interop/Posix/PosixSessionManager.cs b/src/shared/Core/Interop/Posix/PosixSessionManager.cs index b9624e69a2..8709e12e7e 100644 --- a/src/shared/Core/Interop/Posix/PosixSessionManager.cs +++ b/src/shared/Core/Interop/Posix/PosixSessionManager.cs @@ -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")); } } diff --git a/src/shared/Core/Interop/Windows/WindowsSessionManager.cs b/src/shared/Core/Interop/Windows/WindowsSessionManager.cs index 464f2c3fd0..d87d76347f 100644 --- a/src/shared/Core/Interop/Windows/WindowsSessionManager.cs +++ b/src/shared/Core/Interop/Windows/WindowsSessionManager.cs @@ -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(); } diff --git a/src/shared/Core/ProcessManager.cs b/src/shared/Core/ProcessManager.cs index 78b0e2c9c6..2e15443979 100644 --- a/src/shared/Core/ProcessManager.cs +++ b/src/shared/Core/ProcessManager.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Threading.Tasks; namespace GitCredentialManager; diff --git a/src/shared/Core/WslUtils.cs b/src/shared/Core/WslUtils.cs index 3e062a6fa3..1db63d3292 100644 --- a/src/shared/Core/WslUtils.cs +++ b/src/shared/Core/WslUtils.cs @@ -23,6 +23,14 @@ public static class WslUtils private const string DefaultWslMountPrefix = "/mnt"; private const string DefaultWslSysDriveMountName = "c"; + internal const string WslViewShellHandlerName = "wslview"; + + /// + /// Cached Windows host session ID. + /// + /// A value less than 0 represents "unknown". + private static int _windowsSessionId = -1; + /// /// Cached WSL version. /// @@ -176,6 +184,34 @@ public static bool IsWslPath(string path) return new Process { StartInfo = psi }; } + /// + /// Get the host Windows session ID. + /// + /// Windows session ID, or a negative value if it is not known. + 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)