diff --git a/doc/new.md b/doc/new.md
index e0471c2a..d2fbd9bc 100644
--- a/doc/new.md
+++ b/doc/new.md
@@ -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.
⚠️ _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 |
diff --git a/doc/update.md b/doc/update.md
index 513647d2..04f1f97c 100644
--- a/doc/update.md
+++ b/doc/update.md
@@ -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.
⚠️ _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. |
diff --git a/src/WingetCreateCLI/Commands/BaseCommand.cs b/src/WingetCreateCLI/Commands/BaseCommand.cs
index 42228493..d1891bf4 100644
--- a/src/WingetCreateCLI/Commands/BaseCommand.cs
+++ b/src/WingetCreateCLI/Commands/BaseCommand.cs
@@ -308,9 +308,10 @@ protected static async Task GitHubOAuthLoginFlow()
///
/// Downloads the package file from the provided installer url.
///
- /// /// Installer Url to be downloaded.
+ /// Installer Url to be downloaded.
+ /// The flag indicating whether to allow HTTP downloads.
/// Package file.
- protected static async Task DownloadPackageFile(string installerUrl)
+ protected static async Task DownloadPackageFile(string installerUrl, bool allowHttp)
{
Logger.InfoLocalized(nameof(Resources.DownloadInstaller_Message), installerUrl);
@@ -332,7 +333,7 @@ protected static async Task 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;
@@ -377,6 +378,16 @@ protected static async Task 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;
diff --git a/src/WingetCreateCLI/Commands/NewCommand.cs b/src/WingetCreateCLI/Commands/NewCommand.cs
index a696cbdf..6f82369f 100644
--- a/src/WingetCreateCLI/Commands/NewCommand.cs
+++ b/src/WingetCreateCLI/Commands/NewCommand.cs
@@ -74,6 +74,12 @@ public static IEnumerable Examples
[Option('o', "out", Required = false, HelpText = "OutputDirectory_HelpText", ResourceType = typeof(Resources))]
public string OutputDir { get; set; }
+ ///
+ /// Gets or sets a value indicating whether to allow unsecure downloads.
+ ///
+ [Option("allow-unsecure-downloads", Required = false, HelpText = "AllowUnsecureDownloads_HelpText", ResourceType = typeof(Resources))]
+ public bool AllowUnsecureDownloads { get; set; }
+
///
/// Gets or sets the format of the output manifest files.
///
@@ -116,7 +122,7 @@ public override async Task 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;
diff --git a/src/WingetCreateCLI/Commands/UpdateCommand.cs b/src/WingetCreateCLI/Commands/UpdateCommand.cs
index ae0dfab2..c0aeb5ab 100644
--- a/src/WingetCreateCLI/Commands/UpdateCommand.cs
+++ b/src/WingetCreateCLI/Commands/UpdateCommand.cs
@@ -121,6 +121,12 @@ public static IEnumerable Examples
[Option('f', "format", Required = false, HelpText = "ManifestFormat_HelpText", ResourceType = typeof(Resources))]
public override ManifestFormat Format { get => base.Format; set => base.Format = value; }
+ ///
+ /// Gets or sets a value indicating whether to allow unsecure downloads.
+ ///
+ [Option("allow-unsecure-downloads", Required = false, HelpText = "AllowUnsecureDownloads_HelpText", ResourceType = typeof(Resources))]
+ public bool AllowUnsecureDownloads { get; set; }
+
///
/// Gets or sets the GitHub token used to submit a pull request on behalf of the user.
///
@@ -406,7 +412,7 @@ public async Task 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;
@@ -1005,7 +1011,7 @@ private async Task UpdateSingleInstallerInteractively(Installer installer)
{
string url = Prompt.Input(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))
diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs
index 5d40723a..d2b0f9e0 100644
--- a/src/WingetCreateCLI/Properties/Resources.Designer.cs
+++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs
@@ -168,6 +168,15 @@ public static string AllowedMarkets_KeywordDescription {
}
}
+ ///
+ /// Looks up a localized string similar to Allow unsecure downloads (HTTP) for this operation..
+ ///
+ public static string AllowUnsecureDownloads_HelpText {
+ get {
+ return ResourceManager.GetString("AllowUnsecureDownloads_HelpText", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The manifest creation command line utility generates manifest for submitting apps to the Windows Package Manager repo..
///
@@ -744,6 +753,15 @@ public static string DownloadFileExceedsMaxSize_Error {
}
}
+ ///
+ /// Looks up a localized string similar to Only HTTPS URLs are supported without "--allow-unsecure-downloads"..
+ ///
+ public static string DownloadHttpsOnly_Error {
+ get {
+ return ResourceManager.GetString("DownloadHttpsOnly_Error", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Downloading and parsing: {0}....
///
@@ -753,6 +771,15 @@ public static string DownloadInstaller_Message {
}
}
+ ///
+ /// Looks up a localized string similar to Only HTTPS URLs are supported for downloads. Use the "--allow-unsecure-downloads" option to allow HTTP URLs..
+ ///
+ public static string DownloadProtocolNotSupported_Error {
+ get {
+ return ResourceManager.GetString("DownloadProtocolNotSupported_Error", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to DSC v3 resource commands.
///
@@ -862,7 +889,7 @@ public static string DscResourcePropertyDescriptionSettings {
}
///
- /// Looks up a localized string similar to Execute the Schema command.
+ /// Looks up a localized string similar to Outputs schema of the resource.
///
public static string DscSchema_HelpText {
get {
diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx
index 9f58f851..e5791b33 100644
--- a/src/WingetCreateCLI/Properties/Resources.resx
+++ b/src/WingetCreateCLI/Properties/Resources.resx
@@ -1471,4 +1471,13 @@ Warning: Using this argument may result in the token being logged. Consider an a
The scope value for Microsoft Entra Id authentication
+
+ Only HTTPS URLs are supported for downloads. Use the "--allow-unsecure-downloads" option to allow HTTP URLs.
+
+
+ Only HTTPS URLs are supported without "--allow-unsecure-downloads".
+
+
+ Allow unsecure downloads (HTTP) for this operation.
+
\ No newline at end of file
diff --git a/src/WingetCreateCore/Common/Exceptions/DownloadHttpsOnlyException.cs b/src/WingetCreateCore/Common/Exceptions/DownloadHttpsOnlyException.cs
new file mode 100644
index 00000000..1c9c6459
--- /dev/null
+++ b/src/WingetCreateCore/Common/Exceptions/DownloadHttpsOnlyException.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Microsoft.WingetCreateCore.Common.Exceptions
+{
+ using System;
+
+ ///
+ /// The exception that is thrown when the download URL is not HTTPS.
+ ///
+ public class DownloadHttpsOnlyException : Exception
+ {
+ }
+}
diff --git a/src/WingetCreateCore/Common/PackageParser.cs b/src/WingetCreateCore/Common/PackageParser.cs
index 5c24142c..984650c2 100644
--- a/src/WingetCreateCore/Common/PackageParser.cs
+++ b/src/WingetCreateCore/Common/PackageParser.cs
@@ -117,16 +117,19 @@ public static void ParsePackages(List installerMetadataList,
/// Download file at specified URL to temp directory, unless it's already present.
///
/// The URL of the file to be downloaded.
+ /// Whether to allow HTTP downloads.
/// The maximum file size in bytes to download.
/// Path of downloaded, or previously downloaded, file.
- public static async Task DownloadFileAsync(string url, long? maxDownloadSize = null)
+ public static async Task 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++;
}
@@ -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)
+ {
+ 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();
+ }
+ }
}
}
diff --git a/src/WingetCreateTests/WingetCreateTests/TestUtils.cs b/src/WingetCreateTests/WingetCreateTests/TestUtils.cs
index 5a97585b..99f5e916 100644
--- a/src/WingetCreateTests/WingetCreateTests/TestUtils.cs
+++ b/src/WingetCreateTests/WingetCreateTests/TestUtils.cs
@@ -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;
}
diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/NewCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/NewCommandTests.cs
index 0fa9c109..97a7d278 100644
--- a/src/WingetCreateTests/WingetCreateTests/UnitTests/NewCommandTests.cs
+++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/NewCommandTests.cs
@@ -16,7 +16,16 @@ namespace Microsoft.WingetCreateUnitTests
/// Test cases for verifying that the "new" command is working as expected.
///
public class NewCommandTests
- {
+ {
+ ///
+ /// OneTimeSetup method for the New command unit tests.
+ ///
+ [OneTimeSetUp]
+ public void OneTimeSetUp()
+ {
+ Logger.Initialize();
+ }
+
///
/// Verifies that the CLI errors out on an invalid installer URL.
///
@@ -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");
}
+
+ ///
+ /// Verifies that the CLI errors out on an invalid protocol.
+ ///
+ /// A representing the asynchronous unit test.
+ [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);
+ }
+
+ ///
+ /// Tests that the command execution fails when using non-HTTPS URLs.
+ ///
+ /// A representing the asynchronous unit test.
+ [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);
+ }
}
}