/*
* Copyright (c) 2023 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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using ProtonVPN.Common.Legacy.OS.DeviceIds;
using ProtonVPN.Common.Legacy.OS.Net.Http;
using ProtonVPN.Configurations.Contracts;
using ProtonVPN.Crypto;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.AppLogs;
using ProtonVPN.Logging.Contracts.Events.AppUpdateLogs;
using ProtonVPN.Update.Config;
using ProtonVPN.Update.Releases;
using ProtonVPN.Update.Responses;
namespace ProtonVPN.Update.Storage;
///
/// Reads app release data from provided URL and converts it into sequence of app releases.
///
public class WebReleaseStorage : IReleaseStorage
{
private static readonly JsonSerializer _jsonSerializer = new();
private readonly IAppUpdateConfig _config;
private readonly ILogger _logger;
private readonly IDeviceIdCache _deviceIdCache;
private readonly IConfiguration _configuration;
private readonly Lazy _deviceRolloutProportion;
public WebReleaseStorage(IAppUpdateConfig config,
ILogger logger,
IDeviceIdCache deviceIdCache,
IConfiguration configuration)
{
_config = config;
_logger = logger;
_deviceIdCache = deviceIdCache;
_configuration = configuration;
_deviceRolloutProportion = new(CreateDeviceRolloutProportion);
}
private decimal CreateDeviceRolloutProportion()
{
decimal deviceRolloutProportion;
if (_configuration.DeviceRolloutProportion.HasValue)
{
deviceRolloutProportion = _configuration.DeviceRolloutProportion.Value;
_logger.Info($"Using device rollout proportion {deviceRolloutProportion} " +
$"from configuration.");
}
else
{
deviceRolloutProportion = HashGenerator.HashToPercentage(_deviceIdCache.GetDeviceId() + _config.CurrentVersion);
_logger.Info($"Generated device rollout proportion {deviceRolloutProportion} " +
$"for device Id '{_deviceIdCache.GetDeviceId()}' and version '{_config.CurrentVersion}'.");
}
return deviceRolloutProportion;
}
public async Task> GetReleasesAsync()
{
Uri feedUrl = _config.FeedUriProvider.GetFeedUrl();
IEnumerable releases = (await GetAsync(feedUrl)).Releases.Where(ReleaseFilter);
return new Releases.Releases(_logger, releases, _config.CurrentVersion, _config.EarlyAccessCategoryName);
}
private bool ReleaseFilter(ReleaseResponse r)
{
if (Version.TryParse(r.Version, out Version version) && _config.CurrentVersion >= version)
{
return true;
}
return (r.ReleaseDate is null || r.ReleaseDate <= DateTime.UtcNow) &&
(r.SystemVersion?.Minimum is null ||
!Version.TryParse(r.SystemVersion.Minimum, out Version minimumOsVersion) ||
Environment.OSVersion.Version >= minimumOsVersion) &&
IsCoveredByRollout(r.RolloutProportion);
}
private bool IsCoveredByRollout(decimal? rolloutProportion)
{
if (rolloutProportion is null || rolloutProportion.Value >= 1M)
{
return true;
}
if (rolloutProportion.Value <= 0M)
{
return false;
}
try
{ // deviceRolloutProportion <= rolloutProportion
return decimal.Compare(_deviceRolloutProportion.Value, rolloutProportion.Value) <= 0;
}
catch
{
return false;
}
}
private async Task GetAsync(Uri feedUrl)
{
try
{
using IHttpResponseMessage response = await _config.FeedHttpClient.GetAsync(feedUrl);
if (!response.IsSuccessStatusCode)
{
_logger.Error($"Response status code of feed {feedUrl} is not success.");
return null;
}
using Stream stream = await response.Content.ReadAsStreamAsync();
return ResponseStreamResult(stream);
}
catch (Exception ex)
{
_logger.Error($"An error occurred when obtaining the list of releases from feed '{feedUrl}'.", ex);
}
return null;
}
private static T ResponseStreamResult(Stream stream)
{
using StreamReader streamReader = new(stream);
using JsonTextReader jsonTextReader = new(streamReader);
T result = _jsonSerializer.Deserialize(jsonTextReader);
return result == null ? throw new JsonException() : result;
}
}