using System; using System.Diagnostics; using NuGet.Protocol.Plugins; namespace NugetSecretCredential; sealed class GitTokenCredentialProvider : ICredentialProvider { private const string TokenUserName = "nugetToken"; // can be anything constant private const int MaxCredentialHelperTimeout = 2000; private static Lazy> _credentialHelper = new Lazy>(DetermineGitCredentialHelper); public async Task CanProvideCredentialsAsync(Uri uri, CancellationToken cancel) { Task credHelper; if (!_credentialHelper.IsValueCreated || !_credentialHelper.Value.IsCompleted) { TaskCompletionSource cancelTrigger = new TaskCompletionSource(); using (CancellationTokenRegistration cancelReg = cancel.Register(() => cancelTrigger.SetResult())) { credHelper = _credentialHelper.Value; Task t = await Task.WhenAny(credHelper, cancelTrigger.Task); if (t != credHelper) throw new TaskCanceledException(); } } else credHelper = _credentialHelper.Value; return credHelper.Result != null; } public async Task HandleRequestAsync(GetAuthenticationCredentialsRequest request, CancellationToken cancel) { ProcessStartInfo psiHelper = new ProcessStartInfo(); psiHelper.FileName = _credentialHelper.Value.Result; psiHelper.ArgumentList.Add("get"); psiHelper.RedirectStandardInput = true; psiHelper.RedirectStandardOutput = true; Process? pHelper = Process.Start(psiHelper); if (pHelper == null) // Credential helper executable not found return new GetAuthenticationCredentialsResponse(null, null, null, null, responseCode: MessageResponseCode.Error); pHelper.StandardInput.WriteLine("protocol=https"); pHelper.StandardInput.WriteLine("username=s" + TokenUserName); pHelper.StandardInput.WriteLine("host=" + request.Uri.DnsSafeHost); pHelper.StandardInput.WriteLine(); await pHelper.WaitForExitAsync(cancel); string? line; while ((line = pHelper.StandardOutput.ReadLine()) != null) { const string PasswordKey = "password="; if (line.StartsWith(PasswordKey)) // Password found return new GetAuthenticationCredentialsResponse( TokenUserName, line.Substring(PasswordKey.Length), null, new List { "Basic" }, MessageResponseCode.Success ); } // Password not found (we may be allowed to ask for it interactively) if (request.CanShowDialog) { ProcessStartInfo psiDialog = new ProcessStartInfo(); psiDialog.FileName = "zenity"; foreach (string i in (IEnumerable)["--forms", "--title", "Nuget repository", "--text", request.Uri.DnsSafeHost, "--add-password=Token"]) psiDialog.ArgumentList.Add(i); psiDialog.RedirectStandardOutput = true; Process? pDialog = Process.Start(psiDialog); if (pDialog != null) { // Zenity dialog executable found await pDialog.WaitForExitAsync(cancel); string? password = pDialog.StandardOutput.ReadLine(); if (password != null && password.Length != 0 && pDialog.StandardOutput.ReadLine() == null) { // We have a new password, persist it also ProcessStartInfo psiHelperStore = new ProcessStartInfo(); psiHelperStore.FileName = _credentialHelper.Value.Result; psiHelperStore.ArgumentList.Add("store"); psiHelperStore.RedirectStandardInput = true; Process? pHelperStore = Process.Start(psiHelperStore); if (pHelperStore == null) // Credential helper executable not found return new GetAuthenticationCredentialsResponse(null, null, null, null, responseCode: MessageResponseCode.Error); pHelperStore.StandardInput.WriteLine("protocol=https"); pHelperStore.StandardInput.WriteLine("username=" + TokenUserName); pHelperStore.StandardInput.WriteLine("password=" + password); pHelperStore.StandardInput.WriteLine("host=" + request.Uri.DnsSafeHost); pHelper.StandardInput.WriteLine(); await pHelperStore.WaitForExitAsync(cancel); return new GetAuthenticationCredentialsResponse( TokenUserName, password, null, new List { "Basic" }, MessageResponseCode.Success ); } } } return new GetAuthenticationCredentialsResponse(null, null, null, null, responseCode: MessageResponseCode.NotFound); } private static async Task DetermineGitCredentialHelper() { CancellationTokenSource cancel = new CancellationTokenSource(MaxCredentialHelperTimeout); ProcessStartInfo psi = new ProcessStartInfo(); psi.FileName = "git"; psi.ArgumentList.Add("config"); psi.ArgumentList.Add("--global"); psi.ArgumentList.Add("credential.helper"); psi.RedirectStandardOutput = true; Process? p = Process.Start(psi); if (p == null) // Git executable not found return null; await p.WaitForExitAsync(cancel.Token); string? result = p.StandardOutput.ReadLine(); if (p.StandardOutput.ReadLine() != null) // Expected a single line with the credential helper executable path return null; return result; } }