diff --git a/Azaion.Annotator.sln b/Azaion.Annotator.sln index e62740f..92e8a40 100644 --- a/Azaion.Annotator.sln +++ b/Azaion.Annotator.sln @@ -2,7 +2,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator", "Azaion.Annotator\Azaion.Annotator.csproj", "{8E0809AF-2920-4267-B14D-84BAB334A46F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator.Test", "Azaion.Annotator.Test\Azaion.Annotator.Test.csproj", "{85359558-FB59-4542-A597-FD9E1B04C8E7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Test", "Azaion.Test\Azaion.Test.csproj", "{85359558-FB59-4542-A597-FD9E1B04C8E7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Suite", "Azaion.Suite\Azaion.Suite.csproj", "{BA77500E-8B66-4F31-81B0-E831FC12EDFB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Common", "Azaion.Common\Azaion.Common.csproj", "{1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Dataset", "Azaion.Dataset\Azaion.Dataset.csproj", "{01A5CA37-A62E-4EF3-8678-D72CD9525677}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -18,5 +24,17 @@ Global {85359558-FB59-4542-A597-FD9E1B04C8E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {85359558-FB59-4542-A597-FD9E1B04C8E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {85359558-FB59-4542-A597-FD9E1B04C8E7}.Release|Any CPU.Build.0 = Release|Any CPU + {BA77500E-8B66-4F31-81B0-E831FC12EDFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA77500E-8B66-4F31-81B0-E831FC12EDFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA77500E-8B66-4F31-81B0-E831FC12EDFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA77500E-8B66-4F31-81B0-E831FC12EDFB}.Release|Any CPU.Build.0 = Release|Any CPU + {1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}.Release|Any CPU.Build.0 = Release|Any CPU + {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Azaion.Annotator/MainWindow.xaml b/Azaion.Annotator/Annotator.xaml similarity index 97% rename from Azaion.Annotator/MainWindow.xaml rename to Azaion.Annotator/Annotator.xaml index a056f6c..b2f7911 100644 --- a/Azaion.Annotator/MainWindow.xaml +++ b/Azaion.Annotator/Annotator.xaml @@ -1,9 +1,11 @@ - @@ -85,12 +87,6 @@ - - - - + - @@ -261,9 +257,9 @@ - - - + + + @@ -275,12 +271,12 @@ - - + - - + + + + + + + + + + + + + + + + + + + diff --git a/Azaion.Suite/Loader.xaml.cs b/Azaion.Suite/Loader.xaml.cs new file mode 100644 index 0000000..ecb2890 --- /dev/null +++ b/Azaion.Suite/Loader.xaml.cs @@ -0,0 +1,58 @@ +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Azaion.Suite.Services.DTO; +using Microsoft.Extensions.Options; + +namespace Azaion.Suite; + +public partial class Loader : Window +{ + private readonly IResourceLoader _resourceLoader; + private readonly IOptions _localFilesConfig; + + public Loader(IResourceLoader resourceLoader, IOptions localFilesConfig) + { + _resourceLoader = resourceLoader; + _localFilesConfig = localFilesConfig; + InitializeComponent(); + } + + private async void RunClick(object sender, RoutedEventArgs e) + { + var stream = new MemoryStream(); + await _resourceLoader.LoadAnnotator(TbEmail.Text, TbPassword.Password, stream); + stream.Seek(0, SeekOrigin.Begin); + var loader = new AssemblyLoadContext("DynamicContext", isCollectible: true); + var annotatorAssembly = loader.LoadFromStream(stream); + + var appType = annotatorAssembly.GetType("Azaion.Annotator.App"); + var appInstance = Activator.CreateInstance(appType); + var runMethod = appType.GetMethod("Run", BindingFlags.Public | BindingFlags.Instance); + if (runMethod != null) + { + runMethod.Invoke(appInstance, null); + } + + // var entryPoint = annotatorAssembly.EntryPoint; + // if (entryPoint == null) + // return; + // + // var o = annotatorAssembly.CreateInstance(entryPoint.Name); + // entryPoint.Invoke(o, null); + } + + private void CloseClick(object sender, RoutedEventArgs e) => Close(); + + private void MainMouseMove(object sender, MouseEventArgs e) + { + if (e.OriginalSource is Button || e.OriginalSource is TextBox) + return; + + if (e.LeftButton == MouseButtonState.Pressed) + DragMove(); + } +} diff --git a/Azaion.Suite/MainSuite.xaml b/Azaion.Suite/MainSuite.xaml new file mode 100644 index 0000000..10485a8 --- /dev/null +++ b/Azaion.Suite/MainSuite.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/Azaion.Suite/MainSuite.xaml.cs b/Azaion.Suite/MainSuite.xaml.cs new file mode 100644 index 0000000..8f6158d --- /dev/null +++ b/Azaion.Suite/MainSuite.xaml.cs @@ -0,0 +1,59 @@ +using System.IO; +using System.Windows; +using Azaion.Annotator.Extensions; +using Azaion.Common.DTO.Config; +using Microsoft.Extensions.Options; + +namespace Azaion.Suite; + +public partial class MainSuite : Window +{ + private readonly AppConfig _appConfig; + private readonly IConfigUpdater _configUpdater; + + public MainSuite(IOptions appConfig, IConfigUpdater configUpdater) + { + _configUpdater = configUpdater; + _appConfig = appConfig.Value; + InitializeComponent(); + Loaded += OnLoaded; + Closed += OnFormClosed; + + SizeChanged += async (_, _) => await SaveUserSettings(); + LocationChanged += async (_, _) => await SaveUserSettings(); + StateChanged += async (_, _) => await SaveUserSettings(); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + if (!Directory.Exists(_appConfig.DirectoriesConfig.LabelsDirectory)) + Directory.CreateDirectory(_appConfig.DirectoriesConfig.LabelsDirectory); + if (!Directory.Exists(_appConfig.DirectoriesConfig.ImagesDirectory)) + Directory.CreateDirectory(_appConfig.DirectoriesConfig.ImagesDirectory); + if (!Directory.Exists(_appConfig.DirectoriesConfig.ResultsDirectory)) + Directory.CreateDirectory(_appConfig.DirectoriesConfig.ResultsDirectory); + + + Left = _appConfig.WindowConfig.WindowLocation.X; + Top = _appConfig.WindowConfig.WindowLocation.Y; + Width = _appConfig.WindowConfig.WindowSize.Width; + Height = _appConfig.WindowConfig.WindowSize.Height; + + if (_appConfig.WindowConfig.FullScreen) + WindowState = WindowState.Maximized; + } + + private async Task SaveUserSettings() + { + await ThrottleExt.Throttle(() => + { + _configUpdater.Save(_appConfig); + return Task.CompletedTask; + }, TimeSpan.FromSeconds(5)); + } + + private void OnFormClosed(object? sender, EventArgs e) + { + _configUpdater.Save(_appConfig); + } +} \ No newline at end of file diff --git a/Azaion.Suite/ResourceLoader.cs b/Azaion.Suite/ResourceLoader.cs new file mode 100644 index 0000000..44b2337 --- /dev/null +++ b/Azaion.Suite/ResourceLoader.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Reflection; +using Azaion.Suite.Services; +using Azaion.Suite.Services.DTO; +using Microsoft.Extensions.Options; + +namespace Azaion.Suite; + +public interface IResourceLoader +{ + Task LoadAnnotator(string email, string password, Stream outStream, CancellationToken cancellationToken = default); + Assembly LoadAssembly(string name, CancellationToken cancellationToken = default); +} + +public class ResourceLoader(AzaionApiClient azaionApi, IHardwareService hardwareService, IOptions localFilesConfig) : IResourceLoader +{ + public async Task LoadAnnotator(string email, string password, Stream outStream, CancellationToken cancellationToken = default) + { + var hardwareInfo = await hardwareService.GetHardware(); + azaionApi.Login(email, password); + var key = Security.MakeEncryptionKey(email, password, hardwareInfo.Hash); + + var encryptedStream = await azaionApi.GetResource(password, hardwareInfo, ResourceEnum.AnnotatorDll); + + await encryptedStream.DecryptTo(outStream, key, cancellationToken); + //return Assembly.Load(stream.ToArray()); + } + + public Assembly LoadAssembly(string name, CancellationToken cancellationToken = default) + { + var dllValues = name.Split(","); + var dllName = $"{dllValues[0]}.dll"; + var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + + var asm = Assembly.LoadFile(Path.Combine(currentLocation, localFilesConfig.Value.DllPath, dllName)); + return asm; + } +} \ No newline at end of file diff --git a/Azaion.Suite/Services/AzaionApiClient.cs b/Azaion.Suite/Services/AzaionApiClient.cs new file mode 100644 index 0000000..ccc3f6a --- /dev/null +++ b/Azaion.Suite/Services/AzaionApiClient.cs @@ -0,0 +1,85 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security; +using System.Text; +using Azaion.Suite.Services.DTO; +using Newtonsoft.Json; + +namespace Azaion.Suite.Services; + +public class AzaionApiClient(HttpClient httpClient) +{ + const string JSON_MEDIA = "application/json"; + + private string Email { get; set; } = null!; + private SecureString Password { get; set; } = new(); + private string JwtToken { get; set; } = null!; + + public void Login(string email, string password) + { + if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password)) + throw new Exception("Email or password is empty!"); + + Email = email; + Password = password.ToSecureString(); + } + + public async Task GetResource(string password, HardwareInfo hardware, ResourceEnum resourceEnum) + { + var response = await Send(httpClient, new HttpRequestMessage(HttpMethod.Post, "/resources/get") + { + Content = new StringContent(JsonConvert.SerializeObject(new { password, hardware, resourceEnum }), Encoding.UTF8, JSON_MEDIA) + }); + return await response.Content.ReadAsStreamAsync(); + } + + private async Task Authorize() + { + if (string.IsNullOrEmpty(Email) || Password.Length == 0) + throw new Exception("Email or password is empty! Please do Login first!"); + + var payload = new + { + email = Email, + password = Password.ToRealString() + }; + var response = await httpClient.PostAsync( + "login", + new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, JSON_MEDIA)); + + if (!response.IsSuccessStatusCode) + throw new Exception($"Login failed: {response.StatusCode}"); + + var responseData = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject(responseData); + + if (string.IsNullOrEmpty(result?.Token)) + throw new Exception("JWT Token not found in response"); + + return result.Token; + } + + private async Task Send(HttpClient client, HttpRequestMessage request) + { + if (string.IsNullOrEmpty(JwtToken)) + JwtToken = await Authorize(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken); + var response = await client.SendAsync(request); + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + JwtToken = await Authorize(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken); + response = await client.SendAsync(request); + } + + if (response.IsSuccessStatusCode) + return response; + + var result = await response.Content.ReadAsStringAsync(); + throw new Exception($"Failed: {response.StatusCode}! Result: {result}"); + } +} diff --git a/Azaion.Suite/Services/DTO/ResourceEnum.cs b/Azaion.Suite/Services/DTO/ResourceEnum.cs new file mode 100644 index 0000000..3ba3058 --- /dev/null +++ b/Azaion.Suite/Services/DTO/ResourceEnum.cs @@ -0,0 +1,9 @@ +namespace Azaion.Suite.Services.DTO; + +public enum ResourceEnum +{ + None = 0, + AnnotatorDll = 10, + AIModelRKNN = 20, + AIModelONNX = 30, +} diff --git a/Azaion.Suite/Services/HardwareService.cs b/Azaion.Suite/Services/HardwareService.cs new file mode 100644 index 0000000..8adfbb1 --- /dev/null +++ b/Azaion.Suite/Services/HardwareService.cs @@ -0,0 +1,107 @@ +using System.Diagnostics; +using System.Net.NetworkInformation; +using System.Security.Cryptography; +using System.Text; + +namespace Azaion.Suite.Services; + +public interface IHardwareService +{ + Task GetHardware(); +} + +public class HardwareService : IHardwareService +{ + private const string WIN32_GET_HARDWARE_COMMAND = + "wmic OS get TotalVisibleMemorySize /Value && " + + "wmic CPU get Name /Value && " + + "wmic path Win32_VideoController get Name /Value"; + + private const string UNIX_GET_HARDWARE_COMMAND = + "/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " + + "lscpu | grep 'Model name:' | cut -d':' -f2 && " + + "lspci | grep VGA | cut -d':' -f3\""; + + public async Task GetHardware() + { + try + { + var output = await RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT + ? WIN32_GET_HARDWARE_COMMAND + : UNIX_GET_HARDWARE_COMMAND); + + var lines = output + .Replace("TotalVisibleMemorySize=", "") + .Replace("Name=", "") + .Replace(" ", " ") + .Trim() + .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); + + var memoryStr = "Unknown RAM"; + if (lines.Length > 0) + { + memoryStr = lines[0]; + if (int.TryParse(memoryStr, out var memKb)) + memoryStr = $"{Math.Round(memKb / 1024.0 / 1024.0)} Gb"; + } + + var hardwareInfo = new HardwareInfo + { + Memory = memoryStr, + CPU = lines.Length > 1 && string.IsNullOrEmpty(lines[1]) + ? "Unknown RAM" + : lines[1], + GPU = lines.Length > 2 && string.IsNullOrEmpty(lines[2]) + ? "Unknown GPU" + : lines[2] + }; + hardwareInfo.Hash = ToHash($"Azaion_{MacAddress()}_{hardwareInfo.CPU}_{hardwareInfo.GPU}"); + return hardwareInfo; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw; + } + } + + private string MacAddress() + { + var macAddress = NetworkInterface + .GetAllNetworkInterfaces() + .Where(nic => nic.OperationalStatus == OperationalStatus.Up) + .Select(nic => nic.GetPhysicalAddress().ToString()) + .FirstOrDefault(); + + return macAddress ?? string.Empty; + } + + private async Task RunCommand(string command) + { + try + { + using var process = new Process(); + process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe"; + process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix + ? $"-c \"{command}\"" + : $"/c {command}"; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + + process.Start(); + var result = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); + + return result.Trim(); + } + catch + { + return string.Empty; + } + } + + private static string ToHash(string str) => + Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str))); + +} diff --git a/Azaion.Suite/Services/Security.cs b/Azaion.Suite/Services/Security.cs new file mode 100644 index 0000000..f06e678 --- /dev/null +++ b/Azaion.Suite/Services/Security.cs @@ -0,0 +1,83 @@ +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Text; + +namespace Azaion.Suite.Services; + +public static class Security +{ + private const int BUFFER_SIZE = 524288; // 512 KB buffer size + + public static string ToHash(this string str) => + Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str))); + + public static string MakeEncryptionKey(string email, string password, string? hardwareHash) => + $"{email}-{password}-{hardwareHash}-#%@AzaionKey@%#---".ToHash(); + + public static SecureString ToSecureString(this string str) + { + var secureString = new SecureString(); + foreach (var c in str.ToCharArray()) + secureString.AppendChar(c); + + return secureString; + } + + public static string? ToRealString(this SecureString value) + { + var valuePtr = IntPtr.Zero; + try + { + valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value); + return Marshal.PtrToStringUni(valuePtr); + } + finally + { + Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); + } + } + + + public static async Task EncryptTo(this Stream stream, Stream toStream, string key, CancellationToken cancellationToken = default) + { + if (stream is { CanRead: false }) throw new ArgumentNullException(nameof(stream)); + if (key is not { Length: > 0 }) throw new ArgumentNullException(nameof(key)); + + using var aes = Aes.Create(); + aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key)); + aes.GenerateIV(); + + using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV); + await using var cs = new CryptoStream(toStream, encryptor, CryptoStreamMode.Write, leaveOpen: true); + + // Prepend IV to the encrypted data + await toStream.WriteAsync(aes.IV.AsMemory(0, aes.IV.Length), cancellationToken); + + var buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken)) > 0) + await cs.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); + } + + public static async Task DecryptTo(this Stream encryptedStream, Stream toStream, string key, CancellationToken cancellationToken = default) + { + using var aes = Aes.Create(); + aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key)); + + // Read the IV from the start of the input stream + var iv = new byte[aes.BlockSize / 8]; + _ = await encryptedStream.ReadAsync(iv, cancellationToken); + aes.IV = iv; + + using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); + await using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read, leaveOpen: true); + + // Read and write in chunks + var buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = await cryptoStream.ReadAsync(buffer, cancellationToken)) > 0) + await toStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); + } +} diff --git a/Azaion.Suite/appsettings.json b/Azaion.Suite/appsettings.json new file mode 100644 index 0000000..d385971 --- /dev/null +++ b/Azaion.Suite/appsettings.json @@ -0,0 +1,10 @@ +{ + "ApiConfig": { + "Url": "https://api.azaion.com", + "TimeoutSeconds": 20, + "RetryCount": 3 + }, + "LocalFilesConfig": { + "DllPath": "AzaionSuite" + } +} \ No newline at end of file diff --git a/Azaion.Annotator.Test/Azaion.Annotator.Test.csproj b/Azaion.Test/Azaion.Test.csproj similarity index 81% rename from Azaion.Annotator.Test/Azaion.Annotator.Test.csproj rename to Azaion.Test/Azaion.Test.csproj index 239175f..6d937bd 100644 --- a/Azaion.Annotator.Test/Azaion.Annotator.Test.csproj +++ b/Azaion.Test/Azaion.Test.csproj @@ -4,10 +4,12 @@ enable enable net8.0-windows + Azaion.Annotator.Test + diff --git a/Azaion.Annotator.Test/DictTest.cs b/Azaion.Test/DictTest.cs similarity index 94% rename from Azaion.Annotator.Test/DictTest.cs rename to Azaion.Test/DictTest.cs index 5f8ba53..5b52819 100644 --- a/Azaion.Annotator.Test/DictTest.cs +++ b/Azaion.Test/DictTest.cs @@ -1,4 +1,5 @@ using Azaion.Annotator.DTO; +using Azaion.Common.DTO; using Xunit; namespace Azaion.Annotator.Test; diff --git a/Azaion.Test/HardwareServiceTest.cs b/Azaion.Test/HardwareServiceTest.cs new file mode 100644 index 0000000..b13497e --- /dev/null +++ b/Azaion.Test/HardwareServiceTest.cs @@ -0,0 +1,16 @@ +using Azaion.Suite; +using Azaion.Suite.Services; +using Xunit; + +namespace Azaion.Annotator.Test; + +public class HardwareServiceTest +{ + [Fact] + public async Task GetHardware_Test() + { + var hardwareService = new HardwareService(); + var hw = await hardwareService.GetHardware(); + Console.WriteLine(hw); + } +} \ No newline at end of file diff --git a/Azaion.Annotator.Test/IntervalTreeTest.cs b/Azaion.Test/IntervalTreeTest.cs similarity index 100% rename from Azaion.Annotator.Test/IntervalTreeTest.cs rename to Azaion.Test/IntervalTreeTest.cs diff --git a/Azaion.Annotator.Test/ParallelExtTest.cs b/Azaion.Test/ParallelExtTest.cs similarity index 100% rename from Azaion.Annotator.Test/ParallelExtTest.cs rename to Azaion.Test/ParallelExtTest.cs diff --git a/New Text Document.txt b/visual detections additional.txt similarity index 100% rename from New Text Document.txt rename to visual detections additional.txt