Skip to content

Commit 96950a2

Browse files
committed
oauth: disconnect streams from xdg-open
When using the `Process` class to open the user's default browser on Linux, utilities like `xdg-open` are used. Some of these utilities do not disconnect child processes from our standard input/output/error streams. At the same time, browsers like Chromium like to write to stdout and stderr, which gets fed back to Git or the user's terminal, respectively. The latter looks messy, and the former causes Git to fail. On Linux, we instead manually locate a suitable 'shell execute' utility and launch them directly - this way we can redirect the standard output/error streams. For Windows and macOS, this is not an issue and we continue to use the Framework code to do 'shell execute'.
1 parent d5f8045 commit 96950a2

File tree

8 files changed

+79
-42
lines changed

8 files changed

+79
-42
lines changed

src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public async Task<OAuth2TokenResult> CreateOAuthCredentialsAsync(Uri targetUri)
129129
FailureResponseHtmlFormat = BitbucketResources.AuthenticationResponseFailureHtmlFormat
130130
};
131131

132-
var browser = new OAuth2SystemWebBrowser(browserOptions);
132+
var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions);
133133
var authCodeResult = await oauthClient.GetAuthorizationCodeAsync(Scopes, browser, CancellationToken.None);
134134

135135
return await oauthClient.GetTokenByAuthorizationCodeAsync(authCodeResult, CancellationToken.None);

src/shared/GitHub/GitHubAuthentication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ public async Task<OAuth2TokenResult> GetOAuthTokenAsync(Uri targetUri, IEnumerab
197197
SuccessResponseHtml = GitHubResources.AuthenticationResponseSuccessHtml,
198198
FailureResponseHtmlFormat = GitHubResources.AuthenticationResponseFailureHtmlFormat
199199
};
200-
var browser = new OAuth2SystemWebBrowser(browserOptions);
200+
var browser = new OAuth2SystemWebBrowser(Context.Environment, browserOptions);
201201

202202
// Write message to the terminal (if any is attached) for some feedback that we're waiting for a web response
203203
Context.Terminal.WriteLine("info: please complete authentication in your browser...");

src/shared/Microsoft.Git.CredentialManager/Authentication/OAuth/OAuth2SystemWebBrowser.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,15 @@ public class OAuth2WebBrowserOptions
3535

3636
public class OAuth2SystemWebBrowser : IOAuth2WebBrowser
3737
{
38+
private readonly IEnvironment _environment;
3839
private readonly OAuth2WebBrowserOptions _options;
3940

40-
public OAuth2SystemWebBrowser(OAuth2WebBrowserOptions options)
41+
public OAuth2SystemWebBrowser(IEnvironment environment, OAuth2WebBrowserOptions options)
4142
{
43+
EnsureArgument.NotNull(environment, nameof(environment));
44+
EnsureArgument.NotNull(options, nameof(options));
45+
46+
_environment = environment;
4247
_options = options;
4348
}
4449

@@ -75,18 +80,56 @@ public async Task<Uri> GetAuthenticationCodeAsync(Uri authorizationUri, Uri redi
7580

7681
private void OpenDefaultBrowser(Uri uri)
7782
{
78-
if (!StringComparer.OrdinalIgnoreCase.Equals(Uri.UriSchemeHttp, uri.Scheme) &&
79-
!StringComparer.OrdinalIgnoreCase.Equals(Uri.UriSchemeHttps, uri.Scheme))
83+
if (!uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
84+
!uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
8085
{
8186
throw new ArgumentException("Can only open HTTP/HTTPS URIs", nameof(uri));
8287
}
8388

84-
var pci = new ProcessStartInfo(uri.ToString())
89+
string url = uri.ToString();
90+
91+
ProcessStartInfo psi = null;
92+
if (PlatformUtils.IsLinux())
8593
{
86-
UseShellExecute = true
87-
};
94+
// On Linux, 'shell execute' utilities like xdg-open launch a process without
95+
// detaching from the standard in/out descriptors. Some applications (like
96+
// Chromium) write messages to stdout, which is currently hooked up and being
97+
// consumed by Git, and cause errors.
98+
//
99+
// Sadly, the Framework does not allow us to redirect standard streams if we
100+
// set ProcessStartInfo::UseShellExecute = true, so we must manually launch
101+
// these utilities and redirect the standard streams manually.
102+
//
103+
// We try and use the same 'shell execute' utilities as the Framework does,
104+
// searching for them in the same order until we find one.
105+
foreach (string shellExec in new[] { "xdg-open", "gnome-open", "kfmclient" })
106+
{
107+
if (_environment.TryLocateExecutable(shellExec, out string shellExecPath))
108+
{
109+
psi = new ProcessStartInfo(shellExecPath, url)
110+
{
111+
RedirectStandardOutput = true,
112+
RedirectStandardError = true
113+
};
114+
115+
// We found a way to open the URI; stop searching!
116+
break;
117+
}
118+
}
119+
120+
if (psi is null)
121+
{
122+
throw new Exception("Failed to locate a utility to launch the default web browser.");
123+
}
124+
}
125+
else
126+
{
127+
// On Windows and macOS, `ShellExecute` and `/usr/bin/open` disconnect the child process
128+
// from our standard in/out streams, so we can just use the Framework to do this.
129+
psi = new ProcessStartInfo(url) {UseShellExecute = true};
130+
}
88131

89-
Process.Start(pci);
132+
Process.Start(psi);
90133
}
91134

92135
private async Task<Uri> InterceptRequestsAsync(Uri listenUri, CancellationToken ct)

src/shared/Microsoft.Git.CredentialManager/BrowserHelper.cs

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/windows/Atlassian.Bitbucket.UI.Windows/ViewModels/CredentialsViewModel.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3+
using System.Diagnostics;
34
using System.Security;
45
using System.Windows.Input;
56
using Microsoft.Git.CredentialManager;
@@ -23,8 +24,8 @@ public CredentialsViewModel(string username)
2324
{
2425
LoginCommand = new RelayCommand(Accept, () => IsValid);
2526
CancelCommand = new RelayCommand(Cancel);
26-
ForgotPasswordCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.PasswordResetUrl));
27-
SignUpCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.SignUpLinkUrl));
27+
ForgotPasswordCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.PasswordResetUrl));
28+
SignUpCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.SignUpLinkUrl));
2829

