diff --git a/doBuild.ps1 b/doBuild.ps1 index d997cc44a..cc3edcbbb 100644 --- a/doBuild.ps1 +++ b/doBuild.ps1 @@ -95,8 +95,15 @@ function DoBuild 'Azure.Core' 'Azure.Identity' 'Microsoft.Bcl.AsyncInterfaces' + 'Microsoft.Bcl.Cryptography' + 'Microsoft.Extensions.Configuration.Abstractions' + 'Microsoft.Extensions.DependencyInjection.Abstractions' + 'Microsoft.Extensions.Diagnostics.Abstractions' 'Microsoft.Extensions.FileProviders.Abstractions' 'Microsoft.Extensions.FileSystemGlobbing' + 'Microsoft.Extensions.Hosting.Abstractions' + 'Microsoft.Extensions.Logging.Abstractions' + 'Microsoft.Extensions.Options' 'Microsoft.Extensions.Primitives' 'Microsoft.Identity.Client' 'Microsoft.Identity.Client.Extensions.Msal' @@ -117,18 +124,20 @@ function DoBuild 'System.Collections.Immutable' 'System.ClientModel' 'System.Diagnostics.DiagnosticSource' + 'System.Formats.Asn1' 'System.IO.FileSystem.AccessControl' + 'System.IO.Pipelines' 'System.Memory.Data' 'System.Memory' 'System.Numerics.Vectors' 'System.Runtime.CompilerServices.Unsafe' 'System.Security.AccessControl' + 'System.Security.Cryptography.Pkcs' 'System.Security.Cryptography.ProtectedData' 'System.Security.Principal.Windows' 'System.Text.Encodings.Web' 'System.Text.Json' 'System.Threading.Tasks.Extensions' - 'System.ValueTuple' ) $buildSuccess = $true diff --git a/src/code/Microsoft.PowerShell.PSResourceGet.csproj b/src/code/Microsoft.PowerShell.PSResourceGet.csproj index 1f4221845..b5e4e60ee 100644 --- a/src/code/Microsoft.PowerShell.PSResourceGet.csproj +++ b/src/code/Microsoft.PowerShell.PSResourceGet.csproj @@ -14,19 +14,19 @@ - - - - - - + + + + + + - + - + - + diff --git a/src/code/ModuleInitAndCleanup.cs b/src/code/ModuleInitAndCleanup.cs index b3f1992d1..40b1ad182 100644 --- a/src/code/ModuleInitAndCleanup.cs +++ b/src/code/ModuleInitAndCleanup.cs @@ -10,7 +10,7 @@ public class UnsafeAssemblyHandler : IModuleAssemblyInitializer, IModuleAssembly { private static readonly Assembly s_self; private static readonly string s_dependencyFolder; - private static readonly HashSet s_dependencies; + private static readonly Dictionary s_dependencies; private static readonly AssemblyLoadContextProxy s_proxy; private static readonly HashSet NetFrameworkLoadFromPath = new HashSet(StringComparer.OrdinalIgnoreCase) @@ -30,12 +30,13 @@ static UnsafeAssemblyHandler() { s_self = Assembly.GetExecutingAssembly(); s_dependencyFolder = Path.Combine(Path.GetDirectoryName(s_self.Location), "dependencies"); - s_dependencies = new HashSet(StringComparer.Ordinal); + s_dependencies = new Dictionary(StringComparer.OrdinalIgnoreCase); s_proxy = AssemblyLoadContextProxy.CreateLoadContext("psresourceget-load-context"); foreach (string filePath in Directory.EnumerateFiles(s_dependencyFolder, "*.dll")) { - s_dependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName); + AssemblyName dependency = AssemblyName.GetAssemblyName(filePath); + s_dependencies[dependency.Name] = dependency; } } @@ -58,8 +59,57 @@ private static bool IsAssemblyMatching(AssemblyName assemblyName, Assembly reque // Sometimes the requesting assembly can be one of the NuGet.* dlls, in such case, we check if the // requesting assembly is part of our pre-defined dependencies list. return requestingAssembly is not null - ? (requestingAssembly == s_self || (s_dependencies.Contains(requestingAssembly.FullName))) && s_dependencies.Contains(assemblyName.FullName) - : s_dependencies.Contains(assemblyName.FullName); + ? (requestingAssembly == s_self || IsBundledDependency(requestingAssembly.GetName())) && IsCompatibleDependency(assemblyName) + : IsCompatibleDependency(assemblyName); + } + + private static bool IsBundledDependency(AssemblyName assemblyName) + { + return s_dependencies.TryGetValue(assemblyName.Name, out AssemblyName dependency) + && string.Equals(dependency.FullName, assemblyName.FullName, StringComparison.OrdinalIgnoreCase); + } + + // Dependencies are often compiled against older versions of shared assemblies than the ones bundled + // with this module (e.g. Azure.Core references System.Text.Json 10.0.0.3 while 10.0.0.13 is bundled). + // Without binding redirects, accept any request for the same assembly whose version is not newer than the bundled one. + private static bool IsCompatibleDependency(AssemblyName requested) + { + if (!s_dependencies.TryGetValue(requested.Name, out AssemblyName bundled)) + { + return false; + } + + if (requested.Version is not null && requested.Version > bundled.Version) + { + return false; + } + + if (!string.Equals(requested.CultureName ?? string.Empty, bundled.CultureName ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + byte[] requestedToken = requested.GetPublicKeyToken(); + if (requestedToken is null || requestedToken.Length == 0) + { + return true; + } + + byte[] bundledToken = bundled.GetPublicKeyToken(); + if (bundledToken is null || bundledToken.Length != requestedToken.Length) + { + return false; + } + + for (int i = 0; i < requestedToken.Length; i++) + { + if (requestedToken[i] != bundledToken[i]) + { + return false; + } + } + + return true; } private static Assembly HandleAssemblyResolve(object sender, ResolveEventArgs args) diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 2c121b3db..5a1dea45e 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -52,6 +52,7 @@ internal enum CallerCmdlet private ResourceType resourceType = ResourceType.None; string userAgentString = UserAgentInfo.UserAgentString(); private bool _isNupkgPathSpecified = false; + private readonly bool _allowInsecureConnection; private Hashtable dependencies; private Hashtable parsedMetadata; private PSCredential Credential; @@ -87,7 +88,8 @@ internal PublishHelper(PSCmdlet cmdlet, string destinationPath, bool skipModuleManifestValidate, CancellationToken cancellationToken, - bool isNupkgPathSpecified) + bool isNupkgPathSpecified, + bool allowInsecureConnection) { _callerCmdlet = CallerCmdlet.PublishPSResource; _cmdOperation = "Publish"; @@ -99,6 +101,7 @@ internal PublishHelper(PSCmdlet cmdlet, SkipModuleManifestValidate = skipModuleManifestValidate; _cancellationToken = cancellationToken; _isNupkgPathSpecified = isNupkgPathSpecified; + _allowInsecureConnection = allowInsecureConnection; outputDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString()); outputNupkgDir = System.IO.Path.Combine(outputDir, "nupkg"); } @@ -700,6 +703,7 @@ private bool PushNupkg(string outputNupkgDir, string repoName, string repoUri, N noSymbols: false, noServiceEndpoint: false, // enable server endpoint skipDuplicate: false, // if true-- if a package and version already exists, skip it and continue with the next package in the push, if any. + allowInsecureConnections: _allowInsecureConnection, logger: NullLogger.Instance // nuget logger ).GetAwaiter().GetResult(); } diff --git a/src/code/PublishPSResource.cs b/src/code/PublishPSResource.cs index e734cf3de..cbf558482 100644 --- a/src/code/PublishPSResource.cs +++ b/src/code/PublishPSResource.cs @@ -31,6 +31,12 @@ public sealed class PublishPSResource : PSCmdlet [ValidateNotNullOrEmpty] public string ApiKey { get; set; } + /// + /// Allows publishing to HTTP NuGet repositories. Defaults to false. + /// + [Parameter] + public bool AllowInsecureConnection { get; set; } = false; + /// /// Specifies the repository to publish to. /// @@ -185,7 +191,8 @@ protected override void BeginProcessing() DestinationPath, SkipModuleManifestValidate, _cancellationToken, - _isNupkgPathSpecified); + _isNupkgPathSpecified, + AllowInsecureConnection); _publishHelper.CheckAllParameterPaths(); } diff --git a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 index bb58ddeed..c8965ed24 100644 --- a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 @@ -118,6 +118,66 @@ Describe "Test Publish-PSResource" -tags 'CI' { Remove-Item $pkgsToDelete -Recurse -ErrorAction SilentlyContinue } + It "AllowInsecureConnection is a Boolean parameter defaulting to false in both parameter sets" { + $command = Get-Command Publish-PSResource + $command.Parameters['AllowInsecureConnection'].ParameterType | Should -Be ([bool]) + foreach ($parameterSet in $command.ParameterSets) { + $parameterSet.Parameters.Name | Should -Contain 'AllowInsecureConnection' + } + + $cmdlet = New-Object Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource + $cmdlet.AllowInsecureConnection | Should -BeFalse + } + + It "Publish a module with AllowInsecureConnection set to " -TestCases @( + @{ AllowInsecureConnection = $true } + @{ AllowInsecureConnection = $false } + ) { + param($AllowInsecureConnection) + + $version = "1.0.0" + New-ModuleManifest -Path (Join-Path $script:PublishModuleBase "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" + + Publish-PSResource -Path $script:PublishModuleBase -Repository $testRepository2 -AllowInsecureConnection $AllowInsecureConnection + + $expectedPath = Join-Path $script:repositoryPath2 "$script:PublishModuleName.$version.nupkg" + Test-Path $expectedPath | Should -BeTrue + } + + It "Reject HTTP publishing using with AllowInsecureConnection omitted: " -TestCases @( + @{ ParameterName = 'Path'; OmitParameter = $true } + @{ ParameterName = 'Path'; OmitParameter = $false } + @{ ParameterName = 'NupkgPath'; OmitParameter = $true } + @{ ParameterName = 'NupkgPath'; OmitParameter = $false } + ) { + param($ParameterName, $OmitParameter) + + $repositoryName = 'InsecurePublishTest' + Register-PSResourceRepository -Name $repositoryName -Uri 'http://127.0.0.1:1/v2' -ApiVersion v2 + try { + New-ModuleManifest -Path (Join-Path $script:PublishModuleBase "$script:PublishModuleName.psd1") -ModuleVersion '1.0.0' -Description "$script:PublishModuleName module" + $publishParameters = @{ Path = $script:PublishModuleBase } + if ($ParameterName -eq 'NupkgPath') { + Compress-PSResource -Path $script:PublishModuleBase -DestinationPath $script:destinationPath + $publishParameters = @{ NupkgPath = (Join-Path $script:destinationPath "$script:PublishModuleName.1.0.0.nupkg") } + } + + if (!$OmitParameter) { + $publishParameters.AllowInsecureConnection = $false + } + + $publishErrors = @() + Publish-PSResource @publishParameters -Repository $repositoryName -SkipDependenciesCheck -ErrorAction SilentlyContinue -ErrorVariable publishErrors + + $publishErrors.Count | Should -Be 1 + $publishErrors[0].FullyQualifiedErrorId | Should -Be 'PushNupkgError,Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource' + $publishErrors[0].Exception.Message | Should -Match 'HTTPS' + } + finally { + Unregister-PSResourceRepository -Name $repositoryName + } + } + It "Publish module with required module not installed on the local machine using -SkipModuleManifestValidate" { # Skip the module manifest validation test, which fails from the missing manifest required module. $testModulePath = Join-Path -Path $TestDrive -ChildPath ModuleWithMissingRequiredModule