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:
@@ -6,7 +6,7 @@ namespace ITNexusAgent;
|
|||||||
|
|
||||||
public class AgentWorker
|
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 DataDir = @"C:\ProgramData\IT Nexus Agent";
|
||||||
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
|
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
|
||||||
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.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());
|
var shellService = new ShellService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname());
|
||||||
_ = shellService.RunAsync(_ct);
|
_ = 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)
|
while (!_ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await RunCycleAsync();
|
await RunCycleAsync();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<AssemblyName>IT-Nexus-Agent</AssemblyName>
|
<AssemblyName>IT-Nexus-Agent</AssemblyName>
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
127
agent-cs/Services/RtcService.cs
Normal file
127
agent-cs/Services/RtcService.cs
Normal file
@@ -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<byte>(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<byte>(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(_ => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#define MyAppName "IT Nexus Agent"
|
#define MyAppName "IT Nexus Agent"
|
||||||
#define MyAppVersion "2.1.2"
|
#define MyAppVersion "2.2.0"
|
||||||
#define MyAppPublisher "Cereda Systems GmbH"
|
#define MyAppPublisher "Cereda Systems GmbH"
|
||||||
#define MyAppURL "https://it-nexus.cereda-systems.de"
|
#define MyAppURL "https://it-nexus.cereda-systems.de"
|
||||||
#define MyAppExeName "IT-Nexus-Agent.exe"
|
#define MyAppExeName "IT-Nexus-Agent.exe"
|
||||||
@@ -19,7 +19,7 @@ DefaultGroupName={#MyAppName}
|
|||||||
DisableProgramGroupPage=yes
|
DisableProgramGroupPage=yes
|
||||||
DisableWelcomePage=no
|
DisableWelcomePage=no
|
||||||
OutputDir=..\installer
|
OutputDir=..\installer
|
||||||
OutputBaseFilename=IT-Nexus-Agent-Setup-{#MyAppVersion}
|
OutputBaseFilename=IT-Nexus-Agent-Setup-v{#MyAppVersion}
|
||||||
SetupIconFile=icon.ico
|
SetupIconFile=icon.ico
|
||||||
Compression=lzma2/max
|
Compression=lzma2/max
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const agentSockets = new Map();
|
|||||||
// agentId -> WebSocket
|
// agentId -> WebSocket
|
||||||
const browserSockets = new Map();
|
const browserSockets = new Map();
|
||||||
|
|
||||||
|
// RDP: separate Maps damit Shell nicht kollidiert
|
||||||
|
const rdpAgentSockets = new Map();
|
||||||
|
const rdpBrowserSockets = new Map();
|
||||||
|
|
||||||
function setupWebSocketServer(httpServer) {
|
function setupWebSocketServer(httpServer) {
|
||||||
const wss = new WebSocket.Server({ server: httpServer, path: '/ws' });
|
const wss = new WebSocket.Server({ server: httpServer, path: '/ws' });
|
||||||
|
|
||||||
@@ -24,6 +28,10 @@ function setupWebSocketServer(httpServer) {
|
|||||||
handleAgent(ws, url);
|
handleAgent(ws, url);
|
||||||
} else if (type === 'shell') {
|
} else if (type === 'shell') {
|
||||||
handleBrowser(ws, url);
|
handleBrowser(ws, url);
|
||||||
|
} else if (type === 'rdp-agent') {
|
||||||
|
handleRdpAgent(ws, url);
|
||||||
|
} else if (type === 'rdp') {
|
||||||
|
handleRdpBrowser(ws, url);
|
||||||
} else {
|
} else {
|
||||||
ws.close(1008, 'unknown type');
|
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)
|
// Ankündigung an Agent pushen (für sofortige Zustellung)
|
||||||
function pushAnnouncementToAgent(agentId, announcements) {
|
function pushAnnouncementToAgent(agentId, announcements) {
|
||||||
const aws = agentSockets.get(agentId);
|
const aws = agentSockets.get(agentId);
|
||||||
|
|||||||
@@ -289,120 +289,91 @@ function RemoteShell({ agentId, agentHostname }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Remote Desktop (WebRTC) ─────────────────────────────────────────────────────
|
// ─── Remote Desktop (JPEG-over-WebSocket) ────────────────────────────────────────
|
||||||
|
|
||||||
function RemoteDesktop({ agentId, agentHostname }) {
|
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 [error, setError] = useState('');
|
||||||
const videoRef = useRef(null);
|
const [fps, setFps] = useState(0);
|
||||||
const pcRef = useRef(null);
|
const [resolution, setResolution] = useState('');
|
||||||
|
const imgRef = useRef(null);
|
||||||
const wsRef = useRef(null);
|
const wsRef = useRef(null);
|
||||||
const dcRef = useRef(null);
|
const fpsCounterRef = useRef({ count: 0, last: Date.now() });
|
||||||
const canvasRef = useRef(null);
|
|
||||||
|
|
||||||
const getWsUrl = () => {
|
const getWsUrl = () => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
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 () => {
|
const connect = () => {
|
||||||
setStatus('signaling');
|
setStatus('connecting');
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
const ws = new WebSocket(getWsUrl());
|
const ws = new WebSocket(getWsUrl());
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
const pc = new RTCPeerConnection({
|
ws.onopen = () => {
|
||||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
ws.send(JSON.stringify({ type: 'rdp_start' }));
|
||||||
});
|
|
||||||
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');
|
setStatus('connected');
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ICE Candidates an Agent schicken
|
ws.onmessage = (e) => {
|
||||||
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
|
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(e.data);
|
const msg = JSON.parse(e.data);
|
||||||
if (msg.type === 'rtc_answer') {
|
if (msg.type === 'rdp_frame' && imgRef.current) {
|
||||||
await pc.setRemoteDescription({ type: 'answer', sdp: msg.sdp });
|
imgRef.current.src = 'data:image/jpeg;base64,' + msg.data;
|
||||||
} else if (msg.type === 'rtc_ice' && msg.candidate) {
|
if (msg.w && msg.h) setResolution(`${msg.w}×${msg.h}`);
|
||||||
await pc.addIceCandidate(msg.candidate);
|
// 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 };
|
||||||
}
|
}
|
||||||
} catch { /* kein JSON / Shell-Output → ignorieren */ }
|
} else if (msg.type === 'rdp_disconnected') {
|
||||||
|
setStatus('idle');
|
||||||
|
setError('Agent hat die Verbindung getrennt');
|
||||||
|
}
|
||||||
|
} catch { /* ignorieren */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => { setStatus('error'); setError('WebSocket-Fehler'); };
|
ws.onerror = () => { setStatus('error'); setError('WebSocket-Fehler'); };
|
||||||
ws.onclose = () => { if (status !== 'connected') setStatus('idle'); };
|
ws.onclose = () => { setStatus('idle'); };
|
||||||
};
|
};
|
||||||
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
pcRef.current?.close();
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
wsRef.current?.close();
|
wsRef.current.send(JSON.stringify({ type: 'rdp_stop' }));
|
||||||
pcRef.current = null;
|
wsRef.current.close();
|
||||||
|
}
|
||||||
wsRef.current = null;
|
wsRef.current = null;
|
||||||
if (videoRef.current) videoRef.current.srcObject = null;
|
if (imgRef.current) imgRef.current.src = '';
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
|
setFps(0);
|
||||||
|
setResolution('');
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => () => disconnect(), []);
|
useEffect(() => () => { wsRef.current?.close(); }, []);
|
||||||
|
|
||||||
// Maus-Events auf Canvas → DataChannel
|
const statusColor = status === 'connected' ? '#34d399' : status === 'connecting' ? '#f59e0b' : status === 'error' ? '#ef4444' : '#6b7280';
|
||||||
const sendMouseEvent = (type, e) => {
|
const statusLabel = { idle: 'Getrennt', connecting: 'Verbinde…', connected: 'Verbunden', error: 'Fehler' }[status];
|
||||||
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];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: 24, borderRadius: 16, border: '1px solid var(--border-color)', overflow: 'hidden', background: 'var(--bg-secondary)' }}>
|
<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={{ fontSize: 15 }}>🖥️</span>
|
||||||
<span style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)' }}>Remote Desktop</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={{ fontSize: 12, color: 'var(--text-muted)', marginLeft: 4 }}>{agentHostname}</span>
|
||||||
<span style={{ marginLeft: 8, display: 'flex', alignItems: 'center', gap: 5, fontSize: 12 }}>
|
<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={{ width: 8, height: 8, borderRadius: '50%', background: statusColor, display: 'inline-block' }} />
|
||||||
<span style={{ color: statusColor }}>{statusLabel}</span>
|
<span style={{ color: statusColor }}>{statusLabel}</span>
|
||||||
</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 }}>
|
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
||||||
{status === 'idle' || status === 'error' ? (
|
{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' }}>
|
<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>
|
<span style={{ fontSize: 48 }}>🖥️</span>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 6 }}>Remote Desktop (Beta)</div>
|
<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>}
|
{error && <div style={{ marginTop: 8, color: '#ef4444', fontSize: 12 }}>{error}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ background: '#000', position: 'relative', lineHeight: 0 }}>
|
<div style={{ background: '#000', lineHeight: 0, position: 'relative' }}>
|
||||||
<video
|
<img
|
||||||
ref={videoRef}
|
ref={imgRef}
|
||||||
autoPlay
|
alt="Remote Desktop"
|
||||||
playsInline
|
style={{ width: '100%', display: 'block', maxHeight: 640, objectFit: 'contain' }}
|
||||||
style={{ width: '100%', display: 'block', maxHeight: 600, objectFit: 'contain' }}
|
|
||||||
/>
|
/>
|
||||||
{/* Unsichtbarer Canvas für Maus-Koordinaten */}
|
{status === 'connecting' && (
|
||||||
<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' && (
|
|
||||||
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.7)', color: '#fff', fontSize: 14 }}>
|
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.7)', color: '#fff', fontSize: 14 }}>
|
||||||
Verbinde…
|
Verbinde…
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 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_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 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' };
|
const COMMAND_LABELS = { check_updates: '🔍 Update-Scan', install_updates: '⬇️ Installation', reboot: '🔄 Neustart', upgrade_win11: '🪟 Win 11 Upgrade', update_agent: '⬆️ Agent-Update' };
|
||||||
|
|||||||
Reference in New Issue
Block a user