/* * Copyright (c) 2024 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System.Diagnostics; using System.Security.Cryptography; namespace ProtonInstaller; public abstract class ProtonProductBase : IProtonProduct { private const int MAX_RETRIES = 3; private readonly HttpClient _downloadHttpClient; protected readonly HttpClient VersionHttpClient; private readonly TimeSpan _maxInstallDuration = TimeSpan.FromMinutes(5); public string FeedUrl { get; } protected ProtonProductBase(HttpClient downloadHttpClient, HttpClient versionHttpClient, string feedUrl) { _downloadHttpClient = versionHttpClient; VersionHttpClient = versionHttpClient; FeedUrl = feedUrl; } public async Task InstallAsync(bool isToCreateDesktopShortcut) { try { ILatestRelease? release = await GetLatestReleaseAsync(); if (release != null) { string pathToSave = GetPathToSave(release); await DownloadWithRetryAsync(release.BinaryUrl, pathToSave); if (File.Exists(pathToSave)) { string hash = GetSha512(pathToSave); if (hash == release.Sha512Checksum) { StartProcess(pathToSave, GetCommandLineArguments(release, isToCreateDesktopShortcut)); } else { File.Delete(pathToSave); } } } } catch (Exception) { } } protected abstract Task GetLatestReleaseAsync(); protected abstract string GetSourceArgument(); protected Version GetVersion(string releaseVersion) { return Version.TryParse(releaseVersion, out Version? version) ? version : new(); } private string GetPathToSave(ILatestRelease release) { Uri fileUri = new(release.BinaryUrl); return Path.Combine(Path.GetTempPath(), Path.GetFileName(fileUri.AbsolutePath)); } private async Task DownloadWithRetryAsync(string url, string filename) { TimeSpan delay = TimeSpan.FromSeconds(2); for (int retryCount = 0; retryCount <= MAX_RETRIES; retryCount++) { try { await DownloadAsync(url, filename); if (File.Exists(filename)) { return; } } catch (Exception) when (retryCount < MAX_RETRIES) { await Task.Delay(delay); delay += delay; } } } private async Task DownloadAsync(string url, string filename) { HttpResponseMessage response = await _downloadHttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); await using Stream contentStream = await response.Content.ReadAsStreamAsync(); await using FileStream fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); byte[] buffer = new byte[8192]; while (true) { int bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length); if (bytesRead <= 0) { break; } await fileStream.WriteAsync(buffer, 0, bytesRead); } } private string GetSha512(string filePath) { using FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); using SHA512 sha512 = SHA512.Create(); byte[] hashBytes = sha512.ComputeHash(stream); return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); } private void StartProcess(string path, string arguments) { ProcessStartInfo startInfo = new ProcessStartInfo { FileName = path, Arguments = arguments, UseShellExecute = false, }; Process process = new Process { StartInfo = startInfo }; process.Start(); if (!process.WaitForExit(_maxInstallDuration)) { process.Kill(); } } private string GetCommandLineArguments(ILatestRelease release, bool isToCreateDesktopShortcut) { List arguments = [GetSourceArgument(), release.SilentArgument, release.NoAutoLaunchArgument]; if (!isToCreateDesktopShortcut) { arguments.Add(release.NoDesktopShortcutArgument); } return string.Join(' ', arguments); } }