diff --git a/EonaCat.ConnectionMonitor/Converters/StringToVisibilityConverter.cs b/EonaCat.ConnectionMonitor/Converters/StringToVisibilityConverter.cs new file mode 100644 index 0000000..969d8dc --- /dev/null +++ b/EonaCat.ConnectionMonitor/Converters/StringToVisibilityConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace EonaCat.ConnectionMonitor.Converters +{ + public class StringToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return string.IsNullOrWhiteSpace(value as string) ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + +} diff --git a/EonaCat.ConnectionMonitor/EonaCat.ConnectionMonitor.csproj b/EonaCat.ConnectionMonitor/EonaCat.ConnectionMonitor.csproj index 4b31f99..4e03768 100644 --- a/EonaCat.ConnectionMonitor/EonaCat.ConnectionMonitor.csproj +++ b/EonaCat.ConnectionMonitor/EonaCat.ConnectionMonitor.csproj @@ -9,6 +9,11 @@ EonaCat.ico + + + + + diff --git a/EonaCat.ConnectionMonitor/Helpers/LeafLetHelper.cs b/EonaCat.ConnectionMonitor/Helpers/LeafLetHelper.cs new file mode 100644 index 0000000..c241457 --- /dev/null +++ b/EonaCat.ConnectionMonitor/Helpers/LeafLetHelper.cs @@ -0,0 +1,918 @@ +using EonaCat.ConnectionMonitor.Models; +using EonaCat.Json; +using EonaCat.Json.Linq; +using Microsoft.Web.WebView2.Wpf; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Net.Http; + +namespace EonaCat.ConnectionMonitor.Helpers +{ + internal class LeafLetHelper + { + private readonly Dictionary _countryCentroidCache = new(); + private readonly Dictionary _countryFlagCache = new(); + private readonly HttpClient _mapHttpClient = new HttpClient(); + private WebView2 _mapWebView; + private (double Lat, double Lon)? _localCoord; + + private const string _mapHtml = @" + + + + + + + + + + + + + + + Live Map + Historical Map + + + + + + Stats + + Connections: 0 | Top Country: N/A + Trails: ON + + Trail Duration + + 8s + + + Trail Color + + Auto (By Direction) + Red + Blue + Yellow + White + + + Top Connections:No data yet + + + + + + + + Play + + 0/0 + + + Stats + + Connections: 0 | Top Country: N/A + Top Connections:No data yet + + + + + + + + + + + + +"; + + + public async Task InitializeMapAsync(WebView2 mapWebView) + { + await mapWebView.EnsureCoreWebView2Async(); + + // Disable default dialogs + mapWebView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false; + + // Disable developer tools and context menu + mapWebView.CoreWebView2.Settings.AreDevToolsEnabled = Debugger.IsAttached; + + // Prevent right-click, F12, Ctrl+Shift+I, Ctrl+R, Ctrl+P + string disableContextAndKeys = @" + document.addEventListener('contextmenu', e => e.preventDefault()); + document.addEventListener('keydown', function(e) { + if ( + e.key === 'F12' || + (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'i')) || + (e.ctrlKey && (e.key === 'R' || e.key === 'r')) || + (e.ctrlKey && (e.key === 'P' || e.key === 'p')) + ) { + e.preventDefault(); + } + }); + "; + + if (!Debugger.IsAttached) + { + await mapWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(disableContextAndKeys); + } + + // Now navigate to the HTML + mapWebView.NavigateToString(_mapHtml); + + _mapWebView = mapWebView; + } + + public async Task<(double Lat, double Lon)?> GetCountryLatLonAsync(string countryCode) + { + if (string.IsNullOrEmpty(countryCode)) return null; + countryCode = countryCode.Trim().ToUpperInvariant(); + _countryCentroidCache.TryGetValue(countryCode, out var cachedLatLon); + + if (cachedLatLon != default) + { + return (cachedLatLon.Lat, cachedLatLon.Lon); + } + + try + { + var url = $"https://restcountries.com/v3.1/alpha/{countryCode}"; + var str = await _mapHttpClient.GetStringAsync(url); + var root = JArray.Parse(str); + if (root.Count > 0) + { + var latlng = root[0]["latlng"] as JArray; + var flagUrl = root[0]["flags"]?["png"]?.ToString(); + if (latlng != null && latlng.Count >= 2) + { + var lat = latlng[0].ToObject(); + var lon = latlng[1].ToObject(); + _countryCentroidCache[countryCode] = (lat, lon); + if (flagUrl != null) + { + _countryFlagCache[countryCode] = flagUrl; + } + return (lat, lon); + } + } + } + catch { } + return null; + } + + private async Task<(double Lat, double Lon)?> GetLocalCoordinatesAsync() + { + if (_localCoord != null) return _localCoord; + + try + { + var ipInfo = await _mapHttpClient.GetStringAsync("http://ip-api.com/json"); + var root = JObject.Parse(ipInfo); + if (root["status"]?.ToString() == "success") + { + _localCoord = (root["lat"].ToObject(), root["lon"].ToObject()); + return _localCoord; + } + } + catch { } + + _localCoord = (0.0, 0.0); + return _localCoord; + } + + public async Task UpdateMapAsync( + ObservableCollection countryStats, + List connections = null) + { + try + { + var localCoord = await GetLocalCoordinatesAsync(); + + // Prepare payload for countries + var payloadCountries = new List(); + foreach (var cs in countryStats) + { + if (string.IsNullOrWhiteSpace(cs.CountryCode)) continue; + + var coord = await GetCountryLatLonAsync(cs.CountryCode); + if (coord == null) continue; + + payloadCountries.Add(new + { + countryCode = cs.CountryCode, + countryName = cs.CountryName, + count = cs.ConnectionCount, + lat = coord.Value.Lat, + lon = coord.Value.Lon, + topProcesses = cs.TopProcesses?.Select(p => new { p.ProcessName, p.ConnectionCount }).ToList(), + flag = _countryFlagCache.ContainsKey(cs.CountryCode) ? _countryFlagCache[cs.CountryCode] : null + }); + } + + // Prepare payload for connections and track live top connections + var payloadConnections = new List(); + var liveConnectionCounts = new Dictionary(); + + foreach (var conn in connections ?? Enumerable.Empty()) + { + var fromCoord = await GetCountryLatLonAsync(conn.FromCountryCode) ?? localCoord ?? (0.0, 0.0); + var toCoord = await GetCountryLatLonAsync(conn.ToCountryCode); + if (toCoord == null) continue; + + var fromCountry = countryStats.FirstOrDefault(c => c.CountryCode == conn.FromCountryCode); + var toCountry = countryStats.FirstOrDefault(c => c.CountryCode == conn.ToCountryCode); + + payloadConnections.Add(new + { + fromLat = fromCoord.Lat, + fromLon = fromCoord.Lon, + toLat = toCoord.Value.Lat, + toLon = toCoord.Value.Lon, + connectionCount = conn.ConnectionCount, + fromCountryCode = conn.FromCountryCode, + fromCountryName = fromCountry?.CountryName, + fromFlagUrl = _countryFlagCache.ContainsKey(conn.FromCountryCode) ? _countryFlagCache[conn.FromCountryCode] : null, + toFlagUrl = _countryFlagCache.ContainsKey(conn.ToCountryCode) ? _countryFlagCache[conn.ToCountryCode] : null, + fromIp = conn.FromIp, + fromTopProcesses = fromCountry?.TopProcesses?.Select(p => new { p.ProcessName, p.ConnectionCount }).ToList(), + toCountryCode = conn.ToCountryCode, + toCountryName = toCountry?.CountryName, + toIp = conn.ToIp, + toTopProcesses = toCountry?.TopProcesses?.Select(p => new { p.ProcessName, p.ConnectionCount }).ToList() + }); + + // Track live top connections + if (!string.IsNullOrEmpty(conn.FromCountryCode) && !string.IsNullOrEmpty(conn.ToCountryCode)) + { + var key = $"{conn.FromCountryCode}→{conn.ToCountryCode}"; + if (!liveConnectionCounts.ContainsKey(key)) + liveConnectionCounts[key] = 0; + liveConnectionCounts[key] += conn.ConnectionCount; + } + } + + // Compute live totals + var totalConnections = countryStats.Sum(c => c.ConnectionCount); + + var topCountry = countryStats + .OrderByDescending(c => c.ConnectionCount) + .FirstOrDefault()?.CountryCode ?? "N/A"; + + var topConnections = liveConnectionCounts + .OrderByDescending(kv => kv.Value) + .Take(10) + .ToDictionary(kv => kv.Key, kv => kv.Value); + + // Send minimal payload to WebView2 + if (_mapWebView?.CoreWebView2 != null) + { + var jsonCountries = JsonHelper.ToJson(payloadCountries); + var jsonConnections = JsonHelper.ToJson(payloadConnections); + var jsonLiveTotals = JsonHelper.ToJson(new + { + totalConnections, + topCountry, + topConnections + }); + + await _mapWebView.CoreWebView2.ExecuteScriptAsync($@" + window.updateMarkersAndLines({jsonCountries}, {jsonConnections}); + window.liveTotals = {jsonLiveTotals}; + "); + } + } + catch (Exception ex) + { + Debug.WriteLine($"UpdateMapAsync error: {ex.Message}"); + } + } + } +} diff --git a/EonaCat.ConnectionMonitor/MainWindow.xaml b/EonaCat.ConnectionMonitor/MainWindow.xaml index 640a254..80661ec 100644 --- a/EonaCat.ConnectionMonitor/MainWindow.xaml +++ b/EonaCat.ConnectionMonitor/MainWindow.xaml @@ -1,10 +1,13 @@ + + + + @@ -231,7 +278,24 @@ - + + + + + + + + + + + @@ -331,86 +395,152 @@ - + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -450,59 +580,61 @@ - - - - + + - - - - - - - - - + + + + + + + + + - - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -514,6 +646,9 @@ + + + \ No newline at end of file diff --git a/EonaCat.ConnectionMonitor/MainWindow.xaml.cs b/EonaCat.ConnectionMonitor/MainWindow.xaml.cs index d7b9725..1d4247e 100644 --- a/EonaCat.ConnectionMonitor/MainWindow.xaml.cs +++ b/EonaCat.ConnectionMonitor/MainWindow.xaml.cs @@ -1,25 +1,24 @@ -using Microsoft.Win32; +using EonaCat.ConnectionMonitor.Helpers; +using EonaCat.ConnectionMonitor.Models; +using EonaCat.Json; +using EonaCat.Json.Linq; +using Microsoft.Win32; using System.Collections.Concurrent; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.NetworkInformation; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; -using System.Windows.Media.Imaging; using System.Windows.Threading; namespace EonaCat.ConnectionMonitor { - public partial class MainWindow : Window + public partial class MainWindow : Window, INotifyPropertyChanged { private ObservableCollection _connectionHistory; private DispatcherTimer _refreshTimer; @@ -31,20 +30,59 @@ namespace EonaCat.ConnectionMonitor private CollectionViewSource _allConnectionsView; private string _configFilePath = "connections_config.json"; - private bool _isMonitoring = false; private HttpClient _httpClient; + private LeafLetHelper _leafletHelper; private bool _isRefreshing; + + public int RemoteConnectionCount { get; private set; } + private readonly ConcurrentDictionary _geoCache = new(); private string _connectionEventsLogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ConnectionEvents.log"); + private bool _isMonitoringStarted; + + public bool IsMonitoringStarted + { + get => _isMonitoringStarted; + set + { + _isMonitoringStarted = value; + NotifyPropertyChanged(nameof(IsMonitoringStarted)); + NotifyPropertyChanged(nameof(IsLoadingOverlayVisible)); + } + } + + public bool IsLoadingOverlayVisible => IsMonitoringStarted && !_allConnections.Any(); + public event PropertyChangedEventHandler PropertyChanged; + protected void NotifyPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + public MainWindow() { InitializeComponent(); + StartClock(); //Icon = BitmapFrame.Create(new Uri("pack://application:,,,/Resources/app_icon.ico", UriKind.Absolute)); InitializeCollections(); InitializeTimer(); _httpClient = new HttpClient(); + _leafletHelper = new LeafLetHelper(); + _leafletHelper.InitializeMapAsync(mapWebView).ConfigureAwait(false); LoadConfigurationAsync().ConfigureAwait(false); + StartMonitoring(); + } + + private void StartClock() + { + Task.Run(() => + { + while (true) + { + Dispatcher.Invoke(() => lblClock.Text = $"{DateTime.Now:HH:mm:ss}"); + Thread.Sleep(1000); + } + }); } private void InitializeCollections() @@ -65,8 +103,6 @@ namespace EonaCat.ConnectionMonitor dgConnectionEvents.ItemsSource = _connectionEvents; } - - private void InitializeTimer() { _refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) }; @@ -78,6 +114,48 @@ namespace EonaCat.ConnectionMonitor await RefreshConnectionsAsync(); } + private void BtnKillProcess_Click(object sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.Tag is int pid) + { + try + { + Process process = Process.GetProcessById(pid); + if (MessageBox.Show($"Are you sure you want to kill Process{Environment.NewLine}{process.ProcessName} (PID: {pid})?", + "Confirm Kill", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes) + { + process.Kill(); + process.WaitForExit(); + + MessageBox.Show($"Process {process.ProcessName} (PID: {pid}) has been terminated.", "Process Killed", MessageBoxButton.OK, MessageBoxImage.Information); + BtnRefresh_Click(null, null); + } + } + catch (Exception ex) + { + MessageBox.Show($"Failed to kill process with PID {pid}: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + + private void BtnDonate_Click(object sender, RoutedEventArgs e) + { + string paypalUrl = "https://paypal.me/EonaCat"; + try + { + var psi = new ProcessStartInfo + { + FileName = paypalUrl, + UseShellExecute = true + }; + Process.Start(psi); + } + catch (System.Exception ex) + { + MessageBox.Show($"Could not open browser: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + private void BtnStart_Click(object sender, RoutedEventArgs e) { StartMonitoring(); @@ -100,7 +178,7 @@ namespace EonaCat.ConnectionMonitor private void ChkAutoRefresh_Checked(object sender, RoutedEventArgs e) { - if (_isMonitoring && chkAutoRefresh.IsChecked == true) + if (IsMonitoringStarted && chkAutoRefresh.IsChecked == true) { _refreshTimer.Start(); } @@ -113,7 +191,7 @@ namespace EonaCat.ConnectionMonitor private void StartMonitoring() { - _isMonitoring = true; + IsMonitoringStarted = true; btnStart.IsEnabled = false; btnStop.IsEnabled = true; lblStatus.Text = "Monitoring..."; @@ -127,7 +205,7 @@ namespace EonaCat.ConnectionMonitor private void StopMonitoring() { - _isMonitoring = false; + IsMonitoringStarted = false; _refreshTimer.Stop(); btnStart.IsEnabled = true; btnStop.IsEnabled = false; @@ -141,7 +219,7 @@ namespace EonaCat.ConnectionMonitor try { var dict = _geoCache.ToDictionary(k => k.Key, v => v.Value); - var json = JsonSerializer.Serialize(dict, new JsonSerializerOptions { WriteIndented = true }); + var json = JsonHelper.ToJson(dict, Formatting.Indented); await File.WriteAllTextAsync(_geoCacheFilePath, json); } catch { } @@ -149,11 +227,29 @@ namespace EonaCat.ConnectionMonitor private async Task RefreshConnectionsAsync() { - if (_isRefreshing) return; + if (_isRefreshing) + return; + _isRefreshing = true; try { + // Load geo cache if not already loaded + if (_allConnections.Count == 0 && File.Exists(_geoCacheFilePath)) + { + var json = await File.ReadAllTextAsync(_geoCacheFilePath); + var dict = JsonHelper.ToObject>(json); + if (dict != null) + { + foreach (var kvp in dict) + _geoCache[kvp.Key] = kvp.Value; + } + } + else + { + NotifyPropertyChanged(nameof(IsLoadingOverlayVisible)); + } + Dispatcher.Invoke(() => lblStatus.Text = "Refreshing..."); var connections = await Task.Run(async () => @@ -163,18 +259,12 @@ namespace EonaCat.ConnectionMonitor var tcpConnections = ipProps.GetActiveTcpConnections(); var tcpListeners = ipProps.GetActiveTcpListeners(); var udpListeners = ipProps.GetActiveUdpListeners(); - var pidToProcessName = Process.GetProcesses().ToDictionary(p => p.Id, p => p.ProcessName); var tasks = new List>(); tasks.AddRange(tcpConnections.Select(c => - CreateInfoAsync( - c.LocalEndPoint, - c.RemoteEndPoint, - "TCP", - FormatTcpState(c.State), - pidToProcessName))); + CreateInfoAsync(c.LocalEndPoint, c.RemoteEndPoint, "TCP", FormatTcpState(c.State), pidToProcessName))); tasks.AddRange(tcpListeners.Select(l => CreateInfoAsync(l, null, "TCP", "LISTENING", pidToProcessName))); @@ -187,13 +277,13 @@ namespace EonaCat.ConnectionMonitor return result; }); - // Remove duplicates - connections = connections - .GroupBy(c => $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}") - .Select(g => g.First()) - .ToList(); + // Assign consistent UniqueId + foreach (var c in connections) + { + c.UniqueId = $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}"; + } - // Geolocation + // Geolocation for new IPs var ipsToFetch = connections .Where(c => c.RemoteEndPoint != "N/A") .Select(c => c.RemoteEndPoint.Split(':')[0]) @@ -215,6 +305,7 @@ namespace EonaCat.ConnectionMonitor }); await Task.WhenAll(geoTasks); + // Update geolocation data foreach (var conn in connections) { if (conn.RemoteEndPoint != "N/A") @@ -223,71 +314,71 @@ namespace EonaCat.ConnectionMonitor if (_geoCache.TryGetValue(ip, out var geo)) { conn.CountryCode = geo.CountryCode; - conn.CountryName = geo.CountryName; + conn.CountryName = FixCountry(geo.CountryName); conn.ISP = geo.ISP; conn.CountryFlagUrl = GetFlagUrl(geo.CountryCode)?.ToString(); + conn.Latitude = geo.Latitude; + conn.Longitude = geo.Longitude; } } } - // Track new and disconnected connections by state - var currentKeys = connections - .Select(c => $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}") - .ToHashSet(); + // Merge with existing connections + var currentKeys = connections.Select(c => c.UniqueId).ToHashSet(); + var previousKeys = _allConnections.Select(c => c.UniqueId).ToHashSet(); - var previousKeys = _allConnections - .Select(c => $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}") - .ToHashSet(); - - // New connections - var newConnections = connections.Where(c => !previousKeys.Contains( - $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}")).ToList(); - - foreach (var conn in newConnections) + foreach (var conn in connections) { - conn.StartTime = DateTime.Now; - conn.LastSeen = DateTime.Now; - _allConnections.Add(conn); - - _connectionHistory.Add(new ConnectionInfo + var existing = _allConnections.FirstOrDefault(c => c.UniqueId == conn.UniqueId); + if (existing != null) { - ProcessName = conn.ProcessName, - ProcessId = conn.ProcessId, - Protocol = conn.Protocol, - LocalEndPoint = conn.LocalEndPoint, - RemoteEndPoint = conn.RemoteEndPoint, - State = /*conn.Protocol == "UDP" ? "UDP" :*/ conn.State.ToUpperInvariant(), - CountryCode = conn.CountryCode, - CountryName = conn.CountryName, - CountryFlagUrl = conn.CountryFlagUrl, - ISP = conn.ISP, - StartTime = conn.StartTime, - LastSeen = conn.LastSeen, - UniqueId = conn.UniqueId - }); - LogConnectionEvent(conn, $"Connected ({conn.State})"); - } - - // Disconnected or state-changed connections - foreach (var conn in _allConnections) - { - // Update LastSeen for duration calculation - conn.LastSeen = DateTime.Now; - - // Check if connection still exists in current scan - var key = $"{conn.Protocol}|{conn.LocalEndPoint}|{conn.RemoteEndPoint}|{conn.State}"; - if (!currentKeys.Contains(key)) + // Update existing connection info + existing.LastSeen = DateTime.Now; + existing.State = conn.State; + existing.ProcessId = conn.ProcessId; + existing.ProcessName = conn.ProcessName; + existing.CountryCode = conn.CountryCode; + existing.CountryName = conn.CountryName; + existing.ISP = conn.ISP; + existing.CountryFlagUrl = conn.CountryFlagUrl; + existing.Latitude = conn.Latitude; + existing.Longitude = conn.Longitude; + } + else { - LogConnectionEvent(conn, $"Disconnected ({conn.State})"); + // New connection + conn.StartTime = DateTime.Now; + conn.LastSeen = DateTime.Now; + _allConnections.Add(conn); + _connectionHistory.Add(new ConnectionInfo + { + ProcessName = conn.ProcessName, + ProcessId = conn.ProcessId, + Protocol = conn.Protocol, + LocalEndPoint = conn.LocalEndPoint, + RemoteEndPoint = conn.RemoteEndPoint, + State = conn.State, + CountryCode = conn.CountryCode, + CountryName = conn.CountryName, + CountryFlagUrl = conn.CountryFlagUrl, + ISP = conn.ISP, + StartTime = conn.StartTime, + LastSeen = conn.LastSeen, + UniqueId = conn.UniqueId + }); + LogConnectionEvent(conn, $"Connected ({conn.State})"); } } - // Remove old connections not in the new list + // Detect disconnected connections for (int i = _allConnections.Count - 1; i >= 0; i--) { - var key = $"{_allConnections[i].Protocol}|{_allConnections[i].LocalEndPoint}|{_allConnections[i].RemoteEndPoint}|{_allConnections[i].State}"; - if (!currentKeys.Contains(key)) + var c = _allConnections[i]; + if (!currentKeys.Contains(c.UniqueId)) + { + LogConnectionEvent(c, $"Disconnected ({c.State})"); _allConnections.RemoveAt(i); + } } // Preserve selection @@ -319,6 +410,8 @@ namespace EonaCat.ConnectionMonitor finally { _isRefreshing = false; + RemoteConnectionCount = _allConnections.Count(c => c.RemoteEndPoint != "N/A" && !c.RemoteEndPoint.StartsWith("127.") && !c.RemoteEndPoint.StartsWith("[::1]")); + UpdateTabHeaders(); } } @@ -441,14 +534,16 @@ namespace EonaCat.ConnectionMonitor try { var response = await _httpClient.GetStringAsync($"http://ip-api.com/json/{ipAddress}"); - var root = JsonDocument.Parse(response).RootElement; - if (root.GetProperty("status").GetString() == "success") + var root = JObject.Parse(response); + if (root["status"]?.ToString() == "success") { return new GeolocationInfo { - CountryCode = root.GetProperty("countryCode").GetString(), - CountryName = root.GetProperty("country").GetString(), - ISP = root.GetProperty("isp").GetString() + CountryCode = root["countryCode"]?.ToString(), + CountryName = FixCountry(root["country"]?.ToString()), + ISP = root["isp"]?.ToString(), + Latitude = root["lat"]?.ToObject() ?? 0.0, + Longitude = root["lon"]?.ToObject() ?? 0.0 }; } } @@ -456,6 +551,20 @@ namespace EonaCat.ConnectionMonitor return null; } + private string FixCountry(string? country) + { + if (string.IsNullOrEmpty(country)) + { + return string.Empty; + } + + if (country == "Netherlands") + { + return "The Netherlands"; + } + return country; + } + private Uri GetFlagUrl(string countryCode) { if (string.IsNullOrEmpty(countryCode) || countryCode == "Local") @@ -477,7 +586,12 @@ namespace EonaCat.ConnectionMonitor { configured.Status = "Active"; configured.ProcessName = match.ProcessName; - configured.LastSeen = DateTime.Now; + + if (configured.StartTime == null) + { + configured.StartTime = match.StartTime; + } + configured.LastSeen = match.LastSeen; } else { @@ -486,7 +600,36 @@ namespace EonaCat.ConnectionMonitor } } - private void UpdateStatistics() + private void UpdateTabHeaders() + { + // All Connections + tabControl.Items.Cast() + .FirstOrDefault(t => t.Header.ToString().StartsWith("All Connections")) + ?.SetValue(TabItem.HeaderProperty, $"All Connections ({dgAllConnections.Items.Count}) - Remote Connections: {RemoteConnectionCount}"); + + // Configured Connections + tabControl.Items.Cast() + .FirstOrDefault(t => t.Header.ToString().StartsWith("Configured Connections")) + ?.SetValue(TabItem.HeaderProperty, $"Configured Connections ({dgConfiguredConnections.Items.Count})"); + + // Connection History + tabControl.Items.Cast() + .FirstOrDefault(t => t.Header.ToString().StartsWith("Connection History")) + ?.SetValue(TabItem.HeaderProperty, $"Connection History ({connectionHistoryDataGrid.Items.Count})"); + + // Connection Events + tabControl.Items.Cast() + .FirstOrDefault(t => t.Header.ToString().StartsWith("Connection Events")) + ?.SetValue(TabItem.HeaderProperty, $"Connection Events ({dgConnectionEvents.Items.Count})"); + + // Statistics (optional, e.g., total connections) + tabControl.Items.Cast() + .FirstOrDefault(t => t.Header.ToString().StartsWith("Statistics")) + ?.SetValue(TabItem.HeaderProperty, $"Statistics ({dgProcessStats.Items.Count})"); + } + + + private async void UpdateStatistics() { lblTotalConnections.Text = _allConnections.Count.ToString(); lblEstablishedConnections.Text = _allConnections.Count(c => c.State == "ESTABLISHED").ToString(); @@ -495,6 +638,7 @@ namespace EonaCat.ConnectionMonitor .Where(c => !string.IsNullOrEmpty(c.CountryCode) && c.CountryCode != "Local") .Select(c => c.CountryCode).Distinct().Count().ToString(); + // Process statistics _processStats.Clear(); var processGroups = _allConnections .Where(c => !string.IsNullOrEmpty(c.ProcessName)) @@ -514,6 +658,7 @@ namespace EonaCat.ConnectionMonitor }); } + // Country statistics with top processes _countryStats.Clear(); var countryGroups = _allConnections .Where(c => !string.IsNullOrEmpty(c.CountryCode) && c.CountryCode != "Local") @@ -523,16 +668,79 @@ namespace EonaCat.ConnectionMonitor foreach (var g in countryGroups) { + var topProcesses = _allConnections + .Where(c => c.CountryCode == g.Key.CountryCode) + .GroupBy(c => c.ProcessName) + .OrderByDescending(pg => pg.Count()) + .Take(3) + .Select(pg => new CountryProcess { ProcessName = pg.Key, ConnectionCount = pg.Count() }) + .ToList(); + _countryStats.Add(new CountryStatistic { CountryCode = g.Key.CountryCode, - CountryName = g.Key.CountryName, + CountryName = FixCountry(g.Key.CountryName), FlagUrl = g.Key.CountryFlagUrl, - ConnectionCount = g.Count() + ConnectionCount = g.Count(), + TopProcesses = topProcesses }); } + + // Get LOCAL coordinates dynamically + var localCoord = await GetLocalCoordinatesAsync() ?? (Lat: 0.0, Lon: 0.0, null); + + // Create connections for map + var connections = _allConnections + .Where(c => !string.IsNullOrEmpty(c.CountryCode) && c.CountryCode != "Local") + .GroupBy(c => new { From = "LOCAL", To = c.CountryCode }) + .Select(g => new CountryConnection + { + FromCountryCode = g.Key.From == "LOCAL" ? localCoord.CountryCode.ToUpper() : "LOCAL", + ToCountryCode = g.Key.To, + ConnectionCount = g.Count(), + FromLat = localCoord.Lat, + FromLon = localCoord.Lon, + FromFlag = GetFlagUrl(localCoord.CountryCode).ToString(), + ToFlag = g.First().CountryFlagUrl, + ToLat = g.First().Latitude, + ToLon = g.First().Longitude, + ToIp = g.First().RemoteEndPoint.Split(':')[0], + FromIp = "Local", + Processes = g.Select(c => c.ProcessName).Distinct().ToList() + }) + .ToList(); + + _ = _leafletHelper.UpdateMapAsync(_countryStats, connections).ConfigureAwait(false); } + private async Task<(double Lat, double Lon, string CountryCode)?> GetLocalCoordinatesAsync() + { + try + { + using var http = new HttpClient(); + // Get public IP + var ip = await http.GetStringAsync("https://api.ipify.org"); + ip = ip.Trim(); + + // Get geolocation from IP + var geoJson = await http.GetStringAsync($"http://ip-api.com/json/{ip}"); + var root = JObject.Parse(geoJson); + if (root["status"]?.ToString() == "success") + { + double lat = root["lat"].ToObject(); + double lon = root["lon"].ToObject(); + string countryCode = root["countryCode"].ToObject(); + return (lat, lon, countryCode); + } + } + catch + { + // fallback + } + return null; + } + + private void ApplyFilter() { var filterText = txtFilter.Text ?? string.Empty; @@ -609,12 +817,12 @@ namespace EonaCat.ConnectionMonitor new ConfiguredConnection { DisplayName = "Microsoft Update", IpAddress = "13.107.42.14", Port = 443 } }; - var json = JsonSerializer.Serialize(defaultConfig, new JsonSerializerOptions { WriteIndented = true }); + var json = JsonHelper.ToJson(defaultConfig, Formatting.Indented); await File.WriteAllTextAsync(_configFilePath, json); } var configJson = await File.ReadAllTextAsync(_configFilePath); - var configurations = JsonSerializer.Deserialize>(configJson); + var configurations = JsonHelper.ToObject>(configJson); _configuredConnections.Clear(); foreach (var config in configurations) @@ -635,7 +843,7 @@ namespace EonaCat.ConnectionMonitor try { var configurations = _configuredConnections.ToList(); - var json = JsonSerializer.Serialize(configurations, new JsonSerializerOptions { WriteIndented = true }); + var json = JsonHelper.ToJson(configurations, Formatting.Indented); await File.WriteAllTextAsync(_configFilePath, json); lblStatusBar.Text = $"Configuration saved to {_configFilePath}"; @@ -654,130 +862,4 @@ namespace EonaCat.ConnectionMonitor } } - - public class ConnectionInfo : INotifyPropertyChanged - { - public string ProcessName { get; set; } - public int ProcessId { get; set; } - public string Protocol { get; set; } - public string LocalEndPoint { get; set; } - public string RemoteEndPoint { get; set; } - public string State { get; set; } - public string CountryCode { get; set; } - public string CountryName { get; set; } - public string CountryFlagUrl { get; set; } - public string ISP { get; set; } - public string UniqueId { get; set; } - - private DateTime? _startTime; - public DateTime? StartTime - { - get => _startTime; - set - { - _startTime = value; - OnPropertyChanged(nameof(StartTime)); - OnPropertyChanged(nameof(Duration)); - } - } - - private DateTime? _lastSeen; - public DateTime? LastSeen - { - get => _lastSeen; - set - { - _lastSeen = value; - OnPropertyChanged(nameof(LastSeen)); - OnPropertyChanged(nameof(Duration)); - } - } - - public TimeSpan? Duration => (StartTime != null && LastSeen != null) ? LastSeen - StartTime : null; - - public event PropertyChangedEventHandler PropertyChanged; - private void OnPropertyChanged(string propertyName) => - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - - private BitmapImage _flagImage; - public BitmapImage CountryFlagImage - { - get - { - if (_flagImage != null) - { - return _flagImage; - } - - if (string.IsNullOrEmpty(CountryFlagUrl)) - { - return null; - } - - try - { - var img = new BitmapImage(); - img.BeginInit(); - img.UriSource = new Uri(CountryFlagUrl); - img.CacheOption = BitmapCacheOption.OnLoad; - img.EndInit(); - if (img.IsFrozen == false && img.CanFreeze) - { - img.Freeze(); - } - _flagImage = img; - return _flagImage; - } - catch - { - return null; - } - } - } - } - - public class ConnectionEvent - { - public string ProcessName { get; set; } - public int ProcessId { get; set; } - public string Protocol { get; set; } - public string LocalEndPoint { get; set; } - public string RemoteEndPoint { get; set; } - public string EventType { get; set; } // "Connected" or "Disconnected" - public DateTime Timestamp { get; set; } - public string State { get; set; } - } - - public class ConfiguredConnection : INotifyPropertyChanged - { - public string DisplayName { get; set; } - public string IpAddress { get; set; } - public int Port { get; set; } - public string Status { get; set; } = "Unknown"; - public string ProcessName { get; set; } - public DateTime? LastSeen { get; set; } - public event PropertyChangedEventHandler PropertyChanged; - } - - public class ProcessStatistic - { - public string ProcessName { get; set; } - public int ConnectionCount { get; set; } - public int UniqueIPs { get; set; } - } - - public class CountryStatistic - { - public string CountryCode { get; set; } - public string CountryName { get; set; } - public string FlagUrl { get; set; } - public int ConnectionCount { get; set; } - } - - public class GeolocationInfo - { - public string CountryCode { get; set; } - public string CountryName { get; set; } - public string ISP { get; set; } - } } diff --git a/EonaCat.ConnectionMonitor/Models/ConfiguredConnection.cs b/EonaCat.ConnectionMonitor/Models/ConfiguredConnection.cs new file mode 100644 index 0000000..bb7515e --- /dev/null +++ b/EonaCat.ConnectionMonitor/Models/ConfiguredConnection.cs @@ -0,0 +1,42 @@ +using System.ComponentModel; + +namespace EonaCat.ConnectionMonitor.Models +{ + public class ConfiguredConnection : INotifyPropertyChanged + { + public string DisplayName { get; set; } + public string IpAddress { get; set; } + public int Port { get; set; } + public string Status { get; set; } = "Unknown"; + public string ProcessName { get; set; } + private DateTime? _startTime; + public DateTime? StartTime + { + get => _startTime; + set + { + _startTime = value; + OnPropertyChanged(nameof(StartTime)); + OnPropertyChanged(nameof(Duration)); + } + } + + private DateTime? _lastSeen; + public DateTime? LastSeen + { + get => _lastSeen; + set + { + _lastSeen = value; + OnPropertyChanged(nameof(LastSeen)); + OnPropertyChanged(nameof(Duration)); + } + } + + public TimeSpan? Duration => StartTime != null && LastSeen != null ? LastSeen - StartTime : null; + + public event PropertyChangedEventHandler PropertyChanged; + private void OnPropertyChanged(string propertyName) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/EonaCat.ConnectionMonitor/Models/ConnectionEvent.cs b/EonaCat.ConnectionMonitor/Models/ConnectionEvent.cs new file mode 100644 index 0000000..7ed6bfc --- /dev/null +++ b/EonaCat.ConnectionMonitor/Models/ConnectionEvent.cs @@ -0,0 +1,14 @@ +namespace EonaCat.ConnectionMonitor.Models +{ + public class ConnectionEvent + { + public string ProcessName { get; set; } + public int ProcessId { get; set; } + public string Protocol { get; set; } + public string LocalEndPoint { get; set; } + public string RemoteEndPoint { get; set; } + public string EventType { get; set; } // "Connected" or "Disconnected" + public DateTime Timestamp { get; set; } + public string State { get; set; } + } +} diff --git a/EonaCat.ConnectionMonitor/Models/ConnectionInfo.cs b/EonaCat.ConnectionMonitor/Models/ConnectionInfo.cs new file mode 100644 index 0000000..f9c09b4 --- /dev/null +++ b/EonaCat.ConnectionMonitor/Models/ConnectionInfo.cs @@ -0,0 +1,89 @@ +using System.ComponentModel; +using System.Windows.Media.Imaging; + +namespace EonaCat.ConnectionMonitor.Models +{ + public class ConnectionInfo : INotifyPropertyChanged + { + public string ProcessName { get; set; } + public int ProcessId { get; set; } + public string Protocol { get; set; } + public string LocalEndPoint { get; set; } + public string RemoteEndPoint { get; set; } + public string State { get; set; } + public string CountryCode { get; set; } + public string CountryName { get; set; } + public string CountryFlagUrl { get; set; } + public string ISP { get; set; } + public string UniqueId { get; set; } + + private DateTime? _startTime; + public DateTime? StartTime + { + get => _startTime; + set + { + _startTime = value; + OnPropertyChanged(nameof(StartTime)); + OnPropertyChanged(nameof(Duration)); + } + } + + private DateTime? _lastSeen; + public DateTime? LastSeen + { + get => _lastSeen; + set + { + _lastSeen = value; + OnPropertyChanged(nameof(LastSeen)); + OnPropertyChanged(nameof(Duration)); + } + } + + public TimeSpan? Duration => StartTime != null && LastSeen != null ? LastSeen - StartTime : null; + + public event PropertyChangedEventHandler PropertyChanged; + private void OnPropertyChanged(string propertyName) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + private BitmapImage _flagImage; + public BitmapImage CountryFlagImage + { + get + { + if (_flagImage != null) + { + return _flagImage; + } + + if (string.IsNullOrEmpty(CountryFlagUrl)) + { + return null; + } + + try + { + var img = new BitmapImage(); + img.BeginInit(); + img.UriSource = new Uri(CountryFlagUrl); + img.CacheOption = BitmapCacheOption.OnLoad; + img.EndInit(); + if (img.IsFrozen == false && img.CanFreeze) + { + img.Freeze(); + } + _flagImage = img; + return _flagImage; + } + catch + { + return null; + } + } + } + + public double Longitude { get; set; } + public double Latitude { get; set; } + } +} diff --git a/EonaCat.ConnectionMonitor/Models/CountryConnection.cs b/EonaCat.ConnectionMonitor/Models/CountryConnection.cs new file mode 100644 index 0000000..9088db9 --- /dev/null +++ b/EonaCat.ConnectionMonitor/Models/CountryConnection.cs @@ -0,0 +1,21 @@ +namespace EonaCat.ConnectionMonitor.Models +{ + public class CountryConnection + { + public string FromCountryCode { get; set; } + public string ToCountryCode { get; set; } + public string FromFlag { get; set; } + public string ToFlag { get; set; } + public int ConnectionCount { get; set; } + + public double? FromLat { get; set; } + public double? FromLon { get; set; } + public double? ToLat { get; set; } + public double? ToLon { get; set; } + public string ToIp { get; set; } + public string FromIp { get; set; } + + + public List Processes { get; set; } = new List(); + } +} diff --git a/EonaCat.ConnectionMonitor/Models/CountryProcess.cs b/EonaCat.ConnectionMonitor/Models/CountryProcess.cs new file mode 100644 index 0000000..f618d47 --- /dev/null +++ b/EonaCat.ConnectionMonitor/Models/CountryProcess.cs @@ -0,0 +1,8 @@ +namespace EonaCat.ConnectionMonitor.Models +{ + public class CountryProcess + { + public string ProcessName { get; set; } + public int ConnectionCount { get; set; } + } +} diff --git a/EonaCat.ConnectionMonitor/Models/CountryStatistic.cs b/EonaCat.ConnectionMonitor/Models/CountryStatistic.cs new file mode 100644 index 0000000..0e59c22 --- /dev/null +++ b/EonaCat.ConnectionMonitor/Models/CountryStatistic.cs @@ -0,0 +1,11 @@ +namespace EonaCat.ConnectionMonitor.Models +{ + public class CountryStatistic + { + public string CountryCode { get; set; } + public string CountryName { get; set; } + public string FlagUrl { get; set; } + public int ConnectionCount { get; set; } + public List TopProcesses { get; set; } + } +} diff --git a/EonaCat.ConnectionMonitor/Models/GeolocationInfo.cs b/EonaCat.ConnectionMonitor/Models/GeolocationInfo.cs new file mode 100644 index 0000000..a0f5c8c --- /dev/null +++ b/EonaCat.ConnectionMonitor/Models/GeolocationInfo.cs @@ -0,0 +1,11 @@ +namespace EonaCat.ConnectionMonitor.Models +{ + public class GeolocationInfo + { + public string CountryCode { get; set; } + public string CountryName { get; set; } + public string ISP { get; set; } + public double Longitude { get; set; } + public double Latitude { get; set; } + } +} diff --git a/EonaCat.ConnectionMonitor/Models/ProcessStatistic.cs b/EonaCat.ConnectionMonitor/Models/ProcessStatistic.cs new file mode 100644 index 0000000..d9e918f --- /dev/null +++ b/EonaCat.ConnectionMonitor/Models/ProcessStatistic.cs @@ -0,0 +1,9 @@ +namespace EonaCat.ConnectionMonitor.Models +{ + public class ProcessStatistic + { + public string ProcessName { get; set; } + public int ConnectionCount { get; set; } + public int UniqueIPs { get; set; } + } +} diff --git a/EonaCat.ConnectionMonitor/Models/TopProcessInfo.cs b/EonaCat.ConnectionMonitor/Models/TopProcessInfo.cs new file mode 100644 index 0000000..81adcac --- /dev/null +++ b/EonaCat.ConnectionMonitor/Models/TopProcessInfo.cs @@ -0,0 +1,8 @@ +namespace EonaCat.ConnectionMonitor.Models +{ + public class TopProcessInfo + { + public string ProcessName { get; set; } + public int ConnectionCount { get; set; } + } +}