|
6 | 6 | using Avalonia.Media.Imaging; |
7 | 7 | using Avalonia.Threading; |
8 | 8 | using UniGetUI.Avalonia.ViewModels.Pages; |
| 9 | +using UniGetUI.Core.Logging; |
9 | 10 | using UniGetUI.Core.SettingsEngine; |
10 | 11 | using UniGetUI.Core.Tools; |
11 | 12 | using UniGetUI.Interface.Enums; |
12 | 13 | using UniGetUI.PackageEngine.Interfaces; |
| 14 | +#if WINDOWS |
| 15 | +using UniGetUI.PackageEngine.Managers.WingetManager; |
| 16 | +#endif |
13 | 17 |
|
14 | 18 | // ReSharper disable once CheckNamespace |
15 | 19 | namespace UniGetUI.PackageEngine.PackageClasses; |
@@ -64,6 +68,11 @@ public bool IsChecked |
64 | 68 | public string TagIconPath { get; private set; } = ""; |
65 | 69 | public bool TagIconVisible { get; private set; } |
66 | 70 |
|
| 71 | + public bool InstallerHostChanged { get; private set; } |
| 72 | + public string InstallerHostChangeTooltip { get; private set; } = ""; |
| 73 | + |
| 74 | + private CancellationTokenSource? _installerHostCheckCts; |
| 75 | + |
67 | 76 | public string SourceIconPath => IconTypeToSvgPath(Package.Source.IconId); |
68 | 77 |
|
69 | 78 | private static string IconTypeToSvgPath(IconType icon) |
@@ -94,6 +103,81 @@ public PackageWrapper(IPackage package, PackagesPageViewModel page) |
94 | 103 |
|
95 | 104 | if (!Settings.Get(Settings.K.DisableIconsOnPackageLists)) |
96 | 105 | _ = LoadIconAsync(); |
| 106 | + |
| 107 | + MaybeStartInstallerHostCheck(); |
| 108 | + } |
| 109 | + |
| 110 | + /// <summary> |
| 111 | + /// For upgradable WinGet packages, asynchronously fetches the installer URL host for |
| 112 | + /// both the installed and the new version, and flags the row when the hosts differ. |
| 113 | + /// See issue #4617 — defense-in-depth signal that an upgrade may be redirecting the |
| 114 | + /// download to a different domain than the user originally trusted. |
| 115 | + /// </summary> |
| 116 | + private void MaybeStartInstallerHostCheck() |
| 117 | + { |
| 118 | +#if WINDOWS |
| 119 | + if (!Package.IsUpgradable) return; |
| 120 | + if (Package.Manager is not WinGet) return; |
| 121 | + if (Settings.Get(Settings.K.DisableInstallerHostChangeWarning)) return; |
| 122 | + |
| 123 | + string installedVersion = Package.VersionString; |
| 124 | + string newVersion = Package.NewVersionString; |
| 125 | + if (string.IsNullOrWhiteSpace(installedVersion) || string.IsNullOrWhiteSpace(newVersion)) |
| 126 | + return; |
| 127 | + if (installedVersion == newVersion) return; |
| 128 | + |
| 129 | + _installerHostCheckCts?.Cancel(); |
| 130 | + _installerHostCheckCts = new CancellationTokenSource(); |
| 131 | + CancellationToken token = _installerHostCheckCts.Token; |
| 132 | + |
| 133 | + _ = Task.Run(async () => |
| 134 | + { |
| 135 | + try |
| 136 | + { |
| 137 | + if (token.IsCancellationRequested) return; |
| 138 | + var oldHosts = WinGet.TryGetInstallerHostsForVersion(Package, installedVersion); |
| 139 | + if (token.IsCancellationRequested) return; |
| 140 | + var newHosts = WinGet.TryGetInstallerHostsForVersion(Package, newVersion); |
| 141 | + if (token.IsCancellationRequested) return; |
| 142 | + |
| 143 | + if (oldHosts is null || newHosts is null) return; |
| 144 | + // Only flag when the two host sets are fully disjoint. If they share even |
| 145 | + // one host, the publisher hasn't moved hosting — adding/removing CDN mirrors |
| 146 | + // or architectures shouldn't trigger the warning. |
| 147 | + if (oldHosts.Overlaps(newHosts)) return; |
| 148 | + |
| 149 | + string tooltip = CoreTools.Translate( |
| 150 | + "Installer host changed since the installed version.\n" |
| 151 | + + "Old: {0}\n" |
| 152 | + + "New: {1}\n\n" |
| 153 | + + "This is usually harmless (the publisher moved hosting), " |
| 154 | + + "but can also indicate a hijacked package manifest. " |
| 155 | + + "Verify the new source before upgrading.", |
| 156 | + string.Join(", ", oldHosts), |
| 157 | + string.Join(", ", newHosts) |
| 158 | + ); |
| 159 | + |
| 160 | + await Dispatcher.UIThread.InvokeAsync(() => |
| 161 | + { |
| 162 | + if (token.IsCancellationRequested) return; |
| 163 | + InstallerHostChanged = true; |
| 164 | + InstallerHostChangeTooltip = tooltip; |
| 165 | + PropertyChanged?.Invoke( |
| 166 | + this, |
| 167 | + new PropertyChangedEventArgs(nameof(InstallerHostChanged)) |
| 168 | + ); |
| 169 | + PropertyChanged?.Invoke( |
| 170 | + this, |
| 171 | + new PropertyChangedEventArgs(nameof(InstallerHostChangeTooltip)) |
| 172 | + ); |
| 173 | + }); |
| 174 | + } |
| 175 | + catch (Exception ex) |
| 176 | + { |
| 177 | + Logger.Warn($"Installer-host check failed for {Package.Id}: {ex.Message}"); |
| 178 | + } |
| 179 | + }, token); |
| 180 | +#endif |
97 | 181 | } |
98 | 182 |
|
99 | 183 | private async Task LoadIconAsync() |
@@ -187,6 +271,9 @@ private void UpdateDisplayState() |
187 | 271 | public void Dispose() |
188 | 272 | { |
189 | 273 | Package.PropertyChanged -= Package_PropertyChanged; |
| 274 | + _installerHostCheckCts?.Cancel(); |
| 275 | + _installerHostCheckCts?.Dispose(); |
| 276 | + _installerHostCheckCts = null; |
190 | 277 | } |
191 | 278 | } |
192 | 279 |
|
|
0 commit comments