diff --git a/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj b/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj new file mode 100644 index 0000000000..84170b9bf8 --- /dev/null +++ b/src/Chocolatey.PowerShell/Chocolatey.PowerShell.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + + + + + + + + + + + + + False + ..\..\lib\PowerShell\System.Management.Automation.dll + + + + diff --git a/src/Chocolatey.PowerShell/ChocolateyCmdlet.cs b/src/Chocolatey.PowerShell/ChocolateyCmdlet.cs new file mode 100644 index 0000000000..ec4b09ad8d --- /dev/null +++ b/src/Chocolatey.PowerShell/ChocolateyCmdlet.cs @@ -0,0 +1,175 @@ +// Copyright © 2017-2019 Chocolatey Software, Inc ("Chocolatey") +// Copyright © 2015-2017 RealDimensions Software, LLC +// +// Chocolatey Professional, Chocolatey for Business, and Chocolatey Architect are licensed software. +// +// ===================================================================== +// End-User License Agreement +// Chocolatey Professional, Chocolatey for Service Providers, Chocolatey for Business, +// and/or Chocolatey Architect +// ===================================================================== +// +// IMPORTANT- READ CAREFULLY: This Chocolatey Software ("Chocolatey") End-User License Agreement +// ("EULA") is a legal agreement between you ("END USER") and Chocolatey for all Chocolatey products, +// controls, source code, demos, intermediate files, media, printed materials, and "online" or electronic +// documentation (collectively "SOFTWARE PRODUCT(S)") contained with this distribution. +// +// Chocolatey grants to you as an individual or entity, a personal, nonexclusive license to install and use the +// SOFTWARE PRODUCT(S). By installing, copying, or otherwise using the SOFTWARE PRODUCT(S), you +// agree to be bound by the terms of this EULA. If you do not agree to any part of the terms of this EULA, DO +// NOT INSTALL, USE, OR EVALUATE, ANY PART, FILE OR PORTION OF THE SOFTWARE PRODUCT(S). +// +// In no event shall Chocolatey be liable to END USER for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a result of the use or inability to use the +// SOFTWARE PRODUCT(S) (including but not limited to damages for loss of goodwill, work stoppage, computer +// failure or malfunction, or any and all other commercial damages or losses). +// +// The liability of Chocolatey to END USER for any reason and upon any cause of action related to the +// performance of the work under this agreement whether in tort or in contract or otherwise shall be limited to the +// amount paid by the END USER to Chocolatey pursuant to this agreement. +// +// ALL SOFTWARE PRODUCT(S) are licensed not sold. If you are an individual, you must acquire an individual +// license for the SOFTWARE PRODUCT(S) from Chocolatey or its authorized resellers. If you are an entity, you +// must acquire an individual license for each machine running the SOFTWARE PRODUCT(S) within your +// organization from Chocolatey or its authorized resellers. Both virtual and physical machines running the +// SOFTWARE PRODUCT(S) or benefitting from licensed features such as Package Builder or Package +// Internalizer must be counted in the SOFTWARE PRODUCT(S) licenses quantity of the organization. + +namespace Chocolatey.PowerShell +{ + using chocolatey; + using chocolatey.infrastructure.filesystem; + using chocolatey.infrastructure.logging; + using Chocolatey.PowerShell.Helpers; + using System; + using System.Collections; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Text; + using System.Threading; + + public abstract class ChocolateyCmdlet : PSCmdlet + { + private readonly object _lock = new object(); + private readonly Lazy _fileSystem = new Lazy(() => new DotNetFileSystem()); + private readonly CancellationTokenSource _pipelineStopTokenSource = new CancellationTokenSource(); + + protected CancellationToken PipelineStopToken { get => _pipelineStopTokenSource.Token; } + + protected IFileSystem FileSystem { get => _fileSystem.Value; } + + protected string ErrorId { get => GetType().Name + "Error"; } + + protected override void BeginProcessing() + { + WriteCmdletCallDebugMessage(); + } + + protected override void EndProcessing() + { + WriteCmdletCompletionDebugMessage(); + } + + protected override void StopProcessing() + { + lock (_lock) + { + _pipelineStopTokenSource.Cancel(); + } + } + + protected void WriteCmdletCallDebugMessage() + { + var logMessage = new StringBuilder() + .Append("Running ") + .Append(MyInvocation.InvocationName); + + foreach (var param in MyInvocation.BoundParameters) + { + if (param.Key == "ignoredArguments") + { + continue; + } + + var paramValue = param.Key.IsEqualTo("SensitiveStatements") || param.Key.IsEqualTo("Password") + ? "[REDACTED]" + : param.Value is IList list + ? string.Join(" ", list) + : LanguagePrimitives.ConvertTo(param.Value, typeof(string)); + + logMessage.Append($" -{param.Key} '{paramValue}'"); + } + + WriteDebug(logMessage.ToString()); + } + + protected void WriteCmdletCompletionDebugMessage() + { + WriteDebug($"Finishing '{MyInvocation.InvocationName}'"); + } + + protected string EnvironmentVariable(string environmentVariable) + => Environment.GetEnvironmentVariable(environmentVariable); + + protected void SetEnvironmentVariable(string variable, string value) + => Environment.SetEnvironmentVariable(variable, value); + + protected object GetPSVariable(string variable) + => PowerShellHelper.GetPSVariable(this, variable); + + protected void WriteHost(string message) + => PowerShellHelper.WriteHost(this, message); + + protected new void WriteDebug(string message) + => PowerShellHelper.WriteDebug(this, message); + + protected new void WriteVerbose(string message) + => PowerShellHelper.WriteVerbose(this, message); + + protected new void WriteWarning(string message) + => PowerShellHelper.WriteWarning(this, message); + + protected string CombinePaths(string parent, params string[] childPaths) + => PowerShellHelper.CombinePaths(this, parent, childPaths); + + protected void EnsureDirectoryExists(string directory) + => PowerShellHelper.EnsureDirectoryExists(this, directory); + + protected string GetDirectoryName(string path) + => PowerShellHelper.GetDirectoryName(this, path); + + protected string GetFileName(string path) + => PowerShellHelper.GetFileName(this, path); + + protected string GetUnresolvedPath(string path) + => PowerShellHelper.GetUnresolvedPath(this, path); + + protected string GetCurrentDirectory() + => PowerShellHelper.GetCurrentDirectory(this); + + protected FileInfo GetFileInfoFor(string path) + => PowerShellHelper.GetFileInfoFor(this, path); + + protected string GetFullPath(string path) + => PowerShellHelper.GetFullPath(this, path); + + protected bool ItemExists(string path) + => PowerShellHelper.ItemExists(this, path); + + protected bool ContainerExists(string path) + => PowerShellHelper.ContainerExists(this, path); + + protected void CopyFile(string source, string destination, bool overwriteExisting) + => PowerShellHelper.CopyFile(this, source, destination, overwriteExisting); + + protected void DeleteFile(string path) + => PowerShellHelper.DeleteFile(this, path); + + protected void SetExitCode(int exitCode) + => PowerShellHelper.SetExitCode(this, exitCode); + + protected T ConvertTo(object value) + => PowerShellHelper.ConvertTo(value); + } +} diff --git a/src/Chocolatey.PowerShell/Commands/AssertChecksumValidCommand.cs b/src/Chocolatey.PowerShell/Commands/AssertChecksumValidCommand.cs new file mode 100644 index 0000000000..70b858b5dd --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/AssertChecksumValidCommand.cs @@ -0,0 +1,45 @@ +namespace Chocolatey.PowerShell.Commands +{ + using Chocolatey.PowerShell; + using Chocolatey.PowerShell.Helpers; + using Chocolatey.PowerShell.Shared; + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Management.Automation.Host; + using System.Runtime.InteropServices; + using System.Text; + using System.Threading.Tasks; + + [Cmdlet(VerbsLifecycle.Assert, "ChecksumValid")] + public class AssertChecksumValidCommand : ChocolateyCmdlet + { + [Parameter(Mandatory = true, Position = 0)] + [Alias("File", "FilePath")] + public string Path { get; set; } + + [Parameter(Position = 1)] + public string Checksum { get; set; } = string.Empty; + + [Parameter(Position = 2)] + public ChecksumType ChecksumType { get; set; } = ChecksumType.Md5; + + [Parameter(Position = 3)] + [Alias("OriginalUrl")] + public string Url { get; set; } = string.Empty; + + [Parameter(ValueFromRemainingArguments = true)] + public object[] IgnoredArguments { get; set; } + + protected override void EndProcessing() + { + ChecksumValidator.AssertChecksumValid(this, Path, Checksum, ChecksumType, Url); + base.EndProcessing(); + } + + } +} diff --git a/src/Chocolatey.PowerShell/Commands/ExpandChocolateyArchive.cs b/src/Chocolatey.PowerShell/Commands/ExpandChocolateyArchive.cs new file mode 100644 index 0000000000..c27b11703a --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/ExpandChocolateyArchive.cs @@ -0,0 +1,127 @@ +namespace Chocolatey.PowerShell.Commands +{ + using Chocolatey.PowerShell.Helpers; + using System; + using System.Collections.Generic; + using System.Management.Automation; + using System.Text; + + [Cmdlet(VerbsData.Expand, "ChocolateyArchive")] + [OutputType(typeof(string))] + public class ExpandChocolateyArchive : ChocolateyCmdlet + { + /* + +.SYNOPSIS +Unzips an archive file and returns the location for further processing. + +.DESCRIPTION +This unzips files using the 7-zip command line tool 7z.exe. +Supported archive formats are listed at: +https://sevenzip.osdn.jp/chm/general/formats.htm + +.INPUTS +None + +.OUTPUTS +Returns the passed in $destination. + +.NOTES +If extraction fails, an exception is thrown. + +If you are embedding files into a package, ensure that you have the +rights to redistribute those files if you are sharing this package +publicly (like on the community feed). Otherwise, please use +Install-ChocolateyZipPackage to download those resources from their +official distribution points. + +Will automatically call Set-PowerShellExitCode to set the package exit code +based on 7-zip's exit code. + +.PARAMETER FileFullPath +This is the full path to the zip file. If embedding it in the package +next to the install script, the path will be like +`"$(Split-Path -Parent $MyInvocation.MyCommand.Definition)\\file.zip"` + +`File` is an alias for FileFullPath. + +This can be a 32-bit or 64-bit file. This is mandatory in earlier versions +of Chocolatey, but optional if FileFullPath64 has been provided. + +.PARAMETER FileFullPath64 +Full file path to a 64-bit native installer to run. +If embedding in the package, you can get it to the path with +`"$(Split-Path -parent $MyInvocation.MyCommand.Definition)\\INSTALLER_FILE"` + +Provide this when you want to provide both 32-bit and 64-bit +installers or explicitly only a 64-bit installer (which will cause a package +install failure on 32-bit systems). + +.PARAMETER Destination +This is a directory where you would like the unzipped files to end up. +If it does not exist, it will be created. + +.PARAMETER SpecificFolder +OPTIONAL - This is a specific directory within zip file to extract. The +folder and its contents will be extracted to the destination. + +.PARAMETER PackageName +OPTIONAL - This will facilitate logging unzip activity for subsequent +uninstalls + +.PARAMETER DisableLogging +OPTIONAL - This disables logging of the extracted items. It speeds up +extraction of archives with many files. + +Usage of this parameter will prevent Uninstall-ChocolateyZipPackage +from working, extracted files will have to be cleaned up with +Remove-Item or a similar command instead. + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.EXAMPLE +> +# Path to the folder where the script is executing +$toolsDir = (Split-Path -parent $MyInvocation.MyCommand.Definition) +Get-ChocolateyUnzip -FileFullPath "c:\someFile.zip" -Destination $toolsDir + +.LINK +Install-ChocolateyZipPackage + */ + + [Alias("File", "FileFullPath")] + [Parameter(Position = 0)] + public string Path { get; set; } + + [Alias("UnzipLocation")] + [Parameter(Mandatory = true, Position = 1)] + public string Destination { get; set; } + + [Parameter(Position = 2)] + public string SpecificFolder { get; set; } + + [Parameter(Position = 3)] + public string PackageName { get; set; } + + [Alias("File64", "FileFullPath64")] + [Parameter] + public string Path64 { get; set; } + + [Parameter] + public SwitchParameter DisableLogging { get; set; } + + [Parameter(ValueFromRemainingArguments = true)] + public object[] IgnoredArguments { get; set; } + + protected override void EndProcessing() + { + var bitnessMessage = string.Empty; + var zipFilePath = Path; + + var architecture = ArchitectureWidth.Get(); + + base.EndProcessing(); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetChocolateyConfigValueCommand.cs b/src/Chocolatey.PowerShell/Commands/GetChocolateyConfigValueCommand.cs new file mode 100644 index 0000000000..d3a75b5996 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetChocolateyConfigValueCommand.cs @@ -0,0 +1,91 @@ +namespace Chocolatey.PowerShell.Commands +{ + using chocolatey; + using chocolatey.infrastructure.app; + using chocolatey.infrastructure.app.configuration; + using chocolatey.infrastructure.configuration; + using Chocolatey.PowerShell; + using System; + using System.Collections; + using System.Linq; + using System.Management.Automation; + using System.Text; + using System.Threading.Tasks; + using System.Xml; + + [Cmdlet(VerbsCommon.Get, "ChocolateyConfigValue")] + public class GetChocolateyConfigValueCommand : ChocolateyCmdlet + { + [Parameter(Mandatory = true)] + public string ConfigKey { get; set; } + + [Parameter(ValueFromRemainingArguments = true)] + public object[] IgnoredArguments { get; set; } + + protected override void EndProcessing() + { + var result = GetConfigValue(ConfigKey); + + WriteObject(result); + + base.EndProcessing(); + } + + private string GetConfigValue(string key) + { + if (key is null) + { + return null; + } + + string configString = null; + Exception error = null; + foreach (var reader in InvokeProvider.Content.GetReader(ApplicationParameters.GlobalConfigFileLocation)) + { + try + { + var results = reader.Read(1); + if (results.Count > 0) + { + configString = ConvertTo(results[0]); + break; + } + } + catch (Exception ex) + { + WriteWarning($"Could not read configuration file: {ex.Message}"); + } + } + + if (configString is null) + { + // TODO: Replace RuntimeException + var exception = error is null + ? new RuntimeException("Config file is missing or empty.") + : new RuntimeException($"Config file is missing or empty. Error reading configuration file: {error.Message}", error); + ThrowTerminatingError(exception.ErrorRecord); + } + + var xmlConfig = new XmlDocument(); + xmlConfig.LoadXml(configString); + + foreach (XmlNode configEntry in xmlConfig.SelectNodes("chocolatey/config/add")) + { + var nodeKey = configEntry.Attributes["key"]; + if (nodeKey is null || !nodeKey.Value.IsEqualTo(ConfigKey)) + { + continue; + } + + var value = configEntry.Attributes["value"]; + if (!(value is null)) + { + // We don't support duplicate config entries; once found, we're done here. + return value.Value; + } + } + + return null; + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetChocolateyPathCommand.cs b/src/Chocolatey.PowerShell/Commands/GetChocolateyPathCommand.cs new file mode 100644 index 0000000000..cc5909c801 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetChocolateyPathCommand.cs @@ -0,0 +1,109 @@ +namespace Chocolatey.PowerShell.Commands +{ + using chocolatey.infrastructure.app; + using Chocolatey.PowerShell; + using Chocolatey.PowerShell.Shared; + using System; + using System.Collections.Generic; + using System.Management.Automation; + using System.Text; + + [Cmdlet(VerbsCommon.Get, "ChocolateyPath")] + public class GetChocolateyPathCommand : ChocolateyCmdlet + { + /* +.SYNOPSIS +Retrieve the paths available to be used by maintainers of packages. + +.DESCRIPTION +This function will attempt to retrieve the path according to the specified Path Type +to a valid location that can be used by maintainers in certain scenarios. + +.NOTES +Available in 1.2.0+. + +.INPUTS +None + +.OUTPUTS +This function outputs the full path stored accordingly with specified path type. +If no path could be found, there is no output. + +.PARAMETER pathType +The type of path that should be looked up. +Available values are: +- `PackagePath` - The path to the the package that is being installed. Typically `C:\ProgramData\chocolatey\lib\` +- `InstallPath` - The path to where Chocolatey is installed. Typically `C:\ProgramData\chocolatey` + +.PARAMETER IgnoredArguments +Allows splatting with arguments that do not apply. Do not use directly. + +.EXAMPLE +> +$path = Get-ChocolateyPath -PathType 'PackagePath' + */ + [Parameter(Mandatory = true)] + [Alias("Type")] + public ChocolateyPathType PathType { get; set; } + + protected override void EndProcessing() + { + try + { + string path = GetPathType(PathType); + + if (ContainerExists(path)) + { + WriteObject(path); + } + } + catch (RuntimeException rex) + { + ThrowTerminatingError(rex.ErrorRecord); + } + catch (Exception error) + { + ThrowTerminatingError(new ErrorRecord(error, "GetChocolateyPathCommandError", ErrorCategory.NotSpecified, PathType));) + } + finally + { + base.EndProcessing(); + } + } + + private string GetPathType(ChocolateyPathType pathType) + { + string path = null; + switch (pathType) + { + case ChocolateyPathType.PackagePath: + if (ItemExists($@"env:\${StringResources.EnvironmentVariables.ChocolateyPackagePath}")) + { + path = Environment.GetEnvironmentVariable(StringResources.EnvironmentVariables.ChocolateyPackagePath); + } + // Old fallbacks; do we still need/support these? + else if (ItemExists($@"env:\PackagePath")) + { + path = Environment.GetEnvironmentVariable("PackagePath"); + } + else + { + var installPath = GetPathType(ChocolateyPathType.InstallPath); + var packageName = Environment.GetEnvironmentVariable(StringResources.EnvironmentVariables.ChocolateyPackageName); + + path = CombinePaths(installPath, "lib", packageName); + } + + break; + case ChocolateyPathType.InstallPath: + path = ApplicationParameters.InstallLocation; + break; + default: + // TODO: Replace RuntimeException with custom exception + throw new RuntimeException($"The path type '{pathType}' is not supported."); + }; + + return path; + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/GetOsArchitectureWidthCommand.cs b/src/Chocolatey.PowerShell/Commands/GetOsArchitectureWidthCommand.cs new file mode 100644 index 0000000000..4d7ccb4edc --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/GetOsArchitectureWidthCommand.cs @@ -0,0 +1,72 @@ +namespace Chocolatey.PowerShell.Commands +{ + using Chocolatey.PowerShell.Helpers; + using Chocolatey.PowerShell.Shared; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using System.Text; + + [Cmdlet(VerbsCommon.Get, "OSArchitectureWidth")] + public class GetOsArchitectureWidthCommand : ChocolateyCmdlet + { + /* + +.SYNOPSIS +Get the operating system architecture address width. + +.DESCRIPTION +This will return the system architecture address width (probably 32 or +64 bit). If you pass a comparison, it will return true or false instead +of {`32`|`64`}. + +.NOTES +When your installation script has to know what architecture it is run +on, this simple function comes in handy. + +ARM64 architecture will automatically select 32bit width as +there is an emulator for 32 bit and there are no current plans by Microsoft to +ship 64 bit x86 emulation for ARM64. For more details, see +https://github.com/chocolatey/choco/issues/1800#issuecomment-484293844. + +.INPUTS +None + +.OUTPUTS +None + +.PARAMETER Compare +This optional parameter causes the function to return $true or $false, +depending on whether or not the bit width matches. + */ + [Parameter] + [Alias("Compare")] + [BoolStringSwitchTransform] + public int CompareTo { get; set; } + + [Parameter(ValueFromRemainingArguments = true)] + public object[] IgnoredArguments { get; set; } + + protected override void BeginProcessing() + { + base.BeginProcessing(); + } + + protected override void EndProcessing() + { + var bits = Environment.Is64BitProcess ? 64 : 32; + + if (MyInvocation.BoundParameters.ContainsKey(nameof(CompareTo))) + { + WriteObject(ArchitectureWidth.Matches(CompareTo)); + } + else + { + WriteObject(ArchitectureWidth.Get()); + } + + base.EndProcessing(); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/Resources.txt b/src/Chocolatey.PowerShell/Commands/Resources.txt new file mode 100644 index 0000000000..cba9d9d45a --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/Resources.txt @@ -0,0 +1,34 @@ +Command Precedence +https://technet.microsoft.com/en-us/library/hh848304.aspx?f=255&MSPPError=-2147217396 +https://technet.microsoft.com/en-us/library/hh848304(v=wps.620).aspx + + Windows PowerShell uses the following + precedence order when it runs commands: + + 1. Alias + 2. Function + 3. Cmdlet + 4. Native Windows commands + +Building a Cmdlet +http://blogs.technet.com/b/heyscriptingguy/archive/tags/windows+powershell/guest+blogger/sean+kearney/build+your+own+cmdlet/ + +C# Cmdlets +https://msdn.microsoft.com/en-us/library/ff602031(v=vs.85).aspx + +http://www.powershellmagazine.com/2014/03/18/writing-a-powershell-module-in-c-part-1-the-basics/ +http://www.powershellmagazine.com/2014/04/08/basics-of-writing-a-powershell-module-with-c-part-2-debugging/ +http://infoworks.tv/howto-write-a-powershell-module-or-function-in-c-with-visual-studio-2012/ + +POSH v2 issues? +http://kungfukode.blogspot.it/2011/05/write-your-first-powershell-cmdlet-in-c.html + +Writing a PowerShell Module +https://msdn.microsoft.com/en-us/library/dd878310%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + +Aliases +https://msdn.microsoft.com/en-us/library/ms714640(v=vs.85).aspx +https://technet.microsoft.com/en-us/library/ms714640(v=vs.85).aspx + +Programmatically creating aliases +http://stackoverflow.com/a/13584769/18475 \ No newline at end of file diff --git a/src/Chocolatey.PowerShell/Commands/StartChocolateyProcessCommand.cs b/src/Chocolatey.PowerShell/Commands/StartChocolateyProcessCommand.cs new file mode 100644 index 0000000000..91c3bf6f11 --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/StartChocolateyProcessCommand.cs @@ -0,0 +1,173 @@ +namespace Chocolatey.PowerShell.Commands +{ + using Chocolatey.PowerShell; + using Chocolatey.PowerShell.Helpers; + using System; + using System.IO; + using System.Management.Automation; + + [Cmdlet(VerbsLifecycle.Start, "ChocolateyProcess")] + public class StartChocolateyProcessCommand : ChocolateyCmdlet + { + //.SYNOPSIS + //** NOTE:** Administrative Access Required. + // + //Runs a process with administrative privileges.If `-ExeToRun` is not + //specified, it is run with PowerShell. + // + //.NOTES + //This command will assert UAC/Admin privileges on the machine. + // + //Will automatically call Set-PowerShellExitCode to set the package exit + //code in the following ways: + // + //- 4 if the binary turns out to be a text file. + //- The same exit code returned from the process that is run.If a 3010 is returned, it will set 3010 for the package. + // + //Aliases `Start-ChocolateyProcess` and `Invoke-ChocolateyProcess`. + // + //.INPUTS + // None + // + //.OUTPUTS + //None + // + //.PARAMETER Statements + //Arguments to pass to `ExeToRun` or the PowerShell script block to be + //run. + // + //.PARAMETER ExeToRun + //The executable/application/installer to run.Defaults to `'powershell'`. + // + //.PARAMETER Elevated + //Indicate whether the process should run elevated. + // + //.PARAMETER Minimized + //Switch indicating if a Windows pops up (if not called with a silent + //argument) that it should be minimized. + // + //.PARAMETER NoSleep + //Used only when calling PowerShell - indicates the window that is opened + //should return instantly when it is complete. + // + //.PARAMETER ValidExitCodes + //Array of exit codes indicating success.Defaults to `@(0)`. + // + //.PARAMETER WorkingDirectory + //The working directory for the running process.Defaults to + //`Get-Location`. If current location is a UNC path, uses + //`$env:TEMP` for default. + // + //.PARAMETER SensitiveStatements + //Arguments to pass to `ExeToRun` that are not logged. + // + //Note that only licensed versions of Chocolatey provide a way to pass + //those values completely through without having them in the install + //script or on the system in some way. + // + //.PARAMETER IgnoredArguments + //Allows splatting with arguments that do not apply. Do not use directly. + // + //.EXAMPLE + //Start-ChocolateyProcessAsAdmin -Statements "$msiArgs" -ExeToRun 'msiexec' + // + //.EXAMPLE + //Start-ChocolateyProcessAsAdmin -Statements "$silentArgs" -ExeToRun $file + // + //.EXAMPLE + //Start-ChocolateyProcessAsAdmin -Statements "$silentArgs" -ExeToRun $file -ValidExitCodes @(0,21) + // + //.EXAMPLE + //> + //# Run PowerShell statements + //$psFile = Join-Path "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" 'someInstall.ps1' + //Start-ChocolateyProcessAsAdmin "& `'$psFile`'" + // + //.EXAMPLE + //# This also works for cmd and is required if you have any spaces in the paths within your command + //$appPath = "$env:ProgramFiles\myapp" + //$cmdBatch = "/c `"$appPath\bin\installmyappservice.bat`"" + //Start-ChocolateyProcessAsAdmin $cmdBatch cmd + //# or more explicitly + //Start-ChocolateyProcessAsAdmin -Statements $cmdBatch -ExeToRun "cmd.exe" + // + //.LINK + //Install-ChocolateyPackage + // + //.LINK + //Install-ChocolateyInstallPackage + + private StartChocolateyProcessHelper _helper; + + [Parameter(Position = 0)] + [Alias("Statements")] + public string[] Arguments { get; set; } + + [Parameter(Position = 1)] + [Alias("exeToRun")] + public string ProcessName { get; set; } = "powershell"; + + [Parameter()] + public SwitchParameter Elevated { get; set; } + + [Parameter()] + public SwitchParameter Minimized { get; set; } + + [Parameter()] + public SwitchParameter NoSleep { get; set; } + + [Parameter()] + public int[] ValidExitCodes { get; set; } = new int[] { 0 }; + + [Parameter()] + public string WorkingDirectory { get; set; } + + [Parameter()] + public string SensitiveStatements { get; set; } + + [Parameter(ValueFromRemainingArguments = true)] + public object[] IgnoredArguments { get; set; } + + protected override void BeginProcessing() + { + base.BeginProcessing(); + + if (MyInvocation.InvocationName == "Start-ChocolateyProcessAsAdmin") + { + Elevated = true; + } + } + + protected override void EndProcessing() + { + var arguments = Arguments is null + ? string.Empty + : string.Join(" ", Arguments); + + try + { + _helper = new StartChocolateyProcessHelper(this, PipelineStopToken, FileSystem); + var exitCode = _helper.Start(ProcessName, WorkingDirectory, arguments, SensitiveStatements, ValidExitCodes, Elevated.IsPresent, Minimized.IsPresent, NoSleep.IsPresent); + WriteObject(exitCode); + } + catch (FileNotFoundException notFoundEx) + { + ThrowTerminatingError(new ErrorRecord(notFoundEx, ErrorId, ErrorCategory.ObjectNotFound, ProcessName)); + } + catch (Exception ex) + { + ThrowTerminatingError(new ErrorRecord(ex, ErrorId, ErrorCategory.NotSpecified, ProcessName)); + } + finally + { + base.EndProcessing(); + } + } + + protected override void StopProcessing() + { + _helper?.CancelWait(); + base.StopProcessing(); + } + } +} diff --git a/src/Chocolatey.PowerShell/Commands/TestProcessRunningAsAdminCommand.cs b/src/Chocolatey.PowerShell/Commands/TestProcessRunningAsAdminCommand.cs new file mode 100644 index 0000000000..436c96348b --- /dev/null +++ b/src/Chocolatey.PowerShell/Commands/TestProcessRunningAsAdminCommand.cs @@ -0,0 +1,43 @@ +namespace Chocolatey.PowerShell.Commands +{ + using System.Collections.Generic; + using System; + using System.Management.Automation; + using System.Security.Principal; + using chocolatey.infrastructure.information; + using Chocolatey.PowerShell; + + [Cmdlet(VerbsDiagnostic.Test, "ProcessRunningAsAdmin")] + [OutputType(typeof(bool))] + public class TestProcessRunningAsAdminCommand : ChocolateyCmdlet + { + // .SYNOPSIS + // Tests whether the current process is running with administrative rights. + // + // .DESCRIPTION + // This function checks whether the current process has administrative + // rights by checking if the current user identity is a member of the + // Administrators group. It returns `$true` if the current process is + // running with administrative rights, `$false` otherwise. + // + // On Windows Vista and later, with UAC enabled, the returned value + // represents the actual rights available to the process, e.g. if it + // returns `$true`, the process is running elevated. + // + // .INPUTS + // None + // + // .OUTPUTS + // System.Boolean + protected override void EndProcessing() + { + var result = ProcessInformation.UserIsAdministrator(); + + WriteDebug($"Test-ProcessRunningAsAdmin: returning {result}"); + + WriteObject(result); + + base.EndProcessing(); + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/ArchitectureWidth.cs b/src/Chocolatey.PowerShell/Helpers/ArchitectureWidth.cs new file mode 100644 index 0000000000..f81cf68def --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/ArchitectureWidth.cs @@ -0,0 +1,19 @@ +namespace Chocolatey.PowerShell.Helpers +{ + using System; + using System.Collections.Generic; + using System.Text; + + internal static class ArchitectureWidth + { + internal static int Get() + { + return Environment.Is64BitProcess ? 64 : 32; + } + + internal static bool Matches(int compareTo) + { + return Get() == compareTo; + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/CancellableSleepHelper.cs b/src/Chocolatey.PowerShell/Helpers/CancellableSleepHelper.cs new file mode 100644 index 0000000000..b406553db1 --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/CancellableSleepHelper.cs @@ -0,0 +1,50 @@ +namespace Chocolatey.PowerShell.Helpers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + + internal class CancellableSleepHelper : IDisposable + { + private readonly object _lock = new object(); + + private bool _disposed; + private bool _stopping; + private ManualResetEvent _waitHandle; + + public void Dispose() + { + if (!_disposed) + { + _waitHandle.Dispose(); + _waitHandle = null; + } + + _disposed = true; + } + + // Call from Cmdlet.StopProcessing() + internal void Cancel() + { + _stopping = true; + _waitHandle?.Set(); + } + + internal void Sleep(int milliseconds) + { + lock (_lock) + { + if (!_stopping) + { + _waitHandle = new ManualResetEvent(false); + } + } + + _waitHandle?.WaitOne(milliseconds, true); + } + + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/ChecksumValidator.cs b/src/Chocolatey.PowerShell/Helpers/ChecksumValidator.cs new file mode 100644 index 0000000000..b17780af2e --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/ChecksumValidator.cs @@ -0,0 +1,127 @@ +namespace Chocolatey.PowerShell.Helpers +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Management.Automation.Host; + using System.Management.Automation; + using System.Text; + using System.Threading.Tasks; + using chocolatey; + using chocolatey.infrastructure.app; + using Chocolatey.PowerShell.Shared; + + internal static class ChecksumValidator + { + internal static void AssertChecksumValid(PSCmdlet cmdlet, string path, string checksum, ChecksumType checksumType, string url) + { + if (Environment.GetEnvironmentVariable(ApplicationParameters.Environment.ChocolateyIgnoreChecksums).IsEqualTo("true")) + { + PowerShellHelper.WriteWarning(cmdlet, "Ignoring checksums due to feature checksumFiles turned off or option --ignore-checksums set."); + return; + } + + if (string.IsNullOrWhiteSpace(checksum)) + { + if (Environment.GetEnvironmentVariable(ApplicationParameters.Environment.ChocolateyAllowEmptyChecksums).IsEqualTo("true")) + { + PowerShellHelper.WriteDebug(cmdlet, "Empty checksums are allowed due to allowEmptyChecksums feature or option."); + return; + } + + if (Environment.GetEnvironmentVariable(ApplicationParameters.Environment.ChocolateyAllowEmptyChecksumsSecure).IsEqualTo("true")) + { + PowerShellHelper.WriteDebug(cmdlet, "Download from HTTPS source with feature 'allowEmptyChecksumsSecure' enabled."); + return; + } + + PowerShellHelper.WriteWarning(cmdlet, "Missing package checksums are not allowed (by default for HTTP/FTP, `n HTTPS when feature 'allowEmptyChecksumsSecure' is disabled) for `n safety and security reasons. Although we strongly advise against it, `n if you need this functionality, please set the feature `n 'allowEmptyChecksums' ('choco feature enable -n `n allowEmptyChecksums') `n or pass in the option '--allow-empty-checksums'. You can also pass `n checksums at runtime (recommended). See `choco install -?` for details."); + PowerShellHelper.WriteDebug(cmdlet, "If you are a maintainer attempting to determine the checksum for packaging purposes, please run `n 'choco install checksum' and run 'checksum -t sha256 -f $file' `n Ensure you do this for all remote resources."); + + if (PowerShellHelper.GetPSVersion().Major >= 4) + { + PowerShellHelper.WriteDebug(cmdlet, "Because you are running PowerShell with a major version of v4 or greater, you could also opt to run `n '(Get-FileHash -Path $file -Algorithm SHA256).Hash' `n rather than install a separate tool."); + } + + if (Environment.GetEnvironmentVariable(ApplicationParameters.Environment.ChocolateyPowerShellHost).IsEqualTo("true") + && !(cmdlet.Host is null)) + { + const string prompt = "Do you wish to allow the install to continue (not recommended)?"; + var info = "The integrity of the file '{0}'{1} has not been verified by a checksum in the package scripts".FormatWith( + PowerShellHelper.GetFileName(cmdlet, path), + string.IsNullOrWhiteSpace(url) ? string.Empty : $" from '{url}'"); + + var choices = new Collection + { + new ChoiceDescription("&Yes"), + new ChoiceDescription("&No"), + }; + + var selection = cmdlet.Host.UI.PromptForChoice(info, prompt, choices, 1); + + if (selection == 0) + { + return; + } + } + + var errorMessage = url?.StartsWith("https") == true + ? "This package downloads over HTTPS but does not yet have package checksums to verify the package. We recommend asking the maintainer to add checksums to this package. In the meantime if you need this package to work correctly, please enable the feature allowEmptyChecksumsSecure, provide the runtime switch '--allow-empty-checksums-secure', or pass in checksums at runtime (recommended - see 'choco install -?' / 'choco upgrade -?' for details)." + : "Empty checksums are no longer allowed by default for non-secure sources. Please ask the maintainer to add checksums to this package. In the meantime if you need this package to work correctly, please enable the feature allowEmptyChecksums, provide the runtime switch '--allow-empty-checksums', or pass in checksums at runtime (recommended - see 'choco install -?' / 'choco upgrade -?' for details). It is strongly advised against allowing empty checksums for non-internal HTTP/FTP sources."; + + // TODO: Replace RuntimeException with custom exception type + cmdlet.ThrowTerminatingError(new RuntimeException(errorMessage).ErrorRecord); + } + + if (!PowerShellHelper.ItemExists(cmdlet, path)) + { + cmdlet.ThrowTerminatingError(new ErrorRecord( + new FileNotFoundException($"Unable to checksum a file that doesn't exist - Could not find file '{path}'", path), + "ChecksumValidationError", + ErrorCategory.ObjectNotFound, + path)); + } + + var checksumExe = PowerShellHelper.CombinePaths(cmdlet, ApplicationParameters.InstallLocation, @"tools\checksum.exe"); + if (!PowerShellHelper.ItemExists(cmdlet, checksumExe)) + { + cmdlet.ThrowTerminatingError(new ErrorRecord( + new FileNotFoundException("Unable to locate 'checksum.exe', your Chocolatey installation may be incomplete or damaged. Try reinstalling chocolatey with 'choco install chocolatey --force'."), + "ChecksumValidationError", + ErrorCategory.ObjectNotFound, + checksumExe)); + } + + PowerShellHelper.WriteDebug(cmdlet, $"checksum.exe found at '{checksumExe}'"); + var arguments = "-c=\"{0}\" -t=\"{1}\" -f=\"{2}\"".FormatWith(checksum, checksumType.ToString().ToLower(), path); + + PowerShellHelper.WriteDebug(cmdlet, $"Executing command ['{checksumExe}' {arguments}]"); + + var process = new Process + { + StartInfo = new ProcessStartInfo(checksumExe, arguments) + { + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + }, + }; + + process.Start(); + process.WaitForExit(); + + var exitCode = process.ExitCode; + process.Dispose(); + + PowerShellHelper.WriteDebug(cmdlet, $"Command ['{checksumExe}' {arguments}] exited with '{exitCode}'"); + + if (exitCode != 0) + { + // TODO: Replace RuntimeException with custom exception type + cmdlet.ThrowTerminatingError(new RuntimeException($"Checksum for '{path}' did not match '{checksum}' for checksum type '{checksumType}'. Consider passing the actual checksums through with `--checksum --checksum64` once you validate the checksums are appropriate. A less secure option is to pass `--ignore-checksums` if necessary.").ErrorRecord); + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/PowerShellHelper.cs b/src/Chocolatey.PowerShell/Helpers/PowerShellHelper.cs new file mode 100644 index 0000000000..839d76d522 --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/PowerShellHelper.cs @@ -0,0 +1,144 @@ +namespace Chocolatey.PowerShell.Helpers +{ + using chocolatey; + using chocolatey.infrastructure.app; + using chocolatey.infrastructure.logging; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Reflection; + using System.Text; + using System.Threading.Tasks; + + internal static class PowerShellHelper + { + internal static string CombinePaths(PSCmdlet cmdlet, string parent, params string[] childPaths) + { + var result = parent; + foreach (var path in childPaths) + { + result = cmdlet.SessionState.Path.Combine(result, path); + } + + return result; + } + + internal static T ConvertTo(object value) + => (T)LanguagePrimitives.ConvertTo(value, typeof(T)); + + internal static void CopyFile(PSCmdlet cmdlet, string source, string destination, bool overwriteExisting) + => cmdlet.InvokeProvider.Item.Copy(path: new string[] { source }, destinationPath: destination, recurse: false, copyContainers: CopyContainers.CopyTargetContainer, force: overwriteExisting, literalPath: true); + + internal static void DeleteFile(PSCmdlet cmdlet, string path) + => cmdlet.InvokeProvider.Item.Remove(path, false); + + internal static void EnsureDirectoryExists(PSCmdlet cmdlet, string directory) + { + if (!cmdlet.InvokeProvider.Item.Exists(directory)) + { + cmdlet.InvokeProvider.Item.New(path: new string[] { directory }, name: null, itemTypeName: "Directory", content: null, force: true); + } + } + + internal static bool ItemExists(PSCmdlet cmdlet, string path) + => cmdlet.InvokeProvider.Item.Exists(path) && !cmdlet.InvokeProvider.Item.IsContainer(path); + + internal static bool ContainerExists(PSCmdlet cmdlet, string path) + => cmdlet.InvokeProvider.Item.IsContainer(path); + + internal static string GetCurrentDirectory(PSCmdlet cmdlet) + => cmdlet.SessionState.Path.CurrentFileSystemLocation.ToStringSafe(); + + internal static string GetDirectoryName(PSCmdlet cmdlet, string path) + => cmdlet.SessionState.Path.ParseParent(GetUnresolvedPath(cmdlet, path), null); + + internal static FileInfo GetFileInfoFor(PSCmdlet cmdlet, string path) + => (FileInfo)cmdlet.InvokeProvider.Item.Get(path).FirstOrDefault().BaseObject; + + internal static string GetFileName(PSCmdlet cmdlet, string path) + => cmdlet.SessionState.Path.ParseChildName(GetUnresolvedPath(cmdlet, path)); + + internal static string GetFullPath(PSCmdlet cmdlet, string path) + => string.IsNullOrWhiteSpace(path) + ? string.Empty + : CombinePaths(cmdlet, GetDirectoryName(cmdlet, path), GetFileName(cmdlet, path)); + + internal static object GetPSVariable(PSCmdlet cmdlet, string variable) + => cmdlet.SessionState.PSVariable.GetValue(variable); + + internal static Version GetPSVersion() + { + Version result = null; + var assembly = Assembly.GetAssembly(typeof(Cmdlet)); + + // This type is public in PS v6.2+, this reflection will not be needed once we're using newer assemblies. + var psVersionInfo = assembly.GetType("System.Management.Automation.PSVersionInfo"); + var versionProperty = psVersionInfo?.GetProperty("PSVersion", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + + var getter = versionProperty?.GetGetMethod(true); + result = (Version)getter?.Invoke(null, Array.Empty()); + + // Assume absolute minimum version if we can't determine the version. + return result ?? new Version(2, 0); + } + + internal static string GetUnresolvedPath(PSCmdlet cmdlet, string path) + => cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path); + + internal static void SetExitCode(PSCmdlet cmdlet, int exitCode) + { + Environment.SetEnvironmentVariable(ApplicationParameters.Environment.ChocolateyPackageExitCode, exitCode.ToString()); + cmdlet.Host.SetShouldExit(exitCode); + } + + internal static void WriteHost(PSCmdlet cmdlet, string message) + { + if (cmdlet.Host is null) + { + cmdlet.Log().Info(message); + } + else + { + cmdlet.Host.UI.WriteLine(message); + } + } + + internal static void WriteDebug(PSCmdlet cmdlet, string message) + { + if (cmdlet.Host is null) + { + cmdlet.Log().Debug(message); + } + else + { + cmdlet.WriteDebug(message); + } + } + + internal static void WriteVerbose(PSCmdlet cmdlet, string message) + { + if (cmdlet.Host is null) + { + cmdlet.Log().Info(ChocolateyLoggers.Verbose, message); + } + else + { + cmdlet.WriteVerbose(message); + } + } + + internal static void WriteWarning(PSCmdlet cmdlet, string message) + { + if (cmdlet.Host is null) + { + cmdlet.Log().Warn(message); + } + else + { + cmdlet.WriteWarning(message); + } + } + } +} diff --git a/src/Chocolatey.PowerShell/Helpers/StartChocolateyProcessHelper.cs b/src/Chocolatey.PowerShell/Helpers/StartChocolateyProcessHelper.cs new file mode 100644 index 0000000000..903bb88281 --- /dev/null +++ b/src/Chocolatey.PowerShell/Helpers/StartChocolateyProcessHelper.cs @@ -0,0 +1,338 @@ +namespace Chocolatey.PowerShell.Helpers +{ + using chocolatey; + using chocolatey.infrastructure.commands; + using chocolatey.infrastructure.filesystem; + using chocolatey.infrastructure.information; + using Chocolatey.PowerShell.StringResources; + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Management.Automation; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + + internal class StartChocolateyProcessHelper + { + const string ErrorId = "StartChocolateyProcessError"; + + private BlockingCollection<(string message, bool isError)> _processMessages; + private TaskCompletionSource _eventHandled; + + private static readonly int[] _successExitCodes = { 0, 1605, 1614, 1641, 3010 }; + private readonly PSCmdlet _cmdlet; + private readonly CancellationToken _pipelineStopToken; + private readonly IFileSystem _filesystem; + + internal StartChocolateyProcessHelper(PSCmdlet cmdlet, CancellationToken pipelineStopToken, IFileSystem filesystem) + { + _cmdlet = cmdlet; + _pipelineStopToken = pipelineStopToken; + _filesystem = filesystem; + } + + internal void CancelWait() + { + _eventHandled?.SetCanceled(); + } + + internal int Start(string processName, string workingDirectory, string arguments, string sensitiveStatements, int[] validExitCodes, bool elevated, bool minimized, bool noSleep) + { + if (string.IsNullOrWhiteSpace(workingDirectory)) + { + var currentLocation = _cmdlet.SessionState.Path.CurrentFileSystemLocation; + if (string.IsNullOrEmpty(currentLocation?.ProviderPath)) + { + PowerShellHelper.WriteDebug(_cmdlet, "Unable to use current location for Working Directory. Using Cache Location instead."); + workingDirectory = Environment.GetEnvironmentVariable("TEMP"); + } + else + { + workingDirectory = currentLocation.ProviderPath; + } + } + + var alreadyElevated = ProcessInformation.UserIsAdministrator(); + + var debugMessagePrefix = elevated ? "Elevating permissions and running" : "Running"; + + processName = NormalizeProcessName(processName); + if (!string.IsNullOrWhiteSpace(arguments)) + { + arguments = arguments.Replace("\0", ""); + } + + if (processName.IsEqualTo("powershell")) + { + processName = PowershellExecutor.GetPowerShellLocation(_filesystem); + var helpersPath = PowerShellHelper.ConvertTo(PowerShellHelper.GetPSVariable(_cmdlet, "helpersPath")); + var importChocolateyHelpers = $"Import-Module -Name '{helpersPath}\\chocolateyInstaller.psm1' -Verbose:$false | Out-Null"; + var statements = PowerShellScriptWrapper.FormatWith(noSleep, importChocolateyHelpers, arguments); + + var encodedStatements = Convert.ToBase64String(Encoding.Unicode.GetBytes(statements)); + + arguments = "-NoLogo -NonInteractive -NoProfile -ExecutionPolicy Bypass -InputFormat Text -OutputFormat Text -EncodedCommand {0}".FormatWith(encodedStatements); + PowerShellHelper.WriteDebug(_cmdlet, $@" +{debugMessagePrefix} powershell block: +{statements} +This may take a while, depending on the statements."); + } + else + { + PowerShellHelper.WriteDebug(_cmdlet, $"{debugMessagePrefix} [\"$exeToRun\" {arguments}]. This may take a while, depending on the statements."); + } + + var exeIsTextFile = PowerShellHelper.GetUnresolvedPath(_cmdlet, processName) + ".istext"; + if (PowerShellHelper.ItemExists(_cmdlet, exeIsTextFile)) + { + PowerShellHelper.SetExitCode(_cmdlet, 4); + _cmdlet.ThrowTerminatingError(new ErrorRecord( + new InvalidOperationException($"The file was a text file but is attempting to be run as an executable - '{processName}'"), + ErrorId, + ErrorCategory.InvalidOperation, + processName)); + } + + if (processName.IsEqualTo("msiexec") || processName.IsEqualTo("msiexec.exe")) + { + processName = PowerShellHelper.CombinePaths(_cmdlet, Environment.GetEnvironmentVariable("SystemRoot"), "System32\\msiexec.exe"); + } + else if (!PowerShellHelper.ItemExists(_cmdlet, processName)) + { + _cmdlet.WriteWarning($"May not be able to find '{processName}'. Please use full path for executables."); + } + + var process = new Process + { + EnableRaisingEvents = true, + StartInfo = new ProcessStartInfo + { + FileName = processName, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + WorkingDirectory = workingDirectory, + }, + }; + + process.OutputDataReceived += ProcessOutputHandler; + process.ErrorDataReceived += ProcessErrorHandler; + process.Exited += ProcessExitingHandler; + + if (!string.IsNullOrWhiteSpace(arguments)) + { + process.StartInfo.Arguments = arguments; + } + + if (!string.IsNullOrWhiteSpace(sensitiveStatements)) + { + PowerShellHelper.WriteHost(_cmdlet, "Sensitive statements have been passed. Adding to arguments."); + process.StartInfo.Arguments += $" {sensitiveStatements}"; + } + + if (elevated && !alreadyElevated && Environment.OSVersion.Version > new Version(6, 0)) + { + // This currently doesn't work as we're not using ShellExecute + PowerShellHelper.WriteDebug(_cmdlet, "Setting RunAs for elevation"); + process.StartInfo.Verb = "RunAs"; + } + + if (minimized) + { + process.StartInfo.WindowStyle = ProcessWindowStyle.Minimized; + } + + var exitCode = 0; + + try + { + _eventHandled = new TaskCompletionSource(); + _processMessages = new BlockingCollection<(string message, bool isError)>(); + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + // process.WaitForExit() is a bit unreliable, we use the Exiting event handler to register when the process + // exits. Use of the TaskCompletionSource also allows us to respect calls for StopProcessing() correctly. + PowerShellHelper.WriteDebug(_cmdlet, "Waiting for process to exit"); + + // This will handle dispatching output/error messages until either the process has exited or the pipeline + // has been cancelled. + HandleProcessMessages(); + } + finally + { + process.OutputDataReceived -= ProcessOutputHandler; + process.ErrorDataReceived -= ProcessErrorHandler; + process.Exited -= ProcessExitingHandler; + + exitCode = process.ExitCode; + process.Dispose(); + } + + PowerShellHelper.WriteDebug(_cmdlet, $"Command [\"{process}\" {arguments}] exited with '{exitCode}'."); + var reason = GetExitCodeReason(exitCode); + if (!string.IsNullOrWhiteSpace(reason)) + { + PowerShellHelper.WriteWarning(_cmdlet, reason); + reason = $"Exit code indicates the following: {reason}"; + } + else + { + reason = "See log for possible error messages."; + } + + if (!validExitCodes.Contains(exitCode)) + { + PowerShellHelper.SetExitCode(_cmdlet, exitCode); + // TODO: Replace RuntimeException with custom exception type + _cmdlet.ThrowTerminatingError(new RuntimeException($"Running [\"{process}\" {arguments}] not successful. Exit code was {exitCode}. {reason}").ErrorRecord); + } + else if (!_successExitCodes.Contains(exitCode)) + { + PowerShellHelper.WriteWarning(_cmdlet, $"Exit code '{exitCode}' was considered valid by script, but not as a normal Chocolatey success code. Returning '0'."); + exitCode = 0; + } + + return exitCode; + } + + private void HandleProcessMessages() + { + foreach (var item in _processMessages.GetConsumingEnumerable(_pipelineStopToken)) + { + if (item.isError) + { + _cmdlet.WriteError(new RuntimeException(item.message).ErrorRecord); + } + else + { + PowerShellHelper.WriteVerbose(_cmdlet, item.message); + } + } + } + + private void ProcessExitingHandler(object sender, EventArgs e) + { + _eventHandled?.TrySetResult(true); + _processMessages.CompleteAdding(); + } + + private void ProcessOutputHandler(object sender, DataReceivedEventArgs e) + { + if (!(e.Data is null)) + { + _processMessages.Add((e.Data, false)); + } + } + + private void ProcessErrorHandler(object sender, DataReceivedEventArgs e) + { + if (!(e.Data is null)) + { + _processMessages.Add((e.Data, true)); + } + } + + private const string PowerShellScriptWrapper = @" +$noSleep = ${0} +{1} +try { + $progressPreference = ""SilentlyContinue"" + {2} + + if (-not $noSleep) { + Start-Sleep 6 + } +} +catch { + if (-not $noSleep) { + Start-Sleep 8 + } + + throw $_ +}"; + + private string GetExitCodeReason(int exitCode) + { + var errorMessageAddendum = $" This is most likely an issue with the '{0}' package and not with Chocolatey itself. Please follow up with the package maintainer(s) directly." + .FormatWith(Environment.GetEnvironmentVariable(EnvironmentVariables.ChocolateyPackageName)); + + switch (exitCode) + { + case 0: + case 1: + case 3010: + return string.Empty; + // NSIS - http://nsis.sourceforge.net/Docs/AppendixD.html + // InnoSetup - http://www.jrsoftware.org/ishelp/index.php?topic=setupexitcodes + case 2: + return "Setup was cancelled."; + case 3: + return "A fatal error occurred when preparing or moving to next install phase. Check to be sure you have enough memory to perform an installation and try again."; + case 4: + return "A fatal error occurred during installation process." + errorMessageAddendum; + case 5: + return "User (you) cancelled the installation.'"; + case 6: + return "Setup process was forcefully terminated by the debugger."; + case 7: + return "While preparing to install, it was determined setup cannot proceed with the installation. Please be sure the software can be installed on your system."; + case 8: + return "While preparing to install, it was determined setup cannot proceed with the installation until you restart the system. Please reboot and try again.'"; + // MSI - https://msdn.microsoft.com/en-us/library/windows/desktop/aa376931.aspx + case 1602: + return "User (you) cancelled the installation."; + case 1603: + return "Generic MSI Error. This is a local environment error, not an issue with a package or the MSI itself - it could mean a pending reboot is necessary prior to install or something else (like the same version is already installed). Please see MSI log if available. If not, try again adding '--install-arguments=\"'/l*v c:\\$($env:chocolateyPackageName)_msi_install.log'\"'. Then search the MSI Log for \"Return Value 3\" and look above that for the error."; + case 1618: + return "Another installation currently in progress. Try again later."; + case 1619: + return "MSI could not be found - it is possibly corrupt or not an MSI at all. If it was downloaded and the MSI is less than 30K, try opening it in an editor like Notepad++ as it is likely HTML." + + errorMessageAddendum; + case 1620: + return "MSI could not be opened - it is possibly corrupt or not an MSI at all. If it was downloaded and the MSI is less than 30K, try opening it in an editor like Notepad++ as it is likely HTML." + + errorMessageAddendum; + case 1622: + return "Something is wrong with the install log location specified. Please fix this in the package silent arguments (or in install arguments you specified). The directory specified as part of the log file path must exist for an MSI to be able to log to that directory." + + errorMessageAddendum; + case 1623: + return "This MSI has a language that is not supported by your system. Contact package maintainer(s) if there is an install available in your language and you would like it added to the packaging."; + case 1625: + return "Installation of this MSI is forbidden by system policy. Please contact your system administrators."; + case 1632: + case 1633: + return "Installation of this MSI is not supported on this platform. Contact package maintainer(s) if you feel this is in error or if you need an architecture that is not available with the current packaging."; + case 1638: + return "This MSI requires uninstall prior to installing a different version. Please ask the package maintainer(s) to add a check in the chocolateyInstall.ps1 script and uninstall if the software is installed." + + errorMessageAddendum; + case 1639: + return "The command line arguments passed to the MSI are incorrect. If you passed in additional arguments, please adjust. Otherwise followup with the package maintainer(s) to get this fixed." + + errorMessageAddendum; + case 1640: + case 1645: + return "Cannot install MSI when running from remote desktop (terminal services). This should automatically be handled in licensed editions. For open source editions, you may need to run change.exe prior to running Chocolatey or not use terminal services."; + } + + return string.Empty; + } + + private string NormalizeProcessName(string processName) + { + if (!string.IsNullOrWhiteSpace(processName)) + { + processName = processName.Replace("\0", string.Empty); + + if (!string.IsNullOrWhiteSpace(processName)) + { + processName = processName.Trim().Trim('"', '\''); + } + } + + return processName; + } + } +} diff --git a/src/Chocolatey.PowerShell/Shared/ChecksumType.cs b/src/Chocolatey.PowerShell/Shared/ChecksumType.cs new file mode 100644 index 0000000000..5e1d83b368 --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/ChecksumType.cs @@ -0,0 +1,16 @@ +namespace Chocolatey.PowerShell.Shared +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + public enum ChecksumType + { + Md5, + Sha1, + Sha256, + Sha512, + } +} diff --git a/src/Chocolatey.PowerShell/Shared/ChocolateyPathType.cs b/src/Chocolatey.PowerShell/Shared/ChocolateyPathType.cs new file mode 100644 index 0000000000..3687500b6c --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/ChocolateyPathType.cs @@ -0,0 +1,12 @@ +namespace Chocolatey.PowerShell.Shared +{ + using System; + using System.Collections.Generic; + using System.Text; + + public enum ChocolateyPathType + { + PackagePath, + InstallPath, + } +} diff --git a/src/Chocolatey.PowerShell/Shared/JankySwitchTransformAttribute.cs b/src/Chocolatey.PowerShell/Shared/JankySwitchTransformAttribute.cs new file mode 100644 index 0000000000..011bb309bd --- /dev/null +++ b/src/Chocolatey.PowerShell/Shared/JankySwitchTransformAttribute.cs @@ -0,0 +1,22 @@ +namespace Chocolatey.PowerShell.Shared +{ + using Chocolatey.PowerShell.Helpers; + using System.Management.Automation; + + public class BoolStringSwitchTransform : ArgumentTransformationAttribute + { + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + switch (inputData) + { + case SwitchParameter s: + return s; + case bool b: + return new SwitchParameter(b); + default: + return new SwitchParameter( + !string.IsNullOrEmpty(PowerShellHelper.ConvertTo(inputData))); + } + } + } +} diff --git a/src/Chocolatey.PowerShell/StringResources/EnvironmentVariables.cs b/src/Chocolatey.PowerShell/StringResources/EnvironmentVariables.cs new file mode 100644 index 0000000000..96867e48cc --- /dev/null +++ b/src/Chocolatey.PowerShell/StringResources/EnvironmentVariables.cs @@ -0,0 +1,101 @@ +// Copyright © 2023-Present Chocolatey Software, Inc +// +// 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 Chocolatey.PowerShell.StringResources +{ + using System.ComponentModel; + + /// + /// Resources for the names of available environment variables + /// that will be created or used by provided PowerShell commands as part of executing + /// Chocolatey CLI. + /// + /// + /// DEV NOTICE: Mark anything that is not meant for public consumption as + /// internal constants and not browsable, even if used in other projects. + /// + public static class EnvironmentVariables + { + /// + /// The version of the package that is being handled as it is defined in the embedded + /// nuspec file. + /// + /// + /// Will be sets during package installs, upgrades and uninstalls. + /// Environment variable is only for internal uses. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyPackageNuspecVersion = nameof(ChocolateyPackageNuspecVersion); + + /// + /// The version of the package that is being handled as it is defined in the embedded + /// nuspec file. + /// + /// + /// Will be sets during package installs, upgrades and uninstalls. + /// Environment variable is only for internal uses. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string PackageNuspecVersion = nameof(PackageNuspecVersion); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyRequestTimeout = nameof(ChocolateyRequestTimeout); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyResponseTimeout = nameof(ChocolateyResponseTimeout); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyUrlOverride = nameof(ChocolateyUrlOverride); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyUrl64BitOverride = nameof(ChocolateyUrl64BitOverride); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyForceX86 = nameof(ChocolateyForceX86); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksum32 = nameof(ChocolateyChecksum32); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksumType32 = nameof(ChocolateyChecksumType32); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksum64 = nameof(ChocolateyChecksum64); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksumType64 = nameof(ChocolateyChecksumType64); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public static string ChocolateyPackageName = nameof(ChocolateyPackageName); + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public static string ChocolateyPackagePath = nameof(ChocolateyPackagePath); + } +} \ No newline at end of file diff --git a/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 b/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 index 4b01918725..eb1d7b56bc 100644 --- a/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 +++ b/src/chocolatey.resources/helpers/chocolateyInstaller.psm1 @@ -91,5 +91,13 @@ if (Test-Path $extensionsPath) { } } +Set-Alias -Name 'Start-ChocolateyProcessAsAdmin' -Value 'Start-ChocolateyProcess' +Set-Alias -Name 'Invoke-ChocolateyProcess' -Value 'Start-ChocolateyProcess' +Set-Alias -Name 'Get-CheckSumValid' -Value 'Assert-ChecksumValid' +Set-Alias -Name 'Get-ChocolateyUnzip' -Value 'Expand-ChocolateyArchive' +Set-Alias -Name 'Get-ProcessorBits' -Value 'Get-OSArchitectureWidth' +Set-Alias -Name 'Get-OSBitness' -Value 'Get-OSArchitectureWidth' + + # todo: explore removing this for a future version Export-ModuleMember -Function * -Alias * -Cmdlet * diff --git a/src/chocolatey.sln b/src/chocolatey.sln index c2b6ac88fa..dbe5e6e003 100644 --- a/src/chocolatey.sln +++ b/src/chocolatey.sln @@ -54,6 +54,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "chocolatey", "chocolatey", EndProject Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "chocolatey.install", "chocolatey.install\chocolatey.install.wixproj", "{6B96B4AE-8FD2-4719-AAFB-BA027B798089}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chocolatey.PowerShell", "Chocolatey.PowerShell\Chocolatey.PowerShell.csproj", "{C97AA97A-469B-4E29-B742-F9A5F1796A05}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -240,6 +242,42 @@ Global {6B96B4AE-8FD2-4719-AAFB-BA027B798089}.WIX|Mixed Platforms.Build.0 = WIX|x86 {6B96B4AE-8FD2-4719-AAFB-BA027B798089}.WIX|x86.ActiveCfg = WIX|x86 {6B96B4AE-8FD2-4719-AAFB-BA027B798089}.WIX|x86.Build.0 = WIX|x86 + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Debug|x86.ActiveCfg = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Debug|x86.Build.0 = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.NoResources|Any CPU.ActiveCfg = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.NoResources|Any CPU.Build.0 = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.NoResources|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.NoResources|Mixed Platforms.Build.0 = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.NoResources|x86.ActiveCfg = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.NoResources|x86.Build.0 = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Release|Any CPU.Build.0 = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Release|x86.ActiveCfg = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.Release|x86.Build.0 = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficial|Any CPU.ActiveCfg = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficial|Any CPU.Build.0 = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficial|Mixed Platforms.ActiveCfg = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficial|Mixed Platforms.Build.0 = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficial|x86.ActiveCfg = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficial|x86.Build.0 = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficialNo7zip|Any CPU.ActiveCfg = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficialNo7zip|Any CPU.Build.0 = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficialNo7zip|Mixed Platforms.ActiveCfg = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficialNo7zip|Mixed Platforms.Build.0 = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficialNo7zip|x86.ActiveCfg = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.ReleaseOfficialNo7zip|x86.Build.0 = Release|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.WIX|Any CPU.ActiveCfg = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.WIX|Any CPU.Build.0 = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.WIX|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.WIX|Mixed Platforms.Build.0 = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.WIX|x86.ActiveCfg = Debug|Any CPU + {C97AA97A-469B-4E29-B742-F9A5F1796A05}.WIX|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/chocolatey/Properties/AssemblyInfo.cs b/src/chocolatey/Properties/AssemblyInfo.cs index b580b39064..e57ea65b4c 100644 --- a/src/chocolatey/Properties/AssemblyInfo.cs +++ b/src/chocolatey/Properties/AssemblyInfo.cs @@ -14,6 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM diff --git a/src/chocolatey/StringExtensions.cs b/src/chocolatey/StringExtensions.cs index 5da1455870..f88e0b319c 100644 --- a/src/chocolatey/StringExtensions.cs +++ b/src/chocolatey/StringExtensions.cs @@ -290,6 +290,22 @@ public static string QuoteIfContainsPipe(this string input) return input; } + internal static string AsFileSizeString(this double size) + { + IList units = new List(new[] { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB" }); + foreach (var unit in units) + { + if (size < 1024) + { + return string.Format("{0:0.##} {1}", size, unit); + } + + size /= 1024; + } + + return string.Format("{0:0.##} YB", size); + } + #pragma warning disable IDE1006 [Obsolete("This overload is deprecated and will be removed in v3.")] public static string format_with(this string input, params object[] formatting) diff --git a/src/chocolatey/StringResources.cs b/src/chocolatey/StringResources.cs index 4bbf0f6c99..367b7928e4 100644 --- a/src/chocolatey/StringResources.cs +++ b/src/chocolatey/StringResources.cs @@ -55,6 +55,46 @@ public static class EnvironmentVariables [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] internal const string PackageNuspecVersion = "packageNuspecVersion"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyRequestTimeout = "chocolateyRequestTimeout"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyResponseTimeout = "chocolateyResponseTimeout"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyUrlOverride = "chocolateyUrlOverride"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyUrl64BitOverride = "chocolateyUrl64BitOverride"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyForceX86 = "chocolateyForceX86"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksum32 = "chocolateyChecksum32"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksumType32 = "chocolateyChecksumType32"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksum64 = "chocolateyChecksum64"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal const string ChocolateyChecksumType64 = "chocolateyChecksumType64"; + + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + internal static string ChocolateyPackageName = "chocolateyPackageName"; } } } \ No newline at end of file diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index a945314f6d..6e5d74e64a 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -206,6 +206,7 @@ + diff --git a/src/chocolatey/infrastructure.app/common/ProxySettings.cs b/src/chocolatey/infrastructure.app/common/ProxySettings.cs new file mode 100644 index 0000000000..2f5602379b --- /dev/null +++ b/src/chocolatey/infrastructure.app/common/ProxySettings.cs @@ -0,0 +1,97 @@ +// Copyright © 2017-2019 Chocolatey Software, Inc ("Chocolatey") +// Copyright © 2015-2017 RealDimensions Software, LLC +// +// Chocolatey Professional, Chocolatey for Business, and Chocolatey Architect are licensed software. +// +// ===================================================================== +// End-User License Agreement +// Chocolatey Professional, Chocolatey for Service Providers, Chocolatey for Business, +// and/or Chocolatey Architect +// ===================================================================== +// +// IMPORTANT- READ CAREFULLY: This Chocolatey Software ("Chocolatey") End-User License Agreement +// ("EULA") is a legal agreement between you ("END USER") and Chocolatey for all Chocolatey products, +// controls, source code, demos, intermediate files, media, printed materials, and "online" or electronic +// documentation (collectively "SOFTWARE PRODUCT(S)") contained with this distribution. +// +// Chocolatey grants to you as an individual or entity, a personal, nonexclusive license to install and use the +// SOFTWARE PRODUCT(S). By installing, copying, or otherwise using the SOFTWARE PRODUCT(S), you +// agree to be bound by the terms of this EULA. If you do not agree to any part of the terms of this EULA, DO +// NOT INSTALL, USE, OR EVALUATE, ANY PART, FILE OR PORTION OF THE SOFTWARE PRODUCT(S). +// +// In no event shall Chocolatey be liable to END USER for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a result of the use or inability to use the +// SOFTWARE PRODUCT(S) (including but not limited to damages for loss of goodwill, work stoppage, computer +// failure or malfunction, or any and all other commercial damages or losses). +// +// The liability of Chocolatey to END USER for any reason and upon any cause of action related to the +// performance of the work under this agreement whether in tort or in contract or otherwise shall be limited to the +// amount paid by the END USER to Chocolatey pursuant to this agreement. +// +// ALL SOFTWARE PRODUCT(S) are licensed not sold. If you are an individual, you must acquire an individual +// license for the SOFTWARE PRODUCT(S) from Chocolatey or its authorized resellers. If you are an entity, you +// must acquire an individual license for each machine running the SOFTWARE PRODUCT(S) within your +// organization from Chocolatey or its authorized resellers. Both virtual and physical machines running the +// SOFTWARE PRODUCT(S) or benefitting from licensed features such as Package Builder or Package +// Internalizer must be counted in the SOFTWARE PRODUCT(S) licenses quantity of the organization. +namespace chocolatey.infrastructure.app.common +{ + using System; + using System.Management.Automation; + using System.Management.Automation.Host; + using System.Net; + + public static class ProxySettings + { + public static IWebProxy GetProxy(Uri uri, PSHost host) + { + var explicitProxy = Environment.GetEnvironmentVariable("chocolateyProxyLocation"); + var explicitProxyUser = Environment.GetEnvironmentVariable("chocolateyProxyUser"); + var explicitProxyPassword = Environment.GetEnvironmentVariable("chocolateyProxyPassword"); + var explicitProxyBypassList = Environment.GetEnvironmentVariable("chocolateyProxyBypassList"); + var explicitProxyBypassOnLocal = Environment.GetEnvironmentVariable("chocolateyProxyBypassOnLocal"); + var defaultCredentials = CredentialCache.DefaultCredentials; + + if (explicitProxy != null) + { + var proxy = new WebProxy(explicitProxy, BypassOnLocal: explicitProxyBypassOnLocal.ToStringSafe().IsEqualTo("true")); + if (!string.IsNullOrWhiteSpace(explicitProxyPassword)) + { + var securePassword = explicitProxyPassword.ToSecureStringSafe(); + proxy.Credentials = new NetworkCredential(explicitProxyUser, securePassword); + } + if (!string.IsNullOrWhiteSpace(explicitProxyBypassList)) + { + proxy.BypassList = explicitProxyBypassList.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + "chocolatey".Log().Info(() => "Using explicit proxy server '{0}'.".FormatWith(explicitProxy)); + + return proxy; + } + + var webClient = new WebClient(); + if (webClient.Proxy != null && !webClient.Proxy.IsBypassed(uri)) + { + var credentials = defaultCredentials; + if (credentials == null && host != null) + { + "chocolatey".Log().Debug(() => "Default credentials were null. Attempting backup method"); + PSCredential cred = host.UI.PromptForCredential("Enter username/password", "", "", ""); + credentials = cred.GetNetworkCredential(); + } + + var proxyAddress = webClient.Proxy.GetProxy(uri).Authority; + var proxy = new WebProxy(proxyAddress, BypassOnLocal: true) + { + Credentials = credentials + }; + + "chocolatey".Log().Info(() => "Using system proxy server '{0}'.".FormatWith(proxyAddress)); + return proxy; + } + + return null; + } + } +}