Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/new.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The following arguments are available:
| Argument | Description |
|--------------|-------------|
| **-o,--out** | The output directory where the newly created manifests will be saved locally |
| **--allow-unsecure-downloads** | Allow unsecure downloads (HTTP) for this operation. |
| **-f,--format** | Output format of the manifest. Default is "yaml". |
| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. <br/>⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ |
| **-?, --help** | Gets additional help on this command |
Expand Down
1 change: 1 addition & 0 deletions doc/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ The following arguments are available:
| **-r, --replace** | Boolean value for replacing an existing manifest from the Windows Package Manager repo. Optionally provide a version or else the latest version will be replaced. Default is false. |
| **-i, --interactive** | Boolean value for making the update command interactive. If true, the tool will prompt the user for input. Default is false. |
| **-f,--format** | Output format of the manifest. Default is "yaml". |
| **--allow-unsecure-downloads** | Allow unsecure downloads (HTTP) for this operation. |
| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials. <br/>⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ |
| **-?, --help** | Gets additional help on this command. |

Expand Down
17 changes: 14 additions & 3 deletions src/WingetCreateCLI/Commands/BaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,10 @@ protected static async Task<string> GitHubOAuthLoginFlow()
/// <summary>
/// Downloads the package file from the provided installer url.
/// </summary>
/// /// <param name="installerUrl"> Installer Url to be downloaded. </param>
/// <param name="installerUrl">Installer Url to be downloaded. </param>
/// <param name="allowHttp">The flag indicating whether to allow HTTP downloads.</param>
/// <returns>Package file.</returns>
protected static async Task<string> DownloadPackageFile(string installerUrl)
protected static async Task<string> DownloadPackageFile(string installerUrl, bool allowHttp)
{
Logger.InfoLocalized(nameof(Resources.DownloadInstaller_Message), installerUrl);

Expand All @@ -332,7 +333,7 @@ protected static async Task<string> DownloadPackageFile(string installerUrl)

try
{
string packageFilePath = await PackageParser.DownloadFileAsync(installerUrl);
string packageFilePath = await PackageParser.DownloadFileAsync(installerUrl, allowHttp);
TelemetryManager.Log.WriteEvent(new DownloadInstallerEvent { IsSuccessful = true });
DownloadedInstallers.Add(installerUrl, packageFilePath);
return packageFilePath;
Expand Down Expand Up @@ -377,6 +378,16 @@ protected static async Task<string> DownloadPackageFile(string installerUrl)
Logger.ErrorLocalized(nameof(Resources.DownloadConnectionTimeout_Error));
return null;
}
else if (e is NotSupportedException)
{
Logger.ErrorLocalized(nameof(Resources.DownloadProtocolNotSupported_Error));
return null;
}
else if (e is DownloadHttpsOnlyException)
{
Logger.ErrorLocalized(nameof(Resources.DownloadHttpsOnly_Error));
return null;
}
else
{
throw;
Expand Down
8 changes: 7 additions & 1 deletion src/WingetCreateCLI/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ public static IEnumerable<Example> Examples
[Option('o', "out", Required = false, HelpText = "OutputDirectory_HelpText", ResourceType = typeof(Resources))]
public string OutputDir { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to allow unsecure downloads.
/// </summary>
[Option("allow-unsecure-downloads", Required = false, HelpText = "AllowUnsecureDownloads_HelpText", ResourceType = typeof(Resources))]
public bool AllowUnsecureDownloads { get; set; }
Comment thread
AmelBawa-msft marked this conversation as resolved.

/// <summary>
/// Gets or sets the format of the output manifest files.
/// </summary>
Expand Down Expand Up @@ -116,7 +122,7 @@ public override async Task<bool> Execute()

foreach (var installerUrl in this.InstallerUrls)
{
string packageFile = await DownloadPackageFile(installerUrl);
string packageFile = await DownloadPackageFile(installerUrl, this.AllowUnsecureDownloads);
if (string.IsNullOrEmpty(packageFile))
{
return false;
Expand Down
10 changes: 8 additions & 2 deletions src/WingetCreateCLI/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ public static IEnumerable<Example> Examples
[Option('f', "format", Required = false, HelpText = "ManifestFormat_HelpText", ResourceType = typeof(Resources))]
public override ManifestFormat Format { get => base.Format; set => base.Format = value; }

/// <summary>
/// Gets or sets a value indicating whether to allow unsecure downloads.
/// </summary>
[Option("allow-unsecure-downloads", Required = false, HelpText = "AllowUnsecureDownloads_HelpText", ResourceType = typeof(Resources))]
public bool AllowUnsecureDownloads { get; set; }

/// <summary>
/// Gets or sets the GitHub token used to submit a pull request on behalf of the user.
/// </summary>
Expand Down Expand Up @@ -406,7 +412,7 @@ public async Task<Manifests> UpdateManifestsAutonomously(Manifests manifests)

foreach (var installerUpdate in installerMetadataList)
{
string packageFile = await DownloadPackageFile(installerUpdate.InstallerUrl);
string packageFile = await DownloadPackageFile(installerUpdate.InstallerUrl, this.AllowUnsecureDownloads);
if (string.IsNullOrEmpty(packageFile))
{
return null;
Expand Down Expand Up @@ -1005,7 +1011,7 @@ private async Task UpdateSingleInstallerInteractively(Installer installer)
{
string url = Prompt.Input<string>(Resources.NewInstallerUrl_Message, null, null, new[] { FieldValidation.ValidateProperty(newInstaller, nameof(Installer.InstallerUrl)) });

string packageFile = await DownloadPackageFile(url);
string packageFile = await DownloadPackageFile(url, this.AllowUnsecureDownloads);
string archivePath = null;

if (string.IsNullOrEmpty(packageFile))
Expand Down
29 changes: 28 additions & 1 deletion src/WingetCreateCLI/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/WingetCreateCLI/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1471,4 +1471,13 @@ Warning: Using this argument may result in the token being logged. Consider an a
<data name="MicrosoftEntraIdAuthenticationInfo_Scope_KeywordDescription" xml:space="preserve">
<value>The scope value for Microsoft Entra Id authentication</value>
</data>
<data name="DownloadProtocolNotSupported_Error" xml:space="preserve">
<value>Only HTTPS URLs are supported for downloads. Use the "--allow-unsecure-downloads" option to allow HTTP URLs.</value>
</data>
<data name="DownloadHttpsOnly_Error" xml:space="preserve">
<value>Only HTTPS URLs are supported without "--allow-unsecure-downloads".</value>
</data>
<data name="AllowUnsecureDownloads_HelpText" xml:space="preserve">
<value>Allow unsecure downloads (HTTP) for this operation.</value>
Comment thread
AmelBawa-msft marked this conversation as resolved.
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.

namespace Microsoft.WingetCreateCore.Common.Exceptions
{
using System;

/// <summary>
/// The exception that is thrown when the download URL is not HTTPS.
/// </summary>
public class DownloadHttpsOnlyException : Exception
{
}
}
28 changes: 27 additions & 1 deletion src/WingetCreateCore/Common/PackageParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,19 @@ public static void ParsePackages(List<InstallerMetadata> installerMetadataList,
/// Download file at specified URL to temp directory, unless it's already present.
/// </summary>
/// <param name="url">The URL of the file to be downloaded.</param>
/// <param name="allowHttp">Whether to allow HTTP downloads.</param>
/// <param name="maxDownloadSize">The maximum file size in bytes to download.</param>
/// <returns>Path of downloaded, or previously downloaded, file.</returns>
public static async Task<string> DownloadFileAsync(string url, long? maxDownloadSize = null)
public static async Task<string> DownloadFileAsync(string url, bool allowHttp, long? maxDownloadSize = null)
{
ValidateUrl(url, allowHttp);
var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);

int redirectCount = 0;
while (response.StatusCode == System.Net.HttpStatusCode.Redirect && redirectCount < 2)
{
var redirectUri = response.Headers.Location;
ValidateUrl(redirectUri, allowHttp);
response = await httpClient.GetAsync(redirectUri, HttpCompletionOption.ResponseHeadersRead);
redirectCount++;
}
Expand Down Expand Up @@ -1116,5 +1119,28 @@ private static string RemoveInvalidCharsFromString(string value)
{
return Regex.Replace(value, InvalidCharacters, string.Empty);
}

private static void ValidateUrl(string url, bool allowHttp)
Comment thread
AmelBawa-msft marked this conversation as resolved.
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri downloadUrl))
{
throw new InvalidOperationException();
}

ValidateUrl(downloadUrl, allowHttp);
}

private static void ValidateUrl(Uri url, bool allowHttp)
{
if (url.Scheme != Uri.UriSchemeHttp && url.Scheme != Uri.UriSchemeHttps)
{
throw new NotSupportedException();
}

if (!allowHttp && url.Scheme != Uri.UriSchemeHttps)
{
throw new DownloadHttpsOnlyException();
}
}
}
}
2 changes: 1 addition & 1 deletion src/WingetCreateTests/WingetCreateTests/TestUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public static string MockDownloadFile(string filename)
{
string url = $"https://fakedomain.com/{filename}";
SetMockHttpResponseContent(filename);
string downloadedPath = PackageParser.DownloadFileAsync(url).Result;
string downloadedPath = PackageParser.DownloadFileAsync(url, false).Result;
return downloadedPath;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ namespace Microsoft.WingetCreateUnitTests
/// Test cases for verifying that the "new" command is working as expected.
/// </summary>
public class NewCommandTests
{
{
/// <summary>
/// OneTimeSetup method for the New command unit tests.
/// </summary>
[OneTimeSetUp]
public void OneTimeSetUp()
{
Logger.Initialize();
}

/// <summary>
/// Verifies that the CLI errors out on an invalid installer URL.
/// </summary>
Expand All @@ -27,11 +36,44 @@ public async Task InvalidUrl()
using StringWriter sw = new StringWriter();
Console.SetOut(sw);

Logger.Initialize();
NewCommand command = new NewCommand { InstallerUrls = new[] { "invalidUrl" } };
ClassicAssert.IsFalse(await command.Execute(), "Command should have failed");
string actual = sw.ToString();
Assert.That(actual, Does.Contain(Resources.DownloadFile_Error), "Failed to catch invalid URL");
}

/// <summary>
/// Verifies that the CLI errors out on an invalid protocol.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Test]
public async Task InvalidProtocol()
{
using StringWriter sw = new StringWriter();
Console.SetOut(sw);

NewCommand command = new NewCommand { InstallerUrls = new[] { "ftp://mock" }, AllowUnsecureDownloads = true };
ClassicAssert.IsFalse(await command.Execute(), "Command should have failed");
string actual = sw.ToString();
Assert.That(actual, Does.Contain(Resources.DownloadProtocolNotSupported_Error));
Assert.That(command.AllowUnsecureDownloads, Is.True);
}

/// <summary>
/// Tests that the command execution fails when using non-HTTPS URLs.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Test]
public async Task HttpsOnly()
{
using StringWriter sw = new StringWriter();
Console.SetOut(sw);

NewCommand command = new NewCommand { InstallerUrls = new[] { "http://mock" } };
ClassicAssert.IsFalse(await command.Execute(), "Command should have failed");
string actual = sw.ToString();
Assert.That(actual, Does.Contain(Resources.DownloadHttpsOnly_Error));
Assert.That(command.AllowUnsecureDownloads, Is.False);
}
}
}