133 lines
5.2 KiB
C#
133 lines
5.2 KiB
C#
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<Task<string?>> _credentialHelper = new Lazy<Task<string?>>(DetermineGitCredentialHelper);
|
|
|
|
public async Task<bool> CanProvideCredentialsAsync(Uri uri, CancellationToken cancel)
|
|
{
|
|
Task<string?> 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<GetAuthenticationCredentialsResponse> 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=" + 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<string> { "Basic" }, MessageResponseCode.Success
|
|
);
|
|
}
|
|
|
|
// Password not found (we may be allowed to ask for it interactively)
|
|
if (!request.IsNonInteractive)
|
|
{
|
|
ProcessStartInfo psiDialog = new ProcessStartInfo();
|
|
psiDialog.FileName = "zenity";
|
|
foreach (string i in (IEnumerable<string>)["--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<string> { "Basic" }, MessageResponseCode.Success
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return new GetAuthenticationCredentialsResponse(null, null, null, null, responseCode: MessageResponseCode.NotFound);
|
|
}
|
|
|
|
private static async Task<string?> 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;
|
|
}
|
|
}
|