diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Storage/TelemetryActivityStorage.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Storage/TelemetryActivityStorage.cs index 1123b98db4..6cb1abd464 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Storage/TelemetryActivityStorage.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Storage/TelemetryActivityStorage.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading; @@ -18,10 +19,12 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep private const int MaxFileRetries = 5; private const int RetryDelayMs = 100; private const int MutexTimeoutMs = 5000; + private const string MutexName = "Global\\TelemetryActivityStorage"; + private readonly static byte[] EncryptionKey = "AbpTelemetryStorageKey"u8.ToArray(); private readonly TelemetryActivityStorageOptions _options; private TelemetryActivityStorageState? _cachedState; - private static readonly string MutexName = "Global\\TelemetryActivityStorage"; + public TelemetryActivityStorage(IOptions options) { @@ -150,7 +153,14 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep _cachedState = await WithExclusiveFileLockAsync(async stream => { using var reader = new StreamReader(stream, Encoding.UTF8); - var json = await reader.ReadToEndAsync(); + var encryptedJson = await reader.ReadToEndAsync(); + + if (string.IsNullOrEmpty(encryptedJson)) + { + return new TelemetryActivityStorageState(); + } + + var json = DecryptData(encryptedJson); return JsonSerializer.Deserialize(json, GetJsonSerializerOptions()) ?? new TelemetryActivityStorageState(); }) ?? new TelemetryActivityStorageState(); @@ -158,73 +168,74 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep } private async Task WithExclusiveFileLockAsync(Func> action) + { + return await RetryWithMutexAsync(async () => + { + using var stream = new FileStream( + TelemetryPaths.ActivityStorage, + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.Read + ); + return await action(stream); + }); + } + + private async Task RetryWithMutexAsync(Func> operation) { using var mutex = new Mutex(false, MutexName); - for (int i = 0; i < MaxFileRetries; i++) + for (int attempt = 1; attempt <= MaxFileRetries; attempt++) { try { - // Wait for mutex with timeout - if (!mutex.WaitOne(MutexTimeoutMs)) + if (!await WaitForMutexAsync(mutex)) { - if (i == MaxFileRetries - 1) + if (attempt == MaxFileRetries) { return default; } + await Task.Delay(RetryDelayMs); continue; } try { - using var stream = new FileStream( - TelemetryPaths.ActivityStorage, - FileMode.OpenOrCreate, - FileAccess.ReadWrite, - FileShare.Read // Allow read sharing since we have mutex protection - ); - - return await action(stream); + return await operation(); } finally { - mutex.ReleaseMutex(); + ReleaseMutexSafely(mutex); } } catch (AbandonedMutexException) { - // Previous process crashed, mutex is now owned by us try { - using var stream = new FileStream( - TelemetryPaths.ActivityStorage, - FileMode.OpenOrCreate, - FileAccess.ReadWrite, - FileShare.Read - ); - - return await action(stream); + return await operation(); } catch { - if (i == MaxFileRetries - 1) + if (attempt == MaxFileRetries) { return default; } + await Task.Delay(RetryDelayMs); } finally { - try { mutex.ReleaseMutex(); } catch { } + ReleaseMutexSafely(mutex); } } catch (IOException) { - if (i == MaxFileRetries - 1) + if (attempt == MaxFileRetries) { return default; } + await Task.Delay(RetryDelayMs); } catch @@ -236,10 +247,28 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep return default; } + private async Task WaitForMutexAsync(Mutex mutex) + { + return await Task.Run(() => mutex.WaitOne(MutexTimeoutMs)); + } + + private static void ReleaseMutexSafely(Mutex mutex) + { + try + { + mutex.ReleaseMutex(); + } + catch + { + // Ignore release errors + } + } + private Task SaveAsync() { var json = JsonSerializer.Serialize(_cachedState ?? new TelemetryActivityStorageState(), GetJsonSerializerOptions()); - File.WriteAllText(TelemetryPaths.ActivityStorage, json, Encoding.UTF8); + var encryptedJson = EncryptData(json); + File.WriteAllText(TelemetryPaths.ActivityStorage, encryptedJson, Encoding.UTF8); return Task.CompletedTask; } @@ -257,7 +286,8 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep if (!File.Exists(TelemetryPaths.ActivityStorage)) { var json = JsonSerializer.Serialize(_cachedState ?? new TelemetryActivityStorageState(), GetJsonSerializerOptions()); - File.WriteAllText(TelemetryPaths.ActivityStorage, json, Encoding.UTF8); + var encryptedJson = EncryptData(json); + File.WriteAllText(TelemetryPaths.ActivityStorage, encryptedJson, Encoding.UTF8); } } catch @@ -281,4 +311,65 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep { return lastSend is null || DateTimeOffset.UtcNow - lastSend > _options.InfoExpirationPeriod; } + + private static string EncryptData(string plainText) + { + try + { + using var aes = Aes.Create(); + aes.Key = EncryptionKey; + aes.GenerateIV(); + + var encryptor = aes.CreateEncryptor(aes.Key, aes.IV); + + using var msEncrypt = new MemoryStream(); + using var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write); + using var swEncrypt = new StreamWriter(csEncrypt); + + swEncrypt.Write(plainText); + csEncrypt.FlushFinalBlock(); + + var encrypted = msEncrypt.ToArray(); + var result = new byte[aes.IV.Length + encrypted.Length]; + Array.Copy(aes.IV, 0, result, 0, aes.IV.Length); + Array.Copy(encrypted, 0, result, aes.IV.Length, encrypted.Length); + + return Convert.ToBase64String(result); + } + catch + { + return plainText; + } + } + + private static string DecryptData(string cipherText) + { + try + { + var fullCipher = Convert.FromBase64String(cipherText); + + using var aes = Aes.Create(); + aes.Key = EncryptionKey; + + var iv = new byte[aes.IV.Length]; + var cipher = new byte[fullCipher.Length - iv.Length]; + + Array.Copy(fullCipher, 0, iv, 0, iv.Length); + Array.Copy(fullCipher, iv.Length, cipher, 0, cipher.Length); + + aes.IV = iv; + + var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); + + using var msDecrypt = new MemoryStream(cipher); + using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read); + using var srDecrypt = new StreamReader(csDecrypt); + + return srDecrypt.ReadToEnd(); + } + catch + { + return cipherText; + } + } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/TelemetryPaths.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/TelemetryPaths.cs index e47c83b3a9..b7b475def8 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/TelemetryPaths.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/TelemetryPaths.cs @@ -7,7 +7,7 @@ public static class TelemetryPaths { public static string AccessToken => Path.Combine(AbpRootPath, "cli", "access-token.bin"); public static string ComputerId => Path.Combine(AbpRootPath, "cli", "computer-id.bin"); - public static string ActivityStorage => Path.Combine(AbpRootPath , "telemetry", "activity-storage.json"); + public static string ActivityStorage => Path.Combine(AbpRootPath , "telemetry", "activity-storage.bin"); public static string Studio => Path.Combine(AbpRootPath, "studio"); private readonly static string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp"); } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/TelemetryDataSender.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/TelemetryDataSender.cs index a36854fcf3..b872639e8b 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/TelemetryDataSender.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/TelemetryDataSender.cs @@ -15,6 +15,7 @@ namespace Volo.Abp.Telemetry; public class TelemetryDataSender : ITelemetryDataSender, ISingletonDependency { private readonly ITelemetryActivityStorage _telemetryActivityStorage; + private const int ActivityBatchSize = 50; public TelemetryDataSender(ITelemetryActivityStorage telemetryActivityStorage) @@ -26,16 +27,11 @@ public class TelemetryDataSender : ITelemetryDataSender, ISingletonDependency { try { - using var httpClient = new HttpClient(); - AddAbpAuthenticationTokenAsync(httpClient); - var activities = await _telemetryActivityStorage.GetBufferedActivitiesAsync(); - if (activities.Count == 0) - { - return; - } - + using var httpClient = new HttpClient(); + AddJwtTokenIfAuthenticated(httpClient); + for (var i = 0; i < activities.Count; i += ActivityBatchSize) { var activityBatch = activities.Skip(i).Take(ActivityBatchSize).ToList(); @@ -53,7 +49,7 @@ public class TelemetryDataSender : ITelemetryDataSender, ISingletonDependency } - private static void AddAbpAuthenticationTokenAsync(HttpClient httpClient) + private static void AddJwtTokenIfAuthenticated(HttpClient httpClient) { if (!File.Exists(TelemetryPaths.AccessToken)) {