/* * 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.Collections.Concurrent; using System.Runtime.CompilerServices; using ProtonVPN.Client.EventMessaging.Contracts; using ProtonVPN.Client.Settings.Caching; using ProtonVPN.Client.Settings.Contracts.Messages; using ProtonVPN.Client.Settings.Files; using ProtonVPN.Client.Settings.Repositories.Contracts; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Common.Core.Helpers; using ProtonVPN.Crypto.Contracts.Extensions; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.SettingsLogs; using ProtonVPN.Serialization.Contracts.Json; namespace ProtonVPN.Client.Settings.Repositories; public abstract class SettingsCacheBase : ISettingsCache { protected ILogger Logger { get; } private readonly IJsonSerializer _jsonSerializer; private readonly IEventMessageSender _eventMessageSender; private readonly ISettingsFileReaderWriter _settingsFileReaderWriter; protected readonly ResettableLazy> JsonCache; protected readonly ConcurrentDictionary Cache; public SettingsCacheBase(ILogger logger, IJsonSerializer jsonSerializer, IEventMessageSender eventMessageSender, ISettingsFileReaderWriter settingsFileReaderWriter) { Logger = logger; _jsonSerializer = jsonSerializer; _eventMessageSender = eventMessageSender; _settingsFileReaderWriter = settingsFileReaderWriter; JsonCache = new ResettableLazy>(() => new(_settingsFileReaderWriter.Read())); Cache = new(); } public T? GetValueType(SettingEncryption encryption, [CallerMemberName] string propertyName = "") where T : struct { try { return Get(encryption, propertyName, (pjp) => DeserializeAndCache(pjp, () => null)); } catch (Exception ex) { Logger.Error($"Failed to read the setting '{propertyName}'.", ex); return null; } } private T? Get(SettingEncryption encryption, string propertyName, Func deserializationFunction) { if (Cache.TryGetValue(propertyName, out dynamic? value)) { return value; } PropertyJsonPair propertyJsonPair = new() { PropertyName = propertyName, JsonValue = GetJson(propertyName, encryption) }; return deserializationFunction(propertyJsonPair); } private string? GetJson(string propertyName, SettingEncryption encryption) { return encryption switch { SettingEncryption.Unencrypted => GetUnencrypted(propertyName), SettingEncryption.Encrypted => GetEncrypted(propertyName), _ => throw new NotImplementedException($"The encryption {encryption} is not implemented."), }; } public T? GetReferenceType(SettingEncryption encryption, [CallerMemberName] string propertyName = "") where T : class { try { return Get(encryption, propertyName, (pjp) => DeserializeAndCache(pjp, () => null)); } catch (Exception ex) { Logger.Error($"Failed to read the setting '{propertyName}'.", ex); return null; } } public List? GetListValueType(SettingEncryption encryption, [CallerMemberName] string propertyName = "") where T : struct { try { return Get?>(encryption, propertyName, (pjp) => DeserializeAndCache>(pjp, () => null)); } catch (Exception ex) { Logger.Error($"Failed to read the setting '{propertyName}'.", ex); return new(); } } private T? DeserializeAndCache(PropertyJsonPair propertyJsonPair, Func nullJsonResult) { T? value = Deserialize(propertyJsonPair, nullJsonResult); Cache.AddOrUpdate(propertyJsonPair.PropertyName, value, (_, _) => value); return value; } private T? Deserialize(PropertyJsonPair propertyJsonPair, Func nullJsonResult) { if (propertyJsonPair.JsonValue is null) { return nullJsonResult(); } Type type = typeof(T); if (type == typeof(string)) { return (dynamic)propertyJsonPair.JsonValue; } if (IsEnum(type, propertyJsonPair, out dynamic? enumResult)) { return enumResult; } if (IsNullableEnum(type, propertyJsonPair, out dynamic? underlyingEnumResult)) { return underlyingEnumResult; } return _jsonSerializer.DeserializeFromString(propertyJsonPair.JsonValue); } private bool IsEnum(Type type, PropertyJsonPair propertyJsonPair, out dynamic? enumResult) { enumResult = null; return type.IsEnum && Enum.TryParse(type, propertyJsonPair.JsonValue, out enumResult); } private bool IsNullableEnum(Type type, PropertyJsonPair propertyJsonPair, out dynamic? underlyingEnumResult) { underlyingEnumResult = null; Type? underlyingType = Nullable.GetUnderlyingType(type); return underlyingType != null && IsEnum(underlyingType, propertyJsonPair, out underlyingEnumResult); } private string? GetUnencrypted(string propertyName) { return Get(propertyName); } private string? Get(string propertyName) { return JsonCache.Value.TryGetValue(propertyName, out string? value) ? value : null; } private string? GetEncrypted(string propertyName) { return GetUnencrypted(propertyName)?.Decrypt(); } public void SetValueType(T? newValue, SettingEncryption encryption, [CallerMemberName] string propertyName = "") where T : struct { try { T? oldValue = default; Type? toType = UnwrapNullable(typeof(T)); if (IsValueTypeOrString(toType)) { oldValue = GetValueType(encryption, propertyName); if (Equals(oldValue, newValue)) { return; } } Set(propertyName, oldValue, newValue, encryption); } catch (Exception ex) { Logger.Error($"Failed to write the setting '{propertyName}'.", ex); } } private bool IsValueTypeOrString(Type? toType) { return toType is not null && (toType.IsValueType || toType == typeof(string)); } public void SetReferenceType(T? newValue, SettingEncryption encryption, [CallerMemberName] string propertyName = "") where T : class { try { T? oldValue = default; Type? toType = UnwrapNullable(typeof(T)); oldValue = GetReferenceType(encryption, propertyName); if (EqualityComparer.Default.Equals(oldValue, newValue)) { return; } Set(propertyName, oldValue, newValue, encryption); } catch (Exception ex) { Logger.Error($"Failed to write the setting '{propertyName}'.", ex); } } public void SetListValueType(List? newValue, SettingEncryption encryption, [CallerMemberName] string propertyName = "") where T : struct { try { List? oldValue = GetListValueType(encryption, propertyName); if ((oldValue is null && newValue is null) || (oldValue is not null && newValue is not null && oldValue.SequenceEqual(newValue))) { return; } Set(propertyName, oldValue, newValue, encryption); } catch (Exception ex) { Logger.Error($"Failed to write the setting '{propertyName}'.", ex); } } private void Set(string propertyName, T? oldValue, T? newValue, SettingEncryption encryption) { string? json = newValue is null ? null : Serialize(newValue); if (encryption is SettingEncryption.Encrypted) { json = json?.Encrypt(); } Set(propertyName, newValue, json); OnPropertyChanged(oldValue, newValue, propertyName); LogChange(propertyName, oldValue, newValue, encryption); } private void Set(string propertyName, T? newValue, string? json) { JsonCache.Value.AddOrUpdate(propertyName, json, (_, _) => json); _settingsFileReaderWriter.Write(JsonCache.Value); Cache.AddOrUpdate(propertyName, newValue, (_, _) => newValue); } private Type? UnwrapNullable(Type type) { return IsNullableType(type) ? Nullable.GetUnderlyingType(type) : type; } private bool IsNullableType(Type type) { return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } private string? Serialize(T newValue) { if (newValue is string stringValue) { return stringValue; } if (newValue is Enum enumValue) { return enumValue.ToString(); } return _jsonSerializer.SerializeToString(newValue); } private void OnPropertyChanged(T? oldValue, T? newValue, string propertyName) { _eventMessageSender.Send(new SettingChangedMessage(propertyName, typeof(T), oldValue, newValue)); } private void LogChange(string propertyName, T oldValue, T newValue, SettingEncryption encryption) { #if DEBUG LogChangeUnencrypted(propertyName, oldValue, newValue); #else if (encryption == SettingEncryption.Encrypted) { Logger.Info($"Setting '{propertyName}' changed (encrypted)."); } else { LogChangeUnencrypted(propertyName, oldValue, newValue); } #endif } private void LogChangeUnencrypted(string propertyName, T? oldValue, T? newValue) { string oldValueJson = _jsonSerializer.SerializeToString(oldValue).GetLastChars(64); string newValueJson = _jsonSerializer.SerializeToString(newValue).GetLastChars(64); Logger.Info($"Setting '{propertyName}' " + $"changed from '{oldValueJson}' to '{newValueJson}'."); } public List? GetListReferenceType(SettingEncryption encryption, [CallerMemberName] string propertyName = "") where T : class { try { return Get?>(encryption, propertyName, (pjp) => DeserializeAndCache>(pjp, () => [])); } catch (Exception ex) { Logger.Error($"Failed to read the setting '{propertyName}'.", ex); return new(); } } public void SetListReferenceType(List? newValue, SettingEncryption encryption, [CallerMemberName] string propertyName = "") where T : class { try { List? oldValue = GetListReferenceType(encryption, propertyName); if ((oldValue is null && newValue is null) || (oldValue is not null && newValue is not null && oldValue.SequenceEqual(newValue))) { return; } Set(propertyName, oldValue, newValue, encryption); } catch (Exception ex) { Logger.Error($"Failed to write the setting '{propertyName}'.", ex); } } }