Initial version
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -9,6 +9,11 @@
|
|||||||
<ApplicationIcon>EonaCat.ico</ApplicationIcon>
|
<ApplicationIcon>EonaCat.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="EonaCat.Json" Version="1.1.9" />
|
||||||
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3485.44" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Resource Include="EonaCat.ico" />
|
<Resource Include="EonaCat.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
918
EonaCat.ConnectionMonitor/Helpers/LeafLetHelper.cs
Normal file
918
EonaCat.ConnectionMonitor/Helpers/LeafLetHelper.cs
Normal 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;'>↓↑</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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,13 @@
|
|||||||
<Window x:Class="EonaCat.ConnectionMonitor.MainWindow"
|
<Window x:Class="EonaCat.ConnectionMonitor.MainWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
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">
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
|
<converters:StringToVisibilityConverter x:Key="StringToVisibilityConverter"/>
|
||||||
<Style x:Key="TabItemStyle" TargetType="TabItem">
|
<Style x:Key="TabItemStyle" TargetType="TabItem">
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
@@ -43,10 +46,23 @@
|
|||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<Border Grid.Row="0" Background="#2D2D30" BorderBrush="#3F3F46" BorderThickness="0,0,0,1" Padding="15">
|
<Border Grid.Row="0" Background="#2D2D30" BorderBrush="#3F3F46" BorderThickness="0,0,0,1" Padding="15">
|
||||||
|
<Grid>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="EonaCat Connection Monitor" FontSize="18" FontWeight="Bold" Foreground="White"/>
|
<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"/>
|
<TextBlock Text="Monitor network connections with process information and geographical data" FontSize="12" Foreground="#CCCCCC" Margin="0,2,0,0"/>
|
||||||
</StackPanel>
|
</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>
|
</Border>
|
||||||
|
|
||||||
<!-- Controls -->
|
<!-- Controls -->
|
||||||
@@ -91,7 +107,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- 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}">
|
<TabItem Header="All Connections" Style="{StaticResource TabItemStyle}">
|
||||||
<Grid Background="#1E1E1E">
|
<Grid Background="#1E1E1E">
|
||||||
<DataGrid Name="dgAllConnections"
|
<DataGrid Name="dgAllConnections"
|
||||||
@@ -187,8 +203,39 @@
|
|||||||
<DataGridTextColumn Header="ISP" Binding="{Binding ISP}" Width="150"/>
|
<DataGridTextColumn Header="ISP" Binding="{Binding ISP}" Width="150"/>
|
||||||
<DataGridTextColumn Header="Start Time" Binding="{Binding StartTime, StringFormat=HH:mm:ss}" Width="Auto"/>
|
<DataGridTextColumn Header="Start Time" Binding="{Binding StartTime, StringFormat=HH:mm:ss}" Width="Auto"/>
|
||||||
<DataGridTextColumn Header="Duration" Binding="{Binding Duration, StringFormat=c}" 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.Columns>
|
||||||
</DataGrid>
|
</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>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -231,7 +278,24 @@
|
|||||||
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width="80"/>
|
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width="80"/>
|
||||||
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="100"/>
|
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="100"/>
|
||||||
<DataGridTextColumn Header="Process" Binding="{Binding ProcessName}" Width="Auto"/>
|
<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.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -331,14 +395,47 @@
|
|||||||
<DataGridTextColumn Header="Country Name" Binding="{Binding CountryName}" Width="120"/>
|
<DataGridTextColumn Header="Country Name" Binding="{Binding CountryName}" Width="120"/>
|
||||||
<DataGridTextColumn Header="ISP" Binding="{Binding ISP}" Width="150"/>
|
<DataGridTextColumn Header="ISP" Binding="{Binding ISP}" Width="150"/>
|
||||||
<DataGridTextColumn Header="Start Time" Binding="{Binding StartTime, StringFormat=HH:mm:ss}" Width="Auto"/>
|
<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"/>
|
<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.Columns>
|
||||||
</DataGrid>
|
</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>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="Connection Events" Style="{StaticResource TabItemStyle}">
|
<TabItem Header="Connection Events" Style="{StaticResource TabItemStyle}">
|
||||||
|
<Grid Background="#1E1E1E">
|
||||||
<DataGrid x:Name="dgConnectionEvents" AutoGenerateColumns="False"
|
<DataGrid x:Name="dgConnectionEvents" AutoGenerateColumns="False"
|
||||||
Background="#1E1E1E" Foreground="White"
|
Background="#1E1E1E" Foreground="White"
|
||||||
GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#3F3F46"
|
GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#3F3F46"
|
||||||
@@ -409,8 +506,41 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</DataGridTextColumn.ElementStyle>
|
</DataGridTextColumn.ElementStyle>
|
||||||
</DataGridTextColumn>
|
</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.Columns>
|
||||||
</DataGrid>
|
</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>
|
||||||
|
|
||||||
<TabItem Header="Statistics" Style="{StaticResource TabItemStyle}">
|
<TabItem Header="Statistics" Style="{StaticResource TabItemStyle}">
|
||||||
@@ -450,8 +580,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
<StackPanel Grid.Row="1">
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="Top Processes by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,0,0,10"/>
|
<TextBlock Text="Top Processes by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,0,0,10"/>
|
||||||
<DataGrid Name="dgProcessStats" AutoGenerateColumns="False"
|
<DataGrid Name="dgProcessStats" AutoGenerateColumns="False"
|
||||||
Background="#1E1E1E" Foreground="White" Height="200"
|
Background="#1E1E1E" Foreground="White" Height="200"
|
||||||
@@ -475,7 +604,7 @@
|
|||||||
|
|
||||||
<TextBlock Text="Countries by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,20,0,10"/>
|
<TextBlock Text="Countries by Connection Count" Style="{StaticResource HeaderTextStyle}" Margin="0,20,0,10"/>
|
||||||
<DataGrid Name="dgCountryStats" AutoGenerateColumns="False"
|
<DataGrid Name="dgCountryStats" AutoGenerateColumns="False"
|
||||||
Background="#1E1E1E" Foreground="White" Height="200"
|
Background="#1E1E1E" Foreground="White" Height="177"
|
||||||
GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#3F3F46"
|
GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#3F3F46"
|
||||||
HeadersVisibility="Column" CanUserAddRows="False" IsReadOnly="True"
|
HeadersVisibility="Column" CanUserAddRows="False" IsReadOnly="True"
|
||||||
AlternatingRowBackground="#2D2D30" RowBackground="#1E1E1E">
|
AlternatingRowBackground="#2D2D30" RowBackground="#1E1E1E">
|
||||||
@@ -500,9 +629,12 @@
|
|||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="Map" Style="{StaticResource TabItemStyle}">
|
||||||
|
<wv2:WebView2 x:Name="mapWebView" Grid.Row="0" Grid.Column="2" Margin="6" />
|
||||||
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
|
|
||||||
<!-- Status Bar -->
|
<!-- Status Bar -->
|
||||||
@@ -514,6 +646,9 @@
|
|||||||
<StatusBarItem>
|
<StatusBarItem>
|
||||||
<TextBlock Name="lblLastUpdate" Text="Last Update: Never" Foreground="#CCCCCC"/>
|
<TextBlock Name="lblLastUpdate" Text="Last Update: Never" Foreground="#CCCCCC"/>
|
||||||
</StatusBarItem>
|
</StatusBarItem>
|
||||||
|
<StatusBarItem HorizontalAlignment="Right">
|
||||||
|
<TextBlock Name="lblClock" Foreground="#CCCCCC" HorizontalAlignment="Right"/>
|
||||||
|
</StatusBarItem>
|
||||||
</StatusBar>
|
</StatusBar>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
@@ -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.Concurrent;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
|
|
||||||
namespace EonaCat.ConnectionMonitor
|
namespace EonaCat.ConnectionMonitor
|
||||||
{
|
{
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window, INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
private ObservableCollection<ConnectionInfo> _connectionHistory;
|
private ObservableCollection<ConnectionInfo> _connectionHistory;
|
||||||
private DispatcherTimer _refreshTimer;
|
private DispatcherTimer _refreshTimer;
|
||||||
@@ -31,20 +30,59 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
private CollectionViewSource _allConnectionsView;
|
private CollectionViewSource _allConnectionsView;
|
||||||
|
|
||||||
private string _configFilePath = "connections_config.json";
|
private string _configFilePath = "connections_config.json";
|
||||||
private bool _isMonitoring = false;
|
|
||||||
private HttpClient _httpClient;
|
private HttpClient _httpClient;
|
||||||
|
private LeafLetHelper _leafletHelper;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
|
|
||||||
|
public int RemoteConnectionCount { get; private set; }
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, GeolocationInfo> _geoCache = new();
|
private readonly ConcurrentDictionary<string, GeolocationInfo> _geoCache = new();
|
||||||
private string _connectionEventsLogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ConnectionEvents.log");
|
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()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
StartClock();
|
||||||
//Icon = BitmapFrame.Create(new Uri("pack://application:,,,/Resources/app_icon.ico", UriKind.Absolute));
|
//Icon = BitmapFrame.Create(new Uri("pack://application:,,,/Resources/app_icon.ico", UriKind.Absolute));
|
||||||
InitializeCollections();
|
InitializeCollections();
|
||||||
InitializeTimer();
|
InitializeTimer();
|
||||||
_httpClient = new HttpClient();
|
_httpClient = new HttpClient();
|
||||||
|
_leafletHelper = new LeafLetHelper();
|
||||||
|
_leafletHelper.InitializeMapAsync(mapWebView).ConfigureAwait(false);
|
||||||
LoadConfigurationAsync().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()
|
private void InitializeCollections()
|
||||||
@@ -65,8 +103,6 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
dgConnectionEvents.ItemsSource = _connectionEvents;
|
dgConnectionEvents.ItemsSource = _connectionEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void InitializeTimer()
|
private void InitializeTimer()
|
||||||
{
|
{
|
||||||
_refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
|
_refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
|
||||||
@@ -78,6 +114,48 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
await RefreshConnectionsAsync();
|
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)
|
private void BtnStart_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
StartMonitoring();
|
StartMonitoring();
|
||||||
@@ -100,7 +178,7 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
|
|
||||||
private void ChkAutoRefresh_Checked(object sender, RoutedEventArgs e)
|
private void ChkAutoRefresh_Checked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isMonitoring && chkAutoRefresh.IsChecked == true)
|
if (IsMonitoringStarted && chkAutoRefresh.IsChecked == true)
|
||||||
{
|
{
|
||||||
_refreshTimer.Start();
|
_refreshTimer.Start();
|
||||||
}
|
}
|
||||||
@@ -113,7 +191,7 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
|
|
||||||
private void StartMonitoring()
|
private void StartMonitoring()
|
||||||
{
|
{
|
||||||
_isMonitoring = true;
|
IsMonitoringStarted = true;
|
||||||
btnStart.IsEnabled = false;
|
btnStart.IsEnabled = false;
|
||||||
btnStop.IsEnabled = true;
|
btnStop.IsEnabled = true;
|
||||||
lblStatus.Text = "Monitoring...";
|
lblStatus.Text = "Monitoring...";
|
||||||
@@ -127,7 +205,7 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
|
|
||||||
private void StopMonitoring()
|
private void StopMonitoring()
|
||||||
{
|
{
|
||||||
_isMonitoring = false;
|
IsMonitoringStarted = false;
|
||||||
_refreshTimer.Stop();
|
_refreshTimer.Stop();
|
||||||
btnStart.IsEnabled = true;
|
btnStart.IsEnabled = true;
|
||||||
btnStop.IsEnabled = false;
|
btnStop.IsEnabled = false;
|
||||||
@@ -141,7 +219,7 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var dict = _geoCache.ToDictionary(k => k.Key, v => v.Value);
|
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);
|
await File.WriteAllTextAsync(_geoCacheFilePath, json);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
@@ -149,11 +227,29 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
|
|
||||||
private async Task RefreshConnectionsAsync()
|
private async Task RefreshConnectionsAsync()
|
||||||
{
|
{
|
||||||
if (_isRefreshing) return;
|
if (_isRefreshing)
|
||||||
|
return;
|
||||||
|
|
||||||
_isRefreshing = true;
|
_isRefreshing = true;
|
||||||
|
|
||||||
try
|
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...");
|
Dispatcher.Invoke(() => lblStatus.Text = "Refreshing...");
|
||||||
|
|
||||||
var connections = await Task.Run(async () =>
|
var connections = await Task.Run(async () =>
|
||||||
@@ -163,18 +259,12 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
var tcpConnections = ipProps.GetActiveTcpConnections();
|
var tcpConnections = ipProps.GetActiveTcpConnections();
|
||||||
var tcpListeners = ipProps.GetActiveTcpListeners();
|
var tcpListeners = ipProps.GetActiveTcpListeners();
|
||||||
var udpListeners = ipProps.GetActiveUdpListeners();
|
var udpListeners = ipProps.GetActiveUdpListeners();
|
||||||
|
|
||||||
var pidToProcessName = Process.GetProcesses().ToDictionary(p => p.Id, p => p.ProcessName);
|
var pidToProcessName = Process.GetProcesses().ToDictionary(p => p.Id, p => p.ProcessName);
|
||||||
|
|
||||||
var tasks = new List<Task<ConnectionInfo>>();
|
var tasks = new List<Task<ConnectionInfo>>();
|
||||||
|
|
||||||
tasks.AddRange(tcpConnections.Select(c =>
|
tasks.AddRange(tcpConnections.Select(c =>
|
||||||
CreateInfoAsync(
|
CreateInfoAsync(c.LocalEndPoint, c.RemoteEndPoint, "TCP", FormatTcpState(c.State), pidToProcessName)));
|
||||||
c.LocalEndPoint,
|
|
||||||
c.RemoteEndPoint,
|
|
||||||
"TCP",
|
|
||||||
FormatTcpState(c.State),
|
|
||||||
pidToProcessName)));
|
|
||||||
|
|
||||||
tasks.AddRange(tcpListeners.Select(l =>
|
tasks.AddRange(tcpListeners.Select(l =>
|
||||||
CreateInfoAsync(l, null, "TCP", "LISTENING", pidToProcessName)));
|
CreateInfoAsync(l, null, "TCP", "LISTENING", pidToProcessName)));
|
||||||
@@ -187,13 +277,13 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove duplicates
|
// Assign consistent UniqueId
|
||||||
connections = connections
|
foreach (var c in connections)
|
||||||
.GroupBy(c => $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}")
|
{
|
||||||
.Select(g => g.First())
|
c.UniqueId = $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}";
|
||||||
.ToList();
|
}
|
||||||
|
|
||||||
// Geolocation
|
// Geolocation for new IPs
|
||||||
var ipsToFetch = connections
|
var ipsToFetch = connections
|
||||||
.Where(c => c.RemoteEndPoint != "N/A")
|
.Where(c => c.RemoteEndPoint != "N/A")
|
||||||
.Select(c => c.RemoteEndPoint.Split(':')[0])
|
.Select(c => c.RemoteEndPoint.Split(':')[0])
|
||||||
@@ -215,6 +305,7 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
});
|
});
|
||||||
await Task.WhenAll(geoTasks);
|
await Task.WhenAll(geoTasks);
|
||||||
|
|
||||||
|
// Update geolocation data
|
||||||
foreach (var conn in connections)
|
foreach (var conn in connections)
|
||||||
{
|
{
|
||||||
if (conn.RemoteEndPoint != "N/A")
|
if (conn.RemoteEndPoint != "N/A")
|
||||||
@@ -223,32 +314,42 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
if (_geoCache.TryGetValue(ip, out var geo))
|
if (_geoCache.TryGetValue(ip, out var geo))
|
||||||
{
|
{
|
||||||
conn.CountryCode = geo.CountryCode;
|
conn.CountryCode = geo.CountryCode;
|
||||||
conn.CountryName = geo.CountryName;
|
conn.CountryName = FixCountry(geo.CountryName);
|
||||||
conn.ISP = geo.ISP;
|
conn.ISP = geo.ISP;
|
||||||
conn.CountryFlagUrl = GetFlagUrl(geo.CountryCode)?.ToString();
|
conn.CountryFlagUrl = GetFlagUrl(geo.CountryCode)?.ToString();
|
||||||
|
conn.Latitude = geo.Latitude;
|
||||||
|
conn.Longitude = geo.Longitude;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track new and disconnected connections by state
|
// Merge with existing connections
|
||||||
var currentKeys = connections
|
var currentKeys = connections.Select(c => c.UniqueId).ToHashSet();
|
||||||
.Select(c => $"{c.Protocol}|{c.LocalEndPoint}|{c.RemoteEndPoint}|{c.State}")
|
var previousKeys = _allConnections.Select(c => c.UniqueId).ToHashSet();
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
var previousKeys = _allConnections
|
foreach (var conn in connections)
|
||||||
.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)
|
|
||||||
{
|
{
|
||||||
|
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.StartTime = DateTime.Now;
|
||||||
conn.LastSeen = DateTime.Now;
|
conn.LastSeen = DateTime.Now;
|
||||||
_allConnections.Add(conn);
|
_allConnections.Add(conn);
|
||||||
|
|
||||||
_connectionHistory.Add(new ConnectionInfo
|
_connectionHistory.Add(new ConnectionInfo
|
||||||
{
|
{
|
||||||
ProcessName = conn.ProcessName,
|
ProcessName = conn.ProcessName,
|
||||||
@@ -256,7 +357,7 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
Protocol = conn.Protocol,
|
Protocol = conn.Protocol,
|
||||||
LocalEndPoint = conn.LocalEndPoint,
|
LocalEndPoint = conn.LocalEndPoint,
|
||||||
RemoteEndPoint = conn.RemoteEndPoint,
|
RemoteEndPoint = conn.RemoteEndPoint,
|
||||||
State = /*conn.Protocol == "UDP" ? "UDP" :*/ conn.State.ToUpperInvariant(),
|
State = conn.State,
|
||||||
CountryCode = conn.CountryCode,
|
CountryCode = conn.CountryCode,
|
||||||
CountryName = conn.CountryName,
|
CountryName = conn.CountryName,
|
||||||
CountryFlagUrl = conn.CountryFlagUrl,
|
CountryFlagUrl = conn.CountryFlagUrl,
|
||||||
@@ -267,28 +368,18 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
});
|
});
|
||||||
LogConnectionEvent(conn, $"Connected ({conn.State})");
|
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--)
|
for (int i = _allConnections.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var key = $"{_allConnections[i].Protocol}|{_allConnections[i].LocalEndPoint}|{_allConnections[i].RemoteEndPoint}|{_allConnections[i].State}";
|
var c = _allConnections[i];
|
||||||
if (!currentKeys.Contains(key))
|
if (!currentKeys.Contains(c.UniqueId))
|
||||||
|
{
|
||||||
|
LogConnectionEvent(c, $"Disconnected ({c.State})");
|
||||||
_allConnections.RemoveAt(i);
|
_allConnections.RemoveAt(i);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve selection
|
// Preserve selection
|
||||||
var selectedIds = dgAllConnections.SelectedItems
|
var selectedIds = dgAllConnections.SelectedItems
|
||||||
@@ -319,6 +410,8 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_isRefreshing = false;
|
_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
|
try
|
||||||
{
|
{
|
||||||
var response = await _httpClient.GetStringAsync($"http://ip-api.com/json/{ipAddress}");
|
var response = await _httpClient.GetStringAsync($"http://ip-api.com/json/{ipAddress}");
|
||||||
var root = JsonDocument.Parse(response).RootElement;
|
var root = JObject.Parse(response);
|
||||||
if (root.GetProperty("status").GetString() == "success")
|
if (root["status"]?.ToString() == "success")
|
||||||
{
|
{
|
||||||
return new GeolocationInfo
|
return new GeolocationInfo
|
||||||
{
|
{
|
||||||
CountryCode = root.GetProperty("countryCode").GetString(),
|
CountryCode = root["countryCode"]?.ToString(),
|
||||||
CountryName = root.GetProperty("country").GetString(),
|
CountryName = FixCountry(root["country"]?.ToString()),
|
||||||
ISP = root.GetProperty("isp").GetString()
|
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;
|
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)
|
private Uri GetFlagUrl(string countryCode)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(countryCode) || countryCode == "Local")
|
if (string.IsNullOrEmpty(countryCode) || countryCode == "Local")
|
||||||
@@ -477,7 +586,12 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
{
|
{
|
||||||
configured.Status = "Active";
|
configured.Status = "Active";
|
||||||
configured.ProcessName = match.ProcessName;
|
configured.ProcessName = match.ProcessName;
|
||||||
configured.LastSeen = DateTime.Now;
|
|
||||||
|
if (configured.StartTime == null)
|
||||||
|
{
|
||||||
|
configured.StartTime = match.StartTime;
|
||||||
|
}
|
||||||
|
configured.LastSeen = match.LastSeen;
|
||||||
}
|
}
|
||||||
else
|
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();
|
lblTotalConnections.Text = _allConnections.Count.ToString();
|
||||||
lblEstablishedConnections.Text = _allConnections.Count(c => c.State == "ESTABLISHED").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")
|
.Where(c => !string.IsNullOrEmpty(c.CountryCode) && c.CountryCode != "Local")
|
||||||
.Select(c => c.CountryCode).Distinct().Count().ToString();
|
.Select(c => c.CountryCode).Distinct().Count().ToString();
|
||||||
|
|
||||||
|
// Process statistics
|
||||||
_processStats.Clear();
|
_processStats.Clear();
|
||||||
var processGroups = _allConnections
|
var processGroups = _allConnections
|
||||||
.Where(c => !string.IsNullOrEmpty(c.ProcessName))
|
.Where(c => !string.IsNullOrEmpty(c.ProcessName))
|
||||||
@@ -514,6 +658,7 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Country statistics with top processes
|
||||||
_countryStats.Clear();
|
_countryStats.Clear();
|
||||||
var countryGroups = _allConnections
|
var countryGroups = _allConnections
|
||||||
.Where(c => !string.IsNullOrEmpty(c.CountryCode) && c.CountryCode != "Local")
|
.Where(c => !string.IsNullOrEmpty(c.CountryCode) && c.CountryCode != "Local")
|
||||||
@@ -523,16 +668,79 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
|
|
||||||
foreach (var g in countryGroups)
|
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
|
_countryStats.Add(new CountryStatistic
|
||||||
{
|
{
|
||||||
CountryCode = g.Key.CountryCode,
|
CountryCode = g.Key.CountryCode,
|
||||||
CountryName = g.Key.CountryName,
|
CountryName = FixCountry(g.Key.CountryName),
|
||||||
FlagUrl = g.Key.CountryFlagUrl,
|
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()
|
private void ApplyFilter()
|
||||||
{
|
{
|
||||||
var filterText = txtFilter.Text ?? string.Empty;
|
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 }
|
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);
|
await File.WriteAllTextAsync(_configFilePath, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
var configJson = await File.ReadAllTextAsync(_configFilePath);
|
var configJson = await File.ReadAllTextAsync(_configFilePath);
|
||||||
var configurations = JsonSerializer.Deserialize<List<ConfiguredConnection>>(configJson);
|
var configurations = JsonHelper.ToObject<List<ConfiguredConnection>>(configJson);
|
||||||
|
|
||||||
_configuredConnections.Clear();
|
_configuredConnections.Clear();
|
||||||
foreach (var config in configurations)
|
foreach (var config in configurations)
|
||||||
@@ -635,7 +843,7 @@ namespace EonaCat.ConnectionMonitor
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var configurations = _configuredConnections.ToList();
|
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);
|
await File.WriteAllTextAsync(_configFilePath, json);
|
||||||
|
|
||||||
lblStatusBar.Text = $"Configuration saved to {_configFilePath}";
|
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; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
42
EonaCat.ConnectionMonitor/Models/ConfiguredConnection.cs
Normal file
42
EonaCat.ConnectionMonitor/Models/ConfiguredConnection.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
14
EonaCat.ConnectionMonitor/Models/ConnectionEvent.cs
Normal file
14
EonaCat.ConnectionMonitor/Models/ConnectionEvent.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
89
EonaCat.ConnectionMonitor/Models/ConnectionInfo.cs
Normal file
89
EonaCat.ConnectionMonitor/Models/ConnectionInfo.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
21
EonaCat.ConnectionMonitor/Models/CountryConnection.cs
Normal file
21
EonaCat.ConnectionMonitor/Models/CountryConnection.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
8
EonaCat.ConnectionMonitor/Models/CountryProcess.cs
Normal file
8
EonaCat.ConnectionMonitor/Models/CountryProcess.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace EonaCat.ConnectionMonitor.Models
|
||||||
|
{
|
||||||
|
public class CountryProcess
|
||||||
|
{
|
||||||
|
public string ProcessName { get; set; }
|
||||||
|
public int ConnectionCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
11
EonaCat.ConnectionMonitor/Models/CountryStatistic.cs
Normal file
11
EonaCat.ConnectionMonitor/Models/CountryStatistic.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
11
EonaCat.ConnectionMonitor/Models/GeolocationInfo.cs
Normal file
11
EonaCat.ConnectionMonitor/Models/GeolocationInfo.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
9
EonaCat.ConnectionMonitor/Models/ProcessStatistic.cs
Normal file
9
EonaCat.ConnectionMonitor/Models/ProcessStatistic.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
8
EonaCat.ConnectionMonitor/Models/TopProcessInfo.cs
Normal file
8
EonaCat.ConnectionMonitor/Models/TopProcessInfo.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace EonaCat.ConnectionMonitor.Models
|
||||||
|
{
|
||||||
|
public class TopProcessInfo
|
||||||
|
{
|
||||||
|
public string ProcessName { get; set; }
|
||||||
|
public int ConnectionCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user