Add Remote Desktop (JPEG-over-WebSocket), Agent v2.2.0

- RtcService.cs: Screen capture via Graphics.CopyFromScreen, ~8fps JPEG stream over WebSocket
- shellServer.js: Separate rdp-agent/rdp types with independent socket maps (no shell collision)
- AgentDetailPage: WebSocket-based RemoteDesktop component replaces WebRTC attempt
- setup.iss: Fixed filename to include 'v' prefix (IT-Nexus-Agent-Setup-v2.2.0.exe)
- Removed SIPSorcery dependencies, added System.Drawing.Common

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 14:13:18 +02:00
parent c23091fa6e
commit 0a8b186585
7 changed files with 256 additions and 99 deletions

View File

@@ -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 (
<div style={{ marginTop: 24, borderRadius: 16, border: '1px solid var(--border-color)', overflow: 'hidden', background: 'var(--bg-secondary)' }}>
<div style={{ padding: '14px 20px', borderBottom: '1px solid var(--border-color)', display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ padding: '14px 20px', borderBottom: '1px solid var(--border-color)', display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<span style={{ fontSize: 15 }}>🖥</span>
<span style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)' }}>Remote Desktop</span>
<span style={{ fontSize: 12, color: 'var(--text-muted)', marginLeft: 4 }}>{agentHostname} · WebRTC</span>
<span style={{ marginLeft: 8, display: 'flex', alignItems: 'center', gap: 5, fontSize: 12 }}>
<span style={{ fontSize: 12, color: 'var(--text-muted)', marginLeft: 4 }}>{agentHostname}</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 12 }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: statusColor, display: 'inline-block' }} />
<span style={{ color: statusColor }}>{statusLabel}</span>
</span>
{status === 'connected' && resolution && (
<span style={{ fontSize: 11, color: 'var(--text-muted)', background: 'var(--bg-tertiary)', borderRadius: 6, padding: '2px 8px' }}>
{resolution} · {fps} fps
</span>
)}
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
{status === 'idle' || status === 'error' ? (
<button onClick={connect} style={{ background: '#238636', border: 'none', borderRadius: 8, color: '#fff', padding: '5px 14px', fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
@@ -421,29 +392,18 @@ function RemoteDesktop({ agentId, agentHostname }) {
<span style={{ fontSize: 48 }}>🖥</span>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 6 }}>Remote Desktop (Beta)</div>
<div style={{ fontSize: 13 }}>Direktübertragung via WebRTC Agent muss v2.2.0+ haben</div>
<div style={{ fontSize: 13 }}>Bildschirmübertragung via WebSocket Agent muss v2.2.0+ haben</div>
{error && <div style={{ marginTop: 8, color: '#ef4444', fontSize: 12 }}>{error}</div>}
</div>
</div>
) : (
<div style={{ background: '#000', position: 'relative', lineHeight: 0 }}>
<video
ref={videoRef}
autoPlay
playsInline
style={{ width: '100%', display: 'block', maxHeight: 600, objectFit: 'contain' }}
<div style={{ background: '#000', lineHeight: 0, position: 'relative' }}>
<img
ref={imgRef}
alt="Remote Desktop"
style={{ width: '100%', display: 'block', maxHeight: 640, objectFit: 'contain' }}
/>
{/* Unsichtbarer Canvas für Maus-Koordinaten */}
<canvas
ref={canvasRef}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', cursor: 'none', opacity: 0 }}
onMouseMove={e => 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' && (
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.7)', color: '#fff', fontSize: 14 }}>
Verbinde
</div>

View File

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