/* * 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.Concurrent; using System.Linq; using System.Threading; using System.Threading.Tasks; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Dns.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.DnsLogs; namespace ProtonVPN.Dns.Caching { public class DnsCacheManager : IDnsCacheManager { private readonly ISettings _settings; private readonly ILogger _logger; private readonly SemaphoreSlim _semaphore = new(1, 1); public DnsCacheManager(ISettings settings, ILogger logger) { _settings = settings; _logger = logger; } /// Adds the DnsResponse if the host doesn't exist or replaces the DnsResponse if the host already /// exists. If the cache null, it initializes it and adds the DnsResponse. Returns true if DnsResponse was /// added or replaced, and returns false if an exception was handled and ignored. public async Task AddOrReplaceAsync(string host, DnsResponse dnsResponse) { await _semaphore.WaitAsync(); bool result; try { result = AddOrReplace(host, dnsResponse) == dnsResponse; } catch (Exception e) { result = false; _logger.Error($"DNS cache failed to add or replace host '{host}'.", e); } finally { _semaphore.Release(); } return result; } private DnsResponse AddOrReplace(string host, DnsResponse dnsResponse) { ConcurrentDictionary dnsCache = _settings.DnsCache; DnsResponse cachedValue; if (dnsCache is null) { dnsCache = new ConcurrentDictionary() { [host] = dnsResponse }; cachedValue = dnsResponse; } else { cachedValue = dnsCache.AddOrUpdate(host, dnsResponse, (_, _) => dnsResponse); } // When setting the DnsCache, it needs to be a new entity to trigger the Settings.Set() // code to actually recognize the value change and write it to the settings file _settings.DnsCache = new ConcurrentDictionary(dnsCache); return cachedValue; } /// Updates the value and returns the new value if successful. If it fails, returns null. public async Task UpdateAsync(string host, Func dnsResponseUpdateFactory) { await _semaphore.WaitAsync(); DnsResponse dnsResponse = null; try { dnsResponse = Update(host, dnsResponseUpdateFactory); } catch (Exception e) { _logger.Error($"DNS cache failed to update host '{host}'.", e); } finally { _semaphore.Release(); } return dnsResponse; } private DnsResponse Update(string host, Func dnsResponseUpdateFactory) { ConcurrentDictionary dnsCache = _settings.DnsCache; DnsResponse dnsResponse = null; if (dnsCache is null) { _logger.Warn($"DNS cache failed to update host '{host}' because the cache is null."); } else { if (dnsCache.TryGetValue(host, out DnsResponse oldDnsResponse)) { DnsResponse newDnsResponse = dnsResponseUpdateFactory(oldDnsResponse); if (dnsCache.TryUpdate(host, newDnsResponse, oldDnsResponse)) { _settings.DnsCache = dnsCache; dnsResponse = newDnsResponse; } else { _logger.Error($"DNS cache update operation failed for host '{host}'."); } } else { _logger.Warn($"DNS cache failed to update host '{host}' because this host doesn't exist."); } } return dnsResponse; } } }