Initial version

This commit is contained in:
2025-09-28 01:14:33 +02:00
parent 737bde994b
commit 750e201f30
14 changed files with 1712 additions and 334 deletions

View File

@@ -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();
}
}
}

View File

@@ -9,6 +9,11 @@
<ApplicationIcon>EonaCat.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EonaCat.Json" Version="1.1.9" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3485.44" />
</ItemGroup>
<ItemGroup>
<Resource Include="EonaCat.ico" />
</ItemGroup>

View File

@@ -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<string, (double Lat, double Lon)> _countryCentroidCache = new();
private readonly Dictionary<string, string> _countryFlagCache = new();
private readonly HttpClient _mapHttpClient = new HttpClient();
private WebView2 _mapWebView;
private (double Lat, double Lon)? _localCoord;
private const string _mapHtml = @"<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta http-equiv='Content-Security-Policy' content=""default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'"">
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'/>
<link rel='stylesheet' href='https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css'/>
<link rel='stylesheet' href='https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css'/>
<style>
html, body { height:100%; margin:0; padding:0; font-family: 'Segoe UI', Arial, sans-serif;}
#tabBar { display:flex; background:#222; color:#fff; }
.tab { padding:8px 12px; cursor:pointer; flex:1; text-align:center; }
.tab.active { background:#444; font-weight:bold; }
#liveMapContainer, #historicalMapContainer { height:calc(100% - 36px); width:100%; position:relative; }
.leaflet-popup-content-wrapper { background: #1f1f1f !important; color: #f0f0f0 !important; border-radius: 10px !important; padding: 10px 14px !important; font-size: 13px; }
.pulse-marker { width:20px;height:20px;border-radius:50%;background:#ff3333;box-shadow:0 0 14px #ff3333;animation:pulse 1.5s infinite ease-in-out; }
@keyframes pulse {0%{transform:scale(0.8);opacity:0.7;}50%{transform:scale(1.4);opacity:0.3;}100%{transform:scale(0.8);opacity:0.7;}}
#statsPanel { position:absolute; top:10px; right:10px; background:rgba(25,25,25,0.9); color:#fff; padding:10px 12px; border-radius:6px; font-size:13px; z-index:1000; max-width:260px; display:flex; flex-direction:column; gap:8px; box-sizing:border-box; transition: all 0.3s ease; pointer-events:auto; }
#statsPanel button, #statsPanel input[type='range'], #statsPanel select { width:100%; box-sizing:border-box; }
#statsPanel.collapsed #statsContent { display:none; }
#statsPanel.collapsed #statsHeader::after { content:' ▶'; }
#statsPanel #statsHeader::after { content:' ▼'; }
#topConnections { max-height:120px; overflow:auto; margin-top:6px; font-size:12px; color:#ccc; }
#histControls { position:absolute; bottom:10px; left:10px; background:rgba(25,25,25,0.9); color:#fff; padding:6px 10px; border-radius:6px; font-size:12px; z-index:1000; display:flex; align-items:center; gap:6px; }
#histControls input[type='range'] { width:120px; }
.leaflet-control-attribution {
display: none !important;
}
.leaflet-control-logo,
.leaflet-control .leaflet-control-attribution img {
display: none !important;
}
#histStatsPanel {
position:absolute;
top:10px;
right:10px;
background:rgba(25,25,25,0.9);
color:#fff;
padding:10px 12px;
border-radius:6px;
font-size:13px;
z-index:1000;
max-width:260px;
display:flex;
flex-direction:column;
gap:8px;
box-sizing:border-box;
transition: all 0.3s ease;
pointer-events:auto;
}
#histStatsPanel.collapsed #histStatsContent { display:none; }
#histStatsPanel.collapsed #histStatsHeader::after { content:' ▶'; }
#histStatsPanel #histStatsHeader::after { content:' ▼'; }
</style>
</head>
<body>
<div id=""tabBar"">
<div class=""tab active"" id=""liveTabBtn"">Live Map</div>
<div class=""tab"" id=""histTabBtn"">Historical Map</div>
</div>
<div id=""liveMapContainer"">
<div id=""map"" style=""height:100%; width:100%;""></div>
<div id=""statsPanel"">
<div id=""statsHeader"" style=""font-weight:bold; cursor:pointer; padding:4px; background:#333; border-radius:4px;"">Stats</div>
<div id=""statsContent"" style=""margin-top:6px;"">
<div id=""statsText"">Connections: 0 | Top Country: N/A</div>
<button id=""toggleTrailsBtn"" style=""margin-top:6px; padding:6px 8px; border:none; border-radius:4px; background:#444; color:#fff; cursor:pointer;"">Trails: ON</button>
<div style=""display:flex; flex-direction:column; margin-top:4px;"">
<label for=""trailDuration"" style=""font-size:12px; color:#ccc;"">Trail Duration</label>
<input type=""range"" id=""trailDuration"" min=""2"" max=""20"" value=""8"" step=""1"">
<span id=""trailDurationValue"" style=""font-size:12px; color:#ffcc00;"">8s</span>
</div>
<div style=""display:flex; flex-direction:column; margin-top:4px;"">
<label for=""trailColor"" style=""font-size:12px; color:#ccc;"">Trail Color</label>
<select id=""trailColor"" style=""padding:2px 4px; background:#333; color:#fff; border:none; border-radius:4px;"">
<option value='auto'>Auto (By Direction)</option>
<option value='red'>Red</option>
<option value='blue'>Blue</option>
<option value='yellow'>Yellow</option>
<option value='white'>White</option>
</select>
</div>
<div id=""topConnections""><b>Top Connections:</b><br>No data yet</div>
</div>
</div>
</div>
<div id=""historicalMapContainer"" style=""display:none;"">
<div id=""histMap"" style=""height:100%; width:100%;""></div>
<div id=""histControls"">
<button id=""playPauseBtn"">Play</button>
<input type=""range"" id=""timeSlider"" min=""0"" max=""0"" value=""0"">
<span id=""timeLabel"">0/0</span>
</div>
<div id=""histStatsPanel"">
<div id=""histStatsHeader"" style=""font-weight:bold; cursor:pointer; padding:4px; background:#333; border-radius:4px;"">Stats</div>
<div id=""histStatsContent"" style=""margin-top:6px;"">
<div id=""histStatsText"">Connections: 0 | Top Country: N/A</div>
<div id=""histTopConnections""><b>Top Connections:</b><br>No data yet</div>
</div>
</div>
</div>
<script src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'></script>
<script src='https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/leaflet-polylinedecorator/1.1.0/leaflet.polylineDecorator.min.js'></script>
<script src='https://unpkg.com/leaflet.heat/dist/leaflet-heat.js'></script>
<script>
/* ------------------- TAB SWITCH ------------------- */
const liveTabBtn = document.getElementById('liveTabBtn');
const histTabBtn = document.getElementById('histTabBtn');
const liveMapContainer = document.getElementById('liveMapContainer');
const historicalMapContainer = document.getElementById('historicalMapContainer');
const histConnectionKeys = new Set();
liveTabBtn.addEventListener('click', ()=>{
liveTabBtn.classList.add('active'); histTabBtn.classList.remove('active');
liveMapContainer.style.display='block';
historicalMapContainer.style.display='none';
map.invalidateSize();
ensureHeatLayer();
});
histTabBtn.addEventListener('click', ()=>{
histTabBtn.classList.add('active'); liveTabBtn.classList.remove('active');
liveMapContainer.style.display='none';
historicalMapContainer.style.display='block';
histMap.invalidateSize();
});
/* ------------------- LIVE MAP ------------------- */
const canvasRenderer = L.canvas({ padding: 0.5 });
const map = L.map('map', { worldCopyJump:true, zoomControl:false }).setView([20,0],2);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png').addTo(map);
const logoControl = L.control({ position: 'bottomright' });
logoControl.onAdd = function(map) {
const div = L.DomUtil.create('div', 'leaflet-logo');
div.innerHTML = `<img src=""https://eonacat.com/images/logo.svg"" style=""width:80px; height:auto;"">`;
return div;
};
logoControl.addTo(map);
const markers = L.markerClusterGroup({ disableClusteringAtZoom:6, iconCreateFunction: cluster => {
const count = cluster.getAllChildMarkers().reduce((sum,m)=>sum+(m.options.count||1),0);
return L.divIcon({ html:`<div style='background:#ff5500;border-radius:50%;width:40px;height:40px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;'>${count}</div>`, className:'' });
}});
map.addLayer(markers);
const lines = L.layerGroup().addTo(map);
const trails = L.layerGroup({ renderer: canvasRenderer }).addTo(map);
let localMarker=null;
const markerMap=new Map();
let trailsEnabled=true, trailDuration=8000, trailColorMode='auto';
let heatLayer;
/* ------------------- STATS PANEL ------------------- */
const statsPanel=document.getElementById('statsPanel');
const statsHeader=document.getElementById('statsHeader');
let isDragging=false, offsetX=0, offsetY=0;
statsHeader.addEventListener('mousedown', e=>{ isDragging=true; offsetX=e.clientX-statsPanel.offsetLeft; offsetY=e.clientY-statsPanel.offsetTop; statsPanel.style.cursor='grabbing'; });
document.addEventListener('mousemove', e=>{ if(!isDragging) return; statsPanel.style.left=e.clientX-offsetX+'px'; statsPanel.style.top=e.clientY-offsetY+'px'; });
document.addEventListener('mouseup', ()=>{ if(isDragging){ isDragging=false; statsPanel.style.cursor='default'; } });
statsHeader.addEventListener('click', ()=>{ if(isDragging) return; statsPanel.classList.toggle('collapsed'); });
document.getElementById('trailColor').addEventListener('change', e=>trailColorMode=e.target.value);
document.getElementById('trailDuration').addEventListener('input', e=>{
trailDuration=parseInt(e.target.value)*1000;
document.getElementById('trailDurationValue').innerText=e.target.value+'s';
});
document.getElementById('toggleTrailsBtn').addEventListener('click', ()=>{
trailsEnabled=!trailsEnabled;
document.getElementById('toggleTrailsBtn').innerText='Trails: '+(trailsEnabled?'ON':'OFF');
if(!trailsEnabled) trails.clearLayers();
});
const histStatsPanel = document.getElementById('histStatsPanel');
const histStatsHeader = document.getElementById('histStatsHeader');
histStatsHeader.addEventListener('click', () => {
histStatsPanel.classList.toggle('collapsed');
});
/* ------------------- HELPER FUNCTIONS ------------------- */
function getPopupHtml(conn){
const flag=url=>url?`<img src='${url}' width='24' height='16' style='margin-right:6px;border-radius:2px;'>`:'';
const listItems=arr=>arr?.length?`<ul style='margin:0 0 0 16px;padding:0;list-style-type: disc;'>${arr.map(p=>`<li>${p.ProcessName}: ${p.ConnectionCount}</li>`).join('')}</ul>`:'';
return `<div style='min-width:240px;'>
<div style='display:flex;align-items:center;margin-bottom:6px;'>${flag(conn.fromFlagUrl)}<div style='font-weight:bold;color:#ffcc00;'>${conn.fromCountryName||conn.fromCountryCode}</div>${conn.fromIp?`<div style='margin-left:6px;font-size:11px;color:#aaa;'>(${conn.fromIp})</div>`:''}</div>
<div style='text-align:center;margin-bottom:6px;color:#ff5555;font-size:14px;'>&#8595;&#8593;</div>
<div style='display:flex;align-items:center;margin-bottom:8px;'>${flag(conn.toFlagUrl)}<div style='font-weight:bold;color:#66ccff;'>${conn.toCountryName||conn.toCountryCode}</div>${conn.toIp?`<div style='margin-left:6px;font-size:11px;color:#aaa;'>(${conn.toIp})</div>`:''}</div>
<div style='margin-bottom:8px;font-weight:bold;color:#ff88ff;font-size:13px;'>Connections: ${conn.connectionCount||0}</div>
${conn.fromTopProcesses?.length?`<div style='margin-bottom:6px;'><div style='font-weight:bold;color:#ffcc66;margin-bottom:2px;'>Top Processes at Source:</div>${listItems(conn.fromTopProcesses)}</div>`:''}
${conn.toTopProcesses?.length?`<div><div style='font-weight:bold;color:#66ccff;margin-bottom:2px;'>Top Processes at Destination:</div>${listItems(conn.toTopProcesses)}</div>`:''}
</div>`;
}
function hasValidPopupData(conn){
return !!((conn.fromCountryName||conn.fromCountryCode)||(conn.toCountryName||conn.toCountryCode)||conn.connectionCount>0||(conn.fromTopProcesses?.length)||(conn.toTopProcesses?.length));
}
function addTrail(from, to, direction, country, speed) {
if (!trailsEnabled || map.getZoom() > 6) return;
const color = trailColorMode === 'auto'
? direction === 'inbound' ? 'rgba(255,0,0,0.6)'
: direction === 'outbound' ? 'rgba(0,150,255,0.6)'
: 'rgba(255,215,0,0.6)'
: trailColorMode === 'red' ? 'rgba(255,0,0,0.6)'
: trailColorMode === 'blue' ? 'rgba(0,150,255,0.6)'
: trailColorMode === 'yellow' ? 'rgba(255,215,0,0.6)'
: 'rgba(255,255,255,0.6)';
// Main polyline (canvas renderer, non-interactive)
const trail = L.polyline([from, to], {
color, weight: 2, opacity: 0.6,
renderer: canvasRenderer, interactive: false
}).addTo(trails);
// Arrow decorator (SVG)
const decorator = L.polylineDecorator(trail, {
patterns: [{
offset: '0%',
repeat: '20px',
symbol: L.Symbol.arrowHead({ pixelSize: 8, pathOptions: { color, opacity: 0.8 } })
}]
}).addTo(trails);
// Ensure all SVG elements do not block pointer events
decorator.eachLayer(l => {
if (l._path) l._path.style.pointerEvents = 'none';
});
const trailObj = { trail, decorator, startTime: performance.now(), duration: speed || trailDuration };
const animate = time => {
const elapsed = time - trailObj.startTime;
const progress = elapsed / trailObj.duration;
if (progress >= 1) {
trails.removeLayer(trailObj.trail);
trails.removeLayer(trailObj.decorator);
return;
}
const opacity = 0.6 * (1 - progress);
trail.setStyle({ opacity });
decorator.setPatterns([{
offset: (progress * 100) % 20 + '%',
repeat: '20px',
symbol: L.Symbol.arrowHead({ pixelSize: 8, pathOptions: { color, opacity: opacity * 1.2 } })
}]);
// Make sure new decorator layers are non-interactive
decorator.eachLayer(l => {
if (l._path) l._path.style.pointerEvents = 'none';
});
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
/* ------------------- HEAT LAYER FIX ------------------- */
function ensureHeatLayer(){
if(!heatLayer && map._container.offsetHeight>0){
heatLayer=L.heatLayer([], {radius:25, blur:15, maxZoom:8}).addTo(map);
}
}
const histEventKeys = new Set();
/* ------------------- LIVE MAP UPDATE ------------------- */
window.updateMarkersAndLines = (countries, connections, localCoord) => {
try {
const bounds = [];
let totalConnections = 0;
const activeKeys = new Set();
// Count total connections
connections?.forEach(conn => {
totalConnections += conn.connectionCount || 1;
});
// Update country markers
countries.forEach(item => {
if (!item.lat || !item.lon) return;
const key = item.countryCode;
activeKeys.add(key);
let marker;
if (!markerMap[key]) {
marker = item.count > 50
? L.marker([item.lat, item.lon], { icon: L.divIcon({ className: 'pulse-marker' }) })
: L.circleMarker([item.lat, item.lon], {
radius: Math.min(15, Math.max(5, Math.sqrt(item.count))),
fillColor: '#3388ff',
color: '#3388ff',
weight: 1,
opacity: 0.6,
fillOpacity: 0.6
});
markers.addLayer(marker);
marker.bindPopup(`Total Connections: ${item.count || 0}`);
markerMap[key] = marker;
} else {
marker = markerMap[key];
if (marker.setRadius) marker.setRadius(Math.min(15, Math.max(5, Math.sqrt(item.count))));
marker.bindPopup(`Total Connections: ${item.count || 0}`);
}
bounds.push([item.lat, item.lon]);
});
// Remove markers for inactive countries
for (const key of markerMap.keys()) {
if (!activeKeys.has(key)) {
markers.removeLayer(markerMap.get(key));
markerMap.delete(key);
}
}
// Heat layer
ensureHeatLayer();
if (heatLayer) heatLayer.setLatLngs(countries.map(c => [c.lat, c.lon, Math.sqrt(c.count)]));
// Local marker
if (localCoord && countries.length > 0) {
if (!localMarker) {
localMarker = L.circleMarker([localCoord.Lat, localCoord.Lon], {
radius: 12,
fillColor: '#00ffcc',
color: '#00ffcc',
fillOpacity: 0.8
}).addTo(map);
localMarker.bindPopup('You are here!');
} else {
localMarker.setLatLng([localCoord.Lat, localCoord.Lon]);
localMarker.bindPopup('You are here!');
}
setTimeout(() => { localMarker.openPopup(); }, 50);
} else if (localMarker) {
map.removeLayer(localMarker);
localMarker = null;
}
// Clear lines
lines.clearLayers();
// Track live top connections
const liveConnectionCounts = {};
connections?.forEach(conn => {
if (!conn.fromLat || !conn.fromLon || !conn.toLat || !conn.toLon) return;
// Draw line
const weight = Math.max(2, Math.min(10, 1 + conn.connectionCount / 5));
const lineColor = conn.connectionCount > 50 ? 'darkred' : 'red';
const line = L.polyline([[conn.fromLat, conn.fromLon], [conn.toLat, conn.toLon]], {
color: lineColor,
weight,
opacity: 0.6,
renderer: canvasRenderer,
interactive: true
}).addTo(lines);
if (hasValidPopupData(conn)) {
line.bindPopup(getPopupHtml(conn));
}
// Trail animation
const minSpeed = 2000, maxSpeed = 10000;
const speed = Math.max(minSpeed, maxSpeed - (conn.connectionCount * 50));
let direction = 'mixed';
if (conn.fromCountryCode && conn.toCountryCode) {
if (conn.toCountryCode === 'LOCAL') direction = 'inbound';
else if (conn.fromCountryCode === 'LOCAL') direction = 'outbound';
}
addTrail([conn.fromLat, conn.fromLon], [conn.toLat, conn.toLon], direction, conn.fromCountryCode, speed);
// Track live top connections
if (conn.fromCountryCode && conn.toCountryCode) {
const key = `${conn.fromCountryCode}→${conn.toCountryCode}`;
liveConnectionCounts[key] = (liveConnectionCounts[key] || 0) + (conn.connectionCount || 1);
}
});
// Update live stats panel
let topCountry = 'N/A', topCount = 0;
countries.forEach(c => {
if ((c.count || 0) > topCount) { topCount = c.count; topCountry = c.countryCode; }
});
document.getElementById('statsText').innerText =
`Connections: ${totalConnections} | Top Country: ${topCountry}`;
// Live top connections (top 10)
const sortedLiveConnections = Object.entries(liveConnectionCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
let topLiveConnHtml = '<b>Top Connections:</b><br>';
if (sortedLiveConnections.length) {
topLiveConnHtml += sortedLiveConnections
.map(([pair, count]) => `${pair}: ${count}`)
.join('<br>');
} else {
topLiveConnHtml += 'No data yet';
}
document.getElementById('topConnections').innerHTML = topLiveConnHtml;
// ----------------------
// Track live totals globally for historical map
// ----------------------
window.liveTotals = {
totalConnections,
topCountry,
topConnections: sortedLiveConnections.reduce((acc, [pair, count]) => { acc[pair] = count; return acc; }, {})
};
// Update historical map events
if (histEvents.length) {
updateHistStats(histEvents.length - 1, true);
}
// Historical event logging
const round = n => Math.round(n * 10000) / 10000;
connections?.forEach(conn => {
const histKey = `${round(conn.fromLat)},${round(conn.fromLon)}-${round(conn.toLat)},${round(conn.toLon)}`;
if (!histEventKeys.has(histKey)) {
histEventKeys.add(histKey);
histEvents.push({
timestamp: Date.now(),
fromLat: conn.fromLat,
fromLon: conn.fromLon,
toLat: conn.toLat,
toLon: conn.toLon,
popupHtml: hasValidPopupData(conn) ? getPopupHtml(conn) : undefined,
trail: {
direction: conn.fromCountryCode === 'LOCAL' ? 'outbound' : (conn.toCountryCode === 'LOCAL' ? 'inbound' : 'mixed'),
speed: Math.max(2000, 10000 - (conn.connectionCount * 50)),
colorMode: trailColorMode
},
fromCountryCode: conn.fromCountryCode,
toCountryCode: conn.toCountryCode,
connectionCount: conn.connectionCount || 1
});
timeSlider.max = histEvents.length - 1;
timeLabel.innerText = `${currentTimeIndex + 1}/${histEvents.length}`;
}
});
} catch (e) {
console.error(e);
}
};
/* ------------------- HISTORICAL MAP ------------------- */
const histMap = L.map('histMap', { worldCopyJump:true, zoomControl:false }).setView([20,0],2);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png').addTo(histMap);
const histLogoControl = L.control({ position: 'bottomright' });
histLogoControl.onAdd = function(map) {
const div = L.DomUtil.create('div', 'leaflet-logo');
div.innerHTML = `<img src=""https://eonacat.com/images/logo.svg"" style=""width:80px; height:auto;"">`;
return div;
};
histLogoControl.addTo(histMap);
const histMarkers = L.markerClusterGroup({ disableClusteringAtZoom:6 });
const histLines = L.layerGroup().addTo(histMap);
const histTrails = L.layerGroup({ renderer: canvasRenderer }).addTo(histMap);
histMap.addLayer(histMarkers);
let histEvents = [];
const histMarkerMap = new Map();
const histTrailMap = new Map();
const timeSlider = document.getElementById('timeSlider');
const timeLabel = document.getElementById('timeLabel');
const playPauseBtn = document.getElementById('playPauseBtn');
/* Load historical data */
function loadHistoricalData(events){
histEvents = events?.sort((a,b)=>a.timestamp-b.timestamp) || [];
currentTimeIndex = 0;
timeSlider.max = Math.max(histEvents.length - 1, 0);
timeSlider.value = 0;
timeLabel.innerText = `${histEvents.length ? 1 : 0}/${histEvents.length}`;
showHistoricalEvent(currentTimeIndex);
}
/* Display a single event on the map */
function showHistoricalEvent(index) {
// Clear map layers
histLines.clearLayers();
histTrails.clearLayers();
histTrailMap.clear();
histMarkerMap.clear();
histMarkers.clearLayers();
// Show all events up to current index
for (let i = 0; i <= index; i++) {
const ev = histEvents[i];
if (!ev) continue;
const popupHtml = ev.popupHtml || `From: ${ev.fromLat},${ev.fromLon} <br> To: ${ev.toLat},${ev.toLon}`;
// From marker
const fromKey = `${ev.fromLat},${ev.fromLon}`;
if (!histMarkerMap.has(fromKey)) {
const markerFrom = L.circleMarker([ev.fromLat, ev.fromLon], {
radius: 8, fillColor: '#ff5500', color: '#ff5500', fillOpacity: 0.8
}).bindPopup(popupHtml);
histMarkers.addLayer(markerFrom);
histMarkerMap.set(fromKey, markerFrom);
}
// To marker
const toKey = `${ev.toLat},${ev.toLon}`;
if (!histMarkerMap.has(toKey)) {
const markerTo = L.circleMarker([ev.toLat, ev.toLon], {
radius: 8, fillColor: '#55ccff', color: '#55ccff', fillOpacity: 0.8
}).bindPopup(popupHtml);
histMarkers.addLayer(markerTo);
histMarkerMap.set(toKey, markerTo);
}
// Line
const line = L.polyline([[ev.fromLat, ev.fromLon], [ev.toLat, ev.toLon]], {
color: '#ffcc00', weight: 2, opacity: 0.7
}).addTo(histLines);
line.bindPopup(popupHtml);
// Trail
if (ev.trail && !histTrailMap.has(i)) {
const color = ev.trail.colorMode === 'auto'
? ev.trail.direction === 'inbound' ? 'rgba(255,0,0,0.6)' : 'rgba(0,150,255,0.6)'
: ev.trail.colorMode === 'red' ? 'rgba(255,0,0,0.6)'
: ev.trail.colorMode === 'blue' ? 'rgba(0,150,255,0.6)'
: ev.trail.colorMode === 'yellow' ? 'rgba(255,215,0,0.6)'
: 'rgba(255,255,255,0.6)';
const histTrail = L.polyline([[ev.fromLat, ev.fromLon], [ev.toLat, ev.toLon]], {
color, weight: 2, opacity: 0.6, interactive: false
}).addTo(histTrails);
const decorator = L.polylineDecorator(histTrail, {
patterns: [{ offset: '0%', repeat: '20px',
symbol: L.Symbol.arrowHead({ pixelSize: 8, pathOptions: { color, opacity: 0.8 } })
}]
}).addTo(histTrails);
decorator.eachLayer(l => { if (l._path) l._path.style.pointerEvents = 'none'; });
const trailObj = { trail: histTrail, decorator, startTime: performance.now(), duration: ev.trail.speed || trailDuration };
const animate = (time) => {
const elapsed = time - trailObj.startTime;
const progress = elapsed / trailObj.duration;
const offsetPercent = (progress * 100) % 20;
trailObj.decorator.setPatterns([{
offset: offsetPercent + '%',
repeat: '20px',
symbol: L.Symbol.arrowHead({ pixelSize: 8, pathOptions: { color, opacity: 0.8 } })
}]);
decorator.eachLayer(l => { if (l._path) l._path.style.pointerEvents = 'none'; });
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
histTrailMap.set(i, trailObj);
}
}
updateHistStats(index);
// Fit bounds
const visibleBounds = [];
for (let i = 0; i <= index; i++) {
const ev = histEvents[i];
if (!ev) continue;
visibleBounds.push([ev.fromLat, ev.fromLon]);
visibleBounds.push([ev.toLat, ev.toLon]);
}
if (visibleBounds.length) {
histMap.fitBounds(L.latLngBounds(visibleBounds).pad(0.25), { maxZoom: 6 });
}
}
function updateHistStats(upToIndex, updateLastFrame) {
let totalConnections, topCountry, sortedConnections;
if (upToIndex === histEvents.length - 1 && window.liveTotals) {
totalConnections = window.liveTotals.totalConnections;
topCountry = window.liveTotals.topCountry;
sortedConnections = Object.entries(window.liveTotals.topConnections || {});
} else {
totalConnections = 0;
const countryCounts = {};
const connectionPairs = {};
for (let i = 0; i <= upToIndex; i++) {
const ev = histEvents[i];
if (!ev) continue;
const count = ev.connectionCount || 1;
totalConnections += count;
const fromCountry = ev.fromCountryCode || `${ev.fromLat},${ev.fromLon}`;
countryCounts[fromCountry] = (countryCounts[fromCountry] || 0) + count;
const toCountry = ev.toCountryCode || `${ev.toLat},${ev.toLon}`;
const pairKey = `${fromCountry}→${toCountry}`;
connectionPairs[pairKey] = (connectionPairs[pairKey] || 0) + count;
}
topCountry = Object.entries(countryCounts).reduce((a, b) => b[1] > a[1] ? b : a, ['N/A', 0])[0];
sortedConnections = Object.entries(connectionPairs).sort((a,b)=>b[1]-a[1]).slice(0,10);
}
// Update the text of the last frame
if (!updateLastFrame || timeSlider.value == histEvents.length - 1)
{
document.getElementById('histStatsText').innerText =
`Connections: ${totalConnections} | Top Country: ${topCountry}`;
let topConnHtml = '<b>Top Connections:</b><br>';
if (sortedConnections.length) {
topConnHtml += sortedConnections.map(([pair, count]) => `${pair}: ${count}`).join('<br>');
} else {
topConnHtml += 'No data yet';
}
document.getElementById('histTopConnections').innerHTML = topConnHtml;
}
}
/* Slider control */
timeSlider.addEventListener('input', e => {
currentTimeIndex = parseInt(e.target.value);
showHistoricalEvent(currentTimeIndex);
timeLabel.innerText = `${currentTimeIndex + 1}/${histEvents.length}`;
});
/* Play/Pause control */
let playInterval = null;
let isHistPlaying = false;
let currentTimeIndex = 0;
playPauseBtn.addEventListener('click', () => {
if (playInterval) {
clearInterval(playInterval);
playInterval = null;
playPauseBtn.innerText = 'Play';
isHistPlaying = false;
} else {
if (!histEvents.length) return;
playPauseBtn.innerText = 'Pause';
isHistPlaying = true;
playInterval = setInterval(() => {
showHistoricalEvent(currentTimeIndex);
currentTimeIndex++;
if (currentTimeIndex >= histEvents.length) {
clearInterval(playInterval);
playInterval = null;
playPauseBtn.innerText = 'Play';
isHistPlaying = false;
return;
}
timeSlider.value = currentTimeIndex;
timeLabel.innerText = `${currentTimeIndex + 1}/${histEvents.length}`;
}, 500);
}
});
</script>
</body>
</html>
";
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<double>();
var lon = latlng[1].ToObject<double>();
_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<double>(), root["lon"].ToObject<double>());
return _localCoord;
}
}
catch { }
_localCoord = (0.0, 0.0);
return _localCoord;
}
public async Task UpdateMapAsync(
ObservableCollection<CountryStatistic> countryStats,
List<CountryConnection> connections = null)
{
try
{
var localCoord = await GetLocalCoordinatesAsync();
// Prepare payload for countries
var payloadCountries = new List<object>();
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<object>();
var liveConnectionCounts = new Dictionary<string, int>();
foreach (var conn in connections ?? Enumerable.Empty<CountryConnection>())
{
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}");
}
}
}
}

