diff --git a/Cli-Askpass/GlobalSuppressions.cs b/Cli-Askpass/GlobalSuppressions.cs index 55e80e27f..889d6e494 100644 Binary files a/Cli-Askpass/GlobalSuppressions.cs and b/Cli-Askpass/GlobalSuppressions.cs differ diff --git a/Cli-Askpass/Program.cs b/Cli-Askpass/Program.cs index a1925c196..1bb380a18 100644 --- a/Cli-Askpass/Program.cs +++ b/Cli-Askpass/Program.cs @@ -35,8 +35,8 @@ namespace Microsoft.Alm.Cli { internal partial class Program { - public const string Title = "Askpass Utility for Windows"; - public const string Description = "Secure askpass utility for Windows, by Microsoft"; + public const string AssemblyTitle = "Askpass Utility for Windows"; + public const string AssemblyDesciption = "Secure askpass utility for Windows, by Microsoft"; public const string DefinitionUrlPassphrase = "https://www.visualstudio.com/docs/git/gcm-ssh-passphrase"; private static readonly Regex AskCredentialRegex = new Regex(@"(\S+)\s+for\s+['""]([^'""]+)['""]:\s*", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); @@ -44,7 +44,12 @@ internal partial class Program private static readonly Regex AskPasswordRegex = new Regex(@"(\S+)'s\s+password:\s*", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); private static readonly Regex AskAuthenticityRegex = new Regex(@"^\s*The authenticity of host '([^']+)' can't be established.\s+RSA key fingerprint is ([^\s:]+:[^\.]+).", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - internal static bool TryParseUrlCredentials(string targetUrl, out string username, out string password) + internal Program() + { + Title = AssemblyTitle; + } + + internal bool TryParseUrlCredentials(string targetUrl, out string username, out string password) { // config stored credentials come in the format of [:]@ // with password being optional scheme terminator is actually "://" so we need @@ -90,7 +95,7 @@ internal static bool TryParseUrlCredentials(string targetUrl, out string usernam return false; } - private static void Askpass(string[] args) + internal void Askpass(string[] args) { if (args == null || args.Length == 0) throw new ArgumentException("Arguments cannot be empty."); @@ -271,8 +276,46 @@ private static void Askpass(string[] args) Die("failed to acquire credentials."); } + internal void PrintHelpMessage() + { + const string HelpFileName = "git-askpass.html"; + + Console.Out.WriteLine("usage: git askpass ''"); + + List installations; + if (Git.Where.FindGitInstallations(out installations)) + { + foreach (var installation in installations) + { + if (Directory.Exists(installation.Doc)) + { + string doc = Path.Combine(installation.Doc, HelpFileName); + + // if the help file exists, send it to the operating system to display to the user + if (File.Exists(doc)) + { + Git.Trace.WriteLine($"opening help documentation '{doc}'."); + + Process.Start(doc); + + return; + } + } + } + } + + Die("Unable to open help documentation."); + } + [STAThread] private static void Main(string[] args) + { + Program program = new Program(); + + program.Run(args); + } + + private void Run(string[] args) { EnableDebugTrace(); @@ -310,36 +353,5 @@ private static void Main(string[] args) Trace.Flush(); } - - private static void PrintHelpMessage() - { - const string HelpFileName = "git-askpass.html"; - - Console.Out.WriteLine("usage: git askpass ''"); - - List installations; - if (Git.Where.FindGitInstallations(out installations)) - { - foreach (var installation in installations) - { - if (Directory.Exists(installation.Doc)) - { - string doc = Path.Combine(installation.Doc, HelpFileName); - - // if the help file exists, send it to the operating system to display to the user - if (File.Exists(doc)) - { - Git.Trace.WriteLine($"opening help documentation '{doc}'."); - - Process.Start(doc); - - return; - } - } - } - } - - Die("Unable to open help documentation."); - } } } diff --git a/Cli-Askpass/Properties/AssemblyInfo.cs b/Cli-Askpass/Properties/AssemblyInfo.cs index 93eafda36..2b36f6ba4 100644 --- a/Cli-Askpass/Properties/AssemblyInfo.cs +++ b/Cli-Askpass/Properties/AssemblyInfo.cs @@ -3,11 +3,11 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -[assembly: AssemblyTitle(Microsoft.Alm.Cli.Program.Title)] -[assembly: AssemblyDescription(Microsoft.Alm.Cli.Program.Title + ". " + Microsoft.Alm.Cli.Program.SourceUrl)] +[assembly: AssemblyTitle(Microsoft.Alm.Cli.Program.AssemblyTitle)] +[assembly: AssemblyDescription(Microsoft.Alm.Cli.Program.AssemblyDesciption + ". " + Microsoft.Alm.Cli.Program.SourceUrl)] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft Corporation")] -[assembly: AssemblyProduct(Microsoft.Alm.Cli.Program.Title + " command line interface")] +[assembly: AssemblyProduct(Microsoft.Alm.Cli.Program.AssemblyTitle + " command line interface.")] [assembly: AssemblyCopyright("Copyright © Microsoft Corporation 2017. All rights reserved.")] [assembly: AssemblyTrademark("Microsoft Corporation")] [assembly: AssemblyCulture("")] diff --git a/Cli-CredentialHelper.Test/ProgramTests.cs b/Cli-CredentialHelper.Test/ProgramTests.cs index 2c6a05b30..217a81322 100644 --- a/Cli-CredentialHelper.Test/ProgramTests.cs +++ b/Cli-CredentialHelper.Test/ProgramTests.cs @@ -36,9 +36,11 @@ public class ProgramTests [TestMethod] public void LoadOperationArgumentsTest() { - Program._dieExceptionCallback = (Exception e) => Assert.Fail($"Error: {e.ToString()}"); - Program._dieMessageCallback = (string m) => Assert.Fail($"Error: {m}"); - Program._exitCallback = (int e, string m) => Assert.Fail($"Error: {e} {m}"); + Program program = new Program(); + + program._dieException = (Program caller, Exception e, string path, int line, string name) => Assert.Fail($"Error: {e.ToString()}"); + program._dieMessage = (Program caller, string m, string path, int line, string name) => Assert.Fail($"Error: {m}"); + program._exit = (Program caller, int e, string m, string path, int line, string name) => Assert.Fail($"Error: {e} {m}"); var envvars = new Dictionary(StringComparer.OrdinalIgnoreCase) { @@ -60,7 +62,7 @@ public void LoadOperationArgumentsTest() var opargs = opargsMock.Object; - Program.LoadOperationArguments(opargs); + program.LoadOperationArguments(opargs); Assert.IsNotNull(opargs); } @@ -89,10 +91,12 @@ public void TryReadBooleanTest() opargsMock.Setup(r => r.QueryUri) .Returns(targetUri); - Assert.IsFalse(Program.TryReadBoolean(opargsMock.Object, "notFound", "notFound", out yesno)); + Program program = new Program(); + + Assert.IsFalse(CommonFunctions.TryReadBoolean(program, opargsMock.Object, "notFound", "notFound", out yesno)); Assert.IsFalse(yesno.HasValue); - Assert.IsTrue(Program.TryReadBoolean(opargsMock.Object, Program.ConfigPreserveCredentialsKey, Program.EnvironPreserveCredentialsKey, out yesno)); + Assert.IsTrue(CommonFunctions.TryReadBoolean(program, opargsMock.Object, Program.ConfigPreserveCredentialsKey, Program.EnvironPreserveCredentialsKey, out yesno)); Assert.IsTrue(yesno.HasValue); Assert.IsFalse(yesno.Value); @@ -104,7 +108,7 @@ public void TryReadBooleanTest() opargsMock.Setup(r => r.EnvironmentVariables) .Returns(envvars); - Assert.IsTrue(Program.TryReadBoolean(opargsMock.Object, Program.ConfigPreserveCredentialsKey, Program.EnvironPreserveCredentialsKey, out yesno)); + Assert.IsTrue(CommonFunctions.TryReadBoolean(program, opargsMock.Object, Program.ConfigPreserveCredentialsKey, Program.EnvironPreserveCredentialsKey, out yesno)); Assert.IsTrue(yesno.HasValue); Assert.IsTrue(yesno.Value); @@ -116,7 +120,7 @@ public void TryReadBooleanTest() opargsMock.Setup(r => r.EnvironmentVariables) .Returns(envvars); - Assert.IsFalse(Program.TryReadBoolean(opargsMock.Object, Program.ConfigPreserveCredentialsKey, Program.EnvironPreserveCredentialsKey, out yesno)); + Assert.IsFalse(CommonFunctions.TryReadBoolean(program, opargsMock.Object, Program.ConfigPreserveCredentialsKey, Program.EnvironPreserveCredentialsKey, out yesno)); Assert.IsFalse(yesno.HasValue); } } diff --git a/Cli-CredentialHelper/GlobalSuppressions.cs b/Cli-CredentialHelper/GlobalSuppressions.cs index 48a403b5a..0e8419626 100644 Binary files a/Cli-CredentialHelper/GlobalSuppressions.cs and b/Cli-CredentialHelper/GlobalSuppressions.cs differ diff --git a/Cli-CredentialHelper/Installer.cs b/Cli-CredentialHelper/Installer.cs index 3b6ccfcff..61b174af8 100644 --- a/Cli-CredentialHelper/Installer.cs +++ b/Cli-CredentialHelper/Installer.cs @@ -61,8 +61,10 @@ internal class Installer "git-credential-manager.html", }; - public Installer() + public Installer(Program program) { + _program = program; + var args = Environment.GetCommandLineArgs(); // parse arguments @@ -93,7 +95,24 @@ public Installer() } } - internal static string CygwinPath + private string _customPath = null; + private string _cygwinPath; + private bool _isPassive = false; + private bool _isForced = false; + private Program _program; + private TextWriter _stdout = null; + private TextWriter _stderr = null; + private string _userBinPath = null; + + public int ExitCode + { + get { return (int)Result; } + set { Result = (ResultValue)value; } + } + + public ResultValue Result { get; private set; } + + internal string CygwinPath { get { @@ -130,9 +149,12 @@ internal static string CygwinPath } } - private static string _cygwinPath; + internal Program Program + { + get { return _program; } + } - internal static string UserBinPath + internal string UserBinPath { get { @@ -173,22 +195,6 @@ internal static string UserBinPath } } - private static string _userBinPath = null; - - public int ExitCode - { - get { return (int)Result; } - set { Result = (ResultValue)value; } - } - - public ResultValue Result { get; private set; } - - private bool _isPassive = false; - private bool _isForced = false; - private string _customPath = null; - private TextWriter _stdout = null; - private TextWriter _stderr = null; - public void DeployConsole() { SetOutput(_isPassive, _isPassive && _isForced); @@ -426,7 +432,7 @@ public void DeployConsole() } } - public static bool DetectNetFx(out Version version) + public bool DetectNetFx(out Version version) { const string NetFxKeyBase = @"HKEY_LOCAL_MACHINE\Software\Microsoft\Net Framework Setup\NDP\v4\"; const string NetFxKeyClient = NetFxKeyBase + @"\Client"; @@ -441,9 +447,9 @@ public static bool DetectNetFx(out Version version) Version netfxVerson = null; // query for existing installations of .NET - if ((netfxString = Registry.GetValue(NetFxKeyClient, ValueName, DefaultValue) as String) != null + if ((netfxString = Registry.GetValue(NetFxKeyClient, ValueName, DefaultValue) as string) != null && Version.TryParse(netfxString, out netfxVerson) - || (netfxString = Registry.GetValue(NetFxKeyFull, ValueName, DefaultValue) as String) != null + || (netfxString = Registry.GetValue(NetFxKeyFull, ValueName, DefaultValue) as string) != null && Version.TryParse(netfxString, out netfxVerson)) { Program.LogEvent($"NetFx version {netfxVerson.ToString(3)} detected.", EventLogEntryType.Information); diff --git a/Cli-CredentialHelper/Program.cs b/Cli-CredentialHelper/Program.cs index 33264095e..40e15e72b 100644 --- a/Cli-CredentialHelper/Program.cs +++ b/Cli-CredentialHelper/Program.cs @@ -36,8 +36,8 @@ namespace Microsoft.Alm.Cli [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults")] internal partial class Program { - public const string Title = "Git Credential Manager for Windows"; - public const string Description = "Secure Git credential helper for Windows, by Microsoft"; + public const string AssemblyTitle = "Git Credential Manager for Windows"; + public const string AssemblyDescription = "Secure Git credential helper for Windows, by Microsoft"; internal const string CommandApprove = "approve"; internal const string CommandClear = "clear"; @@ -70,7 +70,12 @@ internal partial class Program CommandVersion }; - private static void Clear() + internal Program() + { + Title = AssemblyTitle; + } + + internal void Clear() { var args = Environment.GetCommandLineArgs(); string url = null; @@ -87,7 +92,7 @@ private static void Clear() Git.Trace.WriteLine("prompting user for url."); - Program.WriteLine(" Target Url:"); + WriteLine(" Target Url:"); url = Console.In.ReadLine(); } else @@ -120,10 +125,10 @@ private static void Clear() return; } - Program.WriteLine(" credentials are protected by preserve flag, clear anyways? [Y]es, [N]o."); + WriteLine(" credentials are protected by preserve flag, clear anyways? [Y]es, [N]o."); ConsoleKeyInfo key; - while ((key = Program.ReadKey(true)).Key != ConsoleKey.Escape) + while ((key = ReadKey(true)).Key != ConsoleKey.Escape) { if (key.KeyChar == 'N' || key.KeyChar == 'n') return; @@ -141,7 +146,7 @@ private static void Clear() } } - private static void Delete() + internal void Delete() { string[] args = Environment.GetCommandLineArgs(); @@ -207,17 +212,17 @@ private static void Delete() Die("Unable to parse target URI."); } - private static void Deploy() + internal void Deploy() { - var installer = new Installer(); + var installer = new Installer(this); installer.DeployConsole(); Git.Trace.WriteLine($"Installer result = '{installer.Result}', exit code = {installer.ExitCode}."); - Program.Exit(installer.ExitCode); + Exit(installer.ExitCode); } - private static void Erase() + internal void Erase() { // parse the operations arguments from stdin (this is how git sends commands) // see: https://www.kernel.org/pub/software/scm/git/docs/technical/api-credentials.html @@ -242,7 +247,7 @@ private static void Erase() } } - private static void Get() + internal void Get() { // parse the operations arguments from stdin (this is how git sends commands) // see: https://www.kernel.org/pub/software/scm/git/docs/technical/api-credentials.html @@ -269,71 +274,11 @@ private static void Get() } } - [STAThread] - private static void Main(string[] args) - { - try - { - EnableDebugTrace(); - - if (args.Length == 0 - || string.Equals(args[0], "--help", StringComparison.OrdinalIgnoreCase) - || string.Equals(args[0], "-h", StringComparison.OrdinalIgnoreCase) - || args[0].Contains('?')) - { - PrintHelpMessage(); - return; - } - - PrintArgs(args); - - // list of arg => method associations (case-insensitive) - Dictionary actions = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { CommandApprove, Store }, - { CommandClear, Clear }, - { CommandDelete, Delete }, - { CommandDeploy, Deploy }, - { CommandErase, Erase }, - { CommandFill, Get }, - { CommandGet, Get }, - { CommandInstall, Deploy }, - { CommandReject, Erase }, - { CommandRemove, Remove }, - { CommandStore, Store }, - { CommandUninstall, Remove }, - { CommandVersion, PrintVersion }, - }; - - // invoke action specified by arg0 - if (actions.ContainsKey(args[0])) - { - actions[args[0]](); - } - } - catch (AggregateException exception) - { - // print out more useful information when an `AggregateException` is encountered - exception = exception.Flatten(); - - // find the first inner exception which isn't an `AggregateException` with fallback - // to the canonical `.InnerException` - Exception innerException = exception.InnerExceptions.FirstOrDefault(e => !(e is AggregateException)) - ?? exception.InnerException; - - Die(innerException); - } - catch (Exception exception) - { - Die(exception); - } - } - - private static void PrintHelpMessage() + internal void PrintHelpMessage() { const string HelpFileName = "git-credential-manager.html"; - Program.WriteLine("usage: git credential-manager [" + string.Join("|", CommandList) + "] []"); + WriteLine("usage: git credential-manager [" + string.Join("|", CommandList) + "] []"); List installations; if (Git.Where.FindGitInstallations(out installations)) @@ -360,17 +305,17 @@ private static void PrintHelpMessage() Die("Unable to open help documentation."); } - private static void Remove() + internal void Remove() { - var installer = new Installer(); + var installer = new Installer(this); installer.RemoveConsole(); Git.Trace.WriteLine($"Installer result = {installer.Result}, exit code = {installer.ExitCode}."); - Program.Exit(installer.ExitCode); + Exit(installer.ExitCode); } - private static void Store() + internal void Store() { // parse the operations arguments from stdin (this is how git sends commands) // see: https://www.kernel.org/pub/software/scm/git/docs/technical/api-credentials.html @@ -414,5 +359,72 @@ private static void Store() authentication.SetCredentials(operationArguments.TargetUri, credentials); } } + + [STAThread] + private static void Main(string[] args) + { + var program = new Program(); + + program.Run(args); + } + + private void Run(string[] args) + { + try + { + EnableDebugTrace(); + + if (args.Length == 0 + || string.Equals(args[0], "--help", StringComparison.OrdinalIgnoreCase) + || string.Equals(args[0], "-h", StringComparison.OrdinalIgnoreCase) + || args[0].Contains('?')) + { + PrintHelpMessage(); + return; + } + + PrintArgs(args); + + // list of arg => method associations (case-insensitive) + Dictionary actions = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { CommandApprove, Store }, + { CommandClear, Clear }, + { CommandDelete, Delete }, + { CommandDeploy, Deploy }, + { CommandErase, Erase }, + { CommandFill, Get }, + { CommandGet, Get }, + { CommandInstall, Deploy }, + { CommandReject, Erase }, + { CommandRemove, Remove }, + { CommandStore, Store }, + { CommandUninstall, Remove }, + { CommandVersion, PrintVersion }, + }; + + // invoke action specified by arg0 + if (actions.ContainsKey(args[0])) + { + actions[args[0]](); + } + } + catch (AggregateException exception) + { + // print out more useful information when an `AggregateException` is encountered + exception = exception.Flatten(); + + // find the first inner exception which isn't an `AggregateException` with fallback + // to the canonical `.InnerException` + Exception innerException = exception.InnerExceptions.FirstOrDefault(e => !(e is AggregateException)) + ?? exception.InnerException; + + Die(innerException); + } + catch (Exception exception) + { + Die(exception); + } + } } } diff --git a/Cli-CredentialHelper/Properties/AssemblyInfo.cs b/Cli-CredentialHelper/Properties/AssemblyInfo.cs index 1d2e51ceb..2acffcfa7 100644 --- a/Cli-CredentialHelper/Properties/AssemblyInfo.cs +++ b/Cli-CredentialHelper/Properties/AssemblyInfo.cs @@ -3,11 +3,11 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -[assembly: AssemblyTitle(Microsoft.Alm.Cli.Program.Title)] -[assembly: AssemblyDescription(Microsoft.Alm.Cli.Program.Title + ". " + Microsoft.Alm.Cli.Program.SourceUrl)] +[assembly: AssemblyTitle(Microsoft.Alm.Cli.Program.AssemblyTitle)] +[assembly: AssemblyDescription(Microsoft.Alm.Cli.Program.AssemblyDescription + ". " + Microsoft.Alm.Cli.Program.SourceUrl)] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft Corporation")] -[assembly: AssemblyProduct(Microsoft.Alm.Cli.Program.Title + " command line interface")] +[assembly: AssemblyProduct(Microsoft.Alm.Cli.Program.AssemblyTitle + " command line interface.")] [assembly: AssemblyCopyright("Copyright © Microsoft Corporation 2017. All rights reserved.")] [assembly: AssemblyTrademark("Microsoft Corporation")] [assembly: AssemblyCulture("")] diff --git a/Cli-Shared/Cli-Shared.projitems b/Cli-Shared/Cli-Shared.projitems index f472ae38f..c8c32bbc5 100644 --- a/Cli-Shared/Cli-Shared.projitems +++ b/Cli-Shared/Cli-Shared.projitems @@ -14,5 +14,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/Cli-Shared/Delegates.cs b/Cli-Shared/Delegates.cs new file mode 100644 index 000000000..c71bbe013 --- /dev/null +++ b/Cli-Shared/Delegates.cs @@ -0,0 +1,73 @@ +/**** Git Credential Manager for Windows **** + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the """"Software""""), to deal + * in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." +**/ + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Alm.Authentication; +using Bitbucket = Atlassian.Bitbucket.Authentication; +using Github = GitHub.Authentication; + +namespace Microsoft.Alm.Cli +{ + partial class Program + { + internal delegate Credential BasicCredentialPromptDelegate(Program program, TargetUri targetUri, string titleMessage); + internal delegate bool BitbucketCredentialPromptDelegate(Program program, string titleMessage, TargetUri targetUri, out string username, out string password); + internal delegate bool BitbucketOAuthPromptDelegate(Program program, string title, TargetUri targetUri, Bitbucket.AuthenticationResultType resultType, string username); + internal delegate Task CreateAuthenticationDelegate(Program program, OperationArguments operationArguments); + internal delegate void DeleteCredentialsDelegate(Program program, OperationArguments operationArguments); + internal delegate void DieExceptionDelegate(Program program, Exception exception, string path, int line, string name); + internal delegate void DieMessageDelegate(Program program, string message, string path, int line, string name); + internal delegate void EnableTraceLoggingDelegate(Program program, OperationArguments operationArguments); + internal delegate void EnableTraceLoggingFileDelegate(Program program, OperationArguments operationArguments, string logFilePath); + internal delegate void ExitDelegate(Program program, int exitcode, string message, string path, int line, string name); + internal delegate bool GitHubAuthCodePromptDelegate(Program program, TargetUri targetUri, Github.GitHubAuthenticationResultType resultType, string username, out string authenticationCode); + internal delegate bool GitHubCredentialPromptDelegate(Program program, TargetUri targetUri, out string username, out string password); + internal delegate void LoadOperationArgumentsDelegate(Program program, OperationArguments operationArguments); + internal delegate void LogEventDelegate(Program program, string message, EventLogEntryType eventType); + internal delegate bool ModalPromptDisplayDialogDelegate(Program program, + ref NativeMethods.CredentialUiInfo credUiInfo, + ref NativeMethods.CredentialPackFlags authPackage, + IntPtr packedAuthBufferPtr, + uint packedAuthBufferSize, + IntPtr inBufferPtr, + int inBufferSize, + bool saveCredentials, + NativeMethods.CredentialUiWindowsFlags flags, + out string username, + out string password); + internal delegate Credential ModalPromptForCredentialsDelegate(Program program, TargetUri targetUri, string message); + internal delegate Credential ModalPromptForPasswordDelegate(Program program, TargetUri targetUri, string message, string username); + internal delegate void PrintArgsDelegate(Program program, string[] args); + internal delegate Credential QueryCredentialsDelegate(Program program, OperationArguments operationArguments); + internal delegate ConsoleKeyInfo ReadKeyDelegate(Program program, bool intercept); + internal delegate bool StandardHandleIsTtyDelegate(Program program, NativeMethods.StandardHandleType handleType); + internal delegate bool TryReadBooleanDelegate(Program program, OperationArguments operationArguments, string configKey, string environKey, out bool? value); + internal delegate bool TryReadStringDelegate(Program program, OperationArguments operationArguments, string configKey, string environKey, out string value); + internal delegate void WriteDelegate(Program program, string message); + internal delegate void WriteLineDelegate(Program program, string message); + } +} diff --git a/Cli-Shared/Functions/Bitbucket.cs b/Cli-Shared/Functions/Bitbucket.cs new file mode 100644 index 000000000..5bd203607 --- /dev/null +++ b/Cli-Shared/Functions/Bitbucket.cs @@ -0,0 +1,103 @@ +/**** Git Credential Manager for Windows **** + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the """"Software""""), to deal + * in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." +**/ + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Alm.Authentication; +using Bitbucket = Atlassian.Bitbucket.Authentication; + +namespace Microsoft.Alm.Cli +{ + internal static class BitbucketFunctions + { + public static bool CredentialPrompt(Program program, string titleMessage, TargetUri targetUri, out string username, out string password) + { + Credential credential; + if ((credential = program.BasicCredentialPrompt(targetUri, titleMessage)) != null) + { + username = credential.Username; + password = credential.Password; + + return true; + } + + username = null; + password = null; + + return false; + } + + public static bool OAuthPrompt(Program program, string title, TargetUri targetUri, Bitbucket.AuthenticationResultType resultType, string username) + { + const int BufferReadSize = 16 * 1024; + + Debug.Assert(targetUri != null); + + var buffer = new StringBuilder(BufferReadSize); + uint read = 0; + uint written = 0; + + string accessToken = null; + + var fileAccessFlags = NativeMethods.FileAccess.GenericRead | NativeMethods.FileAccess.GenericWrite; + var fileAttributes = NativeMethods.FileAttributes.Normal; + var fileCreationDisposition = NativeMethods.FileCreationDisposition.OpenExisting; + var fileShareFlags = NativeMethods.FileShare.Read | NativeMethods.FileShare.Write; + + using (var stdout = NativeMethods.CreateFile(NativeMethods.ConsoleOutName, fileAccessFlags, fileShareFlags, + IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) + { + using (var stdin = NativeMethods.CreateFile(NativeMethods.ConsoleInName, fileAccessFlags, fileShareFlags, + IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) + { + buffer.AppendLine() + .Append(title) + .Append(" OAuth Access Token: "); + + if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) + { + var error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + buffer.Clear(); + + // read input from the user + if (!NativeMethods.ReadConsole(stdin, buffer, BufferReadSize, out read, IntPtr.Zero)) + { + var error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to read from standard input (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + + accessToken = buffer.ToString(0, (int)read); + accessToken = accessToken.Trim(Program.NewLineChars); + } + } + return accessToken != null; + } + } +} diff --git a/Cli-Shared/Functions/Common.cs b/Cli-Shared/Functions/Common.cs new file mode 100644 index 000000000..32bafd803 --- /dev/null +++ b/Cli-Shared/Functions/Common.cs @@ -0,0 +1,762 @@ +/**** Git Credential Manager for Windows **** + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the """"Software""""), to deal + * in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." +**/ + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Alm.Authentication; +using Microsoft.Alm.Git; +using Bitbucket = Atlassian.Bitbucket.Authentication; +using Github = GitHub.Authentication; + +namespace Microsoft.Alm.Cli +{ + internal static class CommonFunctions + { + public static async Task CreateAuthentication(Program program, OperationArguments operationArguments) + { + Debug.Assert(operationArguments != null, "The operationArguments is null"); + Debug.Assert(operationArguments.TargetUri != null, "The operationArgument.TargetUri is null"); + + var secretsNamespace = operationArguments.CustomNamespace ?? Program.SecretsNamespace; + var secrets = new SecretStore(secretsNamespace, null, null, Secret.UriToName); + BaseAuthentication authority = null; + + var basicCredentialCallback = (operationArguments.UseModalUi) + ? new AcquireCredentialsDelegate(program.ModalPromptForCredentials) + : new AcquireCredentialsDelegate(program.BasicCredentialPrompt); + + var bitbucketCredentialCallback = (operationArguments.UseModalUi) + ? Bitbucket.AuthenticationPrompts.CredentialModalPrompt + : new Bitbucket.Authentication.AcquireCredentialsDelegate(program.BitbucketCredentialPrompt); + + var bitbucketOauthCallback = (operationArguments.UseModalUi) + ? Bitbucket.AuthenticationPrompts.AuthenticationOAuthModalPrompt + : new Bitbucket.Authentication.AcquireAuthenticationOAuthDelegate(program.BitbucketOAuthPrompt); + + var githubCredentialCallback = (operationArguments.UseModalUi) + ? new Github.Authentication.AcquireCredentialsDelegate(Github.AuthenticationPrompts.CredentialModalPrompt) + : new Github.Authentication.AcquireCredentialsDelegate(program.GitHubCredentialPrompt); + + var githubAuthcodeCallback = (operationArguments.UseModalUi) + ? new Github.Authentication.AcquireAuthenticationCodeDelegate(Github.AuthenticationPrompts.AuthenticationCodeModalPrompt) + : new Github.Authentication.AcquireAuthenticationCodeDelegate(program.GitHubAuthCodePrompt); + + NtlmSupport basicNtlmSupport = NtlmSupport.Auto; + + switch (operationArguments.Authority) + { + case AuthorityType.Auto: + Git.Trace.WriteLine($"detecting authority type for '{operationArguments.TargetUri}'."); + + // detect the authority + authority = await BaseVstsAuthentication.GetAuthentication(operationArguments.TargetUri, + Program.VstsCredentialScope, + secrets) + ?? Github.Authentication.GetAuthentication(operationArguments.TargetUri, + Program.GitHubCredentialScope, + secrets, + githubCredentialCallback, + githubAuthcodeCallback, + null) + ?? Bitbucket.Authentication.GetAuthentication(operationArguments.TargetUri, + new SecretStore(secretsNamespace, Secret.UriToActualUrl), + bitbucketCredentialCallback, + bitbucketOauthCallback); + + if (authority != null) + { + // set the authority type based on the returned value + if (authority is VstsMsaAuthentication) + { + operationArguments.Authority = AuthorityType.MicrosoftAccount; + goto case AuthorityType.MicrosoftAccount; + } + else if (authority is VstsAadAuthentication) + { + operationArguments.Authority = AuthorityType.AzureDirectory; + goto case AuthorityType.AzureDirectory; + } + else if (authority is Github.Authentication) + { + operationArguments.Authority = AuthorityType.GitHub; + goto case AuthorityType.GitHub; + } + else if (authority is Bitbucket.Authentication) + { + operationArguments.Authority = AuthorityType.Bitbucket; + goto case AuthorityType.Bitbucket; + } + } + goto default; + + case AuthorityType.AzureDirectory: + Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is Azure Directory."); + + Guid tenantId = Guid.Empty; + + // Get the identity of the tenant. + var result = await BaseVstsAuthentication.DetectAuthority(operationArguments.TargetUri); + + if (result.Key) + { + tenantId = result.Value; + } + + // return the allocated authority or a generic AAD backed VSTS authentication object + return authority ?? new VstsAadAuthentication(tenantId, Program.VstsCredentialScope, secrets); + + case AuthorityType.Basic: + // enforce basic authentication only + basicNtlmSupport = NtlmSupport.Never; + goto default; + + case AuthorityType.GitHub: + Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is GitHub."); + + // return a GitHub authentication object + return authority ?? new Github.Authentication(operationArguments.TargetUri, + Program.GitHubCredentialScope, + secrets, + githubCredentialCallback, + githubAuthcodeCallback, + null); + + case AuthorityType.Bitbucket: + Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is Bitbucket"); + + // return a Bitbucket authentication object + return authority ?? new Bitbucket.Authentication(secrets, + bitbucketCredentialCallback, + bitbucketOauthCallback); + + case AuthorityType.MicrosoftAccount: + Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is Microsoft Live."); + + // return the allocated authority or a generic MSA backed VSTS authentication object + return authority ?? new VstsMsaAuthentication(Program.VstsCredentialScope, secrets); + + case AuthorityType.Ntlm: + // enforce NTLM authentication only + basicNtlmSupport = NtlmSupport.Always; + goto default; + + default: + Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is basic with NTLM={basicNtlmSupport}."); + + // return a generic username + password authentication object + return authority ?? new BasicAuthentication(secrets, basicNtlmSupport, basicCredentialCallback, null); + } + } + + public static void DeleteCredentials(Program program, OperationArguments operationArguments) + { + if (ReferenceEquals(operationArguments, null)) + throw new ArgumentNullException("operationArguments"); + + var task = Task.Run(async () => { return await program.CreateAuthentication(operationArguments); }); + + BaseAuthentication authentication = task.Result; + + switch (operationArguments.Authority) + { + default: + case AuthorityType.Basic: + Git.Trace.WriteLine($"deleting basic credentials for '{operationArguments.TargetUri}'."); + authentication.DeleteCredentials(operationArguments.TargetUri); + break; + + case AuthorityType.AzureDirectory: + case AuthorityType.MicrosoftAccount: + Git.Trace.WriteLine($"deleting VSTS credentials for '{operationArguments.TargetUri}'."); + BaseVstsAuthentication vstsAuth = authentication as BaseVstsAuthentication; + vstsAuth.DeleteCredentials(operationArguments.TargetUri); + break; + + case AuthorityType.GitHub: + Git.Trace.WriteLine($"deleting GitHub credentials for '{operationArguments.TargetUri}'."); + Github.Authentication ghAuth = authentication as Github.Authentication; + ghAuth.DeleteCredentials(operationArguments.TargetUri); + break; + + case AuthorityType.Bitbucket: + Git.Trace.WriteLine($"deleting Bitbucket credentials for '{operationArguments.TargetUri}'."); + var bbAuth = authentication as Bitbucket.Authentication; + bbAuth.DeleteCredentials(operationArguments.TargetUri, operationArguments.CredUsername); + break; + } + } + + public static void DieException(Program program, Exception exception, string path, int line, string name) + { + Git.Trace.WriteLine(exception.ToString(), path, line, name); + program.LogEvent(exception.ToString(), EventLogEntryType.Error); + + string message; + if (!string.IsNullOrWhiteSpace(exception.Message)) + { + message = $"{exception.GetType().Name} encountered.\n {exception.Message}"; + } + else + { + message = $"{exception.GetType().Name} encountered."; + } + + program.Die(message, path, line, name); + } + + public static void DieMessage(Program program, string message, string path, int line, string name) + { + message = $"fatal: {message}"; + + program.Exit(-1, message, path, line, name); + } + + public static void EnableTraceLogging(Program program, OperationArguments operationArguments) + { + if (operationArguments.WriteLog) + { + Git.Trace.WriteLine("trace logging enabled."); + + string gitConfigPath; + if (Where.GitLocalConfig(out gitConfigPath)) + { + Git.Trace.WriteLine($"git local config found at '{gitConfigPath}'."); + + string gitDirPath = Path.GetDirectoryName(gitConfigPath); + + if (Directory.Exists(gitDirPath)) + { + program.EnableTraceLogging(operationArguments, gitDirPath); + } + } + else if (Where.GitGlobalConfig(out gitConfigPath)) + { + Git.Trace.WriteLine($"git global config found at '{gitConfigPath}'."); + + string homeDirPath = Path.GetDirectoryName(gitConfigPath); + + if (Directory.Exists(homeDirPath)) + { + program.EnableTraceLogging(operationArguments, homeDirPath); + } + } + } +#if DEBUG + Git.Trace.WriteLine($"GCM arguments:{Environment.NewLine}{operationArguments}"); +#endif + } + + public static void EnableTraceLoggingFile(Program program, OperationArguments operationArguments, string logFilePath) + { + const int LogFileMaxLength = 8 * 1024 * 1024; // 8 MB + + string logFileName = Path.Combine(logFilePath, Path.ChangeExtension(Program.ConfigPrefix, ".log")); + + FileInfo logFileInfo = new FileInfo(logFileName); + if (logFileInfo.Exists && logFileInfo.Length > LogFileMaxLength) + { + for (int i = 1; i < int.MaxValue; i++) + { + string moveName = string.Format("{0}{1:000}.log", Program.ConfigPrefix, i); + string movePath = Path.Combine(logFilePath, moveName); + + if (!File.Exists(movePath)) + { + logFileInfo.MoveTo(movePath); + break; + } + } + } + + Git.Trace.WriteLine($"trace log destination is '{logFilePath}'."); + + using (var fileStream = File.Open(logFileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) + { + var listener = new StreamWriter(fileStream, Encoding.UTF8); + Git.Trace.AddListener(listener); + + // write a small header to help with identifying new log entries + listener.Write('\n'); + listener.Write($"{DateTime.Now:yyyy.MM.dd HH:mm:ss} Microsoft {program.Title} version {program.Version.ToString(3)}\n"); + } + } + + public static void LoadOperationArguments(Program program, OperationArguments operationArguments) + { + if (operationArguments.TargetUri == null) + { + program.Die("No host information, unable to continue."); + } + + string value; + bool? yesno; + + if (program.TryReadBoolean(operationArguments, null, Program.EnvironConfigNoLocalKey, out yesno)) + { + operationArguments.UseConfigLocal = yesno.Value; + } + + if (program.TryReadBoolean(operationArguments, null, Program.EnvironConfigNoSystemKey, out yesno)) + { + operationArguments.UseConfigSystem = yesno.Value; + } + + // load/re-load the Git configuration after setting the use local/system config values + operationArguments.LoadConfiguration(); + + // if a user-agent has been specified in the environment, set it globally + if (program.TryReadString(operationArguments, null, Program.EnvironHttpUserAgent, out value)) + { + Global.UserAgent = value; + } + + // look for authority settings + if (program.TryReadString(operationArguments, Program.ConfigAuthorityKey, Program.EnvironAuthorityKey, out value)) + { + Git.Trace.WriteLine($"{Program.ConfigAuthorityKey} = '{value}'."); + + if (Program.ConfigKeyComparer.Equals(value, "MSA") + || Program.ConfigKeyComparer.Equals(value, "Microsoft") + || Program.ConfigKeyComparer.Equals(value, "MicrosoftAccount") + || Program.ConfigKeyComparer.Equals(value, "Live") + || Program.ConfigKeyComparer.Equals(value, "LiveConnect") + || Program.ConfigKeyComparer.Equals(value, "LiveID")) + { + operationArguments.Authority = AuthorityType.MicrosoftAccount; + } + else if (Program.ConfigKeyComparer.Equals(value, "AAD") + || Program.ConfigKeyComparer.Equals(value, "Azure") + || Program.ConfigKeyComparer.Equals(value, "AzureDirectory")) + { + operationArguments.Authority = AuthorityType.AzureDirectory; + } + else if (Program.ConfigKeyComparer.Equals(value, "Integrated") + || Program.ConfigKeyComparer.Equals(value, "Windows") + || Program.ConfigKeyComparer.Equals(value, "TFS") + || Program.ConfigKeyComparer.Equals(value, "Kerberos") + || Program.ConfigKeyComparer.Equals(value, "NTLM") + || Program.ConfigKeyComparer.Equals(value, "SSO")) + { + operationArguments.Authority = AuthorityType.Ntlm; + } + else if (Program.ConfigKeyComparer.Equals(value, "GitHub")) + { + operationArguments.Authority = AuthorityType.GitHub; + } + else + { + operationArguments.Authority = AuthorityType.Basic; + } + } + + // look for interactivity config settings + if (program.TryReadString(operationArguments, Program.ConfigInteractiveKey, Program.EnvironInteractiveKey, out value)) + { + Git.Trace.WriteLine($"{Program.EnvironInteractiveKey} = '{value}'."); + + if (Program.ConfigKeyComparer.Equals(value, "always") + || Program.ConfigKeyComparer.Equals(value, "true") + || Program.ConfigKeyComparer.Equals(value, "force")) + { + operationArguments.Interactivity = Interactivity.Always; + } + else if (Program.ConfigKeyComparer.Equals(value, "never") + || Program.ConfigKeyComparer.Equals(value, "false")) + { + operationArguments.Interactivity = Interactivity.Never; + } + } + + // look for credential validation config settings + if (program.TryReadBoolean(operationArguments, Program.ConfigValidateKey, Program.EnvironValidateKey, out yesno)) + { + operationArguments.ValidateCredentials = yesno.Value; + } + + // look for write log config settings + if (program.TryReadBoolean(operationArguments, Program.ConfigWritelogKey, Program.EnvironWritelogKey, out yesno)) + { + operationArguments.WriteLog = yesno.Value; + } + + // look for modal prompt config settings + if (program.TryReadBoolean(operationArguments, Program.ConfigUseModalPromptKey, Program.EnvironModalPromptKey, out yesno)) + { + operationArguments.UseModalUi = yesno.Value; + } + + // look for credential preservation config settings + if (program.TryReadBoolean(operationArguments, Program.ConfigPreserveCredentialsKey, Program.EnvironPreserveCredentialsKey, out yesno)) + { + operationArguments.PreserveCredentials = yesno.Value; + } + + // look for http path usage config settings + if (program.TryReadBoolean(operationArguments, Program.ConfigUseHttpPathKey, null, out yesno)) + { + operationArguments.UseHttpPath = yesno.Value; + } + + // look for http proxy config settings + if (program.TryReadString(operationArguments, Program.ConfigHttpProxyKey, Program.EnvironHttpProxyKey, out value)) + { + Git.Trace.WriteLine($"{Program.ConfigHttpProxyKey} = '{value}'."); + + operationArguments.SetProxy(value); + } + else + { + // check the git-config http.proxy setting just-in-case + Configuration.Entry entry; + if (operationArguments.GitConfiguration.TryGetEntry("http", operationArguments.QueryUri, "proxy", out entry) + && !string.IsNullOrWhiteSpace(entry.Value)) + { + Git.Trace.WriteLine($"http.proxy = '{entry.Value}'."); + + operationArguments.SetProxy(entry.Value); + } + } + + // look for custom namespace config settings + if (program.TryReadString(operationArguments, Program.ConfigNamespaceKey, Program.EnvironNamespaceKey, out value)) + { + Git.Trace.WriteLine($"{Program.ConfigNamespaceKey} = '{value}'."); + + operationArguments.CustomNamespace = value; + } + } + + public static void LogEvent(Program program, string message, EventLogEntryType eventType) + { + /*** try-squelch due to UAC issues which require a proper installer to work around ***/ + + Git.Trace.WriteLine(message); + + try + { + EventLog.WriteEntry(Program.EventSource, message, eventType); + } + catch { /* squelch */ } + } + + public static void PrintArgs(Program program, string[] args) + { + Debug.Assert(args != null, $"The `{nameof(args)}` parameter is null."); + + StringBuilder builder = new StringBuilder(); + builder.Append(program.Name) + .Append(" (v") + .Append(program.Version.ToString(3)) + .Append(")"); + + for (int i = 0; i < args.Length; i += 1) + { + builder.Append(" '") + .Append(args[i]) + .Append("'"); + + if (i + 1 < args.Length) + { + builder.Append(","); + } + } + + // fake being part of the Main method for clarity + Git.Trace.WriteLine(builder.ToString(), memberName: "Main"); + builder = null; + } + + public static Credential QueryCredentials(Program program, OperationArguments operationArguments) + { + if (ReferenceEquals(operationArguments, null)) + throw new ArgumentNullException(nameof(operationArguments)); + if (ReferenceEquals(operationArguments.TargetUri, null)) + throw new ArgumentException("TargetUri property returned null", nameof(operationArguments)); + + var task = Task.Run(async () => { return await program.CreateAuthentication(operationArguments); }); + BaseAuthentication authentication = task.Result; + Credential credentials = null; + + switch (operationArguments.Authority) + { + default: + case AuthorityType.Basic: + { + BasicAuthentication basicAuth = authentication as BasicAuthentication; + + Task.Run(async () => + { + // attempt to get cached creds or acquire creds if interactivity is allowed + if ((operationArguments.Interactivity != Interactivity.Always + && (credentials = authentication.GetCredentials(operationArguments.TargetUri)) != null) + || (operationArguments.Interactivity != Interactivity.Never + && (credentials = await basicAuth.AcquireCredentials(operationArguments.TargetUri)) != null)) + { + Git.Trace.WriteLine("credentials found."); + // no need to save the credentials explicitly, as Git will call back with + // a store command if the credentials are valid. + } + else + { + Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' not found."); + program.LogEvent($"Failed to retrieve credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); + } + }).Wait(); + } + break; + + case AuthorityType.AzureDirectory: + { + VstsAadAuthentication aadAuth = authentication as VstsAadAuthentication; + + Task.Run(async () => + { + // attempt to get cached creds -> non-interactive logon -> interactive + // logon note that AAD "credentials" are always scoped access tokens + if (((operationArguments.Interactivity != Interactivity.Always + && ((credentials = aadAuth.GetCredentials(operationArguments.TargetUri)) != null) + && (!operationArguments.ValidateCredentials + || await aadAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) + || (operationArguments.Interactivity != Interactivity.Always + && ((credentials = await aadAuth.NoninteractiveLogon(operationArguments.TargetUri, true)) != null) + && (!operationArguments.ValidateCredentials + || await aadAuth.ValidateCredentials(operationArguments.TargetUri, credentials))) + || (operationArguments.Interactivity != Interactivity.Never + && ((credentials = await aadAuth.InteractiveLogon(operationArguments.TargetUri, true)) != null) + && (!operationArguments.ValidateCredentials + || await aadAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) + { + Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' found."); + program.LogEvent($"Azure Directory credentials for '{operationArguments.TargetUri}' successfully retrieved.", EventLogEntryType.SuccessAudit); + } + else + { + Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' not found."); + program.LogEvent($"Failed to retrieve Azure Directory credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); + } + }).Wait(); + } + break; + + case AuthorityType.MicrosoftAccount: + { + VstsMsaAuthentication msaAuth = authentication as VstsMsaAuthentication; + + Task.Run(async () => + { + // attempt to get cached creds -> interactive logon note that MSA + // "credentials" are always scoped access tokens + if (((operationArguments.Interactivity != Interactivity.Always + && ((credentials = msaAuth.GetCredentials(operationArguments.TargetUri)) != null) + && (!operationArguments.ValidateCredentials + || await msaAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) + || (operationArguments.Interactivity != Interactivity.Never + && ((credentials = await msaAuth.InteractiveLogon(operationArguments.TargetUri, true)) != null) + && (!operationArguments.ValidateCredentials + || await msaAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) + { + Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' found."); + program.LogEvent($"Microsoft Live credentials for '{operationArguments.TargetUri}' successfully retrieved.", EventLogEntryType.SuccessAudit); + } + else + { + Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' not found."); + program.LogEvent($"Failed to retrieve Microsoft Live credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); + } + }).Wait(); + } + break; + + case AuthorityType.GitHub: + { + Github.Authentication ghAuth = authentication as Github.Authentication; + + Task.Run(async () => + { + if ((operationArguments.Interactivity != Interactivity.Always + && ((credentials = ghAuth.GetCredentials(operationArguments.TargetUri)) != null) + && (!operationArguments.ValidateCredentials + || await ghAuth.ValidateCredentials(operationArguments.TargetUri, credentials))) + || (operationArguments.Interactivity != Interactivity.Never + && ((credentials = await ghAuth.InteractiveLogon(operationArguments.TargetUri)) != null) + && (!operationArguments.ValidateCredentials + || await ghAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) + { + Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' found."); + program.LogEvent($"GitHub credentials for '{operationArguments.TargetUri}' successfully retrieved.", EventLogEntryType.SuccessAudit); + } + else + { + Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' not found."); + program.LogEvent($"Failed to retrieve GitHub credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); + } + }).Wait(); + } + break; + + case AuthorityType.Bitbucket: + { + var bbcAuth = authentication as Bitbucket.Authentication; + + Task.Run(async () => + { + if (((operationArguments.Interactivity != Interactivity.Always) + && ((credentials = bbcAuth.GetCredentials(operationArguments.TargetUri, operationArguments.CredUsername)) != null) + && (!operationArguments.ValidateCredentials + || ((credentials = await bbcAuth.ValidateCredentials(operationArguments.TargetUri, operationArguments.CredUsername, credentials)) != null))) + || ((operationArguments.Interactivity != Interactivity.Never) + && ((credentials = await bbcAuth.InteractiveLogon(operationArguments.TargetUri, operationArguments.CredUsername)) != null) + && (!operationArguments.ValidateCredentials + || ((credentials = await bbcAuth.ValidateCredentials(operationArguments.TargetUri, operationArguments.CredUsername, credentials)) != null)))) + { + Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' found."); + // Bitbucket relies on a username + secret, so make sure there is a + // username to return + if (operationArguments.CredUsername != null) + { + credentials = new Credential(operationArguments.CredUsername, credentials.Password); + } + program.LogEvent($"Bitbucket credentials for '{operationArguments.TargetUri}' successfully retrieved.", EventLogEntryType.SuccessAudit); + } + else + { + program.LogEvent($"Failed to retrieve Bitbucket credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); + } + }).Wait(); + } + break; + + case AuthorityType.Ntlm: + { + Git.Trace.WriteLine($"'{operationArguments.TargetUri}' is NTLM."); + credentials = BasicAuthentication.NtlmCredentials; + } + break; + } + + if (credentials != null) + { + operationArguments.SetCredentials(credentials); + } + + return credentials; + } + + public static bool TryReadBoolean(Program program, OperationArguments operationArguments, string configKey, string environKey, out bool? value) + { + if (ReferenceEquals(operationArguments, null)) + throw new ArgumentNullException(nameof(operationArguments)); + + var envars = operationArguments.EnvironmentVariables; + + // look for an entry in the environment variables + string localVal = null; + if (!string.IsNullOrWhiteSpace(environKey) + && envars.TryGetValue(environKey, out localVal)) + { + goto parse_localval; + } + + var config = operationArguments.GitConfiguration; + + // look for an entry in the git config + Configuration.Entry entry; + if (!string.IsNullOrWhiteSpace(configKey) + && config.TryGetEntry(Program.ConfigPrefix, operationArguments.QueryUri, configKey, out entry)) + { + goto parse_localval; + } + + // parse the value into a bool + parse_localval: + + // An empty value is unset / should not be there, so treat it as if it isn't. + if (string.IsNullOrWhiteSpace(localVal)) + { + value = null; + return false; + } + + // Test `localValue` for a Git 'true' equivalent value + if (Program.ConfigValueComparer.Equals(localVal, "yes") + || Program.ConfigValueComparer.Equals(localVal, "true") + || Program.ConfigValueComparer.Equals(localVal, "1") + || Program.ConfigValueComparer.Equals(localVal, "on")) + { + value = true; + return true; + } + + // Test `localValue` for a Git 'false' equivalent value + if (Program.ConfigValueComparer.Equals(localVal, "no") + || Program.ConfigValueComparer.Equals(localVal, "false") + || Program.ConfigValueComparer.Equals(localVal, "0") + || Program.ConfigValueComparer.Equals(localVal, "off")) + { + value = false; + return true; + } + + value = null; + return false; + } + + public static bool TryReadString(Program program, OperationArguments operationArguments, string configKey, string environKey, out string value) + { + if (ReferenceEquals(operationArguments, null)) + throw new ArgumentNullException(nameof(operationArguments)); + + var envars = operationArguments.EnvironmentVariables; + + // look for an entry in the environment variables + string localVal; + if (!string.IsNullOrWhiteSpace(environKey) + && envars.TryGetValue(environKey, out localVal) + && !string.IsNullOrWhiteSpace(localVal)) + { + value = localVal; + return true; + } + + var config = operationArguments.GitConfiguration; + + // look for an entry in the git config + Configuration.Entry entry; + if (!string.IsNullOrWhiteSpace(configKey) + && config.TryGetEntry(Program.ConfigPrefix, operationArguments.QueryUri, configKey, out entry) + && !string.IsNullOrWhiteSpace(entry.Value)) + { + value = entry.Value; + return true; + } + + value = null; + return false; + } + } +} diff --git a/Cli-Shared/Functions/Console.cs b/Cli-Shared/Functions/Console.cs new file mode 100644 index 000000000..6202a175b --- /dev/null +++ b/Cli-Shared/Functions/Console.cs @@ -0,0 +1,207 @@ +/**** Git Credential Manager for Windows **** + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the """"Software""""), to deal + * in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." +**/ + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Alm.Authentication; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.Alm.Cli +{ + internal static class ConsoleFunctions + { + public static Credential CredentialPrompt(Program program, TargetUri targetUri, string titleMessage) + { + // ReadConsole 32768 fail, 32767 ok @linquize [https://github.com/Microsoft/Git-Credential-Manager-for-Windows/commit/a62b9a19f430d038dcd85a610d97e5f763980f85] + const int BufferReadSize = 16 * 1024; + + Debug.Assert(targetUri != null); + + if (!program.StandardErrorIsTty || !program.StandardInputIsTty) + { + Git.Trace.WriteLine("not a tty detected, abandoning prompt."); + return null; + } + + titleMessage = titleMessage ?? "Please enter your credentials for "; + + StringBuilder buffer = new StringBuilder(BufferReadSize); + uint read = 0; + uint written = 0; + + NativeMethods.FileAccess fileAccessFlags = NativeMethods.FileAccess.GenericRead | NativeMethods.FileAccess.GenericWrite; + NativeMethods.FileAttributes fileAttributes = NativeMethods.FileAttributes.Normal; + NativeMethods.FileCreationDisposition fileCreationDisposition = NativeMethods.FileCreationDisposition.OpenExisting; + NativeMethods.FileShare fileShareFlags = NativeMethods.FileShare.Read | NativeMethods.FileShare.Write; + + using (SafeFileHandle stdout = NativeMethods.CreateFile(NativeMethods.ConsoleOutName, fileAccessFlags, fileShareFlags, IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) + using (SafeFileHandle stdin = NativeMethods.CreateFile(NativeMethods.ConsoleInName, fileAccessFlags, fileShareFlags, IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) + { + string username = null; + string password = null; + + // read the current console mode + NativeMethods.ConsoleMode consoleMode; + if (!NativeMethods.GetConsoleMode(stdin, out consoleMode)) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to determine console mode (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + + Git.Trace.WriteLine($"console mode = '{consoleMode}'."); + + // instruct the user as to what they are expected to do + buffer.Append(titleMessage) + .Append(targetUri) + .AppendLine(); + if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + + // clear the buffer for the next operation + buffer.Clear(); + + // prompt the user for the username wanted + buffer.Append("username: "); + if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + + // clear the buffer for the next operation + buffer.Clear(); + + // read input from the user + if (!NativeMethods.ReadConsole(stdin, buffer, BufferReadSize, out read, IntPtr.Zero)) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to read from standard input (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + + // record input from the user into local storage, stripping any eol chars + username = buffer.ToString(0, (int)read); + username = username.Trim(Environment.NewLine.ToCharArray()); + + // clear the buffer for the next operation + buffer.Clear(); + + // set the console mode to current without echo input + NativeMethods.ConsoleMode consoleMode2 = consoleMode ^ NativeMethods.ConsoleMode.EchoInput; + + try + { + if (!NativeMethods.SetConsoleMode(stdin, consoleMode2)) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to set console mode (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + + Git.Trace.WriteLine($"console mode = '{consoleMode2}'."); + + // prompt the user for password + buffer.Append("password: "); + if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + + // clear the buffer for the next operation + buffer.Clear(); + + // read input from the user + if (!NativeMethods.ReadConsole(stdin, buffer, BufferReadSize, out read, IntPtr.Zero)) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to read from standard input (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + + // record input from the user into local storage, stripping any eol chars + password = buffer.ToString(0, (int)read); + password = password.Trim(Environment.NewLine.ToCharArray()); + } + catch { throw; } + finally + { + // restore the console mode to its original value + NativeMethods.SetConsoleMode(stdin, consoleMode); + + Git.Trace.WriteLine($"console mode = '{consoleMode}'."); + } + + if (username != null && password != null) + return new Credential(username, password); + } + + return null; + } + + public static void Exit(Program program, int exitcode, string message, string path, int line, string name) + { + if (!string.IsNullOrWhiteSpace(message)) + { + Git.Trace.WriteLine(message, path, line, name); + program.WriteLine(message); + } + + Git.Trace.Flush(); + + Environment.Exit(exitcode); + } + + public static ConsoleKeyInfo ReadKey(Program program, bool intercept) + { + return (program.StandardInputIsTty) + ? Console.ReadKey(intercept) + : new ConsoleKeyInfo(' ', ConsoleKey.Escape, false, false, false); + } + + public static bool StandardHandleIsTty(Program program, NativeMethods.StandardHandleType handleType) + { + var standardHandle = NativeMethods.GetStdHandle(handleType); + var handleFileType = NativeMethods.GetFileType(standardHandle); + return handleFileType == NativeMethods.FileType.Char; + } + + public static void Write(Program program, string message) + { + if (message == null) + return; + + Console.Error.WriteLine(message); + } + + public static void WriteLine(Program program, string message) + { + Console.Error.WriteLine(message); + } + } +} diff --git a/Cli-Shared/Functions/Dialog.cs b/Cli-Shared/Functions/Dialog.cs new file mode 100644 index 000000000..bb2f24695 --- /dev/null +++ b/Cli-Shared/Functions/Dialog.cs @@ -0,0 +1,236 @@ +/**** Git Credential Manager for Windows **** + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the """"Software""""), to deal + * in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." +**/ + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Alm.Authentication; + +namespace Microsoft.Alm.Cli +{ + internal static class DialogFunctions + { + public static bool DisplayModal(Program program, + ref NativeMethods.CredentialUiInfo credUiInfo, + ref NativeMethods.CredentialPackFlags authPackage, + IntPtr packedAuthBufferPtr, + uint packedAuthBufferSize, + IntPtr inBufferPtr, + int inBufferSize, + bool saveCredentials, + NativeMethods.CredentialUiWindowsFlags flags, + out string username, + out string password) + { + int error; + + try + { + // open a standard Windows authentication dialog to acquire username + password credentials + if ((error = NativeMethods.CredUIPromptForWindowsCredentials(credInfo: ref credUiInfo, + authError: 0, + authPackage: ref authPackage, + inAuthBuffer: inBufferPtr, + inAuthBufferSize: (uint)inBufferSize, + outAuthBuffer: out packedAuthBufferPtr, + outAuthBufferSize: out packedAuthBufferSize, + saveCredentials: ref saveCredentials, + flags: flags)) != NativeMethods.Win32Error.Success) + { + Git.Trace.WriteLine($"credential prompt failed ('{NativeMethods.Win32Error.GetText(error)}')."); + + username = null; + password = null; + + return false; + } + + // use `StringBuilder` references instead of string so that they can be written to + StringBuilder usernameBuffer = new StringBuilder(512); + StringBuilder domainBuffer = new StringBuilder(256); + StringBuilder passwordBuffer = new StringBuilder(512); + int usernameLen = usernameBuffer.Capacity; + int passwordLen = passwordBuffer.Capacity; + int domainLen = domainBuffer.Capacity; + + // unpack the result into locally useful data + if (!NativeMethods.CredUnPackAuthenticationBuffer(flags: authPackage, + authBuffer: packedAuthBufferPtr, + authBufferSize: packedAuthBufferSize, + username: usernameBuffer, + maxUsernameLen: ref usernameLen, + domainName: domainBuffer, + maxDomainNameLen: ref domainLen, + password: passwordBuffer, + maxPasswordLen: ref passwordLen)) + { + username = null; + password = null; + + error = Marshal.GetLastWin32Error(); + Git.Trace.WriteLine($"failed to unpack buffer ('{NativeMethods.Win32Error.GetText(error)}')."); + + return false; + } + + Git.Trace.WriteLine("successfully acquired credentials from user."); + + username = usernameBuffer.ToString(); + password = passwordBuffer.ToString(); + + return true; + } + finally + { + if (packedAuthBufferPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(packedAuthBufferPtr); + } + } + } + + public static Credential CredentialPrompt(Program program, TargetUri targetUri, string message) + { + Debug.Assert(targetUri != null); + Debug.Assert(message != null); + + NativeMethods.CredentialUiInfo credUiInfo = new NativeMethods.CredentialUiInfo + { + BannerArt = IntPtr.Zero, + CaptionText = program.Title, + MessageText = message, + Parent = IntPtr.Zero, + Size = Marshal.SizeOf(typeof(NativeMethods.CredentialUiInfo)) + }; + NativeMethods.CredentialUiWindowsFlags flags = NativeMethods.CredentialUiWindowsFlags.Generic; + NativeMethods.CredentialPackFlags authPackage = NativeMethods.CredentialPackFlags.None; + IntPtr packedAuthBufferPtr = IntPtr.Zero; + IntPtr inBufferPtr = IntPtr.Zero; + uint packedAuthBufferSize = 0; + bool saveCredentials = false; + int inBufferSize = 0; + string username; + string password; + + if (program.ModalPromptDisplayDialog(ref credUiInfo, + ref authPackage, + packedAuthBufferPtr, + packedAuthBufferSize, + inBufferPtr, + inBufferSize, + saveCredentials, + flags, + out username, + out password)) + { + return new Credential(username, password); + } + + return null; + } + + public static Credential PasswordPrompt(Program program, TargetUri targetUri, string message, string username) + { + Debug.Assert(targetUri != null); + Debug.Assert(message != null); + Debug.Assert(username != null); + + NativeMethods.CredentialUiInfo credUiInfo = new NativeMethods.CredentialUiInfo + { + BannerArt = IntPtr.Zero, + CaptionText = program.Title, + MessageText = message, + Parent = IntPtr.Zero, + Size = Marshal.SizeOf(typeof(NativeMethods.CredentialUiInfo)) + }; + NativeMethods.CredentialUiWindowsFlags flags = NativeMethods.CredentialUiWindowsFlags.Generic; + NativeMethods.CredentialPackFlags authPackage = NativeMethods.CredentialPackFlags.None; + IntPtr packedAuthBufferPtr = IntPtr.Zero; + IntPtr inBufferPtr = IntPtr.Zero; + uint packedAuthBufferSize = 0; + bool saveCredentials = false; + int inBufferSize = 0; + string password; + + try + { + int error; + + // execute with `null` to determine buffer size always returns false when determining + // size, only fail if `inBufferSize` looks bad + NativeMethods.CredPackAuthenticationBuffer(flags: authPackage, + username: username, + password: string.Empty, + packedCredentials: IntPtr.Zero, + packedCredentialsSize: ref inBufferSize); + if (inBufferSize <= 0) + { + error = Marshal.GetLastWin32Error(); + Git.Trace.WriteLine($"unable to determine credential buffer size ('{NativeMethods.Win32Error.GetText(error)}')."); + + return null; + } + + inBufferPtr = Marshal.AllocHGlobal(inBufferSize); + + if (!NativeMethods.CredPackAuthenticationBuffer(flags: authPackage, + username: username, + password: string.Empty, + packedCredentials: inBufferPtr, + packedCredentialsSize: ref inBufferSize)) + { + error = Marshal.GetLastWin32Error(); + Git.Trace.WriteLine($"unable to write to credential buffer ('{NativeMethods.Win32Error.GetText(error)}')."); + + return null; + } + + if (program.ModalPromptDisplayDialog(ref credUiInfo, + ref authPackage, + packedAuthBufferPtr, + packedAuthBufferSize, + inBufferPtr, + inBufferSize, + saveCredentials, + flags, + out username, + out password)) + { + return new Credential(username, password); + } + } + finally + { + if (inBufferPtr != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(inBufferPtr); + } + } + + return null; + } + } +} diff --git a/Cli-Shared/Functions/GitHub.cs b/Cli-Shared/Functions/GitHub.cs new file mode 100644 index 000000000..9e7acf5dc --- /dev/null +++ b/Cli-Shared/Functions/GitHub.cs @@ -0,0 +1,111 @@ +/**** Git Credential Manager for Windows **** + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the """"Software""""), to deal + * in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." +**/ + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Alm.Authentication; +using Microsoft.Win32.SafeHandles; +using Github = GitHub.Authentication; + +namespace Microsoft.Alm.Cli +{ + internal static class GitHubFunctions + { + public static bool AuthCodePrompt(Program program, TargetUri targetUri, Github.GitHubAuthenticationResultType resultType, string username, out string authenticationCode) + { + // ReadConsole 32768 fail, 32767 ok @linquize [https://github.com/Microsoft/Git-Credential-Manager-for-Windows/commit/a62b9a19f430d038dcd85a610d97e5f763980f85] + const int BufferReadSize = 16 * 1024; + + Debug.Assert(targetUri != null); + + StringBuilder buffer = new StringBuilder(BufferReadSize); + uint read = 0; + uint written = 0; + + authenticationCode = null; + + NativeMethods.FileAccess fileAccessFlags = NativeMethods.FileAccess.GenericRead | NativeMethods.FileAccess.GenericWrite; + NativeMethods.FileAttributes fileAttributes = NativeMethods.FileAttributes.Normal; + NativeMethods.FileCreationDisposition fileCreationDisposition = NativeMethods.FileCreationDisposition.OpenExisting; + NativeMethods.FileShare fileShareFlags = NativeMethods.FileShare.Read | NativeMethods.FileShare.Write; + + using (SafeFileHandle stdout = NativeMethods.CreateFile(NativeMethods.ConsoleOutName, fileAccessFlags, fileShareFlags, IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) + using (SafeFileHandle stdin = NativeMethods.CreateFile(NativeMethods.ConsoleInName, fileAccessFlags, fileShareFlags, IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) + { + string type = resultType == Github.GitHubAuthenticationResultType.TwoFactorApp + ? "app" + : "sms"; + + Git.Trace.WriteLine($"2fa type = '{type}'."); + + buffer.AppendLine() + .Append("authcode (") + .Append(type) + .Append("): "); + + if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + buffer.Clear(); + + // read input from the user + if (!NativeMethods.ReadConsole(stdin, buffer, BufferReadSize, out read, IntPtr.Zero)) + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error, "Unable to read from standard input (" + NativeMethods.Win32Error.GetText(error) + ")."); + } + + authenticationCode = buffer.ToString(0, (int)read); + authenticationCode = authenticationCode.Trim(Program.NewLineChars); + } + + return authenticationCode != null; + } + + public static bool CredentialPrompt(Program program, TargetUri targetUri, out string username, out string password) + { + const string TitleMessage = "Please enter your GitHub credentials for "; + + Credential credential; + if ((credential = program.BasicCredentialPrompt(targetUri, TitleMessage)) != null) + { + username = credential.Username; + password = credential.Password; + + return true; + } + + username = null; + password = null; + + return false; + } + } +} diff --git a/Cli-Shared/OperationArguments.cs b/Cli-Shared/OperationArguments.cs index 6e3a866a6..0151a1e9a 100644 --- a/Cli-Shared/OperationArguments.cs +++ b/Cli-Shared/OperationArguments.cs @@ -192,9 +192,8 @@ internal Impl(Stream readableStream) throw new ArgumentNullException(nameof(readableStream)); if (readableStream == Stream.Null || !readableStream.CanRead) - { - Program.Die("Unable to read input."); - } + throw new InvalidOperationException("Unable to read input."); + else { // @@ -217,7 +216,7 @@ internal Impl(Stream readableStream) if ((read > 0 && read < 3 && buffer[read - 1] == '\n')) { - Program.Die("Invalid input, please see 'https://www.kernel.org/pub/software/scm/git/docs/git-credential.html'."); + throw new InvalidDataException("Invalid input, please see 'https://www.kernel.org/pub/software/scm/git/docs/git-credential.html'."); } // the input ends with LFLF, check for that and break the read loop unless diff --git a/Cli-Shared/Program.cs b/Cli-Shared/Program.cs index 35d9bc2fd..47c9cd4b1 100644 --- a/Cli-Shared/Program.cs +++ b/Cli-Shared/Program.cs @@ -1,20 +1,14 @@ using System; -using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Runtime.InteropServices; -using System.Text; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.Alm.Authentication; -using Microsoft.Alm.Git; -using Microsoft.Win32.SafeHandles; using Bitbucket = Atlassian.Bitbucket.Authentication; using Github = GitHub.Authentication; namespace Microsoft.Alm.Cli { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters")] partial class Program { public const string SourceUrl = "https://github.com/Microsoft/Git-Credential-Manager-for-Windows"; @@ -59,54 +53,42 @@ partial class Program internal static readonly VstsTokenScope VstsCredentialScope = VstsTokenScope.CodeWrite | VstsTokenScope.PackagingRead; internal static readonly Github.TokenScope GitHubCredentialScope = Github.TokenScope.Gist | Github.TokenScope.Repo; - internal static Action _dieExceptionCallback = (Exception exception) => - { - Git.Trace.WriteLine(exception.ToString()); - LogEvent(exception.ToString(), EventLogEntryType.Error); - - string message; - if (!String.IsNullOrWhiteSpace(exception.Message)) - { - message = $"{exception.GetType().Name} encountered.\n {exception.Message}"; - } - else - { - message = $"{exception.GetType().Name} encountered."; - } - - Die(message); - }; - - internal static Action _dieMessageCallback = (string message) => - { - Git.Trace.WriteLine($"fatal: {message}"); - Program.WriteLine($"fatal: {message}"); - - Git.Trace.Flush(); - - Environment.Exit(-1); - }; - - internal static Action _exitCallback = (int exitcode, string message) => - { - if (!String.IsNullOrWhiteSpace(message)) - { - Git.Trace.WriteLine(message); - Program.WriteLine(message); - } - - Environment.Exit(exitcode); - }; - - private static string _executablePath; - private static string _location; - private static string _name; - private static Version _version; + internal BasicCredentialPromptDelegate _basicCredentialPrompt = ConsoleFunctions.CredentialPrompt; + internal BitbucketCredentialPromptDelegate _bitbucketCredentialPrompt = BitbucketFunctions.CredentialPrompt; + internal BitbucketOAuthPromptDelegate _bitbucketOauthPrompt = BitbucketFunctions.OAuthPrompt; + internal CreateAuthenticationDelegate _createAuthentication = CommonFunctions.CreateAuthentication; + internal DeleteCredentialsDelegate _deleteCredentials = CommonFunctions.DeleteCredentials; + internal DieExceptionDelegate _dieException = CommonFunctions.DieException; + internal DieMessageDelegate _dieMessage = CommonFunctions.DieMessage; + internal EnableTraceLoggingDelegate _enableTraceLogging = CommonFunctions.EnableTraceLogging; + internal EnableTraceLoggingFileDelegate _enableTraceLoggingFile = CommonFunctions.EnableTraceLoggingFile; + internal ExitDelegate _exit = ConsoleFunctions.Exit; + internal GitHubAuthCodePromptDelegate _gitHubAuthCodePrompt = GitHubFunctions.AuthCodePrompt; + internal GitHubCredentialPromptDelegate _gitHubCredentialPrompt = GitHubFunctions.CredentialPrompt; + internal LoadOperationArgumentsDelegate _loadOperationArguments = CommonFunctions.LoadOperationArguments; + internal LogEventDelegate _logEvent = CommonFunctions.LogEvent; + internal ModalPromptDisplayDialogDelegate _modalPromptDisplayDialog = DialogFunctions.DisplayModal; + internal ModalPromptForCredentialsDelegate _modalPromptForCredentials = DialogFunctions.CredentialPrompt; + internal ModalPromptForPasswordDelegate _modalPromptForPassword = DialogFunctions.PasswordPrompt; + internal PrintArgsDelegate _printArgs = CommonFunctions.PrintArgs; + internal QueryCredentialsDelegate _queryCredentials = CommonFunctions.QueryCredentials; + internal ReadKeyDelegate _readKey = ConsoleFunctions.ReadKey; + internal StandardHandleIsTtyDelegate _standardHandleIsTty = ConsoleFunctions.StandardHandleIsTty; + internal TryReadBooleanDelegate _tryReadBoolean = CommonFunctions.TryReadBoolean; + internal TryReadStringDelegate _tryReadString = CommonFunctions.TryReadString; + internal WriteDelegate _write = ConsoleFunctions.Write; + internal WriteLineDelegate _writeLine = ConsoleFunctions.WriteLine; + + private string _executablePath; + private string _location; + private string _name; + private string _title; + private Version _version; /// /// Gets the path to the executable. /// - public static string ExecutablePath + public string ExecutablePath { get { @@ -121,7 +103,7 @@ public static string ExecutablePath /// /// Gets the directory where the executable is contained. /// - public static string Location + public string Location { get { @@ -136,7 +118,7 @@ public static string Location /// /// Gets the name of the application. /// - public static string Name + public string Name { get { @@ -155,7 +137,7 @@ public static string Name /// user are possible. /// /// - public static bool StandardErrorIsTty + public bool StandardErrorIsTty { get { return StandardHandleIsTty(NativeMethods.StandardHandleType.Error); } } @@ -167,7 +149,7 @@ public static bool StandardErrorIsTty /// user are possible. /// /// - public static bool StandardInputIsTty + public bool StandardInputIsTty { get { return StandardHandleIsTty(NativeMethods.StandardHandleType.Input); } } @@ -179,15 +161,21 @@ public static bool StandardInputIsTty /// user are possible. /// /// - public static bool StandardOutputIsTty + public bool StandardOutputIsTty { get { return StandardHandleIsTty(NativeMethods.StandardHandleType.Output); } } + public string Title + { + get { return _title; } + private set { _title = value; } + } + /// /// Gets the version of the application. /// - internal static Version Version + public Version Version { get { @@ -199,926 +187,88 @@ internal static Version Version } } - internal static void Die(Exception exception) - => _dieExceptionCallback(exception); - - internal static void Die(string message) - => _dieMessageCallback(message); - - internal static void Exit(int exitcode = 0, string message = null) - => _exitCallback(exitcode, message); - - internal static void LoadOperationArguments(OperationArguments operationArguments) - { - if (operationArguments.TargetUri == null) - { - Die("No host information, unable to continue."); - } - - string value; - bool? yesno; - - if (TryReadBoolean(operationArguments, null, EnvironConfigNoLocalKey, out yesno)) - { - operationArguments.UseConfigLocal = yesno.Value; - } - - if (TryReadBoolean(operationArguments, null, EnvironConfigNoSystemKey, out yesno)) - { - operationArguments.UseConfigSystem = yesno.Value; - } - - // load/re-load the Git configuration after setting the use local/system config values - operationArguments.LoadConfiguration(); - - // if a user-agent has been specified in the environment, set it globally - if (TryReadString(operationArguments, null, EnvironHttpUserAgent, out value)) - { - Global.UserAgent = value; - } - - // look for authority settings - if (TryReadString(operationArguments, ConfigAuthorityKey, EnvironAuthorityKey, out value)) - { - Git.Trace.WriteLine($"{ConfigAuthorityKey} = '{value}'."); - - if (ConfigKeyComparer.Equals(value, "MSA") - || ConfigKeyComparer.Equals(value, "Microsoft") - || ConfigKeyComparer.Equals(value, "MicrosoftAccount") - || ConfigKeyComparer.Equals(value, "Live") - || ConfigKeyComparer.Equals(value, "LiveConnect") - || ConfigKeyComparer.Equals(value, "LiveID")) - { - operationArguments.Authority = AuthorityType.MicrosoftAccount; - } - else if (ConfigKeyComparer.Equals(value, "AAD") - || ConfigKeyComparer.Equals(value, "Azure") - || ConfigKeyComparer.Equals(value, "AzureDirectory")) - { - operationArguments.Authority = AuthorityType.AzureDirectory; - } - else if (ConfigKeyComparer.Equals(value, "Integrated") - || ConfigKeyComparer.Equals(value, "Windows") - || ConfigKeyComparer.Equals(value, "TFS") - || ConfigKeyComparer.Equals(value, "Kerberos") - || ConfigKeyComparer.Equals(value, "NTLM") - || ConfigKeyComparer.Equals(value, "SSO")) - { - operationArguments.Authority = AuthorityType.Ntlm; - } - else if (ConfigKeyComparer.Equals(value, "GitHub")) - { - operationArguments.Authority = AuthorityType.GitHub; - } - else - { - operationArguments.Authority = AuthorityType.Basic; - } - } - - // look for interactivity config settings - if (TryReadString(operationArguments, ConfigInteractiveKey, EnvironInteractiveKey, out value)) - { - Git.Trace.WriteLine($"{EnvironInteractiveKey} = '{value}'."); + internal void Die(Exception exception, + [CallerFilePath] string path = "", + [CallerLineNumber] int line = 0, + [CallerMemberName] string name = "") + => _dieException(this, exception, path, line, name); - if (ConfigKeyComparer.Equals(value, "always") - || ConfigKeyComparer.Equals(value, "true") - || ConfigKeyComparer.Equals(value, "force")) - { - operationArguments.Interactivity = Interactivity.Always; - } - else if (ConfigKeyComparer.Equals(value, "never") - || ConfigKeyComparer.Equals(value, "false")) - { - operationArguments.Interactivity = Interactivity.Never; - } - } + internal void Die(string message, + [CallerFilePath] string path = "", + [CallerLineNumber] int line = 0, + [CallerMemberName] string name = "") + => _dieMessage(this, message, path, line, name); - // look for credential validation config settings - if (TryReadBoolean(operationArguments, ConfigValidateKey, EnvironValidateKey, out yesno)) - { - operationArguments.ValidateCredentials = yesno.Value; - } + internal void Exit(int exitcode = 0, + string message = null, + [CallerFilePath] string path = "", + [CallerLineNumber] int line = 0, + [CallerMemberName] string name = "") + => _exit(this, exitcode, message, path, line, name); - // look for write log config settings - if (TryReadBoolean(operationArguments, ConfigWritelogKey, EnvironWritelogKey, out yesno)) - { - operationArguments.WriteLog = yesno.Value; - } + internal void LoadOperationArguments(OperationArguments operationArguments) + => _loadOperationArguments(this, operationArguments); - // look for modal prompt config settings - if (TryReadBoolean(operationArguments, ConfigUseModalPromptKey, EnvironModalPromptKey, out yesno)) - { - operationArguments.UseModalUi = yesno.Value; - } + internal void LogEvent(string message, EventLogEntryType eventType) + => _logEvent(this, message, eventType); - // look for credential preservation config settings - if (TryReadBoolean(operationArguments, ConfigPreserveCredentialsKey, EnvironPreserveCredentialsKey, out yesno)) - { - operationArguments.PreserveCredentials = yesno.Value; - } + internal Credential QueryCredentials(OperationArguments operationArguments) + => _queryCredentials(this, operationArguments); - // look for http path usage config settings - if (TryReadBoolean(operationArguments, ConfigUseHttpPathKey, null, out yesno)) - { - operationArguments.UseHttpPath = yesno.Value; - } + internal ConsoleKeyInfo ReadKey(bool intercept = true) + => _readKey(this, intercept); - // look for http proxy config settings - if (TryReadString(operationArguments, ConfigHttpProxyKey, EnvironHttpProxyKey, out value)) - { - Git.Trace.WriteLine($"{ConfigHttpProxyKey} = '{value}'."); + internal void Write(string message) + => _write(this, message); - operationArguments.SetProxy(value); - } - else - { - // check the git-config http.proxy setting just-in-case - Configuration.Entry entry; - if (operationArguments.GitConfiguration.TryGetEntry("http", operationArguments.QueryUri, "proxy", out entry) - && !String.IsNullOrWhiteSpace(entry.Value)) - { - Git.Trace.WriteLine($"http.proxy = '{entry.Value}'."); - - operationArguments.SetProxy(entry.Value); - } - } - - // look for custom namespace config settings - if (TryReadString(operationArguments, ConfigNamespaceKey, EnvironNamespaceKey, out value)) - { - Git.Trace.WriteLine($"{ConfigNamespaceKey} = '{value}'."); + internal void WriteLine(string message = null) + => _writeLine(this, message); - operationArguments.CustomNamespace = value; - } - } - - internal static void LogEvent(string message, EventLogEntryType eventType) - { - /*** try-squelch due to UAC issues which require a proper installer to work around ***/ - - Git.Trace.WriteLine(message); - - try - { - EventLog.WriteEntry(EventSource, message, eventType); - } - catch { /* squelch */ } - } - - internal static Credential QueryCredentials(OperationArguments operationArguments) - { - if (ReferenceEquals(operationArguments, null)) - throw new ArgumentNullException(nameof(operationArguments)); - if (ReferenceEquals(operationArguments.TargetUri, null)) - throw new ArgumentException("TargetUri property returned null", nameof(operationArguments)); - - var task = Task.Run(async () => { return await CreateAuthentication(operationArguments); }); - BaseAuthentication authentication = task.Result; - Credential credentials = null; - - switch (operationArguments.Authority) - { - default: - case AuthorityType.Basic: - { - BasicAuthentication basicAuth = authentication as BasicAuthentication; - - Task.Run(async () => - { - // attempt to get cached creds or acquire creds if interactivity is allowed - if ((operationArguments.Interactivity != Interactivity.Always - && (credentials = authentication.GetCredentials(operationArguments.TargetUri)) != null) - || (operationArguments.Interactivity != Interactivity.Never - && (credentials = await basicAuth.AcquireCredentials(operationArguments.TargetUri)) != null)) - { - Git.Trace.WriteLine("credentials found."); - // no need to save the credentials explicitly, as Git will call back with - // a store command if the credentials are valid. - } - else - { - Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' not found."); - LogEvent($"Failed to retrieve credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); - } - }).Wait(); - } - break; - - case AuthorityType.AzureDirectory: - { - VstsAadAuthentication aadAuth = authentication as VstsAadAuthentication; - - Task.Run(async () => - { - // attempt to get cached creds -> non-interactive logon -> interactive - // logon note that AAD "credentials" are always scoped access tokens - if (((operationArguments.Interactivity != Interactivity.Always - && ((credentials = aadAuth.GetCredentials(operationArguments.TargetUri)) != null) - && (!operationArguments.ValidateCredentials - || await aadAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) - || (operationArguments.Interactivity != Interactivity.Always - && ((credentials = await aadAuth.NoninteractiveLogon(operationArguments.TargetUri, true)) != null) - && (!operationArguments.ValidateCredentials - || await aadAuth.ValidateCredentials(operationArguments.TargetUri, credentials))) - || (operationArguments.Interactivity != Interactivity.Never - && ((credentials = await aadAuth.InteractiveLogon(operationArguments.TargetUri, true)) != null) - && (!operationArguments.ValidateCredentials - || await aadAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) - { - Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' found."); - LogEvent($"Azure Directory credentials for '{operationArguments.TargetUri}' successfully retrieved.", EventLogEntryType.SuccessAudit); - } - else - { - Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' not found."); - LogEvent($"Failed to retrieve Azure Directory credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); - } - }).Wait(); - } - break; - - case AuthorityType.MicrosoftAccount: - { - VstsMsaAuthentication msaAuth = authentication as VstsMsaAuthentication; - - Task.Run(async () => - { - // attempt to get cached creds -> interactive logon note that MSA - // "credentials" are always scoped access tokens - if (((operationArguments.Interactivity != Interactivity.Always - && ((credentials = msaAuth.GetCredentials(operationArguments.TargetUri)) != null) - && (!operationArguments.ValidateCredentials - || await msaAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) - || (operationArguments.Interactivity != Interactivity.Never - && ((credentials = await msaAuth.InteractiveLogon(operationArguments.TargetUri, true)) != null) - && (!operationArguments.ValidateCredentials - || await msaAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) - { - Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' found."); - LogEvent($"Microsoft Live credentials for '{operationArguments.TargetUri}' successfully retrieved.", EventLogEntryType.SuccessAudit); - } - else - { - Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' not found."); - LogEvent($"Failed to retrieve Microsoft Live credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); - } - }).Wait(); - } - break; - - case AuthorityType.GitHub: - { - Github.Authentication ghAuth = authentication as Github.Authentication; - - Task.Run(async () => - { - if ((operationArguments.Interactivity != Interactivity.Always - && ((credentials = ghAuth.GetCredentials(operationArguments.TargetUri)) != null) - && (!operationArguments.ValidateCredentials - || await ghAuth.ValidateCredentials(operationArguments.TargetUri, credentials))) - || (operationArguments.Interactivity != Interactivity.Never - && ((credentials = await ghAuth.InteractiveLogon(operationArguments.TargetUri)) != null) - && (!operationArguments.ValidateCredentials - || await ghAuth.ValidateCredentials(operationArguments.TargetUri, credentials)))) - { - Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' found."); - LogEvent($"GitHub credentials for '{operationArguments.TargetUri}' successfully retrieved.", EventLogEntryType.SuccessAudit); - } - else - { - Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' not found."); - LogEvent($"Failed to retrieve GitHub credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); - } - }).Wait(); - } - break; - - case AuthorityType.Bitbucket: - { - var bbcAuth = authentication as Bitbucket.Authentication; - - Task.Run(async () => - { - if (((operationArguments.Interactivity != Interactivity.Always) - && ((credentials = bbcAuth.GetCredentials(operationArguments.TargetUri, operationArguments.CredUsername)) != null) - && (!operationArguments.ValidateCredentials - || ((credentials = await bbcAuth.ValidateCredentials(operationArguments.TargetUri, operationArguments.CredUsername, credentials)) != null))) - || ((operationArguments.Interactivity != Interactivity.Never) - && ((credentials = await bbcAuth.InteractiveLogon(operationArguments.TargetUri, operationArguments.CredUsername)) != null) - && (!operationArguments.ValidateCredentials - || ((credentials = await bbcAuth.ValidateCredentials(operationArguments.TargetUri, operationArguments.CredUsername, credentials)) != null)))) - { - Git.Trace.WriteLine($"credentials for '{operationArguments.TargetUri}' found."); - // Bitbucket relies on a username + secret, so make sure there is a - // username to return - if (operationArguments.CredUsername != null) - { - credentials = new Credential(operationArguments.CredUsername, credentials.Password); - } - LogEvent($"Bitbucket credentials for '{operationArguments.TargetUri}' successfully retrieved.", EventLogEntryType.SuccessAudit); - } - else - { - LogEvent($"Failed to retrieve Bitbucket credentials for '{operationArguments.TargetUri}'.", EventLogEntryType.FailureAudit); - } - }).Wait(); - } - break; - - case AuthorityType.Ntlm: - { - Git.Trace.WriteLine($"'{operationArguments.TargetUri}' is NTLM."); - credentials = BasicAuthentication.NtlmCredentials; - } - break; - } - - if (credentials != null) - { - operationArguments.SetCredentials(credentials); - } - - return credentials; - } - - internal static ConsoleKeyInfo ReadKey(bool intercept = true) - { - return (StandardInputIsTty) - ? Console.ReadKey(intercept) - : new ConsoleKeyInfo(' ', ConsoleKey.Escape, false, false, false); - } - - internal static void Write(string message) - { - if (message == null) - return; - - Console.Error.WriteLine(message); - } - - internal static void WriteLine(string message = null) - { - Console.Error.WriteLine(message); - } - - private static Credential BasicCredentialPrompt(TargetUri targetUri) + internal Credential BasicCredentialPrompt(TargetUri targetUri) { string message = "Please enter your credentials for "; return BasicCredentialPrompt(targetUri, message); } - private static Credential BasicCredentialPrompt(TargetUri targetUri, string titleMessage) - { - // ReadConsole 32768 fail, 32767 ok @linquize [https://github.com/Microsoft/Git-Credential-Manager-for-Windows/commit/a62b9a19f430d038dcd85a610d97e5f763980f85] - const int BufferReadSize = 16 * 1024; - - Debug.Assert(targetUri != null); - - if (!StandardErrorIsTty || !StandardInputIsTty) - { - Git.Trace.WriteLine("not a tty detected, abandoning prompt."); - return null; - } - - titleMessage = titleMessage ?? "Please enter your credentials for "; + internal Credential BasicCredentialPrompt(TargetUri targetUri, string titleMessage) + => _basicCredentialPrompt(this, targetUri, titleMessage); - StringBuilder buffer = new StringBuilder(BufferReadSize); - uint read = 0; - uint written = 0; - - NativeMethods.FileAccess fileAccessFlags = NativeMethods.FileAccess.GenericRead | NativeMethods.FileAccess.GenericWrite; - NativeMethods.FileAttributes fileAttributes = NativeMethods.FileAttributes.Normal; - NativeMethods.FileCreationDisposition fileCreationDisposition = NativeMethods.FileCreationDisposition.OpenExisting; - NativeMethods.FileShare fileShareFlags = NativeMethods.FileShare.Read | NativeMethods.FileShare.Write; - - using (SafeFileHandle stdout = NativeMethods.CreateFile(NativeMethods.ConsoleOutName, fileAccessFlags, fileShareFlags, IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) - using (SafeFileHandle stdin = NativeMethods.CreateFile(NativeMethods.ConsoleInName, fileAccessFlags, fileShareFlags, IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) - { - string username = null; - string password = null; - - // read the current console mode - NativeMethods.ConsoleMode consoleMode; - if (!NativeMethods.GetConsoleMode(stdin, out consoleMode)) - { - int error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, "Unable to determine console mode (" + NativeMethods.Win32Error.GetText(error) + ")."); - } + internal Task CreateAuthentication(OperationArguments operationArguments) + => _createAuthentication(this, operationArguments); - Git.Trace.WriteLine($"console mode = '{consoleMode}'."); - - // instruct the user as to what they are expected to do - buffer.Append(titleMessage) - .Append(targetUri) - .AppendLine(); - if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) - { - int error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); - } - - // clear the buffer for the next operation - buffer.Clear(); - - // prompt the user for the username wanted - buffer.Append("username: "); - if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) - { - int error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); - } - - // clear the buffer for the next operation - buffer.Clear(); - - // read input from the user - if (!NativeMethods.ReadConsole(stdin, buffer, BufferReadSize, out read, IntPtr.Zero)) - { - int error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, "Unable to read from standard input (" + NativeMethods.Win32Error.GetText(error) + ")."); - } + private void DeleteCredentials(OperationArguments operationArguments) + => _deleteCredentials(this, operationArguments); - // record input from the user into local storage, stripping any eol chars - username = buffer.ToString(0, (int)read); - username = username.Trim(Environment.NewLine.ToCharArray()); - - // clear the buffer for the next operation - buffer.Clear(); - - // set the console mode to current without echo input - NativeMethods.ConsoleMode consoleMode2 = consoleMode ^ NativeMethods.ConsoleMode.EchoInput; - - try - { - if (!NativeMethods.SetConsoleMode(stdin, consoleMode2)) - { - int error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, "Unable to set console mode (" + NativeMethods.Win32Error.GetText(error) + ")."); - } - - Git.Trace.WriteLine($"console mode = '{consoleMode2}'."); - - // prompt the user for password - buffer.Append("password: "); - if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) - { - int error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); - } - - // clear the buffer for the next operation - buffer.Clear(); - - // read input from the user - if (!NativeMethods.ReadConsole(stdin, buffer, BufferReadSize, out read, IntPtr.Zero)) - { - int error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, "Unable to read from standard input (" + NativeMethods.Win32Error.GetText(error) + ")."); - } - - // record input from the user into local storage, stripping any eol chars - password = buffer.ToString(0, (int)read); - password = password.Trim(Environment.NewLine.ToCharArray()); - } - catch { throw; } - finally - { - // restore the console mode to its original value - NativeMethods.SetConsoleMode(stdin, consoleMode); - - Git.Trace.WriteLine($"console mode = '{consoleMode}'."); - } - - if (username != null && password != null) - return new Credential(username, password); - } - - return null; - } - - private static async Task CreateAuthentication(OperationArguments operationArguments) - { - Debug.Assert(operationArguments != null, "The operationArguments is null"); - Debug.Assert(operationArguments.TargetUri != null, "The operationArgument.TargetUri is null"); - - var secretsNamespace = operationArguments.CustomNamespace ?? SecretsNamespace; - var secrets = new SecretStore(secretsNamespace, null, null, Secret.UriToName); - BaseAuthentication authority = null; - - var basicCredentialCallback = (operationArguments.UseModalUi) - ? new AcquireCredentialsDelegate(Program.ModalPromptForCredentials) - : new AcquireCredentialsDelegate(Program.BasicCredentialPrompt); - - var bitbucketCredentialCallback = (operationArguments.UseModalUi) - ? Bitbucket.AuthenticationPrompts.CredentialModalPrompt - : new Bitbucket.Authentication.AcquireCredentialsDelegate(BitbucketCredentialPrompt); - - var bitbucketOauthCallback = (operationArguments.UseModalUi) - ? Bitbucket.AuthenticationPrompts.AuthenticationOAuthModalPrompt - : new Bitbucket.Authentication.AcquireAuthenticationOAuthDelegate(BitbucketOAuthPrompt); - - var githubCredentialCallback = (operationArguments.UseModalUi) - ? new Github.Authentication.AcquireCredentialsDelegate(Github.AuthenticationPrompts.CredentialModalPrompt) - : new Github.Authentication.AcquireCredentialsDelegate(Program.GitHubCredentialPrompt); - - var githubAuthcodeCallback = (operationArguments.UseModalUi) - ? new Github.Authentication.AcquireAuthenticationCodeDelegate(Github.AuthenticationPrompts.AuthenticationCodeModalPrompt) - : new Github.Authentication.AcquireAuthenticationCodeDelegate(Program.GitHubAuthCodePrompt); - - NtlmSupport basicNtlmSupport = NtlmSupport.Auto; - - switch (operationArguments.Authority) - { - case AuthorityType.Auto: - Git.Trace.WriteLine($"detecting authority type for '{operationArguments.TargetUri}'."); - - // detect the authority - authority = await BaseVstsAuthentication.GetAuthentication(operationArguments.TargetUri, - VstsCredentialScope, - secrets) - ?? Github.Authentication.GetAuthentication(operationArguments.TargetUri, - GitHubCredentialScope, - secrets, - githubCredentialCallback, - githubAuthcodeCallback, - null) - ?? Bitbucket.Authentication.GetAuthentication(operationArguments.TargetUri, - new SecretStore(secretsNamespace, Secret.UriToActualUrl), - bitbucketCredentialCallback, - bitbucketOauthCallback); - - if (authority != null) - { - // set the authority type based on the returned value - if (authority is VstsMsaAuthentication) - { - operationArguments.Authority = AuthorityType.MicrosoftAccount; - goto case AuthorityType.MicrosoftAccount; - } - else if (authority is VstsAadAuthentication) - { - operationArguments.Authority = AuthorityType.AzureDirectory; - goto case AuthorityType.AzureDirectory; - } - else if (authority is Github.Authentication) - { - operationArguments.Authority = AuthorityType.GitHub; - goto case AuthorityType.GitHub; - } - else if (authority is Bitbucket.Authentication) - { - operationArguments.Authority = AuthorityType.Bitbucket; - goto case AuthorityType.Bitbucket; - } - } - goto default; - - case AuthorityType.AzureDirectory: - Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is Azure Directory."); - - Guid tenantId = Guid.Empty; - - // Get the identity of the tenant. - var result = await BaseVstsAuthentication.DetectAuthority(operationArguments.TargetUri); - - if (result.Key) - { - tenantId = result.Value; - } - - // return the allocated authority or a generic AAD backed VSTS authentication object - return authority ?? new VstsAadAuthentication(tenantId, VstsCredentialScope, secrets); - - case AuthorityType.Basic: - // enforce basic authentication only - basicNtlmSupport = NtlmSupport.Never; - goto default; - - case AuthorityType.GitHub: - Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is GitHub."); - - // return a GitHub authentication object - return authority ?? new Github.Authentication(operationArguments.TargetUri, - GitHubCredentialScope, - secrets, - githubCredentialCallback, - githubAuthcodeCallback, - null); - - case AuthorityType.Bitbucket: - Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is Bitbucket"); - - // return a Bitbucket authentication object - return authority ?? new Bitbucket.Authentication(secrets, - bitbucketCredentialCallback, - bitbucketOauthCallback); - - case AuthorityType.MicrosoftAccount: - Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is Microsoft Live."); - - // return the allocated authority or a generic MSA backed VSTS authentication object - return authority ?? new VstsMsaAuthentication(VstsCredentialScope, secrets); - - case AuthorityType.Ntlm: - // enforce NTLM authentication only - basicNtlmSupport = NtlmSupport.Always; - goto default; - - default: - Git.Trace.WriteLine($"authority for '{operationArguments.TargetUri}' is basic with NTLM={basicNtlmSupport}."); - - // return a generic username + password authentication object - return authority ?? new BasicAuthentication(secrets, basicNtlmSupport, basicCredentialCallback, null); - } - } - - internal static void DeleteCredentials(OperationArguments operationArguments) - { - if (ReferenceEquals(operationArguments, null)) - throw new ArgumentNullException("operationArguments"); - - var task = Task.Run(async () => { return await CreateAuthentication(operationArguments); }); - - BaseAuthentication authentication = task.Result; - - switch (operationArguments.Authority) - { - default: - case AuthorityType.Basic: - Git.Trace.WriteLine($"deleting basic credentials for '{operationArguments.TargetUri}'."); - authentication.DeleteCredentials(operationArguments.TargetUri); - break; - - case AuthorityType.AzureDirectory: - case AuthorityType.MicrosoftAccount: - Git.Trace.WriteLine($"deleting VSTS credentials for '{operationArguments.TargetUri}'."); - BaseVstsAuthentication vstsAuth = authentication as BaseVstsAuthentication; - vstsAuth.DeleteCredentials(operationArguments.TargetUri); - break; - - case AuthorityType.GitHub: - Git.Trace.WriteLine($"deleting GitHub credentials for '{operationArguments.TargetUri}'."); - Github.Authentication ghAuth = authentication as Github.Authentication; - ghAuth.DeleteCredentials(operationArguments.TargetUri); - break; - - case AuthorityType.Bitbucket: - Git.Trace.WriteLine($"deleting Bitbucket credentials for '{operationArguments.TargetUri}'."); - var bbAuth = authentication as Bitbucket.Authentication; - bbAuth.DeleteCredentials(operationArguments.TargetUri, operationArguments.CredUsername); - break; - } - } - - private static void PrintArgs(string[] args) - { - Debug.Assert(args != null, $"The `{nameof(args)}` parameter is null."); - - StringBuilder builder = new StringBuilder(); - builder.Append(Program.Name) - .Append(" (v") - .Append(Program.Version.ToString(3)) - .Append(")"); - - for (int i = 0; i < args.Length; i += 1) - { - builder.Append(" '") - .Append(args[i]) - .Append("'"); - - if (i + 1 < args.Length) - { - builder.Append(","); - } - } - - // fake being part of the Main method for clarity - Git.Trace.WriteLine(builder.ToString(), memberName: nameof(Main)); - builder = null; - } + private void PrintArgs(string[] args) + => _printArgs(this, args); [Conditional("DEBUG")] - private static void EnableDebugTrace() + private void EnableDebugTrace() { // use the stderr stream for the trace as stdout is used in the cross-process // communications protocol Git.Trace.AddListener(Console.Error); } - private static void EnableTraceLogging(OperationArguments operationArguments) - { - if (operationArguments.WriteLog) - { - Git.Trace.WriteLine("trace logging enabled."); - - string gitConfigPath; - if (Where.GitLocalConfig(out gitConfigPath)) - { - Git.Trace.WriteLine($"git local config found at '{gitConfigPath}'."); - - string gitDirPath = Path.GetDirectoryName(gitConfigPath); - - if (Directory.Exists(gitDirPath)) - { - EnableTraceLogging(operationArguments, gitDirPath); - } - } - else if (Where.GitGlobalConfig(out gitConfigPath)) - { - Git.Trace.WriteLine($"git global config found at '{gitConfigPath}'."); - - string homeDirPath = Path.GetDirectoryName(gitConfigPath); - - if (Directory.Exists(homeDirPath)) - { - EnableTraceLogging(operationArguments, homeDirPath); - } - } - } -#if DEBUG - Git.Trace.WriteLine($"GCM arguments:{Environment.NewLine}{operationArguments}"); -#endif - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "operationArguments")] - private static void EnableTraceLogging(OperationArguments operationArguments, string logFilePath) - { - const int LogFileMaxLength = 8 * 1024 * 1024; // 8 MB - - string logFileName = Path.Combine(logFilePath, Path.ChangeExtension(ConfigPrefix, ".log")); - - FileInfo logFileInfo = new FileInfo(logFileName); - if (logFileInfo.Exists && logFileInfo.Length > LogFileMaxLength) - { - for (int i = 1; i < Int32.MaxValue; i++) - { - string moveName = String.Format("{0}{1:000}.log", ConfigPrefix, i); - string movePath = Path.Combine(logFilePath, moveName); - - if (!File.Exists(movePath)) - { - logFileInfo.MoveTo(movePath); - break; - } - } - } - - Git.Trace.WriteLine($"trace log destination is '{logFilePath}'."); - - using (var fileStream = File.Open(logFileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) - { - var listener = new StreamWriter(fileStream, Encoding.UTF8); - Git.Trace.AddListener(listener); - - // write a small header to help with identifying new log entries - listener.Write('\n'); - listener.Write($"{DateTime.Now:yyyy.MM.dd HH:mm:ss} Microsoft {Program.Title} version {Version.ToString(3)}\n"); - } - } - - private static bool BitbucketCredentialPrompt(string titleMessage, TargetUri targetUri, out string username, out string password) - { - Credential credential; - if ((credential = BasicCredentialPrompt(targetUri, titleMessage)) != null) - { - username = credential.Username; - password = credential.Password; - - return true; - } - - username = null; - password = null; - - return false; - } - - private static bool BitbucketOAuthPrompt(string title, TargetUri targetUri, Bitbucket.AuthenticationResultType resultType, string username) - { - const int BufferReadSize = 16 * 1024; - - Debug.Assert(targetUri != null); - - var buffer = new StringBuilder(BufferReadSize); - uint read = 0; - uint written = 0; - - string accessToken = null; - - var fileAccessFlags = NativeMethods.FileAccess.GenericRead | NativeMethods.FileAccess.GenericWrite; - var fileAttributes = NativeMethods.FileAttributes.Normal; - var fileCreationDisposition = NativeMethods.FileCreationDisposition.OpenExisting; - var fileShareFlags = NativeMethods.FileShare.Read | NativeMethods.FileShare.Write; - - using ( - var stdout = NativeMethods.CreateFile(NativeMethods.ConsoleOutName, fileAccessFlags, fileShareFlags, - IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) - { - using ( - var stdin = NativeMethods.CreateFile(NativeMethods.ConsoleInName, fileAccessFlags, fileShareFlags, - IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) - { - buffer.AppendLine() - .Append(title) - .Append(" OAuth Access Token: "); - - if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) - { - var error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, - "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); - } - buffer.Clear(); - - // read input from the user - if (!NativeMethods.ReadConsole(stdin, buffer, BufferReadSize, out read, IntPtr.Zero)) - { - var error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, - "Unable to read from standard input (" + NativeMethods.Win32Error.GetText(error) + ")."); - } - - accessToken = buffer.ToString(0, (int)read); - accessToken = accessToken.Trim(NewLineChars); - } - } - return accessToken != null; - } - - private static bool GitHubAuthCodePrompt(TargetUri targetUri, Github.GitHubAuthenticationResultType resultType, string username, out string authenticationCode) - { - // ReadConsole 32768 fail, 32767 ok @linquize [https://github.com/Microsoft/Git-Credential-Manager-for-Windows/commit/a62b9a19f430d038dcd85a610d97e5f763980f85] - const int BufferReadSize = 16 * 1024; - - Debug.Assert(targetUri != null); - - StringBuilder buffer = new StringBuilder(BufferReadSize); - uint read = 0; - uint written = 0; - - authenticationCode = null; - - NativeMethods.FileAccess fileAccessFlags = NativeMethods.FileAccess.GenericRead | NativeMethods.FileAccess.GenericWrite; - NativeMethods.FileAttributes fileAttributes = NativeMethods.FileAttributes.Normal; - NativeMethods.FileCreationDisposition fileCreationDisposition = NativeMethods.FileCreationDisposition.OpenExisting; - NativeMethods.FileShare fileShareFlags = NativeMethods.FileShare.Read | NativeMethods.FileShare.Write; - - using (SafeFileHandle stdout = NativeMethods.CreateFile(NativeMethods.ConsoleOutName, fileAccessFlags, fileShareFlags, IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) - using (SafeFileHandle stdin = NativeMethods.CreateFile(NativeMethods.ConsoleInName, fileAccessFlags, fileShareFlags, IntPtr.Zero, fileCreationDisposition, fileAttributes, IntPtr.Zero)) - { - string type = resultType == Github.GitHubAuthenticationResultType.TwoFactorApp - ? "app" - : "sms"; - - Git.Trace.WriteLine($"2fa type = '{type}'."); - - buffer.AppendLine() - .Append("authcode (") - .Append(type) - .Append("): "); - - if (!NativeMethods.WriteConsole(stdout, buffer, (uint)buffer.Length, out written, IntPtr.Zero)) - { - int error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, "Unable to write to standard output (" + NativeMethods.Win32Error.GetText(error) + ")."); - } - buffer.Clear(); - - // read input from the user - if (!NativeMethods.ReadConsole(stdin, buffer, BufferReadSize, out read, IntPtr.Zero)) - { - int error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error, "Unable to read from standard input (" + NativeMethods.Win32Error.GetText(error) + ")."); - } - - authenticationCode = buffer.ToString(0, (int)read); - authenticationCode = authenticationCode.Trim(NewLineChars); - } - - return authenticationCode != null; - } + internal void EnableTraceLogging(OperationArguments operationArguments) + => _enableTraceLogging(this, operationArguments); - private static bool GitHubCredentialPrompt(TargetUri targetUri, out string username, out string password) - { - const string TitleMessage = "Please enter your GitHub credentials for "; + internal void EnableTraceLogging(OperationArguments operationArguments, string logFilePath) + => _enableTraceLoggingFile(this, operationArguments, logFilePath); - Credential credential; - if ((credential = BasicCredentialPrompt(targetUri, TitleMessage)) != null) - { - username = credential.Username; - password = credential.Password; + internal bool BitbucketCredentialPrompt(string titleMessage, TargetUri targetUri, out string username, out string password) + => _bitbucketCredentialPrompt(this, titleMessage, targetUri, out username, out password); - return true; - } + internal bool BitbucketOAuthPrompt(string title, TargetUri targetUri, Bitbucket.AuthenticationResultType resultType, string username) + => _bitbucketOauthPrompt(this, title, targetUri, resultType, username); - username = null; - password = null; + internal bool GitHubAuthCodePrompt(TargetUri targetUri, Github.GitHubAuthenticationResultType resultType, string username, out string authenticationCode) + => _gitHubAuthCodePrompt(this, targetUri, resultType, username, out authenticationCode); - return false; - } + internal bool GitHubCredentialPrompt(TargetUri targetUri, out string username, out string password) + => _gitHubCredentialPrompt(this, targetUri, out username, out password); - private static void LoadAssemblyInformation() + private void LoadAssemblyInformation() { var assembly = System.Reflection.Assembly.GetEntryAssembly(); var asseName = assembly.GetName(); @@ -1129,31 +279,18 @@ private static void LoadAssemblyInformation() _version = asseName.Version; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "targetUri")] - private static Credential ModalPromptForCredentials(TargetUri targetUri, string message) - { - Debug.Assert(targetUri != null); - Debug.Assert(message != null); - - NativeMethods.CredentialUiInfo credUiInfo = new NativeMethods.CredentialUiInfo - { - BannerArt = IntPtr.Zero, - CaptionText = Title, - MessageText = message, - Parent = IntPtr.Zero, - Size = Marshal.SizeOf(typeof(NativeMethods.CredentialUiInfo)) - }; - NativeMethods.CredentialUiWindowsFlags flags = NativeMethods.CredentialUiWindowsFlags.Generic; - NativeMethods.CredentialPackFlags authPackage = NativeMethods.CredentialPackFlags.None; - IntPtr packedAuthBufferPtr = IntPtr.Zero; - IntPtr inBufferPtr = IntPtr.Zero; - uint packedAuthBufferSize = 0; - bool saveCredentials = false; - int inBufferSize = 0; - string username; - string password; - - if (ModalPromptDisplayDialog(ref credUiInfo, + internal bool ModalPromptDisplayDialog(ref NativeMethods.CredentialUiInfo credUiInfo, + ref NativeMethods.CredentialPackFlags authPackage, + IntPtr packedAuthBufferPtr, + uint packedAuthBufferSize, + IntPtr inBufferPtr, + int inBufferSize, + bool saveCredentials, + NativeMethods.CredentialUiWindowsFlags flags, + out string username, + out string password) + => _modalPromptDisplayDialog(this, + ref credUiInfo, ref authPackage, packedAuthBufferPtr, packedAuthBufferSize, @@ -1162,17 +299,14 @@ private static Credential ModalPromptForCredentials(TargetUri targetUri, string saveCredentials, flags, out username, - out password)) - { - return new Credential(username, password); - } + out password); - return null; - } + internal Credential ModalPromptForCredentials(TargetUri targetUri, string message) + => _modalPromptForCredentials(this, targetUri, message); - private static Credential ModalPromptForCredentials(TargetUri targetUri) + internal Credential ModalPromptForCredentials(TargetUri targetUri) { - string message = String.Format("Enter your credentials for {0}.", targetUri.ToString(port: true, path: true)); + string message = string.Format("Enter your credentials for {0}.", targetUri.ToString(port: true, path: true)); if (!string.IsNullOrEmpty(targetUri.ActualUri.UserInfo)) { @@ -1189,271 +323,21 @@ private static Credential ModalPromptForCredentials(TargetUri targetUri) return ModalPromptForCredentials(targetUri, message); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "targetUri")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - private static Credential ModalPromptForPassword(TargetUri targetUri, string message, string username) - { - Debug.Assert(targetUri != null); - Debug.Assert(message != null); - Debug.Assert(username != null); - - NativeMethods.CredentialUiInfo credUiInfo = new NativeMethods.CredentialUiInfo - { - BannerArt = IntPtr.Zero, - CaptionText = Title, - MessageText = message, - Parent = IntPtr.Zero, - Size = Marshal.SizeOf(typeof(NativeMethods.CredentialUiInfo)) - }; - NativeMethods.CredentialUiWindowsFlags flags = NativeMethods.CredentialUiWindowsFlags.Generic; - NativeMethods.CredentialPackFlags authPackage = NativeMethods.CredentialPackFlags.None; - IntPtr packedAuthBufferPtr = IntPtr.Zero; - IntPtr inBufferPtr = IntPtr.Zero; - uint packedAuthBufferSize = 0; - bool saveCredentials = false; - int inBufferSize = 0; - string password; - - try - { - int error; - - // execute with `null` to determine buffer size always returns false when determining - // size, only fail if `inBufferSize` looks bad - NativeMethods.CredPackAuthenticationBuffer(flags: authPackage, - username: username, - password: string.Empty, - packedCredentials: IntPtr.Zero, - packedCredentialsSize: ref inBufferSize); - if (inBufferSize <= 0) - { - error = Marshal.GetLastWin32Error(); - Git.Trace.WriteLine($"unable to determine credential buffer size ('{NativeMethods.Win32Error.GetText(error)}')."); - - return null; - } - - inBufferPtr = Marshal.AllocHGlobal(inBufferSize); - - if (!NativeMethods.CredPackAuthenticationBuffer(flags: authPackage, - username: username, - password: string.Empty, - packedCredentials: inBufferPtr, - packedCredentialsSize: ref inBufferSize)) - { - error = Marshal.GetLastWin32Error(); - Git.Trace.WriteLine($"unable to write to credential buffer ('{NativeMethods.Win32Error.GetText(error)}')."); + private Credential ModalPromptForPassword(TargetUri targetUri, string message, string username) + => _modalPromptForPassword(this, targetUri, message, username); - return null; - } - - if (ModalPromptDisplayDialog(ref credUiInfo, - ref authPackage, - packedAuthBufferPtr, - packedAuthBufferSize, - inBufferPtr, - inBufferSize, - saveCredentials, - flags, - out username, - out password)) - { - return new Credential(username, password); - } - } - finally - { - if (inBufferPtr != IntPtr.Zero) - { - Marshal.FreeCoTaskMem(inBufferPtr); - } - } - - return null; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - private static void PrintVersion() + private void PrintVersion() { - Program.WriteLine($"{Title} version {Version.ToString(3)}"); + WriteLine($"{Title} version {Version.ToString(3)}"); } - private static bool StandardHandleIsTty(NativeMethods.StandardHandleType handleType) - { - var standardHandle = NativeMethods.GetStdHandle(handleType); - var handleFileType = NativeMethods.GetFileType(standardHandle); - return handleFileType == NativeMethods.FileType.Char; - } - - internal static bool TryReadBoolean(OperationArguments operationArguments, string configKey, string environKey, out bool? value) - { - if (ReferenceEquals(operationArguments, null)) - throw new ArgumentNullException(nameof(operationArguments)); - - var envars = operationArguments.EnvironmentVariables; + private bool StandardHandleIsTty(NativeMethods.StandardHandleType handleType) + => _standardHandleIsTty(this, handleType); - // look for an entry in the environment variables - string localVal = null; - if (!String.IsNullOrWhiteSpace(environKey) - && envars.TryGetValue(environKey, out localVal)) - { - goto parse_localval; - } - - var config = operationArguments.GitConfiguration; - - // look for an entry in the git config - Configuration.Entry entry; - if (!String.IsNullOrWhiteSpace(configKey) - && config.TryGetEntry(ConfigPrefix, operationArguments.QueryUri, configKey, out entry)) - { - goto parse_localval; - } + internal bool TryReadBoolean(OperationArguments operationArguments, string configKey, string environKey, out bool? value) + => _tryReadBoolean(this, operationArguments, configKey, environKey, out value); - // parse the value into a bool - parse_localval: - - // An empty value is unset / should not be there, so treat it as if it isn't. - if (String.IsNullOrWhiteSpace(localVal)) - { - value = null; - return false; - } - - // Test `localValue` for a Git 'true' equivalent value - if (ConfigValueComparer.Equals(localVal, "yes") - || ConfigValueComparer.Equals(localVal, "true") - || ConfigValueComparer.Equals(localVal, "1") - || ConfigValueComparer.Equals(localVal, "on")) - { - value = true; - return true; - } - - // Test `localValue` for a Git 'false' equivalent value - if (ConfigValueComparer.Equals(localVal, "no") - || ConfigValueComparer.Equals(localVal, "false") - || ConfigValueComparer.Equals(localVal, "0") - || ConfigValueComparer.Equals(localVal, "off")) - { - value = false; - return true; - } - - value = null; - return false; - } - - private static bool TryReadString(OperationArguments operationArguments, string configKey, string environKey, out string value) - { - if (ReferenceEquals(operationArguments, null)) - throw new ArgumentNullException(nameof(operationArguments)); - - var envars = operationArguments.EnvironmentVariables; - - // look for an entry in the environment variables - string localVal; - if (!String.IsNullOrWhiteSpace(environKey) - && envars.TryGetValue(environKey, out localVal) - && !String.IsNullOrWhiteSpace(localVal)) - { - value = localVal; - return true; - } - - var config = operationArguments.GitConfiguration; - - // look for an entry in the git config - Configuration.Entry entry; - if (!String.IsNullOrWhiteSpace(configKey) - && config.TryGetEntry(ConfigPrefix, operationArguments.QueryUri, configKey, out entry) - && !String.IsNullOrWhiteSpace(entry.Value)) - { - value = entry.Value; - return true; - } - - value = null; - return false; - } - - private static bool ModalPromptDisplayDialog( - ref NativeMethods.CredentialUiInfo credUiInfo, - ref NativeMethods.CredentialPackFlags authPackage, - IntPtr packedAuthBufferPtr, - uint packedAuthBufferSize, - IntPtr inBufferPtr, - int inBufferSize, - bool saveCredentials, - NativeMethods.CredentialUiWindowsFlags flags, - out string username, - out string password) - { - int error; - - try - { - // open a standard Windows authentication dialog to acquire username + password credentials - if ((error = NativeMethods.CredUIPromptForWindowsCredentials(credInfo: ref credUiInfo, - authError: 0, - authPackage: ref authPackage, - inAuthBuffer: inBufferPtr, - inAuthBufferSize: (uint)inBufferSize, - outAuthBuffer: out packedAuthBufferPtr, - outAuthBufferSize: out packedAuthBufferSize, - saveCredentials: ref saveCredentials, - flags: flags)) != NativeMethods.Win32Error.Success) - { - Git.Trace.WriteLine($"credential prompt failed ('{NativeMethods.Win32Error.GetText(error)}')."); - - username = null; - password = null; - - return false; - } - - // use `StringBuilder` references instead of string so that they can be written to - StringBuilder usernameBuffer = new StringBuilder(512); - StringBuilder domainBuffer = new StringBuilder(256); - StringBuilder passwordBuffer = new StringBuilder(512); - int usernameLen = usernameBuffer.Capacity; - int passwordLen = passwordBuffer.Capacity; - int domainLen = domainBuffer.Capacity; - - // unpack the result into locally useful data - if (!NativeMethods.CredUnPackAuthenticationBuffer(flags: authPackage, - authBuffer: packedAuthBufferPtr, - authBufferSize: packedAuthBufferSize, - username: usernameBuffer, - maxUsernameLen: ref usernameLen, - domainName: domainBuffer, - maxDomainNameLen: ref domainLen, - password: passwordBuffer, - maxPasswordLen: ref passwordLen)) - { - username = null; - password = null; - - error = Marshal.GetLastWin32Error(); - Git.Trace.WriteLine($"failed to unpack buffer ('{NativeMethods.Win32Error.GetText(error)}')."); - - return false; - } - - Git.Trace.WriteLine("successfully acquired credentials from user."); - - username = usernameBuffer.ToString(); - password = passwordBuffer.ToString(); - - return true; - } - finally - { - if (packedAuthBufferPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(packedAuthBufferPtr); - } - } - } + internal bool TryReadString(OperationArguments operationArguments, string configKey, string environKey, out string value) + => _tryReadString(this, operationArguments, configKey, environKey, out value); } } diff --git a/Microsoft.Alm.Authentication/Secret.cs b/Microsoft.Alm.Authentication/Secret.cs index 2035c224f..82305d456 100644 --- a/Microsoft.Alm.Authentication/Secret.cs +++ b/Microsoft.Alm.Authentication/Secret.cs @@ -26,10 +26,10 @@ using System; namespace Microsoft.Alm.Authentication -{ - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings")] +{ public abstract class Secret { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings")] public static string UriToName(TargetUri targetUri, string @namespace) { BaseSecureStore.ValidateTargetUri(targetUri); @@ -42,6 +42,7 @@ public static string UriToName(TargetUri targetUri, string @namespace) return targetName; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings")] public static string UriToUrl(TargetUri targetUri, string @namespace) { BaseSecureStore.ValidateTargetUri(targetUri); @@ -58,10 +59,11 @@ public static string UriToUrl(TargetUri targetUri, string @namespace) /// Generate a key based on the ActualUri. /// This may include username, port, etc /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings")] public static string UriToActualUrl(TargetUri targetUri, string @namespace) { BaseSecureStore.ValidateTargetUri(targetUri); - if (String.IsNullOrWhiteSpace(@namespace)) + if (string.IsNullOrWhiteSpace(@namespace)) throw new ArgumentNullException(@namespace); string targetName = $"{@namespace}:{targetUri.ActualUri.AbsoluteUri}"; diff --git a/Microsoft.Alm.Authentication/TargetUri.cs b/Microsoft.Alm.Authentication/TargetUri.cs index 98060b1ee..5b9f95768 100644 --- a/Microsoft.Alm.Authentication/TargetUri.cs +++ b/Microsoft.Alm.Authentication/TargetUri.cs @@ -212,6 +212,7 @@ public override string ToString() return ToString(false, true, true); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public string ToString(bool username = false, bool port = false, bool path = false) { // Start building up a url with the scheme @@ -314,6 +315,7 @@ public static implicit operator TargetUri(Uri uri) /// /// If the already contains a username, that one is kept NOT overwritten. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")] public TargetUri GetPerUserTargetUri(string username) { // belt and braces, don't add a username if the URI already contains one. @@ -328,6 +330,8 @@ public TargetUri GetPerUserTargetUri(string username) /// /// Get a version of this that does NOT contain any username. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")] public TargetUri GetHostTargetUri() { // belt and braces, don't add a username if the URI already contains one. @@ -348,6 +352,7 @@ public TargetUri GetHostTargetUri() /// /// Get username contained in the ActualUri of this /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")] public string TargetUriUsername { get { return this.ActualUri.UserInfo; } } } } diff --git a/Microsoft.Alm.Authentication/Token.cs b/Microsoft.Alm.Authentication/Token.cs index 83e2cdb23..870f01fbc 100644 --- a/Microsoft.Alm.Authentication/Token.cs +++ b/Microsoft.Alm.Authentication/Token.cs @@ -176,7 +176,7 @@ public static void Validate(Token token) if (token == null) throw new ArgumentNullException(nameof(token)); if (String.IsNullOrWhiteSpace(token.Value)) - throw new ArgumentException("Value propertry returned null or empty.", nameof(token)); + throw new ArgumentException("Value property returned null or empty.", nameof(token)); if (token.Value.Length > NativeMethods.Credential.PasswordMaxLength) throw new ArgumentOutOfRangeException(nameof(token)); } diff --git a/Microsoft.Alm.Git/Configuration.cs b/Microsoft.Alm.Git/Configuration.cs index 6f11501bd..c8fa24cb3 100644 --- a/Microsoft.Alm.Git/Configuration.cs +++ b/Microsoft.Alm.Git/Configuration.cs @@ -51,11 +51,13 @@ public static IEnumerable Levels } } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] public virtual string this[string key] { get => throw new NotImplementedException(); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] public virtual int Count { get => throw new NotImplementedException(); @@ -72,7 +74,7 @@ public virtual void LoadGitConfiguration(string directory, ConfigurationLevel ty public static Configuration ReadConfiuration(string directory, bool loadLocal, bool loadSystem) { - if (String.IsNullOrWhiteSpace(directory)) + if (string.IsNullOrWhiteSpace(directory)) throw new ArgumentNullException("directory"); if (!Directory.Exists(directory)) throw new DirectoryNotFoundException(directory); @@ -116,7 +118,7 @@ internal static void ParseGitConfig(TextReader reader, IDictionary= 2 && !String.IsNullOrWhiteSpace(match.Groups[1].Value)) + if (match.Groups.Count >= 2 && !string.IsNullOrWhiteSpace(match.Groups[1].Value)) { section = match.Groups[1].Value.Trim(); // check if the section is named, if so: process the name - if (match.Groups.Count >= 3 && !String.IsNullOrWhiteSpace(match.Groups[2].Value)) + if (match.Groups.Count >= 3 && !string.IsNullOrWhiteSpace(match.Groups[2].Value)) { string val = match.Groups[2].Value.Trim(); @@ -155,8 +157,8 @@ internal static void ParseGitConfig(TextReader reader, IDictionary= 3 - && !String.IsNullOrEmpty(match.Groups[1].Value) - && !String.IsNullOrEmpty(match.Groups[2].Value)) + && !string.IsNullOrEmpty(match.Groups[1].Value) + && !string.IsNullOrEmpty(match.Groups[2].Value)) { string key = section + HostSplitCharacter + match.Groups[1].Value.Trim(); string val = match.Groups[2].Value.Trim(); @@ -300,9 +302,9 @@ public sealed override bool TryGetEntry(string prefix, string key, string suffix if (ReferenceEquals(suffix, null)) throw new ArgumentNullException(nameof(suffix)); - string match = String.IsNullOrEmpty(key) - ? String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}.{1}", prefix, suffix) - : String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}.{1}.{2}", prefix, key, suffix); + string match = string.IsNullOrEmpty(key) + ? string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}.{1}", prefix, suffix) + : string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}.{1}.{2}", prefix, key, suffix); // if there's a match, return it if (ContainsKey(match)) @@ -325,11 +327,11 @@ public sealed override bool TryGetEntry(string prefix, Uri targetUri, string key { // return match seeking from most specific (.://.) to // least specific (credential.) - if (TryGetEntry(prefix, String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}://{1}", targetUri.Scheme, targetUri.Host), key, out entry) + if (TryGetEntry(prefix, string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}://{1}", targetUri.Scheme, targetUri.Host), key, out entry) || TryGetEntry(prefix, targetUri.Host, key, out entry)) return true; - if (!String.IsNullOrWhiteSpace(targetUri.Host)) + if (!string.IsNullOrWhiteSpace(targetUri.Host)) { string[] fragments = targetUri.Host.Split(HostSplitCharacter); string host = null; @@ -338,7 +340,7 @@ public sealed override bool TryGetEntry(string prefix, Uri targetUri, string key // match against a top-level domain (aka ".com") for (int i = 1; i < fragments.Length - 1; i++) { - host = String.Join(".", fragments, i, fragments.Length - i); + host = string.Join(".", fragments, i, fragments.Length - i); if (TryGetEntry(prefix, host, key, out entry)) return true; } @@ -346,7 +348,7 @@ public sealed override bool TryGetEntry(string prefix, Uri targetUri, string key } // try to find an unadorned match as a complete fallback - if (TryGetEntry(prefix, String.Empty, key, out entry)) + if (TryGetEntry(prefix, string.Empty, key, out entry)) return true; // nothing found @@ -406,7 +408,7 @@ public sealed override void LoadGitConfiguration(string directory, Configuration private void ParseGitConfig(ConfigurationLevel level, string configPath) { Debug.Assert(Enum.IsDefined(typeof(ConfigurationLevel), level), $"The `{nameof(level)}` parameter is not defined."); - Debug.Assert(!String.IsNullOrWhiteSpace(configPath), $"The `{nameof(configPath)}` parameter is null or invalid."); + Debug.Assert(!string.IsNullOrWhiteSpace(configPath), $"The `{nameof(configPath)}` parameter is null or invalid."); Debug.Assert(File.Exists(configPath), $"The `{nameof(configPath)}` parameter references a non-existent file."); if (!_values.ContainsKey(level)) @@ -458,7 +460,7 @@ public override int GetHashCode() public override string ToString() { - return String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} = {1}", Key, Value); + return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} = {1}", Key, Value); } public static bool operator ==(Entry left, Entry right) diff --git a/Microsoft.Vsts.Authentication/BaseVstsAuthentication.cs b/Microsoft.Vsts.Authentication/BaseVstsAuthentication.cs index 2d12e273e..61b1f1c04 100644 --- a/Microsoft.Vsts.Authentication/BaseVstsAuthentication.cs +++ b/Microsoft.Vsts.Authentication/BaseVstsAuthentication.cs @@ -121,6 +121,7 @@ public override void DeleteCredentials(TargetUri targetUri) /// /// if the authority is Visual Studio Online; otherwise /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")] public static async Task> DetectAuthority(TargetUri targetUri) {