diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 1f26c71dd8..0f8c447a96 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -329,6 +329,7 @@ SRL srs standalone startswith +stl streambuf strtoull subdir diff --git a/doc/Settings.md b/doc/Settings.md index 8e463f7b28..37e9aa36fd 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -116,7 +116,7 @@ The `downloader` setting controls which code is used when downloading packages. `wininet` uses the [WinINet](https://docs.microsoft.com/windows/win32/wininet/about-wininet) APIs, while `do` uses the [Delivery Optimization](https://support.microsoft.com/windows/delivery-optimization-in-windows-10-0656e53c-15f2-90de-a87a-a2172c94cf6d) service. -The `doProgressTimeoutInSeconds` setting updates the number of seconds to wait without progress before fallback. The default number of seconds is 60, minimum is 1 and the maximum is 600. +The `doProgressTimeoutInSeconds` setting updates the number of seconds to wait without progress before fallback. The default number of seconds is 60, minimum is 1 and the maximum is 600. ```json "network": { @@ -125,6 +125,19 @@ The `doProgressTimeoutInSeconds` setting updates the number of seconds to wait w } ``` +### Proxy + +The `proxy` setting specifies the proxy server used for download packages. If not specified, winget will retrieve current proxy settings from the registry. + +The `proxyOverride` setting specifies a list of host names or IP addresses, or both, where no proxy should be used. Local hosts are always bypassed as if the list is extended by `["localhost", "loopback", "127.0.0.1", "[::1]"]`. + +```json + "network": { + "proxy": "http://127.0.0.1:7890", + "proxyOverride": ["192.168.*", "github.com"] + } +``` + ## Experimental Features To allow work to be done and distributed to early adopters for feedback, settings can be used to enable "experimental" features. diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 2a9ab5bbd8..9d6a34a21a 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -113,6 +113,17 @@ "default": 60, "minimum": 1, "maximum": 600 + }, + "proxy": { + "description": "Proxy server used for download packages", + "type": "string" + }, + "proxyOverride": { + "description": "List of hosts where no proxy should be used", + "type": "array", + "items": { + "type": "string" + } } } }, diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index d7cf9dd0ec..36eb3a887d 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -60,6 +60,10 @@ namespace AppInstaller::CLI return Argument{ "file", 'f', Args::Type::HashFile, Resource::String::FileArgumentDescription, ArgumentType::Positional, true }; case Args::Type::Msix: return Argument{ "msix", 'm', Args::Type::Msix, Resource::String::MsixArgumentDescription, ArgumentType::Flag }; + case Args::Type::NetworkProxy: + return Argument{ "proxy", NoAlias, Args::Type::NetworkProxy, Resource::String::NetworkProxyArgumentDescription, ArgumentType::Standard, Visibility::Help }; + case Args::Type::NetworkProxyOverride: + return Argument{ "no-proxy", NoAlias, Args::Type::NetworkProxyOverride, Resource::String::NetworkProxyOverrideArgumentDescription, ArgumentType::Standard, Visibility::Help }; case Args::Type::ListVersions: return Argument{ "versions", NoAlias, Args::Type::ListVersions, Resource::String::VersionsArgumentDescription, ArgumentType::Flag }; case Args::Type::Help: diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp index 9ae6d2b338..5a859ad1f5 100644 --- a/src/AppInstallerCLICore/Commands/InstallCommand.cpp +++ b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -43,6 +43,8 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::HashOverride), Argument::ForType(Args::Type::DependencySource), Argument::ForType(Args::Type::AcceptPackageAgreements), + Argument::ForType(Args::Type::NetworkProxy), + Argument::ForType(Args::Type::NetworkProxyOverride), Argument::ForType(Args::Type::CustomHeader), Argument::ForType(Args::Type::AcceptSourceAgreements), }; diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp index 9a0b182690..56bf593b1a 100644 --- a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp +++ b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -49,6 +49,8 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::InstallLocation), Argument::ForType(Args::Type::HashOverride), Argument::ForType(Args::Type::AcceptPackageAgreements), + Argument::ForType(Args::Type::NetworkProxy), + Argument::ForType(Args::Type::NetworkProxyOverride), Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Execution::Args::Type::CustomHeader), Argument{ "all", Argument::NoAlias, Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index f5d231c6af..9339d618fe 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -73,6 +73,10 @@ namespace AppInstaller::CLI::Execution AdminSettingEnable, AdminSettingDisable, + // Network Behavior + NetworkProxy, + NetworkProxyOverride, + // Other All, // Used in Update command to update all installed packages to latest ListVersions, // Used in Show command to list all available versions of an app diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h index d37abc7b12..2349ac4990 100644 --- a/src/AppInstallerCLICore/ExecutionContextData.h +++ b/src/AppInstallerCLICore/ExecutionContextData.h @@ -6,6 +6,7 @@ #include "CompletionData.h" #include "PackageCollection.h" #include "Workflows/WorkflowBase.h" +#include "AppInstallerDownloader.h" #include #include @@ -50,6 +51,7 @@ namespace AppInstaller::CLI::Execution Dependencies, DependencySource, AllowedArchitectures, + NetworkProxyInfo, Max }; @@ -207,5 +209,11 @@ namespace AppInstaller::CLI::Execution { using value_t = std::vector; }; + + template <> + struct DataMapping + { + using value_t = Utility::ProxyInfo; + }; } } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 5f5af6ce33..b24a088579 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -182,6 +182,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MultipleInstalledPackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(NameArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(NetworkProxyArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(NetworkProxyOverrideArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(NoApplicableInstallers); WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage); WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledPackageFound); diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp index 06fca0ad86..481d98e26a 100644 --- a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -185,6 +185,16 @@ namespace AppInstaller::CLI::Workflow } } + void GetProxyInfo(Execution::Context& context) + { + auto proxy = context.Args.GetArg(Execution::Args::Type::NetworkProxy); + auto proxyOverride = context.Args.GetArg(Execution::Args::Type::NetworkProxyOverride); + if (!proxy.empty() || !proxyOverride.empty()) + context.Add({std::string{proxy}, std::string{proxyOverride}}); + else + context.Add(Utility::GetProxyInfo()); + } + void DownloadInstaller(Execution::Context& context) { // Check if file was already downloaded. @@ -308,7 +318,8 @@ namespace AppInstaller::CLI::Workflow Utility::DownloadType::Installer, std::placeholders::_1, true, - downloadInfo)); + downloadInfo, + context.Get())); success = true; } diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.h b/src/AppInstallerCLICore/Workflows/DownloadFlow.h index 744de69467..fadd626b4a 100644 --- a/src/AppInstallerCLICore/Workflows/DownloadFlow.h +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.h @@ -5,6 +5,12 @@ namespace AppInstaller::CLI::Workflow { + // Gets the proxy settings + // Required Args: None + // Inputs: None + // Outputs: None + void GetProxyInfo(Execution::Context& context); + // Composite flow that chooses what to do based on the installer type. // Required Args: None // Inputs: Manifest, Installer diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 7f3712e4e1..8ff8fcb428 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -388,6 +388,7 @@ namespace AppInstaller::CLI::Workflow void InstallSinglePackage(Execution::Context& context) { context << + GetProxyInfo << Workflow::DownloadSinglePackage << Workflow::InstallPackageInstaller; } diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp index 861d97f42c..0dba4b90dc 100644 --- a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -6,6 +6,7 @@ #include "DependenciesFlow.h" #include "InstallFlow.h" #include "UpdateFlow.h" +#include "DownloadFlow.h" #include "ManifestComparator.h" using namespace AppInstaller::Repository; @@ -140,6 +141,8 @@ namespace AppInstaller::CLI::Workflow void UpdateAllApplicable(Execution::Context& context) { + context << GetProxyInfo; + const auto& matches = context.Get().Matches; std::vector> packagesToInstall; bool updateAllFoundUpdate = false; diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index bdc7b6b6d7..263526a31e 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1265,4 +1265,10 @@ Please specify one of them using the `--source` option to proceed. package has a version number that cannot be determined. Use "--include-unknown" to see all results. {Locked="--include-unknown"} This string is preceded by a (integer) number of packages that do not have notated versions. + + Use the specified proxy server + + + Semicolon-separated list of hosts where proxy should not be used + \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp index 5f77cfdd14..d5bd756951 100644 --- a/src/AppInstallerCommonCore/Downloader.cpp +++ b/src/AppInstallerCommonCore/Downloader.cpp @@ -11,6 +11,8 @@ #include "Public/AppInstallerTelemetry.h" #include "Public/winget/UserSettings.h" #include "DODownloader.h" +#include +#include using namespace AppInstaller::Runtime; using namespace AppInstaller::Settings; @@ -21,19 +23,22 @@ namespace AppInstaller::Utility const std::string& url, std::ostream& dest, IProgressCallback& progress, - bool computeHash) + bool computeHash, + ProxyInfo info) { // For AICLI_LOG usages with string literals. #pragma warning(push) #pragma warning(disable:26449) AICLI_LOG(Core, Info, << "WinINet downloading from url: " << url); + AICLI_LOG(Core, Info, << "Proxy: " << info.Proxy); + AICLI_LOG(Core, Info, << "Proxy Bypass: " << info.ProxyOverride); wil::unique_hinternet session(InternetOpenA( "winget-cli", - INTERNET_OPEN_TYPE_PRECONFIG, - NULL, - NULL, + info.Proxy.empty() ? INTERNET_OPEN_TYPE_PRECONFIG : INTERNET_OPEN_TYPE_PROXY, + info.Proxy.empty() ? NULL : info.Proxy.data(), + info.ProxyOverride.empty() ? NULL : info.ProxyOverride.data(), 0)); THROW_LAST_ERROR_IF_NULL_MSG(session, "InternetOpen() failed."); @@ -136,6 +141,37 @@ namespace AppInstaller::Utility return result; } + ProxyInfo GetProxyInfo() + { + ProxyInfo info; + if ((info.Proxy = Settings::User().Get()).empty()) + { + std::wstring allProxy; + wil::GetEnvironmentVariableW(L"ALL_PROXY", allProxy); + if ((info.Proxy = Utility::ConvertToUTF8(allProxy)).empty()) + { + std::wstring httpProxy, httpsProxy, ftpProxy; + wil::GetEnvironmentVariableW(L"HTTP_PROXY", httpProxy); + wil::GetEnvironmentVariableW(L"HTTPS_PROXY", httpsProxy); + wil::GetEnvironmentVariableW(L"FTP_PROXY", ftpProxy); + if (!httpProxy.empty()) + info.Proxy = "HTTP=" + Utility::ConvertToUTF8(httpProxy); + if (!httpsProxy.empty()) + info.Proxy += (info.Proxy.empty() ? "HTTPS=" : " HTTPS=") + Utility::ConvertToUTF8(httpsProxy); + if (!ftpProxy.empty()) + info.Proxy += (info.Proxy.empty() ? "FTP=" : " FTP=") + Utility::ConvertToUTF8(ftpProxy); + } + } + if ((info.ProxyOverride = Settings::User().Get()).empty()) + { + std::wstring noProxy; + wil::GetEnvironmentVariableW(L"NO_PROXY", noProxy); + info.ProxyOverride = Utility::ConvertToUTF8(noProxy); + std::replace_if(info.ProxyOverride.begin(), info.ProxyOverride.end(), [](char ch) { return ch == ','; }, ';'); + } + return info; + } + std::optional> DownloadToStream( const std::string& url, std::ostream& dest, @@ -145,7 +181,7 @@ namespace AppInstaller::Utility std::optional) { THROW_HR_IF(E_INVALIDARG, url.empty()); - return WinINetDownloadToStream(url, dest, progress, computeHash); + return WinINetDownloadToStream(url, dest, progress, computeHash, GetProxyInfo()); } std::optional> Download( @@ -154,7 +190,8 @@ namespace AppInstaller::Utility DownloadType type, IProgressCallback& progress, bool computeHash, - std::optional info) + std::optional downloadInfo, + std::optional proxyInfo) { THROW_HR_IF(E_INVALIDARG, url.empty()); THROW_HR_IF(E_INVALIDARG, dest.empty()); @@ -177,7 +214,7 @@ namespace AppInstaller::Utility { try { - auto result = DODownload(url, dest, progress, computeHash, info); + auto result = DODownload(url, dest, progress, computeHash, downloadInfo); // Since we cannot pre-apply to the file with DO, post-apply the MotW to the file. // Only do so if the file exists, because cancellation will not throw here. if (std::filesystem::exists(dest)) @@ -219,7 +256,7 @@ namespace AppInstaller::Utility // Use std::ofstream::app to append to previous empty file so that it will not // create a new file and clear motw. std::ofstream outfile(dest, std::ofstream::binary | std::ofstream::app); - return WinINetDownloadToStream(url, outfile, progress, computeHash); + return WinINetDownloadToStream(url, outfile, progress, computeHash, proxyInfo ? *proxyInfo : GetProxyInfo()); } using namespace std::string_view_literals; diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h index 0152346b8c..44000df9c4 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h @@ -31,31 +31,42 @@ namespace AppInstaller::Utility std::string ContentId; }; + struct ProxyInfo + { + std::string Proxy; + std::string ProxyOverride; + }; + + // Retrieve proxy info from UserSettings and environment variables. + ProxyInfo GetProxyInfo(); + // Downloads a file from the given URL and places it in the given location. // url: The url to be downloaded from. http->https redirection is allowed. // dest: The stream to be downloaded to. // computeHash: Optional. Indicates if SHA256 hash should be calculated when downloading. - // downloadIdentifier: Optional. Currently only used by DO to identify the download. + // downloadInfo: Optional. Currently only used by DO to identify the download. std::optional> DownloadToStream( const std::string& url, std::ostream& dest, DownloadType type, IProgressCallback& progress, bool computeHash = false, - std::optional info = {}); + std::optional downloadInfo = {}); // Downloads a file from the given URL and places it in the given location. // url: The url to be downloaded from. http->https redirection is allowed. // dest: The path to local file to be downloaded to. // computeHash: Optional. Indicates if SHA256 hash should be calculated when downloading. - // downloadIdentifier: Optional. Currently only used by DO to identify the download. + // downloadInfo: Optional. Currently only used by DO to identify the download. + // proxyInfo: Optional. Currently only used by WinINet for proxy settings. std::optional> Download( const std::string& url, const std::filesystem::path& dest, DownloadType type, IProgressCallback& progress, bool computeHash = false, - std::optional info = {}); + std::optional downloadInfo = {}, + std::optional proxyInfo = {}); // Determines if the given url is a remote location. bool IsUrlRemote(std::string_view url); diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 7cf95b2ec1..4296295822 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -76,6 +76,8 @@ namespace AppInstaller::Settings InstallScopeRequirement, NetworkDownloader, NetworkDOProgressTimeoutInSeconds, + NetworkProxy, + NetworkProxyOverride, InstallArchitecturePreference, InstallArchitectureRequirement, InstallLocalePreference, @@ -127,6 +129,8 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::InstallScopeRequirement, std::string, ScopePreference, ScopePreference::None, ".installBehavior.requirements.scope"sv); SETTINGMAPPING_SPECIALIZATION(Setting::NetworkDownloader, std::string, InstallerDownloader, InstallerDownloader::Default, ".network.downloader"sv); SETTINGMAPPING_SPECIALIZATION(Setting::NetworkDOProgressTimeoutInSeconds, uint32_t, std::chrono::seconds, 60s, ".network.doProgressTimeoutInSeconds"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::NetworkProxy, std::string, std::string, {}, ".network.proxy"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::NetworkProxyOverride, std::vector, std::string, {}, ".network.proxyOverride"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallLocalePreference, std::vector, std::vector, {}, ".installBehavior.preferences.locale"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallLocaleRequirement, std::vector, std::vector, {}, ".installBehavior.requirements.locale"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 8ea3b34585..394c5e59cd 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -10,6 +10,7 @@ #include "AppInstallerArchitecture.h" #include "winget/Locale.h" +#include namespace AppInstaller::Settings { @@ -316,6 +317,15 @@ namespace AppInstaller::Settings { return std::chrono::seconds(value); } + + WINGET_VALIDATE_PASS_THROUGH(NetworkProxy) + + WINGET_VALIDATE_SIGNATURE(NetworkProxyOverride) + { + if (value.empty()) + return {}; + return std::accumulate(std::next(value.begin()), value.end(), value[0], [](auto& a, auto& b) { return a + ';' + b; }); + } } #ifndef AICLI_DISABLE_TEST_HOOKS