JetBrains.Etw.HostService.Updater/src/Views/DownloadingWindow.xaml.cs (163 lines of code) (raw):

using System; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using JetBrains.Annotations; using JetBrains.DownloadPgpVerifier; using JetBrains.Etw.HostService.Updater.Progress; using JetBrains.Etw.HostService.Updater.Util; using JetBrains.Etw.HostService.Updater.ViewModel; namespace JetBrains.Etw.HostService.Updater.Views { internal partial class DownloadingWindow : Window { private const string SpecialContext = nameof(PgpSignaturesVerifier) + Logger.Delimiter + nameof(PgpSignaturesVerifier.Verify); private readonly CancellationTokenSource myCancellationTokenSource = new(); private readonly Util.ILogger myLogger; private string myMsiFile; public DownloadingWindow([NotNull] Util.ILogger logger, [NotNull] UpdateRequest updateRequest, bool downloadDelay = false) { if (updateRequest == null) throw new ArgumentNullException(nameof(updateRequest)); myLogger = logger ?? throw new ArgumentNullException(nameof(logger)); InitializeComponent(); var viewModel = new DownloadingViewModel(); DataContext = viewModel; var deactivateSystemCloseButton = true; Closing += (_, args) => { args.Cancel = deactivateSystemCloseButton; myCancellationTokenSource.Cancel(); }; var loggerContext = Logger.Context; async void DoAsync() { try { myMsiFile = await Task.Run(() => DownloadAndVerifyMsi(logger, updateRequest, viewModel, myCancellationTokenSource.Token, downloadDelay), myCancellationTokenSource.Token); logger.Info($"{loggerContext} res=downloaded"); } catch (OperationCanceledException) { logger.Info($"{loggerContext} res=cancelled"); } catch (Exception ex) { logger.Info($"{loggerContext} res=error"); logger.Exception(ex); MessageBox.Show(ex.GetBaseException().Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); } finally { deactivateSystemCloseButton = false; DialogResult = myMsiFile != null; } } DoAsync(); } [NotNull] public string MsiFile => DialogResult == true ? myMsiFile : throw new InvalidOperationException(); [NotNull] private static string DownloadAndVerifyMsi(Util.ILogger logger, [NotNull] UpdateRequest updateRequest, [NotNull] IProgress mainProgress, CancellationToken ct, bool downloadDelay = false) { logger.Info($"{Logger.Context} size={updateRequest.Size}\n\tlink={updateRequest.Link}\n\tchecksumLink={updateRequest.ChecksumLink}\n\tsignedChecksumLink={updateRequest.SignedChecksumLink}"); mainProgress.Start(100); var digestSha256 = updateRequest.ChecksumLink.OpenSeekableStreamFromWeb(dataStream => { var verificationResult = PgpSignaturesVerifier.MasterPublicKey.OpenStreamFromString(masterPublicKeyStream => PgpSignaturesVerifier.PublicKeysUri.OpenSeekableStreamFromWeb(publicKeysStream => updateRequest.SignedChecksumLink.OpenSeekableStreamFromWeb(signaturesStream => { mainProgress.Advance(5); return PgpSignaturesVerifier.Verify(masterPublicKeyStream, publicKeysStream, signaturesStream, dataStream, new SubLogger(logger, SpecialContext)); }))); mainProgress.Advance(4); if (!verificationResult) throw new InvalidOperationException("The PGP signature for SHA256 checksum file is incorrect"); dataStream.Position = 0; using var reader = new BinaryReader(dataStream, Encoding.UTF8, false); return reader.ReadChars(2 * 256 / 8).FromHexString(); }); logger.Info($"{Logger.Context} digestSha256={digestSha256.ToLoverHexString()}"); mainProgress.Advance(1); ct.ThrowIfCancellationRequested(); var fileProgress = new SubProgress(mainProgress, 90); var downloadFile = Path.GetTempFileName(); logger.Info($"{Logger.Context} downloadFile={downloadFile}"); try { using var fileStream = File.Create(downloadFile); var fileSha256 = updateRequest.Link.OpenStreamFromWeb(stream => { fileProgress.Start(Math.Max(1, updateRequest.Size)); var buffer = new byte[4 * 1024]; using var hasher = SHA256.Create(); while (true) { var received = stream.Read(buffer, 0, buffer.Length); ct.ThrowIfCancellationRequested(); if (received == 0) { hasher.TransformFinalBlock(Array.Empty<byte>(), 0, 0); ct.ThrowIfCancellationRequested(); return hasher.Hash; } hasher.TransformBlock(buffer, 0, received, null, 0); ct.ThrowIfCancellationRequested(); fileStream.Write(buffer, 0, received); ct.ThrowIfCancellationRequested(); fileProgress.Advance(received); if (downloadDelay) Thread.Sleep(1); } }).NotNull(); fileProgress.Stop(); logger.Info($"{Logger.Context} fileSha256={fileSha256.ToLoverHexString()}"); if (!digestSha256.SequenceEqual(fileSha256)) throw new InvalidOperationException("The file has invalid SHA256 checksum"); mainProgress.Stop(); return downloadFile; } catch { if (File.Exists(downloadFile)) File.Delete(downloadFile); throw; } } private void OnCancel(object sender, RoutedEventArgs e) { myLogger.Info(Logger.Context); myCancellationTokenSource.Cancel(); } private sealed class SubLogger : DownloadPgpVerifier.ILogger { private readonly string myContext; private readonly Util.ILogger myLogger; public SubLogger([NotNull] Util.ILogger logger, [NotNull] string context) { myLogger = logger ?? throw new ArgumentNullException(nameof(logger)); myContext = context ?? throw new ArgumentNullException(nameof(context)); } public void Info(string str) { myLogger.Info($"{myContext} {str}"); } public void Warning(string str) { myLogger.Warning($"{myContext} {str}"); } public void Error(string str) { myLogger.Error($"{myContext} {str}"); } } } }