Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "[Feature] Implement Support for NuGet Authentication Plugins … #6160

Merged
merged 1 commit into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/NuGet.Core/NuGet.Protocol/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
[assembly: SuppressMessage("Build", "CA1031:Modify 'FireBeforeClose' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "<Pending>", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.Plugin.FireBeforeClose")]
[assembly: SuppressMessage("Build", "CA1031:Modify 'FireClosed' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "<Pending>", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.Plugin.FireClosed")]
[assembly: SuppressMessage("Build", "CA2000:Call System.IDisposable.Dispose on object created by 'new MonitorNuGetProcessExitRequestHandler(plugin)' before all references to it are out of scope.", Justification = "<Pending>", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginFactory.CreateFromCurrentProcessAsync(NuGet.Protocol.Plugins.IRequestHandlers,NuGet.Protocol.Plugins.ConnectionOptions,System.Threading.CancellationToken)~System.Threading.Tasks.Task{NuGet.Protocol.Plugins.IPlugin}")]
[assembly: SuppressMessage("Build", "CA2000:Use recommended dispose pattern to ensure that object created by 'new PluginProcess(startInfo)' is disposed on all paths. If possible, wrap the creation within a 'using' statement or a 'using' declaration. Otherwise, use a try-finally pattern, with a dedicated local variable declared before the try region and an unconditional Dispose invocation on non-null value in the 'finally' region, say 'x?.Dispose()'. If the object is explicitly disposed within the try region or the dispose ownership is transfered to another object or method, assign 'null' to the local variable just after such an operation to prevent double dispose in 'finally'.", Justification = "The responsibility to dispose the object is transferred to another object or wrapper that's created in the method and returned to the caller", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginFactory.CreatePluginAsync(System.String,System.Collections.Generic.IEnumerable{System.String},NuGet.Protocol.Plugins.IRequestHandlers,NuGet.Protocol.Plugins.ConnectionOptions,System.Threading.CancellationToken)~System.Threading.Tasks.Task{NuGet.Protocol.Plugins.IPlugin}")]
[assembly: SuppressMessage("Build", "CA1031:Modify 'SendCloseRequest' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "<Pending>", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginFactory.SendCloseRequest(NuGet.Protocol.Plugins.IPlugin)")]
[assembly: SuppressMessage("Build", "CA1822:Member GetPluginOperationClaimsAsync does not access instance data and can be marked as static (Shared in VisualBasic)", Justification = "<Pending>", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginManager.GetPluginOperationClaimsAsync(NuGet.Protocol.Plugins.IPlugin,System.String,Newtonsoft.Json.Linq.JObject,System.Threading.CancellationToken)~System.Threading.Tasks.Task{System.Collections.Generic.IReadOnlyList{NuGet.Protocol.Plugins.OperationClaim}}")]
[assembly: SuppressMessage("Build", "CA1031:Modify 'TryCreatePluginAsync' to catch a more specific allowed exception type, or rethrow the exception.", Justification = "<Pending>", Scope = "member", Target = "~M:NuGet.Protocol.Plugins.PluginManager.TryCreatePluginAsync(NuGet.Protocol.Plugins.PluginDiscoveryResult,NuGet.Protocol.Plugins.OperationClaim,NuGet.Protocol.Plugins.PluginManager.PluginRequestKey,System.String,Newtonsoft.Json.Linq.JObject,System.Threading.CancellationToken)~System.Threading.Tasks.Task{System.Tuple{System.Boolean,NuGet.Protocol.Plugins.PluginCreationResult}}")]
Expand Down
8 changes: 4 additions & 4 deletions src/NuGet.Core/NuGet.Protocol/Plugins/IPluginFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ namespace NuGet.Protocol.Plugins
/// <summary>
/// A plugin factory.
/// </summary>
internal interface IPluginFactory : IDisposable
public interface IPluginFactory : IDisposable
{
/// <summary>
/// Asynchronously gets an existing plugin instance or creates a new instance and connects to it.
/// </summary>
/// <param name="pluginFile">A plugin file.</param>
/// <param name="filePath">The file path of the plugin.</param>
/// <param name="arguments">Command-line arguments to be supplied to the plugin.</param>
/// <param name="requestHandlers">Request handlers.</param>
/// <param name="options">Connection options.</param>
/// <param name="sessionCancellationToken">A cancellation token for the plugin's lifetime.</param>
/// <returns>A task that represents the asynchronous operation.
/// The task result (<see cref="Task{TResult}.Result" />) returns a <see cref="Plugin" />
/// instance.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="pluginFile.Path" />
/// <exception cref="ArgumentException">Thrown if <paramref name="filePath" />
/// is either <see langword="null" /> or empty.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="arguments" />
/// is <see langword="null" />.</exception>
Expand All @@ -37,7 +37,7 @@ internal interface IPluginFactory : IDisposable
/// <exception cref="ObjectDisposedException">Thrown if this object is disposed.</exception>
/// <remarks>This is intended to be called by NuGet client tools.</remarks>
Task<IPlugin> GetOrCreateAsync(
PluginFile pluginFile,
string filePath,
IEnumerable<string> arguments,
IRequestHandlers requestHandlers,
ConnectionOptions options,
Expand Down
256 changes: 20 additions & 236 deletions src/NuGet.Core/NuGet.Protocol/Plugins/PluginDiscoverer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
Expand All @@ -18,42 +17,17 @@ public sealed class PluginDiscoverer : IPluginDiscoverer
{
private bool _isDisposed;
private List<PluginFile> _pluginFiles;
private readonly string _netCoreOrNetFXPluginPaths;
private readonly string _nuGetPluginPaths;
private readonly string _rawPluginPaths;
private IEnumerable<PluginDiscoveryResult> _results;
private readonly SemaphoreSlim _semaphore;
private readonly IEnvironmentVariableReader _environmentVariableReader;
private static bool IsDesktop
{
get
{
#if IS_DESKTOP
return true;
#else
return false;
#endif
}
}

public PluginDiscoverer()
: this(EnvironmentVariableWrapper.Instance)
{
}

internal PluginDiscoverer(IEnvironmentVariableReader environmentVariableReader)
/// <summary>
/// Instantiates a new <see cref="PluginDiscoverer" /> class.
/// </summary>
/// <param name="rawPluginPaths">The raw semicolon-delimited list of supposed plugin file paths.</param>
public PluginDiscoverer(string rawPluginPaths)
{
_environmentVariableReader = environmentVariableReader;
#if IS_DESKTOP
_netCoreOrNetFXPluginPaths = environmentVariableReader.GetEnvironmentVariable(EnvironmentVariableConstants.DesktopPluginPaths);
#else
_netCoreOrNetFXPluginPaths = environmentVariableReader.GetEnvironmentVariable(EnvironmentVariableConstants.CorePluginPaths);
#endif

if (string.IsNullOrEmpty(_netCoreOrNetFXPluginPaths))
{
_nuGetPluginPaths = _environmentVariableReader.GetEnvironmentVariable(EnvironmentVariableConstants.PluginPaths);
}

_rawPluginPaths = rawPluginPaths;
_semaphore = new SemaphoreSlim(initialCount: 1, maxCount: 1);
}

Expand Down Expand Up @@ -101,40 +75,7 @@ public async Task<IEnumerable<PluginDiscoveryResult>> DiscoverAsync(Cancellation
return _results;
}

if (!string.IsNullOrEmpty(_netCoreOrNetFXPluginPaths))
{
// NUGET_NETFX_PLUGIN_PATHS, NUGET_NETCORE_PLUGIN_PATHS have been set.
var filePaths = _netCoreOrNetFXPluginPaths.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
_pluginFiles = GetPluginFiles(filePaths, cancellationToken);
}
else if (!string.IsNullOrEmpty(_nuGetPluginPaths))
{
// NUGET_PLUGIN_PATHS has been set
_pluginFiles = GetPluginsInNuGetPluginPaths();
}
else
{
// restore to default plugins search.
// Search for plugins in %user%/.nuget/plugins
var directories = new List<string> { PluginDiscoveryUtility.GetNuGetHomePluginsPath() };
#if IS_DESKTOP
// Internal plugins are only supported for .NET Framework scenarios, namely msbuild.exe
directories.Add(PluginDiscoveryUtility.GetInternalPlugins());
#endif
var filePaths = PluginDiscoveryUtility.GetConventionBasedPlugins(directories);
_pluginFiles = GetPluginFiles(filePaths, cancellationToken);

// Search for .Net tools plugins in PATH
if (_pluginFiles != null)
{
_pluginFiles.AddRange(GetPluginsInPath());
}
else
{
_pluginFiles = GetPluginsInPath();
}
}

_pluginFiles = GetPluginFiles(cancellationToken);
var results = new List<PluginDiscoveryResult>();

for (var i = 0; i < _pluginFiles.Count; ++i)
Expand All @@ -156,16 +97,13 @@ public async Task<IEnumerable<PluginDiscoveryResult>> DiscoverAsync(Cancellation
return _results;
}

private static List<PluginFile> GetPluginFiles(IEnumerable<string> filePaths, CancellationToken cancellationToken)
private List<PluginFile> GetPluginFiles(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var files = new List<PluginFile>();
var filePaths = GetPluginFilePaths();

if (filePaths == null)
{
return files;
}
var files = new List<PluginFile>();

foreach (var filePath in filePaths)
{
Expand All @@ -179,180 +117,26 @@ private static List<PluginFile> GetPluginFiles(IEnumerable<string> filePaths, Ca
{
return PluginFileState.InvalidFilePath;
}
}), requiresDotnetHost: !IsDesktop);
}));
files.Add(pluginFile);
}

return files;
}

/// <summary>
/// Retrieves authentication plugins by searching through directories and files specified in the `NuGET_PLUGIN_PATHS`
/// environment variable. The method looks for files prefixed with 'nuget-plugin-' and verifies their validity for .net tools plugins.
/// </summary>
/// <returns>A list of valid <see cref="PluginFile"/> objects representing the discovered plugins.</returns>
internal List<PluginFile> GetPluginsInNuGetPluginPaths()
private IEnumerable<string> GetPluginFilePaths()
{
var pluginFiles = new List<PluginFile>();
string[] paths = _nuGetPluginPaths?.Split(Path.PathSeparator) ?? Array.Empty<string>();

foreach (var path in paths)
if (string.IsNullOrEmpty(_rawPluginPaths))
{
if (PathValidator.IsValidLocalPath(path) || PathValidator.IsValidUncPath(path))
{
if (File.Exists(path))
{
FileInfo fileInfo = new FileInfo(path);
if (fileInfo.Name.StartsWith("nuget-plugin-", StringComparison.CurrentCultureIgnoreCase))
{
// A DotNet tool plugin
if (IsValidPluginFile(fileInfo))
{
PluginFile pluginFile = new PluginFile(fileInfo.FullName, new Lazy<PluginFileState>(() => PluginFileState.Valid), requiresDotnetHost: false);
pluginFiles.Add(pluginFile);
}
}
else
{
// A non DotNet tool plugin file
var state = new Lazy<PluginFileState>(() => PluginFileState.Valid);
pluginFiles.Add(new PluginFile(fileInfo.FullName, state, requiresDotnetHost: !IsDesktop));
}
}
else if (Directory.Exists(path))
{
pluginFiles.AddRange(GetNetToolsPluginsInDirectory(path) ?? new List<PluginFile>());
}
}
else
{
pluginFiles.Add(new PluginFile(path, new Lazy<PluginFileState>(() => PluginFileState.InvalidFilePath), requiresDotnetHost: !IsDesktop));
}
}

return pluginFiles;
}

/// <summary>
/// Retrieves .NET tools authentication plugins by searching through directories specified in `PATH`
/// </summary>
/// <returns>A list of valid <see cref="PluginFile"/> objects representing the discovered plugins.</returns>
internal List<PluginFile> GetPluginsInPath()
{
var pluginFiles = new List<PluginFile>();
var nugetPluginPaths = _environmentVariableReader.GetEnvironmentVariable("PATH");
string[] paths = nugetPluginPaths?.Split(Path.PathSeparator) ?? Array.Empty<string>();

foreach (var path in paths)
{
if (PathValidator.IsValidLocalPath(path) || PathValidator.IsValidUncPath(path))
{
pluginFiles.AddRange(GetNetToolsPluginsInDirectory(path) ?? new List<PluginFile>());
}
else
{
pluginFiles.Add(new PluginFile(path, new Lazy<PluginFileState>(() => PluginFileState.InvalidFilePath), requiresDotnetHost: false));
}
}

return pluginFiles;
}

private static List<PluginFile> GetNetToolsPluginsInDirectory(string directoryPath)
{
var pluginFiles = new List<PluginFile>();

if (Directory.Exists(directoryPath))
{
var directoryInfo = new DirectoryInfo(directoryPath);
var files = directoryInfo.GetFiles("nuget-plugin-*");

foreach (var file in files)
{
if (IsValidPluginFile(file))
{
PluginFile pluginFile = new PluginFile(file.FullName, new Lazy<PluginFileState>(() => PluginFileState.Valid), requiresDotnetHost: false);
pluginFiles.Add(pluginFile);
}
}
}

return pluginFiles;
}

/// <summary>
/// Checks whether a file is a valid plugin file for windows/Unix.
/// Windows: It should be either .bat or .exe
/// Unix: It should be executable
/// </summary>
/// <param name="fileInfo"></param>
/// <returns></returns>
internal static bool IsValidPluginFile(FileInfo fileInfo)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return fileInfo.Extension.Equals(".exe", StringComparison.OrdinalIgnoreCase) ||
fileInfo.Extension.Equals(".bat", StringComparison.OrdinalIgnoreCase);
}
else
{
#if NET8_0_OR_GREATER
var fileMode = File.GetUnixFileMode(fileInfo.FullName);

return fileInfo.Exists &&
((fileMode & UnixFileMode.UserExecute) != 0 ||
(fileMode & UnixFileMode.GroupExecute) != 0 ||
(fileMode & UnixFileMode.OtherExecute) != 0);
#else
return fileInfo.Exists && IsExecutable(fileInfo);
var directories = new List<string> { PluginDiscoveryUtility.GetNuGetHomePluginsPath() };
#if IS_DESKTOP
// Internal plugins are only supported for .NET Framework scenarios, namely msbuild.exe
directories.Add(PluginDiscoveryUtility.GetInternalPlugins());
#endif
return PluginDiscoveryUtility.GetConventionBasedPlugins(directories);
}
}

#if !NET8_0_OR_GREATER
/// <summary>
/// Checks whether a file is executable or not in Unix.
/// This is done by running bash code: `if [ -x {fileInfo.FullName} ]; then echo yes; else echo no; fi`
/// </summary>
/// <param name="fileInfo"></param>
/// <returns></returns>
internal static bool IsExecutable(FileInfo fileInfo)
{
#pragma warning disable CA1031 // Do not catch general exception types
try
{
string output;
using (var process = new System.Diagnostics.Process())
{
// Use a shell command to check if the file is executable
process.StartInfo.FileName = "/bin/bash";
process.StartInfo.Arguments = $" -c \"if [ -x '{fileInfo.FullName}' ]; then echo yes; else echo no; fi\"";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;

process.Start();
output = process.StandardOutput.ReadToEnd().Trim();

if (!process.HasExited && !process.WaitForExit(1000))
{
process.Kill();
return false;
}
else if (process.ExitCode != 0)
{
return false;
}

// Check if the output is "yes"
return output.Equals("yes", StringComparison.OrdinalIgnoreCase);
}
}
catch
{
return false;
}
#pragma warning restore CA1031 // Do not catch general exception types
return _rawPluginPaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
}
#endif
}
}
Loading
Loading