Add RDP user consent popup before screen sharing
When rdp_start arrives, agent first spawns a WPF consent dialog in the user session (via schtasks). User has 30s to accept or deny. On deny, agent sends rdp_denied to browser which shows "Zugriff abgelehnt". On accept, screen capture starts as before. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
21
agent-cs/ConsentModeRunner.cs
Normal file
21
agent-cs/ConsentModeRunner.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using ITNexusAgent.UI;
|
||||||
|
|
||||||
|
namespace ITNexusAgent;
|
||||||
|
|
||||||
|
// Läuft als User-Prozess (via schtasks), zeigt Consent-Dialog und sendet Antwort via TCP
|
||||||
|
public static class ConsentModeRunner
|
||||||
|
{
|
||||||
|
public static void Run(string portStr)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(portStr, out var port) || port <= 0) return;
|
||||||
|
|
||||||
|
var app = new System.Windows.Application();
|
||||||
|
app.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose;
|
||||||
|
|
||||||
|
var win = new RdpConsentWindow(port);
|
||||||
|
win.Topmost = true;
|
||||||
|
win.Show();
|
||||||
|
win.Activate();
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,10 @@ internal class Program
|
|||||||
RunNotification(args.Length > 1 ? args[1] : "");
|
RunNotification(args.Length > 1 ? args[1] : "");
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case "--rdp-consent":
|
||||||
|
ConsentModeRunner.Run(args.Length > 1 ? args[1] : "");
|
||||||
|
return;
|
||||||
|
|
||||||
case "--rdp-capture":
|
case "--rdp-capture":
|
||||||
CaptureModeRunner.Run(
|
CaptureModeRunner.Run(
|
||||||
args.Length > 1 ? args[1] : "",
|
args.Length > 1 ? args[1] : "",
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ public class RtcService
|
|||||||
{
|
{
|
||||||
var screenIdx = obj["screen"]?.ToObject<int>() ?? 0;
|
var screenIdx = obj["screen"]?.ToObject<int>() ?? 0;
|
||||||
captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
_ = CapturePipeLoopAsync(ws, screenIdx, captureCts.Token);
|
_ = ConsentAndCaptureAsync(ws, screenIdx, captureCts.Token);
|
||||||
AgentWorker.Log($"RDP: Screen-Capture gestartet (screen={screenIdx})");
|
AgentWorker.Log($"RDP: Consent angefordert (screen={screenIdx})");
|
||||||
}
|
}
|
||||||
else if (type == "rdp_stop" && captureCts != null)
|
else if (type == "rdp_stop" && captureCts != null)
|
||||||
{
|
{
|
||||||
@@ -82,6 +82,100 @@ public class RtcService
|
|||||||
AgentWorker.Log("RDP: Getrennt");
|
AgentWorker.Log("RDP: Getrennt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ConsentAndCaptureAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
|
||||||
|
|
||||||
|
var consentListener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
consentListener.Start();
|
||||||
|
var consentPort = ((IPEndPoint)consentListener.LocalEndpoint).Port;
|
||||||
|
|
||||||
|
if (!SpawnConsentHelper(exePath, consentPort.ToString()))
|
||||||
|
{
|
||||||
|
consentListener.Stop();
|
||||||
|
AgentWorker.Log("RDP: Consent-Helper fehlgeschlagen, starte ohne Consent");
|
||||||
|
await CapturePipeLoopAsync(ws, screenIdx, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool accepted = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(35));
|
||||||
|
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
|
||||||
|
using var tcp = await consentListener.AcceptTcpClientAsync(linked.Token);
|
||||||
|
var b = tcp.GetStream().ReadByte();
|
||||||
|
accepted = b == 1;
|
||||||
|
}
|
||||||
|
catch { accepted = false; }
|
||||||
|
finally { consentListener.Stop(); }
|
||||||
|
|
||||||
|
if (!accepted)
|
||||||
|
{
|
||||||
|
AgentWorker.Log("RDP: User hat Zugriff abgelehnt");
|
||||||
|
var denied = Encoding.UTF8.GetBytes("{\"type\":\"rdp_denied\"}");
|
||||||
|
if (ws.State == WebSocketState.Open)
|
||||||
|
await ws.SendAsync(new ArraySegment<byte>(denied), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentWorker.Log("RDP: User hat Zugriff erlaubt, starte Capture");
|
||||||
|
await CapturePipeLoopAsync(ws, screenIdx, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SpawnConsentHelper(string exePath, string portStr)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fullUser = NotificationService.GetLoggedOnUser();
|
||||||
|
if (string.IsNullOrEmpty(fullUser))
|
||||||
|
{
|
||||||
|
AgentWorker.Log("RDP: Kein eingeloggter User für Consent");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var taskName = $"ITNexus-RDPConsent-{portStr}";
|
||||||
|
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
|
||||||
|
{ CreateNoWindow = true })?.WaitForExit();
|
||||||
|
|
||||||
|
var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss");
|
||||||
|
var ruArg = fullUser.StartsWith("AzureAD\\", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "/ru \"INTERACTIVE\""
|
||||||
|
: $"/ru \"{fullUser}\"";
|
||||||
|
|
||||||
|
var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-consent {portStr}\" " +
|
||||||
|
$"/sc ONCE /st {triggerTime} {ruArg} /it /f";
|
||||||
|
|
||||||
|
var p = Process.Start(new ProcessStartInfo("schtasks.exe", args)
|
||||||
|
{ CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false });
|
||||||
|
p?.WaitForExit();
|
||||||
|
|
||||||
|
if (p?.ExitCode != 0)
|
||||||
|
{
|
||||||
|
AgentWorker.Log($"RDP: schtasks consent fehlgeschlagen (ExitCode={p?.ExitCode})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"")
|
||||||
|
{ CreateNoWindow = true })?.WaitForExit();
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(40000);
|
||||||
|
Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f")
|
||||||
|
{ CreateNoWindow = true })?.WaitForExit();
|
||||||
|
});
|
||||||
|
|
||||||
|
AgentWorker.Log($"RDP: Consent-Helper gestartet als '{fullUser}'");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AgentWorker.Log($"RDP: SpawnConsentHelper Fehler: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CapturePipeLoopAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct)
|
private async Task CapturePipeLoopAsync(ClientWebSocket ws, int screenIdx, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
|
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName;
|
||||||
|
|||||||
142
agent-cs/UI/RdpConsentWindow.xaml
Normal file
142
agent-cs/UI/RdpConsentWindow.xaml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<Window x:Class="ITNexusAgent.UI.RdpConsentWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="IT Nexus – Bildschirmzugriff"
|
||||||
|
Width="480" Height="Auto"
|
||||||
|
SizeToContent="Height"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
WindowStyle="None"
|
||||||
|
AllowsTransparency="False"
|
||||||
|
Background="#1A1D2E"
|
||||||
|
Topmost="True"
|
||||||
|
FontFamily="Segoe UI">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="AcceptBtn" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="Height" Value="46"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bg" CornerRadius="8" Background="{TemplateBinding Background}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bg" Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bg" Property="Opacity" Value="0.7"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="DenyBtn" TargetType="Button">
|
||||||
|
<Setter Property="Foreground" Value="#B0B8D1"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#3D4270"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="Height" Value="46"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bg" CornerRadius="8"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bg" Property="Opacity" Value="0.75"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Border Background="#1A1D2E" BorderBrush="#2D3250" BorderThickness="1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="4"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Orange Akzentlinie oben -->
|
||||||
|
<Border Grid.Row="0" Background="#E67E22" CornerRadius="12,12,0,0"/>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="24,20,24,16" VerticalAlignment="Center">
|
||||||
|
<Border Width="44" Height="44" CornerRadius="10"
|
||||||
|
Background="#3D2010" BorderBrush="#6B3A18" BorderThickness="1"
|
||||||
|
VerticalAlignment="Top" Margin="0,0,14,0">
|
||||||
|
<TextBlock Text="🖥️" FontSize="22" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="BILDSCHIRMZUGRIFF ANGEFORDERT" FontSize="10" FontWeight="Bold"
|
||||||
|
Foreground="#E67E22" TextOptions.TextFormattingMode="Display"/>
|
||||||
|
<TextBlock Text="IT-Abteilung möchte Ihren Bildschirm anzeigen" FontSize="16" FontWeight="Bold"
|
||||||
|
Foreground="White" TextWrapping="Wrap" MaxWidth="360" Margin="0,3,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Info-Box -->
|
||||||
|
<Border Grid.Row="2" Margin="24,0,24,16"
|
||||||
|
Background="#252840" CornerRadius="8"
|
||||||
|
BorderBrush="#3D4270" BorderThickness="1" Padding="14,12">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock FontSize="13" Foreground="#B0B8D1" TextWrapping="Wrap" LineHeight="20">
|
||||||
|
Ein IT-Administrator möchte sich mit Ihrem Bildschirm verbinden.
|
||||||
|
Ihr Bildschirminhalt wird dabei übertragen und ist für die IT sichtbar.
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Margin="0,8,0,0" FontSize="12" Foreground="#6B7590"
|
||||||
|
TextWrapping="Wrap">
|
||||||
|
Lehnen Sie ab, falls Sie diese Anfrage nicht erwartet haben oder
|
||||||
|
gerade sensible Daten auf dem Bildschirm haben.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Countdown -->
|
||||||
|
<TextBlock Grid.Row="3" x:Name="CountdownText"
|
||||||
|
Margin="24,0,24,14" FontSize="11" Foreground="#555E7A"
|
||||||
|
HorizontalAlignment="Center"/>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<Grid Grid.Row="4" Margin="24,0,24,24">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="12"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Button Grid.Column="0" x:Name="DenyButton"
|
||||||
|
Style="{StaticResource DenyBtn}"
|
||||||
|
Background="#1E2030"
|
||||||
|
Click="DenyButton_Click">
|
||||||
|
<TextBlock Text="✗ Ablehnen" FontSize="13"/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Grid.Column="2" x:Name="AcceptButton"
|
||||||
|
Style="{StaticResource AcceptBtn}"
|
||||||
|
Background="#238636"
|
||||||
|
Click="AcceptButton_Click">
|
||||||
|
<TextBlock Text="✓ Zulassen" FontSize="13" FontWeight="Bold"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
61
agent-cs/UI/RdpConsentWindow.xaml.cs
Normal file
61
agent-cs/UI/RdpConsentWindow.xaml.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace ITNexusAgent.UI;
|
||||||
|
|
||||||
|
public partial class RdpConsentWindow : Window
|
||||||
|
{
|
||||||
|
private readonly int _port;
|
||||||
|
private bool _answered = false;
|
||||||
|
private System.Threading.CancellationTokenSource _countdownCts = new();
|
||||||
|
|
||||||
|
public RdpConsentWindow(int port)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_port = port;
|
||||||
|
_ = RunCountdownAsync(_countdownCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunCountdownAsync(System.Threading.CancellationToken ct)
|
||||||
|
{
|
||||||
|
for (int i = 30; i > 0; i--)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
Dispatcher.Invoke(() => CountdownText.Text = $"Automatische Ablehnung in {i} Sekunden");
|
||||||
|
await Task.Delay(1000, ct).ContinueWith(_ => { });
|
||||||
|
}
|
||||||
|
if (!_answered)
|
||||||
|
{
|
||||||
|
SendResponse(false);
|
||||||
|
Dispatcher.Invoke(Close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AcceptButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_countdownCts.Cancel();
|
||||||
|
SendResponse(true);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DenyButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_countdownCts.Cancel();
|
||||||
|
SendResponse(false);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendResponse(bool accepted)
|
||||||
|
{
|
||||||
|
if (_answered) return;
|
||||||
|
_answered = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var tcp = new TcpClient();
|
||||||
|
tcp.Connect("127.0.0.1", _port);
|
||||||
|
tcp.GetStream().WriteByte((byte)(accepted ? 1 : 0));
|
||||||
|
tcp.GetStream().Flush();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,10 @@ export default function RemoteDesktopPanel({ agentId, agentHostname, autoConnect
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
img.src = 'data:image/jpeg;base64,' + msg.data;
|
img.src = 'data:image/jpeg;base64,' + msg.data;
|
||||||
|
} else if (msg.type === 'rdp_denied') {
|
||||||
|
setStatus('error');
|
||||||
|
setError('Zugriff wurde vom Benutzer abgelehnt');
|
||||||
|
ws.close();
|
||||||
} else if (msg.type === 'rdp_disconnected') {
|
} else if (msg.type === 'rdp_disconnected') {
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
setError('Agent hat die Verbindung getrennt');
|
setError('Agent hat die Verbindung getrennt');
|
||||||
|
|||||||
Reference in New Issue
Block a user