Skip to content
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
4 changes: 2 additions & 2 deletions src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public async Task<CredentialsPromptResult> GetCredentialsAsync(Uri targetUri, st
private async Task<CredentialsPromptResult> GetCredentialsViaUiAsync(
Uri targetUri, string userName, AuthenticationModes modes)
{
var viewModel = new CredentialsViewModel(Context.Environment)
var viewModel = new CredentialsViewModel(Context.SessionManager)
{
ShowOAuth = (modes & AuthenticationModes.OAuth) != 0,
ShowBasic = (modes & AuthenticationModes.Basic) != 0
Expand Down Expand Up @@ -259,7 +259,7 @@ public async Task<OAuth2TokenResult> CreateOAuthCredentialsAsync(InputArguments
FailureResponseHtmlFormat = BitbucketResources.AuthenticationResponseFailureHtmlFormat
};

var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions);
var browser = new OAuth2SystemWebBrowser(Context.SessionManager, browserOptions);
var oauth2Client = _oauth2ClientRegistry.Get(input);

var authCodeResult = await oauth2Client.GetAuthorizationCodeAsync(browser, CancellationToken.None);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ protected CredentialsCommand(ICommandContext context)

private async Task<int> ExecuteAsync(Uri url, string userName, bool showOAuth, bool showBasic)
{
var viewModel = new CredentialsViewModel(Context.Environment)
var viewModel = new CredentialsViewModel(Context.SessionManager)
{
Url = url,
UserName = userName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Atlassian.Bitbucket.UI.ViewModels
{
public class CredentialsViewModel : WindowViewModel
{
private readonly IEnvironment _environment;
private readonly ISessionManager _sessionManager;

private Uri _url;
private string _userName;
Expand All @@ -22,11 +22,11 @@ public CredentialsViewModel()
// Constructor the XAML designer
}

public CredentialsViewModel(IEnvironment environment)
public CredentialsViewModel(ISessionManager sessionManager)
{
EnsureArgument.NotNull(environment, nameof(environment));
EnsureArgument.NotNull(sessionManager, nameof(sessionManager));

_environment = environment;
_sessionManager = sessionManager;

Title = "Connect to Bitbucket";
LoginCommand = new RelayCommand(AcceptBasic, CanLogin);
Expand Down Expand Up @@ -77,7 +77,7 @@ private void ForgotPassword()
? new Uri(BitbucketConstants.HelpUrls.PasswordReset)
: new Uri(_url, BitbucketConstants.HelpUrls.DataCenterPasswordReset);

BrowserUtils.OpenDefaultBrowser(_environment, passwordResetUri);
_sessionManager.OpenBrowser(passwordResetUri);
}

private void SignUp()
Expand All @@ -86,7 +86,7 @@ private void SignUp()
? new Uri(BitbucketConstants.HelpUrls.SignUp)
: new Uri(_url, BitbucketConstants.HelpUrls.DataCenterLogin);

BrowserUtils.OpenDefaultBrowser(_environment, signUpUri);
_sessionManager.OpenBrowser(signUpUri);
}

public Uri Url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ public class OAuth2SystemWebBrowserTests
[Fact]
public void OAuth2SystemWebBrowser_UpdateRedirectUri_NonLoopback_ThrowsError()
{
var env = new TestEnvironment();
var sm = new TestSessionManager();
var options = new OAuth2WebBrowserOptions();
var browser = new OAuth2SystemWebBrowser(env, options);
var browser = new OAuth2SystemWebBrowser(sm, options);

Assert.Throws<ArgumentException>(() => browser.UpdateRedirectUri(new Uri("http://example.com")));
}
Expand All @@ -28,9 +28,9 @@ public void OAuth2SystemWebBrowser_UpdateRedirectUri_NonLoopback_ThrowsError()
[InlineData("http://127.0.0.7:1234/oauth-callback/", "http://127.0.0.7:1234/oauth-callback/")]
public void OAuth2SystemWebBrowser_UpdateRedirectUri_SpecificPort(string input, string expected)
{
var env = new TestEnvironment();
var sm = new TestSessionManager();
var options = new OAuth2WebBrowserOptions();
var browser = new OAuth2SystemWebBrowser(env, options);
var browser = new OAuth2SystemWebBrowser(sm, options);

Uri actualUri = browser.UpdateRedirectUri(new Uri(input));

Expand All @@ -48,9 +48,9 @@ public void OAuth2SystemWebBrowser_UpdateRedirectUri_SpecificPort(string input,
[InlineData("http://127.0.0.7/oauth-callback/")]
public void OAuth2SystemWebBrowser_UpdateRedirectUri_AnyPort(string input)
{
var env = new TestEnvironment();
var sm = new TestSessionManager();
var options = new OAuth2WebBrowserOptions();
var browser = new OAuth2SystemWebBrowser(env, options);
var browser = new OAuth2SystemWebBrowser(sm, options);

var inputUri = new Uri(input);
Uri actualUri = browser.UpdateRedirectUri(inputUri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ private async Task<bool> UseDefaultAccountAsync(string userName)
}
}

var viewModel = new DefaultAccountViewModel(Context.Environment)
var viewModel = new DefaultAccountViewModel(Context.SessionManager)
{
UserName = userName
};
Expand Down
11 changes: 5 additions & 6 deletions src/shared/Core/Authentication/OAuth/OAuth2SystemWebBrowser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Threading;
Expand Down Expand Up @@ -33,15 +32,15 @@ public class OAuth2WebBrowserOptions

public class OAuth2SystemWebBrowser : IOAuth2WebBrowser
{
private readonly IEnvironment _environment;
private readonly ISessionManager _sessionManager;
private readonly OAuth2WebBrowserOptions _options;

public OAuth2SystemWebBrowser(IEnvironment environment, OAuth2WebBrowserOptions options)
public OAuth2SystemWebBrowser(ISessionManager sessionManager, OAuth2WebBrowserOptions options)
{
EnsureArgument.NotNull(environment, nameof(environment));
EnsureArgument.NotNull(sessionManager, nameof(sessionManager));
EnsureArgument.NotNull(options, nameof(options));

_environment = environment;
_sessionManager = sessionManager;
_options = options;
}

Expand Down Expand Up @@ -71,7 +70,7 @@ public async Task<Uri> GetAuthenticationCodeAsync(Uri authorizationUri, Uri redi

Task<Uri> interceptTask = InterceptRequestsAsync(redirectUri, ct);

BrowserUtils.OpenDefaultBrowser(_environment, authorizationUri);
_sessionManager.OpenBrowser(authorizationUri);

return await interceptTask;
}
Expand Down
4 changes: 2 additions & 2 deletions src/shared/Core/Authentication/OAuthAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public async Task<OAuth2TokenResult> GetTokenByBrowserAsync(OAuth2Client client,
}

var browserOptions = new OAuth2WebBrowserOptions();
var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions);
var browser = new OAuth2SystemWebBrowser(Context.SessionManager, browserOptions);
var authCode = await client.GetAuthorizationCodeAsync(scopes, browser, CancellationToken.None);
return await client.GetTokenByAuthorizationCodeAsync(authCode, CancellationToken.None);
}
Expand Down Expand Up @@ -241,7 +241,7 @@ public async Task<OAuth2TokenResult> GetTokenByDeviceCodeAsync(OAuth2Client clie

private Task ShowDeviceCodeViaUiAsync(OAuth2DeviceCodeResult dcr, CancellationToken ct)
{
var viewModel = new DeviceCodeViewModel(Context.Environment)
var viewModel = new DeviceCodeViewModel(Context.SessionManager)
{
UserCode = dcr.UserCode,
VerificationUrl = dcr.VerificationUri.ToString(),
Expand Down
88 changes: 0 additions & 88 deletions src/shared/Core/BrowserUtils.cs

This file was deleted.

40 changes: 40 additions & 0 deletions src/shared/Core/ISessionManager.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using System.Diagnostics;

namespace GitCredentialManager
{
public interface ISessionManager
Expand All @@ -13,6 +16,26 @@ public interface ISessionManager
/// </summary>
/// <returns>True if the session can display a web browser, false otherwise.</returns>
bool IsWebBrowserAvailable { get; }

/// <summary>
/// Open the system web browser to the specified URL.
/// </summary>
/// <param name="uri"><see cref="Uri"/> to open the browser at.</param>
/// <exception cref="InvalidOperationException">Thrown if <see cref="IsWebBrowserAvailable"/> is false.</exception>
void OpenBrowser(Uri uri);
}

public static class SessionManagerExtensions
{
public static void OpenBrowser(this ISessionManager sm, string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
{
throw new ArgumentException($"Not a valid URI: '{url}'");
}

sm.OpenBrowser(uri);
}
}

public abstract class SessionManager : ISessionManager
Expand All @@ -32,5 +55,22 @@ protected SessionManager(IEnvironment env, IFileSystem fs)
public abstract bool IsDesktopSession { get; }

public virtual bool IsWebBrowserAvailable => IsDesktopSession;

public void OpenBrowser(Uri uri)
{
if (!uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
!uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Can only open HTTP/HTTPS URIs", nameof(uri));
}

OpenBrowserInternal(uri.ToString());
}

protected virtual void OpenBrowserInternal(string url)
{
var psi = new ProcessStartInfo(url) { UseShellExecute = true };
Process.Start(psi);
}
}
}
54 changes: 52 additions & 2 deletions src/shared/Core/Interop/Linux/LinuxSessionManager.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Diagnostics;
using GitCredentialManager.Interop.Posix;

namespace GitCredentialManager.Interop.Linux;
Expand All @@ -18,15 +20,45 @@ public override bool IsWebBrowserAvailable
return _isWebBrowserAvailable ??= GetWebBrowserAvailable();
}
}


protected override void OpenBrowserInternal(string url)
{
//
// On Linux, 'shell execute' utilities like xdg-open launch a process without
// detaching from the standard in/out descriptors. Some applications (like
// Chromium) write messages to stdout, which is currently hooked up and being
// consumed by Git, and cause errors.
//
// Sadly, the Framework does not allow us to redirect standard streams if we
// set ProcessStartInfo::UseShellExecute = true, so we must manually launch
// these utilities and redirect the standard streams manually.
//
// We try and use the same 'shell execute' utilities as the Framework does,
// searching for them in the same order until we find one.
//
if (!TryGetShellExecuteHandler(Environment, out string shellExecPath))
{
throw new Exception("Failed to locate a utility to launch the default web browser.");
}

var psi = new ProcessStartInfo(shellExecPath, url)
{
RedirectStandardOutput = true,
// Ok to redirect stderr for non-git-related processes
RedirectStandardError = true
};

Process.Start(psi);
}

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 _))
if (!TryGetShellExecuteHandler(Environment, out _))
{
return false;
}
Expand Down Expand Up @@ -61,4 +93,22 @@ private bool GetWebBrowserAvailable()
// We require an interactive desktop session to be able to launch a browser
return IsDesktopSession;
}

private static bool TryGetShellExecuteHandler(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;
}
}
2 changes: 1 addition & 1 deletion src/shared/Core/UI/Commands/DefaultAccountCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ protected DefaultAccountCommand(ICommandContext context)

private async Task<int> ExecuteAsync(string title, string userName, bool noLogo)
{
var viewModel = new DefaultAccountViewModel(Context.Environment)
var viewModel = new DefaultAccountViewModel(Context.SessionManager)
{
Title = !string.IsNullOrWhiteSpace(title)
? title
Expand Down
Loading
Loading