PowerShell/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/BootstrapRequest.cs
PowerShell Team c748652c34 Copy all mapped files from [SD:725290]
commit 8cec8f150da7583b7af5efbe2853efee0179750c
2016-07-28 23:23:03 -07:00

695 lines
29 KiB
C#

//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
namespace Microsoft.PackageManagement.Providers.Internal.Bootstrap {
using System;
using System.Collections;
using System.Globalization;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System.Security.Cryptography;
using System.Management.Automation;
using PackageManagement.Internal;
using PackageManagement.Internal.Implementation;
using PackageManagement.Internal.Packaging;
using PackageManagement.Internal.Utility.Platform;
using PackageManagement.Internal.Utility.Collections;
using PackageManagement.Internal.Utility.Extensions;
using ErrorCategory = PackageManagement.Internal.ErrorCategory;
using System.IO.Compression;
using File = System.IO.File;
using Directory = System.IO.Directory;
public abstract class BootstrapRequest : Request {
internal Uri[] _urls
{
get
{
string testfeed = Environment.GetEnvironmentVariable("BootstrapProviderTestfeedUrl");
// if testfeed exists, use that
if (!string.IsNullOrWhiteSpace(testfeed))
{
Uri result = null;
// test whether the uri is valid
if (Uri.TryCreate(testfeed, UriKind.Absolute, out result))
{
return new Uri[] {
result
};
}
}
return new Uri[] {
#if LOCAL_DEBUG
new Uri("https://localhost:81/providers.swidtag"),
#endif
#if CORECLR
new Uri("https://go.microsoft.com/fwlink/?LinkID=627340&clcid=0x409"),
// starting in 2015/05 builds, we bootstrap from here:
#else
new Uri("https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409"),
#endif
};
}
}
private IEnumerable<Feed> _feeds;
private IEnumerable<string> _fileSource = null;
private IEnumerable<Feed> Feeds {
get {
if (_feeds == null) {
if (LocalSource.Any()) {
Verbose(Resources.Messages.UseLocalSource, LocalSource.FirstOrDefault());
return Enumerable.Empty<Feed>();
}
if (!System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) {
Warning(Constants.Messages.NetworkNotAvailable);
Warning(string.Format(CultureInfo.CurrentCulture, Resources.Messages.ProviderBootstrapFailed));
}
// right now, we only have one feed (can have many urls tho')
// so we just return a single feed in the collection
// but later, we can expand it to support multiple feeds.
var feed = new Feed(this, _urls);
if (feed.IsValid) {
_feeds = feed.SingleItemAsEnumerable().ReEnumerable();
} else {
Warning(Constants.Messages.ProviderSwidtagUnavailable);
return Enumerable.Empty<Feed>();
}
}
return _feeds;
}
}
internal IEnumerable<string> LocalSource {
get
{
if (_fileSource == null)
{
if (Sources.IsNullOrEmpty())
{
_fileSource = Enumerable.Empty<string>();
} else {
_fileSource = Sources.Where(each => !string.IsNullOrWhiteSpace(each) && (System.IO.File.Exists(each) || System.IO.Directory.Exists(each))).WhereNotNull();
}
}
return _fileSource;
}
}
internal string DestinationPath(Request request) {
var pms = PackageManagementService as PackageManagementService;
var scope = GetValue("Scope");
if (!string.IsNullOrWhiteSpace(scope)) {
if (scope.EqualsIgnoreCase("CurrentUser")) {
return pms.UserAssemblyLocation;
}
if (AdminPrivilege.IsElevated) {
return pms.SystemAssemblyLocation;
} else {
//a user specifies 'AllUsers' that requires Admin provilege. However his console gets launched by non-elevated.
Error(ErrorCategory.InvalidOperation, ErrorCategory.InvalidOperation.ToString(),
PackageManagement.Resources.Messages.InstallRequiresCurrentUserScopeParameterForNonAdminUser, pms.SystemAssemblyLocation, pms.UserAssemblyLocation);
return null;
}
}
var v = GetValue("DestinationPath");
if (String.IsNullOrWhiteSpace(v)) {
// use a well-known path.
v = AdminPrivilege.IsElevated ? pms.SystemAssemblyLocation : pms.UserAssemblyLocation;
if (String.IsNullOrWhiteSpace(v)) {
return null;
}
}
return Path.GetFullPath(v);
}
internal IEnumerable<Package> Providers {
get {
return Feeds.SelectMany(feed => feed.Query());
}
}
private string GetValue(string name) {
// get the value from the request
return (GetOptionValues(name) ?? Enumerable.Empty<string>()).LastOrDefault();
}
internal Package GetProvider(Uri uri) {
return new Package(this, uri.SingleItemAsEnumerable());
}
internal Package GetProvider(string name) {
return Feeds.SelectMany(feed => feed.Query(name)).FirstOrDefault();
}
internal Package GetProvider(string name, string version) {
return Feeds.SelectMany(feed => feed.Query(name, version)).FirstOrDefault();
}
internal IEnumerable<Package> GetProviderAll(string name, string minimumversion, string maximumversion) {
return Feeds.SelectMany(feed => feed.Query(name, minimumversion, maximumversion));
}
internal IEnumerable<Package> GetProvider(string name, string minimumversion, string maximumversion) {
return new[] {
GetProviderAll(name, minimumversion, maximumversion)
.OrderByDescending(each => SoftwareIdentityVersionComparer.Instance).FirstOrDefault()
};
}
internal string DownloadAndValidateFile(Swidtag swidtag) {
var file = DownLoadFileFromLinks(swidtag.Links.Where(each => each.Relationship == Iso19770_2.Relationship.InstallationMedia));
if (string.IsNullOrWhiteSpace(file)) {
return null;
}
var payload = swidtag.Payload;
if (payload == null) {
//We let the providers that are already posted in the public continue to be installed.
return file;
} else {
//validate the file hash
var valid = ValidateFileHash(file, payload);
if (!valid) {
//if the hash does not match, delete the file in the temp folder
file.TryHardToDelete();
return null;
}
return file;
}
}
/// <summary>
/// Extract zipped package and return the unzipped folder
/// </summary>
/// <param name="zippedPackagePath"></param>
/// <returns></returns>
private string ExtractZipPackage(string zippedPackagePath)
{
if (zippedPackagePath != null && zippedPackagePath.FileExists())
{
// extracted folder
string extractedFolder = FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory();
try
{
//unzip the file
ZipFile.ExtractToDirectory(zippedPackagePath, extractedFolder);
// extraction fails
if (!Directory.Exists(extractedFolder))
{
Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.FailToExtract, zippedPackagePath, extractedFolder));
return string.Empty;
}
// the zipped folder
var zippedDirectory = Directory.EnumerateDirectories(extractedFolder).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(zippedDirectory) && Directory.Exists(zippedDirectory))
{
return zippedDirectory;
}
}
catch (Exception ex)
{
Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.FailToInstallZipFolder, zippedPackagePath, ex.Message));
Debug(ex.StackTrace);
// remove the extracted folder
extractedFolder.TryHardToDelete();
}
}
return string.Empty;
}
/// <summary>
/// Helper function to retry downloading a file.
/// downloadFileFunction is the main function that is used to download the file when given a uri
/// numberOfTry is how many times we can try to download it
/// </summary>
/// <param name="downloadFileFunction"></param>
/// <param name="location"></param>
/// <param name="numberOfTry"></param>
/// <returns></returns>
internal string RetryDownload(Func<Uri, string> downloadFileFunction, Uri location, uint numberOfTry = 3) {
string file = null;
// if scheme is not https, write warning and ignores this link
if (!string.Equals(location.Scheme, "https")) {
Warning(string.Format(CultureInfo.CurrentCulture, Resources.Messages.OnlyHttpsSchemeSupported, location.AbsoluteUri));
return file;
}
// try 3 times to see whether we can download this
int remainingTry = 3;
// try to download the file for remainingTry times
while (remainingTry > 0) {
try {
file = downloadFileFunction(location);
} finally {
if (file == null || !file.FileExists()) {
// file cannot be download
file = null;
remainingTry -= 1;
Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.RetryDownload, location.AbsoluteUri, remainingTry));
} else {
// file downloaded, no need to retry.
remainingTry = 0;
}
}
}
return file;
}
internal string DownLoadFileFromLinks(IEnumerable<Link> links) {
string file = null;
foreach (var link in links) {
file = RetryDownload(
// the download function takes in a uri link and download it
(uri) => {
var tmpFile = FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory();
return ProviderServices.DownloadFile(uri, tmpFile, -1, true, this);
},
link.HRef);
// got a valid file!
if (file != null && file.FileExists()) {
// if file is zip, unpack it and return the unpacked folder
if (link.MediaType == Iso19770_2.MediaType.ZipPackage)
{
try
{
// let's extract the zipped file
return ExtractZipPackage(file);
}
finally
{
// delete the zipped file
file.TryHardToDelete();
}
}
return file;
}
}
return file;
}
private bool ValidateFileHash(string fileFullPath, Payload payload) {
Debug("BoostrapRequest::ValidateFileHash");
/* format:
* <Payload>
* <File name="nuget-anycpu-2.8.5.205.exe" sha512:hash="a314fc2dc663ae7a6b6bc6787594057396e6b3f569cd50fd5ddb4d1bbafd2b6a" />
* </Payload>
*/
if (payload == null || fileFullPath == null || !fileFullPath.FileExists()) {
return false;
}
try {
if ((payload.Files == null) || !payload.Files.Any()) {
Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.MissingFileTag);
return false;
}
var fileTag = payload.Files.FirstOrDefault();
if ((fileTag.Attributes == null) || (fileTag.Attributes.Keys == null)) {
Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.MissingHashAttribute);
return false;
}
var hashtag = fileTag.Attributes.Keys.FirstOrDefault(each => each.LocalName.Equals("hash"));
if (hashtag == null) {
Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.MissingHashAttribute);
return false;
}
//Note we cannot use switch here because these xname like Iso19770_2.Hash.Hash512, is not compiler time constant
string packageHash = null;
HashAlgorithm hashAlgorithm = null;
if (hashtag.Equals(Iso19770_2.Hash.Hash512)) {
hashAlgorithm = SHA512.Create();
packageHash = fileTag.GetAttribute(Iso19770_2.Hash.Hash512);
} else if (hashtag.Equals(Iso19770_2.Hash.Hash256)) {
hashAlgorithm = SHA256.Create();
packageHash = fileTag.GetAttribute(Iso19770_2.Hash.Hash256);
} else if (hashtag.Equals(Iso19770_2.Hash.Md5)) {
hashAlgorithm = MD5.Create();
packageHash = fileTag.GetAttribute(Iso19770_2.Hash.Md5);
} else {
//hash alroghtme not supported, we support 512, 256, md5 only
Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.UnsupportedHashAlgorithm, hashtag,
new[] {Iso19770_2.HashAlgorithm.Sha512, Iso19770_2.HashAlgorithm.Sha256, Iso19770_2.HashAlgorithm.Md5}.JoinWithComma());
return false;
}
if (string.IsNullOrWhiteSpace(packageHash) || hashAlgorithm == null) {
//missing hash content?
Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.MissingHashContent);
return false;
}
// Verify the hash
using (FileStream stream = System.IO.File.OpenRead(fileFullPath)) {
// compute the hash from the file
byte[] computedHash = hashAlgorithm.ComputeHash(stream);
try {
// convert the original hash we got from the payload tag
byte[] expectedHash = Convert.FromBase64String(packageHash);
//check if hash is equal
if (!computedHash.SequenceEqual(expectedHash)) {
// the file downloaded is not the same as expected. The file is modified.
Error(ErrorCategory.SecurityError, "Payload", Constants.Messages.HashNotEqual, packageHash, Convert.ToBase64String(computedHash));
return false;
}
return true;
} catch (FormatException ex) {
Warning(ex.Message);
Error(ErrorCategory.SecurityError, "Payload", Constants.Messages.InvalidHashFormat, packageHash);
}
}
} catch (Exception ex) {
Warning(ex.Message);
}
return false;
}
internal bool YieldFromSwidtag(Package provider, string requiredVersion, string minimumVersion, string maximumVersion, string searchKey) {
if (provider == null) {
// if the provider isn't there, just return.
return !IsCanceled;
}
if (AnyNullOrEmpty(provider.Name, provider.Version, provider.VersionScheme)) {
Debug("Skipping yield on swid due to missing field \r\n", provider.ToString());
return !IsCanceled;
}
if (!String.IsNullOrWhiteSpace(requiredVersion)) {
if (provider.Version != requiredVersion) {
return !IsCanceled;
}
} else {
if (!String.IsNullOrWhiteSpace(minimumVersion) && SoftwareIdentityVersionComparer.CompareVersions(provider.VersionScheme, provider.Version, minimumVersion) < 0) {
return !IsCanceled;
}
if (!String.IsNullOrWhiteSpace(maximumVersion) && SoftwareIdentityVersionComparer.CompareVersions(provider.VersionScheme, provider.Version, maximumVersion) > 0) {
return !IsCanceled;
}
}
return YieldFromSwidtag(provider, searchKey);
}
internal bool YieldFromSwidtag(Package pkg, string searchKey) {
if (pkg == null) {
return !IsCanceled;
}
var provider = pkg._swidtag;
var fastPackageReference = LocalSource.Any() ? pkg.Location.LocalPath : pkg.Location.AbsoluteUri;
var source = pkg.Source ?? fastPackageReference;
var summary = pkg.Name;
var targetFileName = pkg.Name;
if (!LocalSource.Any()) {
summary = new MetadataIndexer(provider)[Iso19770_2.Attributes.Summary.LocalName].FirstOrDefault();
targetFileName = provider.Links.Select(each => each.Attributes[Iso19770_2.Discovery.TargetFilename]).WhereNotNull().FirstOrDefault();
}
if (YieldSoftwareIdentity(fastPackageReference, provider.Name, provider.Version, provider.VersionScheme, summary, source, searchKey, null, targetFileName) != null) {
// yield all the meta/attributes
if (provider.Meta.Any(
m => {
var element = AddMeta(fastPackageReference);
var attributes = m.Attributes;
return attributes.Keys.Any(key => {
var nspace = key.Namespace.ToString();
if (String.IsNullOrWhiteSpace(nspace)) {
return AddMetadata(element, key.LocalName, attributes[key]) == null;
}
return AddMetadata(element, new Uri(nspace), key.LocalName, attributes[key]) == null;
});
})) {
return !IsCanceled;
}
if (provider.Links.Any(link => AddLink(link.HRef, link.Relationship, link.MediaType, link.Ownership, link.Use, link.Media, link.Artifact) == null)) {
return !IsCanceled;
}
if (provider.Entities.Any(entity => AddEntity(entity.Name, entity.RegId, entity.Role, entity.Thumbprint) == null)) {
return !IsCanceled;
}
//installing a package from bootstrap site needs to prompt a user. Only auto-boostrap is not prompted.
var pm = PackageManagementService as PackageManagementService;
string isTrustedSource = pm.InternalPackageManagementInstallOnly ? "false" : "true";
if (AddMetadata(fastPackageReference, "FromTrustedSource", isTrustedSource) == null) {
return !IsCanceled;
}
}
return !IsCanceled;
}
private static bool AnyNullOrEmpty(params string[] args) {
return args.Any(String.IsNullOrWhiteSpace);
}
/// <summary>
/// Get a package provider from a given path.
/// </summary>
/// <param name="filePath"></param>
/// <param name="suppressErrorsAndWarnings"></param>
/// <param name="copyFileToTemp"></param>
/// <returns></returns>
internal Package GetProviderFromFile(string filePath, bool copyFileToTemp = false, bool suppressErrorsAndWarnings = false) {
if (string.IsNullOrWhiteSpace(filePath) && !System.IO.File.Exists(filePath)) {
Warning(Constants.Messages.FileNotFound, filePath);
return null;
}
// support providers with .dll file extention only
if (!Path.GetExtension(filePath).EqualsIgnoreCase(".dll")) {
if (!suppressErrorsAndWarnings)
{
Warning(Resources.Messages.InvalidFileType, ".dll", filePath);
}
return null;
}
string tempFile = filePath;
IEnumerable<XElement> manifests = Enumerable.Empty<XElement>();
if (copyFileToTemp) {
try {
// Manifest.LoadFrom() does not work with network share, so we need to copy the dll to temp location
tempFile = CopyToTempLocation(filePath);
if (string.IsNullOrWhiteSpace(tempFile) && !System.IO.File.Exists(tempFile))
{
Warning(Constants.Messages.FileNotFound, tempFile);
return null;
}
manifests = Manifest.LoadFrom(tempFile).ToArray();
}
finally {
if (!string.IsNullOrWhiteSpace(tempFile)) {
tempFile.TryHardToDelete();
}
}
} else {
// providers have the provider manifest embeded?
manifests = Manifest.LoadFrom(filePath).ToArray();
}
if (!manifests.Any()) {
if (!suppressErrorsAndWarnings)
{
Warning(Resources.Messages.MissingProviderManifest, tempFile);
}
return null;
}
var source = new Uri(filePath);
foreach (var manifest in manifests) {
var swidTagObject = new Swidtag(manifest);
if (Swidtag.IsSwidtag(manifest) && swidTagObject.IsApplicable(new Hashtable())) {
return new Package(this, swidTagObject) {
Location = source,
Source = source.LocalPath
};
}
}
return null;
}
/// <summary>
/// Find a package provider from a file path.
/// </summary>
/// <param name="name"></param>
/// <param name="requiredVersion"></param>
/// <param name="minimumVersion"></param>
/// <param name="maximumVersion"></param>
internal void FindProviderFromFile(string name, string requiredVersion, string minimumVersion, string maximumVersion) {
// find the providers from the given Source location
var pkgs = FindProviderByNameFromFile(name).Where(each => FilterOnName(each, name) && FilterOnVersion(each, requiredVersion, minimumVersion, maximumVersion)).ReEnumerable();
Debug("Total {0} providers found".format(pkgs.Count()));
//A user does not provide version info, we choose the latest
if (!GetOptionValue("AllVersions").IsTrue() && (string.IsNullOrWhiteSpace(requiredVersion) && string.IsNullOrWhiteSpace(minimumVersion) && string.IsNullOrWhiteSpace(maximumVersion)))
{
pkgs = pkgs.GroupBy(p => p.Name).Select(each => each.OrderByDescending(pp => pp.Version).FirstOrDefault()).ReEnumerable();
}
foreach (var package in pkgs)
{
YieldFromSwidtag(package, name);
}
}
private IEnumerable<Package> FindProviderByNameFromFile(string name) {
foreach (var each in LocalSource) {
// each can be file full path or folder directory
var assemblies = System.IO.File.Exists(each) ? new[] {each} : System.IO.Directory.EnumerateFiles(each, "*.dll", SearchOption.AllDirectories);
foreach (var item in assemblies) {
yield return GetProviderFromFile(item, true, false);
}
}
}
private bool FilterOnName(Package pkg, string name)
{
if (pkg == null)
{
return false;
}
if(string.IsNullOrWhiteSpace(name))
{
return true;
}
if (WildcardPattern.ContainsWildcardCharacters(name))
{
// Applying the wildcard pattern matching
const WildcardOptions wildcardOptions = WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase;
var wildcardPattern = new WildcardPattern(name, wildcardOptions);
return wildcardPattern.IsMatch(pkg.Name);
}
else
{
return pkg.Name.EqualsIgnoreCase(name);
}
}
private bool FilterOnVersion(Package pkg, string requiredVersion, string minimumVersion, string maximumVersion) {
if(pkg == null) {
return false;
}
if (string.IsNullOrWhiteSpace(requiredVersion) || (SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, requiredVersion) == 0))
{
if (string.IsNullOrWhiteSpace(minimumVersion) || (SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, minimumVersion) >= 0))
{
if (string.IsNullOrWhiteSpace(maximumVersion) || (SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, maximumVersion) <= 0))
{
return true;
}
}
}
return false;
}
public static string GetTempFileFullPath(string filePath) {
if (string.IsNullOrWhiteSpace(filePath)) {
return filePath;
}
// get a temp location
var file = Path.Combine(Path.GetTempPath(), Path.GetFileName(filePath));
if (System.IO.File.Exists(file)) {
//if exists already, delete it
file.TryHardToDelete();
}
// is that file still there?
if (System.IO.File.Exists(file)) {
//try it again if the generated file already exists
file = GetTempFileFullPath(filePath);
}
return file;
}
private string CopyToTempLocation(string filePath) {
if (string.IsNullOrWhiteSpace(filePath)) {
return filePath;
}
var targetFile = GetTempFileFullPath(filePath);
if (filePath.EqualsIgnoreCase(targetFile)) {
return filePath;
}
Debug("Copying file '{0}' to '{1}'", filePath, targetFile);
try {
System.IO.File.Copy(filePath, targetFile);
return targetFile;
} catch (Exception ex) {
Debug(ex.StackTrace);
return string.Empty;
}
}
}
}