View File

@@ -1,10 +1,13 @@
<Window x:Class="EonaCat.ConnectionMonitor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="EonaCat Connection Monitor" Height="800" Width="1400"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
xmlns:converters="clr-namespace:EonaCat.ConnectionMonitor.Converters"
Title="EonaCat Connection Monitor" Height="850" Width="1400"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<converters:StringToVisibilityConverter x:Key="StringToVisibilityConverter"/>
<Style x:Key="TabItemStyle" TargetType="TabItem">
<Setter Property="Template">
<Setter.Value>
@@ -43,10 +46,23 @@
<!-- Header -->
<Border Grid.Row="0" Background="#2D2D30" BorderBrush="#3F3F46" BorderThickness="0,0,0,1" Padding="15">
<Grid>
<StackPanel>
<TextBlock Text="EonaCat Connection Monitor" FontSize="18" FontWeight="Bold" Foreground="White"/>
<TextBlock Text="Monitor network connections with process information and geographical data" FontSize="12" Foreground="#CCCCCC" Margin="0,2,0,0"/>
</StackPanel>
<Button Name="btnDonate" Click="BtnDonate_Click" Width="180" Height="35" Cursor="Hand" HorizontalAlignment="Right">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="#003087" BorderBrush="#003087" CornerRadius="4">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Donate with PayPal" Foreground="White" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</Border>
<!-- Controls -->
@@ -91,7 +107,7 @@
</Border>
<!-- Main Content -->
<TabControl Grid.Row="2" Background="#1E1E1E" BorderBrush="#3F3F46" Style="{x:Null}">
<TabControl x:Name="tabControl" Grid.Row="2" Background="#1E1E1E" BorderBrush="#3F3F46" Style="{x:Null}">
<TabItem Header="All Connections" Style="{StaticResource TabItemStyle}">
<Grid Background="#1E1E1E">
<DataGrid Name="dgAllConnections"
@@ -187,8 +203,39 @@
<DataGridTextColumn Header="ISP" Binding="{Binding ISP}" Width="150"/>
<DataGridTextColumn Header="Start Time" Binding="{Binding StartTime, StringFormat=HH:mm:ss}" Width="Auto"/>
<DataGridTextColumn Header="Duration" Binding="{Binding Duration, StringFormat=c}" Width="Auto"/>
<DataGridTemplateColumn Header="Actions" Width="80">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Kill"
Background="#D83B01"
Foreground="White"
Padding="5,2"
Cursor="Hand"
Click="BtnKillProcess_Click"
Tag="{Binding ProcessId}"
Visibility="{Binding ProcessName, Converter={StaticResource StringToVisibilityConverter}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Border Background="#80000000" CornerRadius="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsLoadingOverlayVisible, RelativeSource={RelativeSource AncestorType=Window}}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="Loading..." Foreground="White" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
</TabItem>
@@ -231,7 +278,24 @@
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width="80"/>
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="100"/>
<DataGridTextColumn Header="Process" Binding="{Binding ProcessName}" Width="Auto"/>
<DataGridTextColumn Header="Last Seen" Binding="{Binding LastSeen}" Width="150"/>
<DataGridTextColumn Header="First Seen" Binding="{Binding StartTime, StringFormat=HH:mm:ss}" Width="Auto"/>
<DataGridTextColumn Header="Last Seen" Binding="{Binding LastSeen, StringFormat=HH:mm:ss}" Width="Auto"/>
<DataGridTextColumn Header="Duration" Binding="{Binding Duration, StringFormat=c}" Width="Auto"/>
<DataGridTemplateColumn Header="Actions" Width="80">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Kill"
Background="#D83B01"
Foreground="White"
Padding="5,2"
Cursor="Hand"
Click="BtnKillProcess_Click"
Tag="{Binding ProcessId}"
Visibility="{Binding ProcessName, Converter={StaticResource StringToVisibilityConverter}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
@@ -331,14 +395,47 @@
<DataGridTextColumn Header="Country Name" Binding="{Binding CountryName}" Width="120"/>
<DataGridTextColumn Header="ISP" Binding="{Binding ISP}" Width="150"/>
<DataGridTextColumn Header="Start Time" Binding="{Binding StartTime, StringFormat=HH:mm:ss}" Width="Auto"/>
<DataGridTextColumn Header="End Time" Binding="{Binding EndTime, StringFormat=HH:mm:ss}" Width="Auto"/>
<DataGridTextColumn Header="End Time" Binding="{Binding LastSeen, StringFormat=HH:mm:ss}" Width="Auto"/>
<DataGridTextColumn Header="Duration" Binding="{Binding Duration, StringFormat=c}" Width="Auto"/>
<DataGridTemplateColumn Header="Actions" Width="80">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Kill"
Background="#D83B01"
Foreground="White"
Padding="5,2"
Cursor="Hand"
Click="BtnKillProcess_Click"
Tag="{Binding ProcessId}"
Visibility="{Binding ProcessName, Converter={StaticResource StringToVisibilityConverter}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Border Background="#80000000" CornerRadius="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Items.Count, ElementName=connectionHistoryDataGrid}" Value="0"/>
<Condition Binding="{Binding IsMonitoringStarted, RelativeSource={RelativeSource AncestorType=Window}}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="Loading..." Foreground="White" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
</TabItem>
<TabItem Header="Connection Events" Style="{StaticResource TabItemStyle}">
<Grid Background="#1E1E1E">
<DataGrid x:Name="dgConnectionEvents" AutoGenerateColumns="False"
Background="#1E1E1E" Foreground="White"
GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#3F3F46"
@@ -409,8 +506,41 @@
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTemplateColumn Header="Actions" Width="80">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Kill"
Background="#D83B01"
Foreground="White"
Padding="5,2"
Cursor="Hand"
Click="BtnKillProcess_Click"
Tag="{Binding ProcessId}"
Visibility="{Binding ProcessName, Converter={StaticResource StringToVisibilityConverter}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Border Background="#80000000" CornerRadius="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Items.Count, ElementName=dgConnectionEvents}" Value="0"/>
<Condition Binding="{Binding IsMonitoringStarted, RelativeSource={RelativeSource AncestorType=Window}}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="Loading..." Foreground="White" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
</TabItem>
<TabItem Header="Statistics" Style="{StaticResource TabItemStyle}">
@@ -450,8 +580,7 @@
</Border>
</StackPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<StackPanel Grid.Row="1">
<TextBlock Text="Top Processes by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,0,0,10"/>
<DataGrid Name="dgProcessStats" AutoGenerateColumns="False"
Background="#1E1E1E" Foreground="White" Height="200"
@@ -475,7 +604,7 @@
<TextBlock Text="Countries by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,20,0,10"/>
<DataGrid Name="dgCountryStats" AutoGenerateColumns="False"
Background="#1E1E1E" Foreground="White" Height="200"
Background="#1E1E1E" Foreground="White" Height="177"
GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#3F3F46"
HeadersVisibility="Column" CanUserAddRows="False" IsReadOnly="True"
AlternatingRowBackground="#2D2D30" RowBackground="#1E1E1E">
@@ -500,9 +629,12 @@
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</ScrollViewer>
</Grid>
</TabItem>
<TabItem Header="Map" Style="{StaticResource TabItemStyle}">
<wv2:WebView2 x:Name="mapWebView" Grid.Row="0" Grid.Column="2" Margin="6" />
</TabItem>
</TabControl>
<!-- Status Bar -->
@@ -514,6 +646,9 @@
<StatusBarItem>
<TextBlock Name="lblLastUpdate" Text="Last Update: Never" Foreground="#CCCCCC"/>
</StatusBarItem>
<StatusBarItem HorizontalAlignment="Right">
<TextBlock Name="lblClock" Foreground="#CCCCCC" HorizontalAlignment="Right"/>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>

