Upload folder via GUI - src
This commit is contained in:
8
src/editor-preload.js
Normal file
8
src/editor-preload.js
Normal file
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('editorApi', {
|
||||
exportPdf: (html) => ipcRenderer.invoke('export-pdf', html),
|
||||
pingDevice: (ip) => ipcRenderer.invoke('ping-device', ip),
|
||||
onPingResult: (cb) => ipcRenderer.on('ping-result', (e, d) => cb(d))
|
||||
});
|
||||
1013
src/editor.html
Normal file
1013
src/editor.html
Normal file
File diff suppressed because it is too large
Load Diff
1335
src/index.html
Normal file
1335
src/index.html
Normal file
File diff suppressed because it is too large
Load Diff
625
src/main.js
Normal file
625
src/main.js
Normal file
@@ -0,0 +1,625 @@
|
||||
'use strict';
|
||||
|
||||
const { app, BrowserWindow, ipcMain, Notification, shell, Tray, Menu, nativeImage } = require('electron');
|
||||
const path = require('path');
|
||||
const { execFile, exec } = require('child_process');
|
||||
const os = require('os');
|
||||
const net = require('net');
|
||||
const fs = require('fs');
|
||||
|
||||
let mainWindow;
|
||||
var DEBUG_SCAN = false; // verbose console logging for scan diagnostics — set to true if troubleshooting again
|
||||
let tray = null;
|
||||
var autoScanTimer = null;
|
||||
var autoScanInterval = 0; // minutes, 0 = off
|
||||
var isQuitting = false;
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────
|
||||
function dataPath(file) { return path.join(app.getPath('userData'), file); }
|
||||
function readJson(file, fallback) {
|
||||
try { var d = fs.readFileSync(dataPath(file), 'utf8'); return JSON.parse(d); } catch(e) { return fallback; }
|
||||
}
|
||||
function writeJson(file, data) {
|
||||
try { fs.writeFileSync(dataPath(file), JSON.stringify(data, null, 2), 'utf8'); return true; } catch(e) { return false; }
|
||||
}
|
||||
|
||||
// ── Port config ───────────────────────────────────────────
|
||||
ipcMain.handle('get-ports', () => readJson('custom-ports.json', null) || DEFAULT_PORTS);
|
||||
ipcMain.handle('save-ports', (e, ports) => { COMMON_PORTS = ports; return writeJson('custom-ports.json', ports); });
|
||||
|
||||
// ── Device names ──────────────────────────────────────────
|
||||
ipcMain.handle('get-device-names', () => readJson('device-names.json', {}));
|
||||
ipcMain.handle('save-device-names', (e, names) => writeJson('device-names.json', names));
|
||||
|
||||
// ── Favorites ─────────────────────────────────────────────
|
||||
ipcMain.handle('get-favorites', () => readJson('favorites.json', []));
|
||||
ipcMain.handle('save-favorites', (e, favs) => writeJson('favorites.json', favs));
|
||||
|
||||
// ── Settings ──────────────────────────────────────────────
|
||||
ipcMain.handle('get-settings', () => readJson('settings.json', { theme: 'dark', autoScan: 0, notifyNew: true, notifyOffline: true }));
|
||||
ipcMain.handle('save-settings', (e, s) => {
|
||||
writeJson('settings.json', s);
|
||||
autoScanInterval = s.autoScan || 0;
|
||||
resetAutoScan();
|
||||
return true;
|
||||
});
|
||||
|
||||
// ── Scan history ──────────────────────────────────────────
|
||||
function loadHistory() { return readJson('scan-history.json', []); }
|
||||
function saveHistory(h) { writeJson('scan-history.json', h.slice(0, 200)); }
|
||||
|
||||
ipcMain.handle('get-history', () => loadHistory());
|
||||
ipcMain.handle('clear-history', () => { writeJson('scan-history.json', []); return true; });
|
||||
|
||||
function addHistoryEntry(entry) {
|
||||
var h = loadHistory();
|
||||
h.unshift(entry);
|
||||
saveHistory(h);
|
||||
}
|
||||
|
||||
// ── Uptime tracking ────────────────────────────────────────
|
||||
// Records per-IP: { onlineSince: timestamp|null, lastSeen: timestamp }
|
||||
function loadUptimeData() { return readJson('uptime.json', {}); }
|
||||
function saveUptimeData(d) { writeJson('uptime.json', d); }
|
||||
|
||||
function updateUptime(ip, isOnline) {
|
||||
var data = loadUptimeData();
|
||||
var now = Date.now();
|
||||
if (!data[ip]) data[ip] = { onlineSince: null, lastSeen: null, lastOffline: null };
|
||||
if (isOnline) {
|
||||
if (!data[ip].onlineSince) data[ip].onlineSince = now;
|
||||
data[ip].lastSeen = now;
|
||||
} else {
|
||||
data[ip].onlineSince = null;
|
||||
data[ip].lastOffline = now;
|
||||
}
|
||||
saveUptimeData(data);
|
||||
return data[ip];
|
||||
}
|
||||
|
||||
ipcMain.handle('get-uptime', (e, ip) => {
|
||||
var data = loadUptimeData();
|
||||
return data[ip] || { onlineSince: null, lastSeen: null, lastOffline: null };
|
||||
});
|
||||
|
||||
// ── Ping history for sparkline graphs ─────────────────────
|
||||
// In-memory only (resets on restart) – keeps last N points per IP
|
||||
var pingHistory = {}; // { ip: [{t,ms}, ...] }
|
||||
var PING_HISTORY_MAX = 40;
|
||||
|
||||
function recordPingHistory(ip, ms) {
|
||||
if (!pingHistory[ip]) pingHistory[ip] = [];
|
||||
pingHistory[ip].push({ t: Date.now(), ms: ms });
|
||||
if (pingHistory[ip].length > PING_HISTORY_MAX) pingHistory[ip].shift();
|
||||
}
|
||||
|
||||
ipcMain.handle('get-ping-history', (e, ip) => pingHistory[ip] || []);
|
||||
|
||||
// ── Named network plans (multiple saved schaltplans) ──────
|
||||
ipcMain.handle('get-plans', () => readJson('plans.json', {}));
|
||||
ipcMain.handle('save-plan', (e, { name, data }) => {
|
||||
var plans = readJson('plans.json', {});
|
||||
plans[name] = { data: data, savedAt: Date.now() };
|
||||
return writeJson('plans.json', plans);
|
||||
});
|
||||
ipcMain.handle('delete-plan', (e, name) => {
|
||||
var plans = readJson('plans.json', {});
|
||||
delete plans[name];
|
||||
return writeJson('plans.json', plans);
|
||||
});
|
||||
|
||||
// ── Multi-subnet monitoring ────────────────────────────────
|
||||
ipcMain.handle('get-monitored-subnets', () => readJson('monitored-subnets.json', []));
|
||||
ipcMain.handle('save-monitored-subnets', (e, subnets) => writeJson('monitored-subnets.json', subnets));
|
||||
|
||||
// ── Window ────────────────────────────────────────────────
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200, height: 780, minWidth: 900, minHeight: 600,
|
||||
frame: false, backgroundColor: '#0d1117',
|
||||
webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false },
|
||||
icon: path.join(__dirname, '../assets/icon.png')
|
||||
});
|
||||
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||
|
||||
// F12 opens DevTools even though the window is frameless (no menu bar)
|
||||
mainWindow.webContents.on('before-input-event', function(event, input) {
|
||||
if (input.key === 'F12') mainWindow.webContents.toggleDevTools();
|
||||
});
|
||||
|
||||
// Mirror every renderer console.log/error/warn into this terminal so
|
||||
// frontend errors are visible without needing to open DevTools manually.
|
||||
mainWindow.webContents.on('console-message', function(event, level, message, line, sourceId) {
|
||||
var levelNames = ['LOG','WARN','ERROR','DEBUG'];
|
||||
console.log('[renderer:' + (levelNames[level] || level) + ']', message, '(line ' + line + ')');
|
||||
});
|
||||
|
||||
mainWindow.on('close', function(e) {
|
||||
var settings = readJson('settings.json', {});
|
||||
if (settings.closeToTray && !isQuitting) {
|
||||
e.preventDefault();
|
||||
mainWindow.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
try {
|
||||
var iconPath = path.join(__dirname, '../assets/icon.png');
|
||||
var img = fs.existsSync(iconPath) ? nativeImage.createFromPath(iconPath) : nativeImage.createEmpty();
|
||||
tray = new Tray(img.isEmpty() ? nativeImage.createEmpty() : img.resize({ width: 16, height: 16 }));
|
||||
tray.setToolTip('NetScanner – läuft im Hintergrund');
|
||||
var contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Öffnen', click: function() { mainWindow.show(); } },
|
||||
{ label: 'Jetzt scannen', click: function() { mainWindow.show(); mainWindow.webContents.send('auto-scan-trigger'); } },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Beenden', click: function() { isQuitting = true; app.quit(); } }
|
||||
]);
|
||||
tray.setContextMenu(contextMenu);
|
||||
tray.on('click', function() { mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); });
|
||||
} catch(e) { /* tray not supported on this platform/config */ }
|
||||
}
|
||||
|
||||
process.on('uncaughtException', function(err) {
|
||||
console.error('[UNCAUGHT EXCEPTION]', err);
|
||||
});
|
||||
process.on('unhandledRejection', function(reason) {
|
||||
console.error('[UNHANDLED REJECTION]', reason);
|
||||
});
|
||||
|
||||
app.whenReady().then(function() {
|
||||
var saved = readJson('custom-ports.json', null);
|
||||
if (saved) COMMON_PORTS = saved;
|
||||
var settings = readJson('settings.json', { autoScan: 0 });
|
||||
autoScanInterval = settings.autoScan || 0;
|
||||
createWindow();
|
||||
createTray();
|
||||
resetAutoScan();
|
||||
});
|
||||
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
|
||||
app.on('before-quit', () => { isQuitting = true; });
|
||||
|
||||
// ── Window controls ───────────────────────────────────────
|
||||
ipcMain.on('win-minimize', () => mainWindow.minimize());
|
||||
ipcMain.on('win-maximize', () => { mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize(); });
|
||||
ipcMain.on('win-close', () => mainWindow.close());
|
||||
ipcMain.on('quit-app', () => { isQuitting = true; app.quit(); });
|
||||
|
||||
// ── IPv6 neighbor discovery ────────────────────────────────
|
||||
function getIPv6Neighbors() {
|
||||
return new Promise(function(resolve) {
|
||||
var cmd = process.platform === 'win32' ? 'netsh interface ipv6 show neighbors' : 'ip -6 neigh show';
|
||||
exec(cmd, { timeout: 3000 }, function(err, stdout) {
|
||||
if (err || !stdout) return resolve([]);
|
||||
var results = [];
|
||||
var lines = stdout.split('\n');
|
||||
lines.forEach(function(line) {
|
||||
// Match IPv6 address (not link-local fe80:: unless specifically wanted) + MAC
|
||||
var ipMatch = line.match(/([0-9a-f]{0,4}:[0-9a-f:]+:[0-9a-f]{0,4})/i);
|
||||
var macMatch = line.match(/([0-9a-f]{2}[:-]){5}[0-9a-f]{2}/i);
|
||||
if (ipMatch && macMatch && !ipMatch[1].startsWith('fe80')) {
|
||||
results.push({ ip: ipMatch[1], mac: macMatch[0].toUpperCase() });
|
||||
}
|
||||
});
|
||||
resolve(results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle('get-ipv6-neighbors', async () => await getIPv6Neighbors());
|
||||
|
||||
// ── Auto scan ─────────────────────────────────────────────
|
||||
function resetAutoScan() {
|
||||
if (autoScanTimer) { clearInterval(autoScanTimer); autoScanTimer = null; }
|
||||
if (autoScanInterval > 0) {
|
||||
autoScanTimer = setInterval(function() {
|
||||
if (mainWindow) mainWindow.webContents.send('auto-scan-trigger');
|
||||
}, autoScanInterval * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Notifications ─────────────────────────────────────────
|
||||
ipcMain.on('notify', (e, { title, body, type }) => {
|
||||
var settings = readJson('settings.json', { notifyNew: true, notifyOffline: true });
|
||||
if (type === 'new' && !settings.notifyNew) return;
|
||||
if (type === 'offline' && !settings.notifyOffline) return;
|
||||
if (Notification.isSupported()) {
|
||||
new Notification({ title: title, body: body, silent: false }).show();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Vendor lookup ─────────────────────────────────────────
|
||||
ipcMain.handle('lookup-vendor', async (e, mac) => await lookupVendor(mac));
|
||||
|
||||
// ── Network info ──────────────────────────────────────────
|
||||
ipcMain.handle('get-network-info', () => {
|
||||
var ifaces = os.networkInterfaces();
|
||||
var results = [];
|
||||
Object.keys(ifaces).forEach(function(name) {
|
||||
ifaces[name].forEach(function(iface) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
var parts = iface.address.split('.');
|
||||
results.push({ name: name, address: iface.address, subnet: parts[0]+'.'+parts[1]+'.'+parts[2]+'.', mac: iface.mac });
|
||||
}
|
||||
});
|
||||
});
|
||||
return results;
|
||||
});
|
||||
|
||||
// ── Ping ──────────────────────────────────────────────────
|
||||
function pingHost(ip) {
|
||||
return new Promise(function(resolve) {
|
||||
var start = Date.now();
|
||||
var args = process.platform === 'win32' ? ['-n','1','-w','500',ip] : ['-c','1','-W','1',ip];
|
||||
execFile('ping', args, { timeout: 2000 }, function(err, stdout) {
|
||||
stdout = stdout || '';
|
||||
var alive = !err && (stdout.includes('TTL=') || stdout.includes('ttl=') || stdout.includes('1 received'));
|
||||
var ttl = null, os_guess = null;
|
||||
var ttlMatch = stdout.match(/TTL=(\d+)/i);
|
||||
if (ttlMatch) {
|
||||
ttl = parseInt(ttlMatch[1]);
|
||||
if (ttl >= 128) os_guess = 'Windows';
|
||||
else if (ttl >= 64) os_guess = 'Linux/macOS';
|
||||
else if (ttl >= 32) os_guess = 'Network Device';
|
||||
}
|
||||
var ms = null;
|
||||
if (alive) {
|
||||
var msMatch = stdout.match(/(\d+(?:\.\d+)?)\s*ms\b/i);
|
||||
ms = msMatch ? Math.round(parseFloat(msMatch[1])) : (Date.now() - start);
|
||||
}
|
||||
if (DEBUG_SCAN) console.log('[pingHost]', ip, '-> alive:', alive, 'err:', err ? err.message : null, 'stdout snippet:', stdout.substring(0,120).replace(/\n/g,' | '));
|
||||
resolve({ ip: ip, alive: alive, ms: ms, ttl: ttl, os: os_guess });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle('ping-device', async (e, ip) => await pingHost(ip));
|
||||
|
||||
// ── Ping multiple devices at once (for live updates) ──────
|
||||
// Pings are chunked instead of all-at-once: spawning 30+ ping processes
|
||||
// simultaneously creates OS scheduling contention that inflates wall-clock
|
||||
// timings across the board. Chunks of 12 keep this from happening.
|
||||
ipcMain.handle('ping-devices-batch', async function(event, ips) {
|
||||
var results = [];
|
||||
var chunkSize = 12;
|
||||
for (var i = 0; i < ips.length; i += chunkSize) {
|
||||
var chunk = ips.slice(i, i + chunkSize);
|
||||
var chunkResults = await Promise.all(chunk.map(function(ip) { return pingHost(ip); }));
|
||||
results = results.concat(chunkResults);
|
||||
}
|
||||
results.forEach(function(r) {
|
||||
recordPingHistory(r.ip, r.alive ? r.ms : null);
|
||||
updateUptime(r.ip, r.alive);
|
||||
});
|
||||
return results;
|
||||
});
|
||||
|
||||
// ── Hostname ──────────────────────────────────────────────
|
||||
function getHostname(ip) {
|
||||
return new Promise(function(resolve) {
|
||||
exec('nslookup ' + ip, { timeout: 2000 }, function(err, stdout) {
|
||||
if (!err && stdout) { var m = stdout.match(/Name:\s+(.+)/i); if (m) return resolve(m[1].trim().split('.')[0]); }
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── MAC from ARP ──────────────────────────────────────────
|
||||
function getMacFromArp(ip) {
|
||||
return new Promise(function(resolve) {
|
||||
exec('arp -a ' + ip, { timeout: 2000 }, function(err, stdout) {
|
||||
if (!err && stdout) { var m = stdout.match(/([0-9a-f]{2}[:-]){5}[0-9a-f]{2}/i); if (m) return resolve(m[0].toUpperCase()); }
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── OUI lookup ────────────────────────────────────────────
|
||||
var OUI = {
|
||||
'00:50:56':'VMware','00:0C:29':'VMware','00:1A:4B':'VMware','00:05:69':'VMware',
|
||||
'52:54:00':'QEMU/KVM','00:16:3E':'Xen',
|
||||
'B8:27:EB':'Raspberry Pi','DC:A6:32':'Raspberry Pi','E4:5F:01':'Raspberry Pi','28:CD:C1':'Raspberry Pi',
|
||||
'00:1B:21':'Intel','8C:8D:28':'Intel','00:21:6A':'Intel','A4:C3:F0':'Intel','00:1F:3C':'Intel','A0:36:9F':'Intel',
|
||||
'AC:22:05':'ASUS','04:D4:C4':'ASUS','14:DA:E9':'ASUS','2C:4D:54':'ASUS','BC:AE:C5':'ASUS','74:D0:2B':'ASUS',
|
||||
'18:31:BF':'Apple','3C:06:30':'Apple','F0:18:98':'Apple','50:1A:C5':'Apple','A4:CF:99':'Apple',
|
||||
'AC:BC:32':'Apple','F4:F1:5A':'Apple','DC:2B:2A':'Apple','A8:51:AB':'Apple','3C:22:FB':'Apple',
|
||||
'AC:E2:D3':'Hewlett Packard','00:17:08':'Hewlett Packard','00:1F:29':'Hewlett Packard',
|
||||
'3C:D9:2B':'Hewlett Packard','94:57:A5':'Hewlett Packard','B4:B5:2F':'Hewlett Packard',
|
||||
'00:14:22':'Dell','F8:DB:88':'Dell','BC:30:5B':'Dell','18:FB:7B':'Dell','B8:AC:6F':'Dell',
|
||||
'00:23:AE':'Lenovo','54:EE:75':'Lenovo','E8:6A:64':'Lenovo','28:D2:44':'Lenovo',
|
||||
'FC:AA:14':'Synology','00:11:32':'Synology','BC:5F:F4':'Synology',
|
||||
'CC:9F:06':'TP-Link','F4:F2:6D':'TP-Link','18:D6:C7':'TP-Link','50:C7:BF':'TP-Link','AC:84:C6':'TP-Link',
|
||||
'B4:FB:E4':'AVM (Fritz!Box)','00:04:0E':'AVM (Fritz!Box)','C4:86:E9':'AVM (Fritz!Box)',
|
||||
'AC:16:2D':'AVM (Fritz!Box)','3C:37:12':'AVM (Fritz!Box)','A0:63:91':'AVM (Fritz!Box)',
|
||||
'08:96:D7':'AVM (Fritz!Box)','1C:4B:D6':'AVM (Fritz!Box)','34:31:C4':'AVM (Fritz!Box)',
|
||||
'7C:FF:4D':'AVM (Fritz!Box)','9C:C7:A6':'AVM (Fritz!Box)','D4:21:22':'AVM (Fritz!Box)',
|
||||
'E0:14:C8':'AVM (Fritz!Box)','F8:1A:67':'AVM (Fritz!Box)','BC:05:43':'AVM (Fritz!Box)',
|
||||
'D8:61:62':'Ubiquiti','24:A4:3C':'Ubiquiti','80:2A:A8':'Ubiquiti','F0:9F:C2':'Ubiquiti',
|
||||
'00:1E:67':'Cisco','00:50:0F':'Cisco','AC:F2:C5':'Cisco','00:00:0C':'Cisco',
|
||||
'C8:D3:A3':'Huawei','00:E0:FC':'Huawei','28:6E:D4':'Huawei',
|
||||
'00:12:47':'Samsung','08:D4:0C':'Samsung','F4:7B:5E':'Samsung','CC:07:AB':'Samsung','8C:77:12':'Samsung',
|
||||
'98:DA:C4':'Xiaomi','64:09:80':'Xiaomi','50:64:2B':'Xiaomi',
|
||||
'00:E0:4C':'Realtek',
|
||||
'00:17:9A':'D-Link','1C:7E:E5':'D-Link','C8:BE:19':'D-Link',
|
||||
'00:14:6C':'Netgear','20:E5:2A':'Netgear','A0:21:B7':'Netgear',
|
||||
'4C:5E:0C':'MikroTik','B8:69:F4':'MikroTik','CC:2D:E0':'MikroTik',
|
||||
'AC:1F:6B':'Supermicro','0C:C4:7A':'Supermicro',
|
||||
'00:08:9B':'QNAP','24:5E:BE':'QNAP',
|
||||
'00:09:BF':'Nintendo','00:17:AB':'Nintendo','58:BD:A3':'Nintendo',
|
||||
'00:13:A9':'Sony','F8:DA:0C':'Sony','30:17:C8':'Sony',
|
||||
'BC:24:11':'Proxmox Server Solutions GmbH','00:23:24':'G-PRO COMPUTER',
|
||||
'8C:11:CB':'ABUS Security-Center GmbH','94:B3:F7':'Hui Zhou Gaoshengda Technology',
|
||||
'E0:03:6B':'Samsung Electronics Co.,Ltd','F4:B8:5E':'Texas Instruments',
|
||||
'00:1B:A9':'Brother Industries','E0:D3:62':'TP-Link Systems Inc.',
|
||||
'12:C5:74':'Private/Unknown'
|
||||
};
|
||||
|
||||
var vendorCache = {};
|
||||
function lookupVendorApi(mac) {
|
||||
return new Promise(function(resolve) {
|
||||
var normalized = mac.replace(/[:\-]/g,'').toUpperCase().match(/.{2}/g).join(':');
|
||||
var oui = normalized.substring(0,8);
|
||||
if (vendorCache[oui]) return resolve(vendorCache[oui]);
|
||||
var url = 'https://api.maclookup.app/v2/macs/' + normalized;
|
||||
var https = require('https');
|
||||
var req = https.get(url, { timeout: 3000 }, function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(c) { data += c; });
|
||||
res.on('end', function() {
|
||||
try {
|
||||
var json = JSON.parse(data);
|
||||
var vendor = (json.company && json.company !== 'Private') ? json.company : 'Unbekannt';
|
||||
vendorCache[oui] = vendor; resolve(vendor);
|
||||
} catch(e) { resolve('Unbekannt'); }
|
||||
});
|
||||
});
|
||||
req.on('error', () => resolve('Unbekannt'));
|
||||
req.on('timeout', () => { req.destroy(); resolve('Unbekannt'); });
|
||||
});
|
||||
}
|
||||
|
||||
async function lookupVendor(mac) {
|
||||
if (!mac || mac === '–') return 'Unbekannt';
|
||||
var normalized = mac.replace(/-/g,':').toUpperCase();
|
||||
var oui = normalized.substring(0,8);
|
||||
if (OUI[oui]) return OUI[oui];
|
||||
return await lookupVendorApi(normalized);
|
||||
}
|
||||
|
||||
// ── Ports ─────────────────────────────────────────────────
|
||||
var DEFAULT_PORTS = [
|
||||
{ port:21,name:'FTP',desc:'Dateiübertragung (File Transfer Protocol)' },
|
||||
{ port:22,name:'SSH',desc:'Sichere Fernsteuerung / Terminal-Zugriff' },
|
||||
{ port:23,name:'Telnet',desc:'Unverschlüsselter Fernzugriff (veraltet)' },
|
||||
{ port:25,name:'SMTP',desc:'E-Mail-Versand (Simple Mail Transfer Protocol)' },
|
||||
{ port:53,name:'DNS',desc:'Domain Name System – Namensauflösung' },
|
||||
{ port:80,name:'HTTP',desc:'Unverschlüsselter Web-Server' },
|
||||
{ port:110,name:'POP3',desc:'E-Mail-Empfang (Post Office Protocol)' },
|
||||
{ port:135,name:'RPC',desc:'Windows Remote Procedure Call' },
|
||||
{ port:139,name:'NetBIOS',desc:'Windows Netzwerkfreigabe (NetBIOS)' },
|
||||
{ port:143,name:'IMAP',desc:'E-Mail-Empfang (Internet Message Access Protocol)' },
|
||||
{ port:443,name:'HTTPS',desc:'Verschlüsselter Web-Server (SSL/TLS)' },
|
||||
{ port:445,name:'SMB',desc:'Windows Dateifreigabe / Netzlaufwerke' },
|
||||
{ port:1883,name:'MQTT',desc:'Smart-Home Nachrichtenprotokoll (IoT)' },
|
||||
{ port:3306,name:'MySQL',desc:'MySQL/MariaDB Datenbank-Server' },
|
||||
{ port:3389,name:'RDP',desc:'Windows Remote Desktop – Fernsteuerung' },
|
||||
{ port:5000,name:'DSM HTTP',desc:'Synology DiskStation Manager (HTTP)' },
|
||||
{ port:5001,name:'DSM HTTPS',desc:'Synology DiskStation Manager (HTTPS)' },
|
||||
{ port:5900,name:'VNC',desc:'Virtual Network Computing – Desktop-Fernzugriff' },
|
||||
{ port:8006,name:'Proxmox',desc:'Proxmox VE Web-Oberfläche' },
|
||||
{ port:8080,name:'HTTP-Alt',desc:'Alternativer HTTP-Port (oft Web-Apps)' },
|
||||
{ port:8443,name:'HTTPS-Alt',desc:'Alternativer HTTPS-Port (oft Web-Apps)' },
|
||||
{ port:9000,name:'Portainer',desc:'Portainer – Docker-Verwaltung' },
|
||||
{ port:9090,name:'Cockpit',desc:'Linux Cockpit Server-Verwaltung' },
|
||||
{ port:9100,name:'Prometheus',desc:'Prometheus Node Exporter (Monitoring)' },
|
||||
{ port:19999,name:'Netdata',desc:'Netdata Echtzeit-Monitoring Dashboard' },
|
||||
{ port:25565,name:'Minecraft',desc:'Minecraft Java Edition Server' },
|
||||
{ port:25575,name:'MC RCON',desc:'Minecraft Remote Console (RCON)' },
|
||||
{ port:32400,name:'Plex',desc:'Plex Media Server' }
|
||||
];
|
||||
var COMMON_PORTS = DEFAULT_PORTS;
|
||||
|
||||
function scanPort(ip, port) {
|
||||
return new Promise(function(resolve) {
|
||||
var socket = new net.Socket(); var done = false; socket.setTimeout(600);
|
||||
socket.on('connect', function() { done=true; socket.destroy(); resolve(true); });
|
||||
socket.on('timeout', function() { if(!done){done=true;socket.destroy();resolve(false);} });
|
||||
socket.on('error', function() { if(!done){done=true;resolve(false);} });
|
||||
socket.connect(port, ip);
|
||||
});
|
||||
}
|
||||
|
||||
async function scanPorts(ip) {
|
||||
var results = []; var batch = 5;
|
||||
for (var i = 0; i < COMMON_PORTS.length; i += batch) {
|
||||
var slice = COMMON_PORTS.slice(i, i+batch);
|
||||
var checks = await Promise.all(slice.map(function(p) { return scanPort(ip,p.port).then(function(open){return open?p:null;}); }));
|
||||
checks.forEach(function(r) { if(r) results.push(r); });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ── Security warnings: flag insecure/risky open ports ─────
|
||||
var INSECURE_PORTS = {
|
||||
21: 'FTP übermittelt Zugangsdaten unverschlüsselt',
|
||||
23: 'Telnet ist komplett unverschlüsselt – Fernzugriff vermeiden',
|
||||
139: 'NetBIOS ist ein häufiges Angriffsziel im LAN',
|
||||
445: 'SMB war Ziel diverser Würmer (z.B. WannaCry) – nur intern erlauben',
|
||||
3389: 'RDP sollte nie direkt ins Internet exponiert werden',
|
||||
5900: 'VNC ist oft unverschlüsselt – Passwortschutz prüfen'
|
||||
};
|
||||
|
||||
function getSecurityWarnings(openPorts) {
|
||||
var warnings = [];
|
||||
openPorts.forEach(function(p) {
|
||||
if (INSECURE_PORTS[p.port]) warnings.push({ port: p.port, name: p.name, warning: INSECURE_PORTS[p.port] });
|
||||
});
|
||||
return warnings;
|
||||
}
|
||||
|
||||
ipcMain.handle('get-security-warnings', (e, openPorts) => getSecurityWarnings(openPorts));
|
||||
|
||||
// ── Full scan ─────────────────────────────────────────────
|
||||
var lastScanResults = [];
|
||||
|
||||
async function performScan(subnet, sendProgress) {
|
||||
if (DEBUG_SCAN) console.log('[performScan] START subnet:', subnet, 'sendProgress:', sendProgress);
|
||||
var found = []; var batch = 50; var ips = [];
|
||||
for (var i=1;i<=254;i++) ips.push(subnet+i);
|
||||
|
||||
for (var j=0;j<ips.length;j+=batch) {
|
||||
var slice = ips.slice(j,j+batch);
|
||||
var pings = await Promise.all(slice.map(pingHost));
|
||||
for (var k=0;k<pings.length;k++) {
|
||||
if (pings[k].alive) {
|
||||
if (sendProgress) mainWindow.webContents.send('device-found', { ip:pings[k].ip, ms:pings[k].ms });
|
||||
found.push(pings[k]);
|
||||
}
|
||||
}
|
||||
if (sendProgress) mainWindow.webContents.send('scan-progress', Math.min(Math.round(((j+batch)/ips.length)*70),70));
|
||||
}
|
||||
if (DEBUG_SCAN) console.log('[performScan] ping phase done. Alive devices found:', found.length, found.map(function(f){return f.ip;}));
|
||||
|
||||
// IPv6 neighbors are a nice-to-have enrichment — never let a failure here
|
||||
// block the main scan results from being returned.
|
||||
var ipv6Neighbors = [];
|
||||
try {
|
||||
ipv6Neighbors = await getIPv6Neighbors();
|
||||
if (DEBUG_SCAN) console.log('[performScan] IPv6 neighbors found:', ipv6Neighbors.length);
|
||||
} catch(e) {
|
||||
if (DEBUG_SCAN) console.log('[performScan] getIPv6Neighbors threw (ignored):', e.message);
|
||||
ipv6Neighbors = [];
|
||||
}
|
||||
|
||||
var enriched = [];
|
||||
for (var d=0;d<found.length;d++) {
|
||||
var ip = found[d].ip;
|
||||
try {
|
||||
var mac = await getMacFromArp(ip);
|
||||
var hostname = await getHostname(ip);
|
||||
var vendor = await lookupVendor(mac);
|
||||
var ipv6Match = ipv6Neighbors.find(function(n) { return mac && n.mac === mac; });
|
||||
enriched.push({
|
||||
ip:ip, ms:found[d].ms, mac:mac||'–', hostname:hostname||ip, vendor:vendor,
|
||||
os:found[d].os, ttl:found[d].ttl, ipv6: ipv6Match ? ipv6Match.ip : null,
|
||||
firstSeen: Date.now(), lastSeen: Date.now()
|
||||
});
|
||||
recordPingHistory(ip, found[d].ms);
|
||||
updateUptime(ip, true);
|
||||
} catch(e) {
|
||||
if (DEBUG_SCAN) console.log('[performScan] enrichment failed for', ip, '-', e.message, '— including with basic info anyway');
|
||||
// Even if enrichment fails for this IP, still include it with basic info
|
||||
enriched.push({ ip:ip, ms:found[d].ms, mac:'–', hostname:ip, vendor:'Unbekannt', os:found[d].os, ttl:found[d].ttl, ipv6:null, firstSeen: Date.now(), lastSeen: Date.now() });
|
||||
}
|
||||
if (sendProgress) mainWindow.webContents.send('scan-progress', 70+Math.round((d/Math.max(found.length,1))*20));
|
||||
}
|
||||
if (DEBUG_SCAN) console.log('[performScan] DONE. Enriched device count:', enriched.length);
|
||||
return enriched;
|
||||
}
|
||||
|
||||
ipcMain.handle('scan-network', async function(event, subnet) {
|
||||
if (DEBUG_SCAN) console.log('[scan-network] handler invoked with subnet:', subnet);
|
||||
var enriched = [];
|
||||
try {
|
||||
enriched = await performScan(subnet, true);
|
||||
|
||||
// Compare with last scan – detect new/offline devices
|
||||
var settings = readJson('settings.json', { notifyNew:true, notifyOffline:true });
|
||||
var prevIps = lastScanResults.map(function(d){return d.ip;});
|
||||
var currIps = enriched.map(function(d){return d.ip;});
|
||||
|
||||
enriched.forEach(function(d) {
|
||||
if (prevIps.length > 0 && prevIps.indexOf(d.ip) < 0) {
|
||||
mainWindow.webContents.send('device-new', d);
|
||||
if (settings.notifyNew && Notification.isSupported()) {
|
||||
new Notification({ title:'NetScanner – Neues Gerät', body: (d.hostname||d.ip) + ' (' + d.ip + ') ist dem Netzwerk beigetreten.', silent:false }).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lastScanResults.forEach(function(d) {
|
||||
if (currIps.indexOf(d.ip) < 0) {
|
||||
mainWindow.webContents.send('device-offline', d);
|
||||
updateUptime(d.ip, false);
|
||||
if (settings.notifyOffline && Notification.isSupported()) {
|
||||
new Notification({ title:'NetScanner – Gerät offline', body: (d.hostname||d.ip) + ' (' + d.ip + ') ist nicht mehr erreichbar.', silent:false }).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lastScanResults = enriched;
|
||||
addHistoryEntry({ time: Date.now(), subnet: subnet, count: enriched.length, devices: enriched.map(function(d){return {ip:d.ip,hostname:d.hostname,ms:d.ms,os:d.os};}) });
|
||||
} catch(e) {
|
||||
console.error('scan-network error:', e);
|
||||
}
|
||||
try { mainWindow.webContents.send('scan-progress', 100); } catch(e2) {}
|
||||
if (DEBUG_SCAN) console.log('[scan-network] returning', enriched.length, 'devices to renderer');
|
||||
return enriched;
|
||||
});
|
||||
|
||||
// ── Scan an additional subnet (multi-subnet monitoring) ───
|
||||
ipcMain.handle('scan-subnet-silent', async function(event, subnet) {
|
||||
return await performScan(subnet, false);
|
||||
});
|
||||
|
||||
ipcMain.handle('scan-ports', async (e,ip) => await scanPorts(ip));
|
||||
|
||||
// ── Export device list as CSV/JSON ────────────────────────
|
||||
ipcMain.handle('export-device-list', async function(event, { devices, format }) {
|
||||
var { dialog } = require('electron');
|
||||
var ext = format === 'json' ? 'json' : 'csv';
|
||||
var result = await dialog.showSaveDialog(mainWindow, {
|
||||
title: 'Geräteliste exportieren',
|
||||
defaultPath: 'NetScanner-Geraete-' + new Date().toISOString().slice(0,10) + '.' + ext,
|
||||
filters: [{ name: ext.toUpperCase(), extensions: [ext] }]
|
||||
});
|
||||
if (result.canceled || !result.filePath) return { success: false };
|
||||
|
||||
var content;
|
||||
if (format === 'json') {
|
||||
content = JSON.stringify(devices, null, 2);
|
||||
} else {
|
||||
var header = 'IP,Name,Hostname,MAC,Hersteller,Ping(ms),OS,IPv6\n';
|
||||
var rows = devices.map(function(d) {
|
||||
return [d.ip, (d.displayName||''), (d.hostname||''), (d.mac||''), (d.vendor||''), (d.ms!=null?d.ms:''), (d.os||''), (d.ipv6||'')]
|
||||
.map(function(v){ return '"' + String(v).replace(/"/g,'""') + '"'; }).join(',');
|
||||
});
|
||||
content = header + rows.join('\n');
|
||||
}
|
||||
fs.writeFileSync(result.filePath, content, 'utf8');
|
||||
shell.showItemInFolder(result.filePath);
|
||||
return { success: true };
|
||||
});
|
||||
ipcMain.handle('open-editor', async function(event, layoutData) {
|
||||
var { BrowserWindow: BW } = require('electron');
|
||||
var editorWin = new BW({ width:1300, height:800, title:'Netzwerk-Editor', frame:true, backgroundColor:'#0d1117',
|
||||
webPreferences:{ nodeIntegration:false, contextIsolation:true, preload:path.join(__dirname,'editor-preload.js') }
|
||||
});
|
||||
editorWin.loadFile(path.join(__dirname,'editor.html'));
|
||||
editorWin.webContents.on('did-finish-load', function() {
|
||||
editorWin.webContents.executeJavaScript('window.postMessage('+JSON.stringify({type:'init',nodes:layoutData.nodes,connections:layoutData.connections,groups:layoutData.groups||[]})+', "*")');
|
||||
});
|
||||
return { success:true };
|
||||
});
|
||||
|
||||
// ── PDF Export ────────────────────────────────────────────
|
||||
ipcMain.handle('export-pdf', async function(event, htmlContent) {
|
||||
var { BrowserWindow: BW, dialog } = require('electron');
|
||||
var result = await dialog.showSaveDialog(mainWindow, {
|
||||
title:'Netzwerk-Schaltplan speichern',
|
||||
defaultPath:'NetScanner-Schaltplan-'+new Date().toISOString().slice(0,10)+'.pdf',
|
||||
filters:[{name:'PDF',extensions:['pdf']}]
|
||||
});
|
||||
if (result.canceled || !result.filePath) return { success:false };
|
||||
var tmpHtml = path.join(os.tmpdir(),'netscanner-export-'+Date.now()+'.html');
|
||||
fs.writeFileSync(tmpHtml, htmlContent, 'utf8');
|
||||
var win = new BW({ show:false, width:1200, height:900, webPreferences:{nodeIntegration:false,contextIsolation:true} });
|
||||
await win.loadFile(tmpHtml);
|
||||
await new Promise(function(r){setTimeout(r,2000);});
|
||||
var pdfData = await win.webContents.printToPDF({ printBackground:true, pageSize:'A4', landscape:true, margins:{top:0.4,bottom:0.4,left:0.4,right:0.4} });
|
||||
win.close();
|
||||
try { fs.unlinkSync(tmpHtml); } catch(e) {}
|
||||
fs.writeFileSync(result.filePath, pdfData);
|
||||
shell.openPath(result.filePath);
|
||||
return { success:true };
|
||||
});
|
||||
44
src/preload.js
Normal file
44
src/preload.js
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('api', {
|
||||
getNetworkInfo: () => ipcRenderer.invoke('get-network-info'),
|
||||
scanNetwork: (s) => ipcRenderer.invoke('scan-network', s),
|
||||
scanSubnetSilent: (s) => ipcRenderer.invoke('scan-subnet-silent', s),
|
||||
scanPorts: (ip) => ipcRenderer.invoke('scan-ports', ip),
|
||||
pingDevice: (ip) => ipcRenderer.invoke('ping-device', ip),
|
||||
pingDevicesBatch: (ips) => ipcRenderer.invoke('ping-devices-batch', ips),
|
||||
lookupVendor: (mac) => ipcRenderer.invoke('lookup-vendor', mac),
|
||||
getPorts: () => ipcRenderer.invoke('get-ports'),
|
||||
savePorts: (p) => ipcRenderer.invoke('save-ports', p),
|
||||
getDeviceNames: () => ipcRenderer.invoke('get-device-names'),
|
||||
saveDeviceNames: (n) => ipcRenderer.invoke('save-device-names', n),
|
||||
getFavorites: () => ipcRenderer.invoke('get-favorites'),
|
||||
saveFavorites: (f) => ipcRenderer.invoke('save-favorites', f),
|
||||
getSettings: () => ipcRenderer.invoke('get-settings'),
|
||||
saveSettings: (s) => ipcRenderer.invoke('save-settings', s),
|
||||
getHistory: () => ipcRenderer.invoke('get-history'),
|
||||
clearHistory: () => ipcRenderer.invoke('clear-history'),
|
||||
getUptime: (ip) => ipcRenderer.invoke('get-uptime', ip),
|
||||
getPingHistory: (ip) => ipcRenderer.invoke('get-ping-history', ip),
|
||||
getSecurityWarnings: (ports) => ipcRenderer.invoke('get-security-warnings', ports),
|
||||
getPlans: () => ipcRenderer.invoke('get-plans'),
|
||||
savePlan: (n,d) => ipcRenderer.invoke('save-plan', { name:n, data:d }),
|
||||
deletePlan: (n) => ipcRenderer.invoke('delete-plan', n),
|
||||
getMonitoredSubnets: () => ipcRenderer.invoke('get-monitored-subnets'),
|
||||
saveMonitoredSubnets:(s) => ipcRenderer.invoke('save-monitored-subnets', s),
|
||||
getIpv6Neighbors: () => ipcRenderer.invoke('get-ipv6-neighbors'),
|
||||
exportDeviceList: (d,f) => ipcRenderer.invoke('export-device-list', { devices:d, format:f }),
|
||||
openEditor: (l) => ipcRenderer.invoke('open-editor', l),
|
||||
exportPdf: (h) => ipcRenderer.invoke('export-pdf', h),
|
||||
notify: (d) => ipcRenderer.send('notify', d),
|
||||
onDeviceFound: (cb) => ipcRenderer.on('device-found', (e,d) => cb(d)),
|
||||
onScanProgress: (cb) => ipcRenderer.on('scan-progress', (e,p) => cb(p)),
|
||||
onDeviceNew: (cb) => ipcRenderer.on('device-new', (e,d) => cb(d)),
|
||||
onDeviceOffline: (cb) => ipcRenderer.on('device-offline', (e,d) => cb(d)),
|
||||
onAutoScan: (cb) => ipcRenderer.on('auto-scan-trigger', () => cb()),
|
||||
winMinimize: () => ipcRenderer.send('win-minimize'),
|
||||
winMaximize: () => ipcRenderer.send('win-maximize'),
|
||||
winClose: () => ipcRenderer.send('win-close'),
|
||||
quitApp: () => ipcRenderer.send('quit-app')
|
||||
});
|
||||
Reference in New Issue
Block a user