2930
LoginValidator = PropertyValidator.For(this, x => x.Login).Required(BitbucketResources.LoginRequired);
3031
PasswordValidator = PropertyValidator.For(this, x => x.Password).Required(BitbucketResources.PasswordRequired);

src/windows/Atlassian.Bitbucket.UI.Windows/ViewModels/OAuthViewModel.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3+
using System.Diagnostics;
34
using System.Windows.Input;
45
using Microsoft.Git.CredentialManager;
56
using Microsoft.Git.CredentialManager.UI;
@@ -16,9 +17,9 @@ public OAuthViewModel()
1617
{
1718
OkCommand = new RelayCommand(Accept);
1819
CancelCommand = new RelayCommand(Cancel);
19-
LearnMoreCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.TwoFactorLearnMoreLinkUrl));
20-
ForgotPasswordCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.PasswordResetUrl));
21-
SignUpCommand = new RelayCommand(() => BrowserHelper.OpenDefaultBrowser(BitbucketResources.SignUpLinkUrl));
20+
LearnMoreCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.TwoFactorLearnMoreLinkUrl));
21+
ForgotPasswordCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.PasswordResetUrl));
22+
SignUpCommand = new RelayCommand(() => OpenDefaultBrowser(BitbucketResources.SignUpLinkUrl));
2223
}
2324

2425
public override string Title => BitbucketResources.OAuthWindowTitle;

src/windows/GitHub.UI.Windows/Login/Login2FaViewModel.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Windows.Input;
1+
using System.Diagnostics;
2+
using System.Windows.Input;
23
using Microsoft.Git.CredentialManager;
34
using Microsoft.Git.CredentialManager.UI;
45
using Microsoft.Git.CredentialManager.UI.ViewModels;
@@ -87,7 +88,7 @@ private void Verify()
8788

8889
private void NavigateLearnMore()
8990
{
90-
BrowserHelper.OpenDefaultBrowser(NavigateLearnMoreUrl);
91+
OpenDefaultBrowser(NavigateLearnMoreUrl);
9192
}
9293

9394
public string NavigateLearnMoreUrl => "https://aka.ms/vs-core-github-auth-help";

src/windows/Shared.UI.Windows/ViewModels/WindowViewModel.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33
using System;
4+
using System.Diagnostics;
45

56
namespace Microsoft.Git.CredentialManager.UI.ViewModels
67
{
@@ -20,5 +21,21 @@ public void Cancel()
2021
{
2122
Canceled?.Invoke(this, EventArgs.Empty);
2223
}
24+
25+
public static void OpenDefaultBrowser(string url)
26+
{
27+
if (!url.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
28+
!url.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
29+
{
30+
throw new ArgumentException("Can only open HTTP/HTTPS URLs", nameof(url));
31+
}
32+
33+
var psi = new ProcessStartInfo(url)
34+
{
35+
UseShellExecute = true
36+
};
37+
38+
Process.Start(psi);
39+
}
2340
}
2441
}

0 commit comments

Comments
 (0)