View File

@@ -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<ConnectionInfo> _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<string, GeolocationInfo> _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<Dictionary<string, GeolocationInfo>>(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<Task<ConnectionInfo>>();
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,32 +314,42 @@ 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)
{
var existing = _allConnections.FirstOrDefault(c => c.UniqueId == conn.UniqueId);
if (existing != null)
{
// 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
{
// New connection
conn.StartTime = DateTime.Now;
conn.LastSeen = DateTime.Now;
_allConnections.Add(conn);
_connectionHistory.Add(new ConnectionInfo
{
ProcessName = conn.ProcessName,
@@ -256,7 +357,7 @@ namespace EonaCat.ConnectionMonitor
Protocol = conn.Protocol,
LocalEndPoint = conn.LocalEndPoint,
RemoteEndPoint = conn.RemoteEndPoint,
State = /*conn.Protocol == "UDP" ? "UDP" :*/ conn.State.ToUpperInvariant(),
State = conn.State,
CountryCode = conn.CountryCode,
CountryName = conn.CountryName,
CountryFlagUrl = conn.CountryFlagUrl,
@@ -267,28 +368,18 @@ namespace EonaCat.ConnectionMonitor
});
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))
{
LogConnectionEvent(conn, $"Disconnected ({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
var selectedIds = dgAllConnections.SelectedItems
@@ -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<double>() ?? 0.0,
Longitude = root["lon"]?.ToObject<double>() ?? 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<TabItem>()
.FirstOrDefault(t => t.Header.ToString().StartsWith("All Connections"))
?.SetValue(TabItem.HeaderProperty, $"All Connections ({dgAllConnections.Items.Count}) - Remote Connections: {RemoteConnectionCount}");
// Configured Connections
tabControl.Items.Cast<TabItem>()
.FirstOrDefault(t => t.Header.ToString().StartsWith("Configured Connections"))
?.SetValue(TabItem.HeaderProperty, $"Configured Connections ({dgConfiguredConnections.Items.Count})");
// Connection History
tabControl.Items.Cast<TabItem>()
.FirstOrDefault(t => t.Header.ToString().StartsWith("Connection History"))
?.SetValue(TabItem.HeaderProperty, $"Connection History ({connectionHistoryDataGrid.Items.Count})");
// Connection Events
tabControl.Items.Cast<TabItem>()
.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<TabItem>()
.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>();
double lon = root["lon"].ToObject<double>();
string countryCode = root["countryCode"].ToObject<string>();
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<List<ConfiguredConnection>>(configJson);
var configurations = JsonHelper.ToObject<List<ConfiguredConnection>>(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; }
}
}

View File

@@ -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));
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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<string> Processes { get; set; } = new List<string>();
}
}

View File

@@ -0,0 +1,8 @@
namespace EonaCat.ConnectionMonitor.Models
{
public class CountryProcess
{
public string ProcessName { get; set; }
public int ConnectionCount { get; set; }
}
}

View File

@@ -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<CountryProcess> TopProcesses { get; set; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,8 @@
namespace EonaCat.ConnectionMonitor.Models
{
public class TopProcessInfo
{
public string ProcessName { get; set; }
public int ConnectionCount { get; set; }
}
}