diff --git a/agent-cs/AgentWorker.cs b/agent-cs/AgentWorker.cs index e13286b..4125c54 100644 --- a/agent-cs/AgentWorker.cs +++ b/agent-cs/AgentWorker.cs @@ -6,7 +6,7 @@ namespace ITNexusAgent; public class AgentWorker { - private const string Version = "2.1.2"; + private const string Version = "2.2.0"; private const string DataDir = @"C:\ProgramData\IT Nexus Agent"; private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json"; private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json"; @@ -43,6 +43,10 @@ public class AgentWorker var shellService = new ShellService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname()); _ = shellService.RunAsync(_ct); + // WebRTC Remote Desktop Service im Hintergrund starten + var rtcService = new RtcService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname()); + _ = rtcService.RunAsync(_ct); + while (!_ct.IsCancellationRequested) { await RunCycleAsync(); diff --git a/agent-cs/IT-Nexus-Agent.csproj b/agent-cs/IT-Nexus-Agent.csproj index f82243e..ffb64ef 100644 --- a/agent-cs/IT-Nexus-Agent.csproj +++ b/agent-cs/IT-Nexus-Agent.csproj @@ -2,7 +2,7 @@ WinExe - net8.0-windows + net8.0-windows10.0.17763.0 true true IT-Nexus-Agent @@ -17,7 +17,7 @@ - + PreserveNewest @@ -28,6 +28,7 @@ + diff --git a/agent-cs/Services/RtcService.cs b/agent-cs/Services/RtcService.cs new file mode 100644 index 0000000..4aaaa09 --- /dev/null +++ b/agent-cs/Services/RtcService.cs @@ -0,0 +1,127 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Net.WebSockets; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace ITNexusAgent.Services; + +// Überträgt den Bildschirm via JPEG-Frames über WebSocket an den Browser +public class RtcService +{ + private readonly string _serverUrl; + private readonly string _agentKey; + private readonly string _hostname; + + private static readonly ImageCodecInfo JpegCodec = + ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid); + + public RtcService(string serverUrl, string agentKey, string hostname) + { + _serverUrl = serverUrl; + _agentKey = agentKey; + _hostname = hostname; + } + + public async Task RunAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try { await ConnectAsync(ct); } + catch (Exception ex) { AgentWorker.Log($"RDP: {ex.Message}"); } + + if (!ct.IsCancellationRequested) + await Task.Delay(TimeSpan.FromSeconds(15), ct).ContinueWith(_ => { }); + } + } + + private async Task ConnectAsync(CancellationToken ct) + { + var wsUrl = _serverUrl + .Replace("https://", "wss://") + .Replace("http://", "ws://") + + $"/ws?type=rdp-agent&key={Uri.EscapeDataString(_agentKey)}&hostname={Uri.EscapeDataString(_hostname)}"; + + using var ws = new ClientWebSocket(); + await ws.ConnectAsync(new Uri(wsUrl), ct); + AgentWorker.Log("RDP: Bereit"); + + CancellationTokenSource? captureCts = null; + + var buf = new byte[4096]; + while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested) + { + WebSocketReceiveResult res; + try { res = await ws.ReceiveAsync(new ArraySegment(buf), ct); } + catch { break; } + + if (res.MessageType == WebSocketMessageType.Close) break; + + var raw = Encoding.UTF8.GetString(buf, 0, res.Count); + JObject? obj; + try { obj = JObject.Parse(raw); } catch { continue; } + + var type = obj["type"]?.ToString(); + + if (type == "rdp_start" && captureCts == null) + { + captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + _ = CaptureLoopAsync(ws, captureCts.Token); + AgentWorker.Log("RDP: Screen-Capture gestartet"); + } + else if (type == "rdp_stop" && captureCts != null) + { + captureCts.Cancel(); + captureCts = null; + AgentWorker.Log("RDP: Screen-Capture gestoppt"); + } + } + + captureCts?.Cancel(); + AgentWorker.Log("RDP: Getrennt"); + } + + private static async Task CaptureLoopAsync(ClientWebSocket ws, CancellationToken ct) + { + var encParams = new EncoderParameters(1); + encParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L); + + while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open) + { + try + { + var screen = System.Windows.Forms.Screen.PrimaryScreen; + if (screen == null) { await Task.Delay(500, ct).ContinueWith(_ => { }); continue; } + + var bounds = screen.Bounds; + using var bmp = new Bitmap(bounds.Width, bounds.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + using (var g = Graphics.FromImage(bmp)) + g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size); + + byte[] jpeg; + using (var ms = new MemoryStream()) + { + bmp.Save(ms, JpegCodec, encParams); + jpeg = ms.ToArray(); + } + + var payload = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new + { + type = "rdp_frame", + data = Convert.ToBase64String(jpeg), + w = bounds.Width, + h = bounds.Height + })); + + if (ws.State == WebSocketState.Open) + await ws.SendAsync(new ArraySegment(payload), WebSocketMessageType.Text, true, CancellationToken.None); + } + catch (OperationCanceledException) { break; } + catch (Exception ex) { AgentWorker.Log($"RDP: Capture-Fehler: {ex.Message}"); } + + // ~8 fps + await Task.Delay(125, ct).ContinueWith(_ => { }); + } + } +} diff --git a/agent-cs/setup.iss b/agent-cs/setup.iss index 0da1d61..319688d 100644 --- a/agent-cs/setup.iss +++ b/agent-cs/setup.iss @@ -1,5 +1,5 @@ #define MyAppName "IT Nexus Agent" -#define MyAppVersion "2.1.2" +#define MyAppVersion "2.2.0" #define MyAppPublisher "Cereda Systems GmbH" #define MyAppURL "https://it-nexus.cereda-systems.de" #define MyAppExeName "IT-Nexus-Agent.exe" @@ -19,7 +19,7 @@ DefaultGroupName={#MyAppName} DisableProgramGroupPage=yes DisableWelcomePage=no OutputDir=..\installer -OutputBaseFilename=IT-Nexus-Agent-Setup-{#MyAppVersion} +OutputBaseFilename=IT-Nexus-Agent-Setup-v{#MyAppVersion} SetupIconFile=icon.ico Compression=lzma2/max SolidCompression=yes diff --git a/backend/src/ws/shellServer.js b/backend/src/ws/shellServer.js index f2c5d26..5a36665 100644 --- a/backend/src/ws/shellServer.js +++ b/backend/src/ws/shellServer.js @@ -7,6 +7,10 @@ const agentSockets = new Map(); // agentId -> WebSocket const browserSockets = new Map(); +// RDP: separate Maps damit Shell nicht kollidiert +const rdpAgentSockets = new Map(); +const rdpBrowserSockets = new Map(); + function setupWebSocketServer(httpServer) { const wss = new WebSocket.Server({ server: httpServer, path: '/ws' }); @@ -24,6 +28,10 @@ function setupWebSocketServer(httpServer) { handleAgent(ws, url); } else if (type === 'shell') { handleBrowser(ws, url); + } else if (type === 'rdp-agent') { + handleRdpAgent(ws, url); + } else if (type === 'rdp') { + handleRdpBrowser(ws, url); } else { ws.close(1008, 'unknown type'); } @@ -124,6 +132,63 @@ function handleBrowser(ws, url) { }); } +function handleRdpAgent(ws, url) { + const key = url.searchParams.get('key'); + if (key !== process.env.AGENT_API_KEY) { ws.close(1008, 'unauthorized'); return; } + const hostname = url.searchParams.get('hostname'); + if (!hostname) { ws.close(1008, 'hostname required'); return; } + + const db = getDatabase(); + const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname); + if (!agent) { ws.close(1008, 'agent not found'); return; } + + const agentId = agent.id; + const oldWs = rdpAgentSockets.get(agentId); + if (oldWs?.readyState === WebSocket.OPEN) oldWs.close(); + rdpAgentSockets.set(agentId, ws); + console.log(`[WS-RDP] Agent verbunden: ${hostname}`); + + ws.on('message', (data) => { + const bws = rdpBrowserSockets.get(agentId); + if (bws?.readyState === WebSocket.OPEN) bws.send(data.toString()); + }); + + ws.on('close', () => { + rdpAgentSockets.delete(agentId); + const bws = rdpBrowserSockets.get(agentId); + if (bws?.readyState === WebSocket.OPEN) { + bws.send(JSON.stringify({ type: 'rdp_disconnected' })); + } + console.log(`[WS-RDP] Agent getrennt: ${hostname}`); + }); +} + +function handleRdpBrowser(ws, url) { + const token = url.searchParams.get('token'); + try { jwt.verify(token, process.env.JWT_SECRET); } + catch { ws.close(1008, 'unauthorized'); return; } + + const agentId = parseInt(url.searchParams.get('agentId')); + if (!agentId) { ws.close(1008, 'agentId required'); return; } + + const oldBws = rdpBrowserSockets.get(agentId); + if (oldBws?.readyState === WebSocket.OPEN) oldBws.close(); + rdpBrowserSockets.set(agentId, ws); + + ws.on('message', (data) => { + const aws = rdpAgentSockets.get(agentId); + if (aws?.readyState === WebSocket.OPEN) aws.send(data.toString()); + }); + + ws.on('close', () => { + rdpBrowserSockets.delete(agentId); + const aws = rdpAgentSockets.get(agentId); + if (aws?.readyState === WebSocket.OPEN) { + aws.send(JSON.stringify({ type: 'rdp_stop' })); + } + }); +} + // Ankündigung an Agent pushen (für sofortige Zustellung) function pushAnnouncementToAgent(agentId, announcements) { const aws = agentSockets.get(agentId); diff --git a/frontend/src/pages/AgentDetailPage.jsx b/frontend/src/pages/AgentDetailPage.jsx index 3f9d5df..817a1e6 100644 --- a/frontend/src/pages/AgentDetailPage.jsx +++ b/frontend/src/pages/AgentDetailPage.jsx @@ -289,120 +289,91 @@ function RemoteShell({ agentId, agentHostname }) { ); } -// ─── Remote Desktop (WebRTC) ───────────────────────────────────────────────────── +// ─── Remote Desktop (JPEG-over-WebSocket) ──────────────────────────────────────── function RemoteDesktop({ agentId, agentHostname }) { - const [status, setStatus] = useState('idle'); // idle | signaling | connected | error + const [status, setStatus] = useState('idle'); // idle | connecting | connected | error const [error, setError] = useState(''); - const videoRef = useRef(null); - const pcRef = useRef(null); + const [fps, setFps] = useState(0); + const [resolution, setResolution] = useState(''); + const imgRef = useRef(null); const wsRef = useRef(null); - const dcRef = useRef(null); - const canvasRef = useRef(null); + const fpsCounterRef = useRef({ count: 0, last: Date.now() }); const getWsUrl = () => { const token = localStorage.getItem('token'); const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'; - return `${proto}://${window.location.host}/ws?type=shell&agentId=${agentId}&token=${token}`; + return `${proto}://${window.location.host}/ws?type=rdp&agentId=${agentId}&token=${token}`; }; - const connect = async () => { - setStatus('signaling'); + const connect = () => { + setStatus('connecting'); setError(''); const ws = new WebSocket(getWsUrl()); wsRef.current = ws; - const pc = new RTCPeerConnection({ - iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] - }); - pcRef.current = pc; - - // Video-Stream vom Agent empfangen - pc.ontrack = (e) => { - if (videoRef.current && e.streams[0]) { - videoRef.current.srcObject = e.streams[0]; - setStatus('connected'); - } + ws.onopen = () => { + ws.send(JSON.stringify({ type: 'rdp_start' })); + setStatus('connected'); }; - // ICE Candidates an Agent schicken - pc.onicecandidate = (e) => { - if (e.candidate && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'rtc_ice', candidate: e.candidate })); - } - }; - - pc.onconnectionstatechange = () => { - if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') { - setStatus('idle'); - } - }; - - // DataChannel für Maus/Tastatur-Input - const dc = pc.createDataChannel('input'); - dcRef.current = dc; - - // Offer erstellen und an Agent senden - ws.onopen = async () => { - const offer = await pc.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: false }); - await pc.setLocalDescription(offer); - ws.send(JSON.stringify({ type: 'rtc_offer', sdp: offer.sdp })); - }; - - ws.onmessage = async (e) => { - // Shell-Output ignorieren, nur RTC-Messages verarbeiten + ws.onmessage = (e) => { try { const msg = JSON.parse(e.data); - if (msg.type === 'rtc_answer') { - await pc.setRemoteDescription({ type: 'answer', sdp: msg.sdp }); - } else if (msg.type === 'rtc_ice' && msg.candidate) { - await pc.addIceCandidate(msg.candidate); + if (msg.type === 'rdp_frame' && imgRef.current) { + imgRef.current.src = 'data:image/jpeg;base64,' + msg.data; + if (msg.w && msg.h) setResolution(`${msg.w}×${msg.h}`); + // FPS-Counter + const now = Date.now(); + fpsCounterRef.current.count++; + if (now - fpsCounterRef.current.last >= 1000) { + setFps(fpsCounterRef.current.count); + fpsCounterRef.current = { count: 0, last: now }; + } + } else if (msg.type === 'rdp_disconnected') { + setStatus('idle'); + setError('Agent hat die Verbindung getrennt'); } - } catch { /* kein JSON / Shell-Output → ignorieren */ } + } catch { /* ignorieren */ } }; ws.onerror = () => { setStatus('error'); setError('WebSocket-Fehler'); }; - ws.onclose = () => { if (status !== 'connected') setStatus('idle'); }; + ws.onclose = () => { setStatus('idle'); }; }; const disconnect = () => { - pcRef.current?.close(); - wsRef.current?.close(); - pcRef.current = null; + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ type: 'rdp_stop' })); + wsRef.current.close(); + } wsRef.current = null; - if (videoRef.current) videoRef.current.srcObject = null; + if (imgRef.current) imgRef.current.src = ''; setStatus('idle'); + setFps(0); + setResolution(''); }; - useEffect(() => () => disconnect(), []); + useEffect(() => () => { wsRef.current?.close(); }, []); - // Maus-Events auf Canvas → DataChannel - const sendMouseEvent = (type, e) => { - if (!dcRef.current || dcRef.current.readyState !== 'open') return; - const rect = canvasRef.current?.getBoundingClientRect(); - if (!rect) return; - const scaleX = 1920 / rect.width; - const scaleY = 1080 / rect.height; - dcRef.current.send(JSON.stringify({ - type, x: Math.round((e.clientX - rect.left) * scaleX), - y: Math.round((e.clientY - rect.top) * scaleY), button: e.button - })); - }; - - const statusColor = status === 'connected' ? '#34d399' : status === 'signaling' ? '#f59e0b' : status === 'error' ? '#ef4444' : '#6b7280'; - const statusLabel = { idle: 'Getrennt', signaling: 'Verbinde…', connected: 'Verbunden', error: 'Fehler' }[status]; + const statusColor = status === 'connected' ? '#34d399' : status === 'connecting' ? '#f59e0b' : status === 'error' ? '#ef4444' : '#6b7280'; + const statusLabel = { idle: 'Getrennt', connecting: 'Verbinde…', connected: 'Verbunden', error: 'Fehler' }[status]; return ( - + 🖥️ Remote Desktop - {agentHostname} · WebRTC - + {agentHostname} + {statusLabel} + {status === 'connected' && resolution && ( + + {resolution} · {fps} fps + + )} {status === 'idle' || status === 'error' ? ( @@ -421,29 +392,18 @@ function RemoteDesktop({ agentId, agentHostname }) { 🖥️ Remote Desktop (Beta) - Direktübertragung via WebRTC — Agent muss v2.2.0+ haben + Bildschirmübertragung via WebSocket — Agent muss v2.2.0+ haben {error && {error}} ) : ( - - + - {/* Unsichtbarer Canvas für Maus-Koordinaten */} - sendMouseEvent('mousemove', e)} - onMouseDown={e => sendMouseEvent('mousedown', e)} - onMouseUp={e => sendMouseEvent('mouseup', e)} - onClick={e => sendMouseEvent('click', e)} - onContextMenu={e => { e.preventDefault(); sendMouseEvent('rightclick', e); }} - /> - {status === 'signaling' && ( + {status === 'connecting' && ( Verbinde… diff --git a/frontend/src/pages/PatchManagementPage.jsx b/frontend/src/pages/PatchManagementPage.jsx index cb33419..604fea2 100644 --- a/frontend/src/pages/PatchManagementPage.jsx +++ b/frontend/src/pages/PatchManagementPage.jsx @@ -5,7 +5,7 @@ import { useAuth } from '../context/AuthContext'; const ANN_TYPES = { maintenance: { icon: '🔧', label: 'Wartung', color: '#f59e0b' }, warning: { icon: '⚠️', label: 'Warnung', color: '#ef4444' }, info: { icon: 'ℹ️', label: 'Info', color: '#6366f1' } }; -const LATEST_AGENT_VERSION = '2.1.2'; +const LATEST_AGENT_VERSION = '2.2.0'; const SEVERITY_LABELS = { critical: 'Kritisch', important: 'Wichtig', moderate: 'Moderat', low: 'Niedrig', all: 'Alle' }; const SEVERITY_COLORS = { critical: '#EF4444', important: '#F59E0B', moderate: '#3B82F6', low: '#6B7280', all: '#0D9488' }; const COMMAND_LABELS = { check_updates: '🔍 Update-Scan', install_updates: '⬇️ Installation', reboot: '🔄 Neustart', upgrade_win11: '🪟 Win 11 Upgrade', update_agent: '⬆️ Agent-Update' };