EonaCat.Port.Monitor/EonaCat.PortMonitor/MainWindow.xaml.cs

669 lines
27 KiB
C#

using EonaCat.Json;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Xml.Serialization;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.Windows.Input;
using EonaCat.Helpers.Commands;
using System.Text;
using MessageBox = System.Windows.MessageBox;
using System.Net.Http;
using System.Windows.Media;
using Application = System.Windows.Application;
using Brushes = System.Drawing.Brushes;
using Brush = System.Drawing.Brush;
using System.Windows.Threading;
namespace EonaCat.PortMonitor
{
public partial class MainWindow : Window
{
public ObservableCollection<ConnectionInfo> Connections { get; set; } = new ObservableCollection<ConnectionInfo>();
private List<ConnectionInfo> _previousConnections = new List<ConnectionInfo>();
private BackgroundWorker _monitorWorker;
private int _interval = 1 * 1000;
WPF.Tray.TrayIcon _trayIcon = new WPF.Tray.TrayIcon();
public bool MinimizeToTray { get; private set; }
public bool MinimizeOnStartup { get; private set; }
public bool ShowPopups { get; private set; } = true;
public bool FirstRun { get; private set; } = true;
public MainWindow()
{
InitializeComponent();
DataContext = this;
NetworkDataGrid.Items.Clear();
NetworkDataGrid.ItemsSource = Connections;
this.StateChanged += MainWindow_StateChanged;
SetupTrayIcon();
LoadSettings();
_monitorWorker = new BackgroundWorker();
_monitorWorker.DoWork += MonitorWorker_DoWork;
_monitorWorker.RunWorkerAsync();
}
protected override void OnActivated(EventArgs e)
{
base.OnActivated(e);
if (FirstRun)
{
if (MinimizeToTray || MinimizeOnStartup)
{
// Minimize to tray on first run
Hide();
FirstRun = false;
}
}
}
private void LoadSettings()
{
if (File.Exists("settings.json"))
{
var json = File.ReadAllText("settings.json");
var settings = JsonHelper.ToObject<dynamic>(json);
if (settings != null)
{
if (settings.MinimizeToTray != null)
{
MinimizeToTray = (bool)settings.MinimizeToTray;
}
if (settings.MinimizeOnStartup != null)
{
MinimizeOnStartup = (bool)settings.MinimizeOnStartup;
chkMinimize.IsChecked = MinimizeOnStartup;
}
if (settings.ShowPopups != null)
{
ShowPopups = (bool)settings.ShowPopups;
chkShowPopups.IsChecked = ShowPopups;
}
}
}
}
private void MainWindow_StateChanged(object? sender, EventArgs e)
{
if (WindowState == WindowState.Minimized)
{
// Minimize to tray and hide the window
this.Hide();
}
}
private void SetupTrayIcon()
{
// Load the icon from embedded resource
var icon = System.Drawing.Icon.ExtractAssociatedIcon(System.Reflection.Assembly.GetExecutingAssembly().Location);
_trayIcon.Icon = icon;
_trayIcon.TrayLeftMouseUp += (sender, args) =>
{
// Show the main window when the tray icon is clicked
WindowState = WindowState.Normal;
Show();
Activate();
};
_trayIcon.TrayRightMouseUp += (sender, args) =>
{
// Generate a context menu for the tray icon
var contextMenu = new System.Windows.Controls.ContextMenu();
contextMenu.Items.Add(new MenuItem { Header = "Show", Command = new GiveCommand(() => { Show(); WindowState = WindowState.Normal; }) });
contextMenu.Items.Add(new MenuItem
{
Header = "Exit",
Command = new GiveCommand(() =>
{
Close();
})
});
_trayIcon.ContextMenu = contextMenu;
};
}
protected override void OnClosing(CancelEventArgs e)
{
// Save settings before closing
var settings = new
{
MinimizeToTray = this.WindowState == WindowState.Minimized,
MinimizeOnStartup = chkMinimize.IsChecked == true,
ShowPopups = chkShowPopups.IsChecked == true
};
File.WriteAllBytes("settings.json", Encoding.UTF8.GetBytes(JsonHelper.ToJson(settings)));
base.OnClosing(e);
}
private void MonitorWorker_DoWork(object sender, DoWorkEventArgs e)
{
while (true)
{
MonitorConnections(null);
Thread.Sleep(_interval);
}
}
private async Task NotifyNewConnectionAsync(ConnectionInfo conn)
{
if (!ShowPopups)
{
return;
}
// Create a new notification manager
var notificationManager = new WPF.Popup.NotificationManager();
// Set up the popup's title and content
string popupTitle = $"New Connection: {conn.Protocol} {conn.LocalAddress}:{conn.LocalPort} -> {conn.RemoteAddress}:{conn.RemotePort}";
string popupContent = $"Process: {conn.ProcessName}\nState: {conn.State}";
// Set up the popup
var notification = new WPF.Popup.NotificationContent
{
Title = popupTitle,
Message = popupContent,
Type = WPF.Popup.NotificationType.Information,
};
// Show the popup
await notificationManager.ShowAsync(notification, expirationTime: TimeSpan.FromSeconds(3), onClick: new Action(() => { ShowConnectionDetails(conn); }));
}
private void ShowConnectionDetails(ConnectionInfo conn)
{
// Play a sound when the notification is clicked
System.Media.SystemSounds.Asterisk.Play();
var detailsWindow = new ConnectionDetailsWindow(conn);
detailsWindow.Show();
}
private void DataGrid_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// Get the DataGrid that triggered the event
DataGrid dataGrid = sender as DataGrid;
if (dataGrid.SelectedItem != null)
{
ShowConnectionDetails(dataGrid.SelectedItem as ConnectionInfo);
}
}
private List<ConnectionInfo> GetActiveConnections()
{
var connectionList = new List<ConnectionInfo>();
foreach (var conn in RunNetstatCommand())
{
if (string.IsNullOrEmpty(conn)) continue;
var match = ParseNetstatLine(conn);
if (match.Success)
{
var connInfo = new ConnectionInfo
{
Protocol = match.Groups["protocol"].Value,
LocalAddress = match.Groups["localAddress"].Value,
LocalPort = int.Parse(match.Groups["localPort"].Value),
RemoteAddress = match.Groups["remoteAddress"].Value,
RemotePort = int.Parse(match.Groups["remotePort"].Value),
State = string.IsNullOrEmpty(match.Groups["state"].Value) ? string.Empty : match.Groups["state"].Value, // Handle missing state for UDP
ProcessId = string.IsNullOrEmpty(match.Groups["pid"].Value) ? 0 : int.Parse(match.Groups["pid"].Value), // Default PID to 0 if missing
ProcessName = string.IsNullOrEmpty(match.Groups["pid"].Value) ? string.Empty : GetProcessName(int.Parse(match.Groups["pid"].Value)),
ConnectionTime = DateTime.Now,
ConnectionStatus = string.Empty
};
connectionList.Add(connInfo);
}
}
return connectionList;
}
private async void MonitorConnections(object state)
{
// Store the currently selected connection before the update
ConnectionInfo selectedConnection = null;
// Store the selected item from the DataGrid
Dispatcher.Invoke(new Action(() =>
{
selectedConnection = NetworkDataGrid.SelectedItem as ConnectionInfo;
}));
var currentConnections = await Task.Run(() => GetActiveConnections());
var inboundConnections = currentConnections.Where(c => IsInboundConnection(c)).ToList();
var outboundConnections = currentConnections.Where(c => IsOutboundConnection(c)).ToList();
int totalConnections = currentConnections.Count;
// Compare and update connections with the existing ones
var newConnections = currentConnections.Where(c => !_previousConnections.Any(p => p.LocalAddress == c.LocalAddress && p.LocalPort == c.LocalPort)).ToList();
var closedConnections = _previousConnections.Where(p => !currentConnections.Any(c => c.LocalAddress == p.LocalAddress && c.LocalPort == p.LocalPort)).ToList();
var stateChangedConnections = currentConnections.Where(c => _previousConnections.Any(p => p.LocalAddress == c.LocalAddress && p.LocalPort == c.LocalPort && p.State != c.State)).ToList();
// Add new connections or update existing ones
foreach (var newConn in newConnections)
{
newConn.ConnectionStatus = "New";
if (newConn.FirstConnectionTime == DateTime.MinValue)
{
newConn.FirstConnectionTime = DateTime.Now;
}
// Add the new connection to the collection
Dispatcher.Invoke(() => Connections.Add(newConn));
await NotifyNewConnectionAsync(newConn);
}
// Handle closed connections
foreach (var closedConn in closedConnections)
{
var conn = Connections.FirstOrDefault(c => c.LocalAddress == closedConn.LocalAddress && c.LocalPort == closedConn.LocalPort);
if (conn != null)
{
conn.ConnectionStatus = "Closed";
// Remove the connection if the status was set to closed for more than 5 seconds
if (conn.ConnectionDuration.TotalSeconds > 5)
{
await Dispatcher.InvokeAsync(() => Connections.Remove(conn));
}
}
}
// Handle state changes
foreach (var stateChangedConn in stateChangedConnections)
{
var conn = Connections.FirstOrDefault(c => c.LocalAddress == stateChangedConn.LocalAddress && c.LocalPort == stateChangedConn.LocalPort);
if (conn != null)
{
conn.ConnectionStatus = "State Changed";
conn.BackgroundColor = Brushes.Yellow;
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
timer.Tick += (sender, args) =>
{
conn.BackgroundColor = Brushes.Transparent;
timer.Stop();
};
timer.Start();
}
}
await Dispatcher.InvokeAsync(() =>
{
InboundDataGrid.ItemsSource = Connections.Where(x => IsInboundConnection(x));
OutboundDataGrid.ItemsSource = Connections.Where(x => IsOutboundConnection(x));
NetworkDataGrid.Items.Refresh();
FilterConnections_KeyUp(null, null);
UpdateConnectionStats(totalConnections, inboundConnections.Count, outboundConnections.Count);
});
// Re-select the previously selected item after the refresh
if (selectedConnection != null)
{
var selectedItem = Connections.FirstOrDefault(c => c.LocalAddress == selectedConnection.LocalAddress && c.LocalPort == selectedConnection.LocalPort);
if (selectedItem != null)
{
Dispatcher.Invoke(() =>
{
// Set the previously selected item back to the DataGrid
NetworkDataGrid.SelectedItem = selectedItem;
});
}
}
// Update the previousConnections list for the next iteration
_previousConnections = currentConnections;
}
private void UpdateConnectionStats(int total, int inbound, int outbound)
{
var tcpConnections = Connections.Count(c => c.Protocol == "TCP");
var udpConnections = Connections.Count(c => c.Protocol == "UDP");
var avgConnectionTime = Connections.Average(c => (DateTime.Now - c.ConnectionTime).TotalSeconds);
TcpUdpStatsText.Text = $"TCP Connections: {tcpConnections}\nUDP Connections: {udpConnections}\nAverage Connection Time: {avgConnectionTime:F2} seconds\nTotal: {total}\nInbound: {inbound}\nOutbound: {outbound}";
}
private bool IsInboundConnection(ConnectionInfo connection)
{
// Inbound: LocalAddress is valid, and the machine is accepting the connection (server-side).
return IsValidIpAddress(connection.LocalAddress) &&
connection.LocalAddress != "0.0.0.0" &&
connection.LocalAddress != "::" &&
!string.IsNullOrEmpty(connection.RemoteAddress) &&
connection.RemoteAddress != "0.0.0.0" &&
connection.RemoteAddress != "::" &&
connection.RemoteAddress != "N/A" &&
connection.RemotePort != 0;
}
private bool IsOutboundConnection(ConnectionInfo connection)
{
// Outbound: LocalAddress is valid, and the machine is initiating the connection (client-side).
return IsValidIpAddress(connection.LocalAddress) &&
connection.LocalAddress != "0.0.0.0" &&
connection.LocalAddress != "::" &&
IsValidIpAddress(connection.RemoteAddress) &&
connection.RemoteAddress != "0.0.0.0" &&
connection.RemoteAddress != "::" &&
connection.RemoteAddress != "N/A" &&
connection.LocalPort != 0;
}
private bool IsValidIpAddress(string ipAddress)
{
return !string.IsNullOrEmpty(ipAddress) && (IsValidIPv4(ipAddress) || IsValidIPv6(ipAddress));
}
private bool IsValidIPv4(string ipAddress)
{
return System.Net.IPAddress.TryParse(ipAddress, out var ip) && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork;
}
private bool IsValidIPv6(string ipAddress)
{
return System.Net.IPAddress.TryParse(ipAddress, out var ip) && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6;
}
private List<string> RunNetstatCommand()
{
var result = new List<string>();
try
{
ProcessStartInfo startInfo = new ProcessStartInfo("netstat", "-ano")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
using (var reader = process.StandardOutput)
{
string line;
while ((line = reader.ReadLine()) != null)
{
result.Add(line);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error running netstat: {ex.Message}");
}
return result;
}
private Match ParseNetstatLine(string line)
{
// Regular expression for both TCP and UDP connections, IPv4 and IPv6.
var regex = new Regex(@"(?<protocol>TCP|UDP)\s+(?<localAddress>[\d\.]+|\[?[A-F0-9:]+\]?):(?<localPort>\d+)\s+(?<remoteAddress>[\d\.]+|\[?[A-F0-9:]+\]?):(?<remotePort>\d+)\s*(?<state>\S*)?\s*(?<pid>\d+)?");
var match = regex.Match(line);
return match;
}
private string GetProcessName(int pid)
{
try
{
var process = Process.GetProcessById(pid);
return process.ProcessName;
}
catch
{
return "N/A";
}
}
private void FilterConnections_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
// Get the filter keyword from the input box
var keywordFilter = KeywordFilter.Text.ToLower();
// Filter the connections based on the keyword
var filteredConnections = Connections?.Where(c =>
(string.IsNullOrEmpty(keywordFilter) ||
c.ProcessName.ToLower().Contains(keywordFilter) ||
c.RemoteAddress.ToLower().Contains(keywordFilter) ||
c.LocalAddress.ToLower().Contains(keywordFilter) ||
c.State.ToLower().Contains(keywordFilter) ||
c.Protocol.ToLower().Contains(keywordFilter) ||
c.LocalPort.ToString().Contains(keywordFilter) ||
c.RemotePort.ToString().Contains(keywordFilter)
)
).ToList();
// Clear the existing items and add filtered ones to the ObservableCollection
Connections.Clear();
if (filteredConnections != null)
{
foreach (var conn in filteredConnections)
{
Connections.Add(conn);
}
}
}
private void FilterConnections_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
FilterConnections_KeyUp(sender, null);
}
private void ExportToJsonButton_Click(object sender, RoutedEventArgs e)
{
var json = JsonHelper.ToJson(Connections, Json.Formatting.Indented);
File.WriteAllText("connections.json", json);
MessageBox.Show("Connections exported to connections.json", "Export Successful", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void ExportToXmlButton_Click(object sender, RoutedEventArgs e)
{
var xmlSerializer = new XmlSerializer(typeof(List<ConnectionInfo>));
using (var writer = new StreamWriter("connections.xml"))
{
xmlSerializer.Serialize(writer, Connections);
}
MessageBox.Show("Connections exported to connections.xml", "Export Successful", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void ExportToCsvButton_Click(object sender, RoutedEventArgs e)
{
var csvLines = new List<string> { "Protocol,LocalAddress,LocalPort,RemoteAddress,RemotePort,State,ProcessName,ConnectionTime" };
csvLines.AddRange(Connections.Select(c => $"{c.Protocol},{c.LocalAddress},{c.LocalPort},{c.RemoteAddress},{c.RemotePort},{c.State},{c.ProcessName},{c.ConnectionTime}"));
File.WriteAllLines("connections.csv", csvLines);
MessageBox.Show("Connections exported to connections.csv", "Export Successful", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void SaveFilterPresetButton_Click(object sender, RoutedEventArgs e)
{
var filters = new
{
Keywords = KeywordFilter.Text,
};
var json = JsonHelper.ToJson(filters);
File.WriteAllText("filterPreset.json", json);
}
private void LoadFilterPresetButton_Click(object sender, RoutedEventArgs e)
{
if (File.Exists("filterPreset.json"))
{
var json = File.ReadAllText("filterPreset.json");
var filters = JsonHelper.ToObject<dynamic>(json);
KeywordFilter.Text = filters.Keywords;
FilterConnections_KeyUp(null, null);
}
}
private void ExportToHtmlButton_Click(object sender, RoutedEventArgs e)
{
var htmlContent = "<html><body><table border='1'><tr><th>Protocol</th><th>LocalAddress</th><th>LocalPort</th><th>RemoteAddress</th><th>RemotePort</th><th>State</th><th>ProcessName</th><th>ConnectionTime</th></tr>";
foreach (var conn in Connections)
{
htmlContent += $"<tr><td>{conn.Protocol}</td><td>{conn.LocalAddress}</td><td>{conn.LocalPort}</td><td>{conn.RemoteAddress}</td><td>{conn.RemotePort}</td><td>{conn.State}</td><td>{conn.ProcessName}</td><td>{conn.ConnectionTime}</td></tr>";
}
htmlContent += "</table></body></html>";
File.WriteAllText("connections.html", htmlContent);
MessageBox.Show("Connections exported to connections.html", "Export Successful", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void chkMinimize_Checked(object sender, RoutedEventArgs e)
{
MinimizeOnStartup = chkMinimize.IsChecked == true;
}
private void chkShowPopups_Checked(object sender, RoutedEventArgs e)
{
ShowPopups = chkShowPopups.IsChecked == true;
}
private void KillProcessButton_Click(object sender, RoutedEventArgs e)
{
if (NetworkDataGrid.SelectedItem is ConnectionInfo selectedConnection)
{
try
{
var process = Process.GetProcessById(selectedConnection.ProcessId);
process.Kill();
MessageBox.Show($"Process {selectedConnection.ProcessName} (PID: {selectedConnection.ProcessId}) terminated.", "Process Killed", MessageBoxButton.OK, MessageBoxImage.Warning);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to terminate process: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
private async void IpLookupButton_Click(object sender, RoutedEventArgs e)
{
if (NetworkDataGrid.SelectedItem is ConnectionInfo selectedConnection)
{
string apiUrl = $"https://reallyfreegeoip.org/json/{selectedConnection.RemoteAddress}";
using (HttpClient client = new HttpClient())
{
var response = await client.GetStringAsync(apiUrl);
MessageBox.Show(response, "IP Lookup", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
}
private void BlockIpButton_Click(object sender, RoutedEventArgs e)
{
// Check if an item is selected in the DataGrid
if (NetworkDataGrid.SelectedItem is ConnectionInfo selectedConnection)
{
// Ask the user for confirmation
var result = MessageBox.Show(
$"Are you sure you want to block the IP address: {selectedConnection.RemoteAddress}?",
"Confirm Block",
MessageBoxButton.YesNo,
MessageBoxImage.Question
);
// If the user clicks 'Yes', proceed with blocking the IP
if (result == MessageBoxResult.Yes)
{
string command = $"netsh advfirewall firewall add rule name=\"Blocked {selectedConnection.RemoteAddress}\" dir=in action=block remoteip={selectedConnection.RemoteAddress}";
Process.Start(new ProcessStartInfo("cmd.exe", "/C " + command) { CreateNoWindow = true });
MessageBox.Show($"Blocked IP: {selectedConnection.RemoteAddress}", "Firewall Rule Added", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
}
private void btnTheme_Click(object sender, RoutedEventArgs e)
{
// Ensure the resources exist
if (!Application.Current.Resources.Contains("DynamicBackgroundColor") ||
!Application.Current.Resources.Contains("DynamicTextColor"))
{
MessageBox.Show("Theme resources are missing!", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
// Retrieve current background color
SolidColorBrush currentBg = (SolidColorBrush)Application.Current.Resources["DynamicBackgroundColor"];
if (currentBg.Color == Colors.Black) // Dark Mode → Light Mode
{
Application.Current.Resources["DynamicBackgroundColor"] = new SolidColorBrush(Colors.White);
Application.Current.Resources["DynamicTextColor"] = new SolidColorBrush(Colors.Black);
btnTheme.Content = "🌙"; // Set icon to moon for dark mode
}
else // Light Mode → Dark Mode
{
Application.Current.Resources["DynamicBackgroundColor"] = new SolidColorBrush(Colors.Black);
Application.Current.Resources["DynamicTextColor"] = new SolidColorBrush(Colors.White);
btnTheme.Content = "🌞"; // Set icon to sun for light mode
}
}
}
public class ConnectionInfo : INotifyPropertyChanged
{
private Brush _backgroundColor;
public string Protocol { get; set; }
public string LocalAddress { get; set; }
public int LocalPort { get; set; }
public string RemoteAddress { get; set; }
public int RemotePort { get; set; }
public string State { get; set; }
public int ProcessId { get; set; }
public string ProcessName { get; set; }
public DateTime ConnectionTime { get; set; }
public DateTime FirstConnectionTime { get; set; }
public string ConnectionStatus { get; set; }
public TimeSpan ConnectionDuration => DateTime.Now - FirstConnectionTime;
public string FormattedConnectionDuration => ConnectionDuration.ToString(@"hh\:mm\:ss");
public Brush BackgroundColor
{
get { return _backgroundColor; }
set
{
if (_backgroundColor != value)
{
_backgroundColor = value;
OnPropertyChanged(nameof(BackgroundColor));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}