You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rdpwrap/src-csharp/RDPConf/MainForm.cs

541 lines
22 KiB

// Copyright 2026 sjackson0109 — Apache License 2.0
//
// All logic, registry keys, and label text match the Delphi original.
using System.Runtime.InteropServices;
using RDPWrap.Common;
namespace RDPConf;
internal sealed class MainForm : Form
{
// ── Controls ──────────────────────────────────────────────────────────────
private readonly CheckBox cbAllowTSConnections = new();
private readonly CheckBox cbSingleSessionPerUser = new();
private readonly CheckBox cbHideUsers = new();
private readonly CheckBox cbCustomPrg = new();
private readonly NumericUpDown seRDPPort = new();
private readonly Label lRDPPort = new();
private readonly GroupBox gbGeneral = new();
// NLA radio group
private readonly GroupBox gbNLA = new();
private readonly RadioButton rbNLA0 = new();
private readonly RadioButton rbNLA1 = new();
private readonly RadioButton rbNLA2 = new();
// Shadow radio group
private readonly GroupBox gbShadow = new();
private readonly RadioButton rbShadow0 = new();
private readonly RadioButton rbShadow1 = new();
private readonly RadioButton rbShadow2 = new();
private readonly RadioButton rbShadow3 = new();
private readonly RadioButton rbShadow4 = new();
// Diagnostics
private readonly GroupBox gbDiag = new();
private readonly Label lService = new();
private readonly Label lsService = new();
private readonly Label lListener = new();
private readonly Label lsListener = new();
private readonly Label lWrapper = new();
private readonly Label lsWrapper = new();
private readonly Label lTSVer = new();
private readonly Label lsTSVer = new();
private readonly Label lWrapVer = new();
private readonly Label lsWrapVer = new();
private readonly Label lsSuppVer = new();
// Buttons / Timer
private readonly Button bOK = new();
private readonly Button bCancel = new();
private readonly Button bApply = new();
private readonly Button bLicense = new();
private readonly System.Windows.Forms.Timer timer = new();
// State (mirrors Delphi globals)
private bool _ready;
private int _oldPort;
// ── Constructor ───────────────────────────────────────────────────────────
public MainForm()
{
SuspendLayout();
BuildLayout();
ResumeLayout(false);
PerformLayout();
// Wire events
cbAllowTSConnections.CheckedChanged += OnAnyChange;
cbSingleSessionPerUser.CheckedChanged += OnAnyChange;
cbHideUsers.CheckedChanged += OnAnyChange;
cbCustomPrg.CheckedChanged += OnAnyChange;
seRDPPort.ValueChanged += OnAnyChange;
rbNLA0.CheckedChanged += OnAnyChange;
rbNLA1.CheckedChanged += OnAnyChange;
rbNLA2.CheckedChanged += OnAnyChange;
rbShadow0.CheckedChanged += OnAnyChange;
rbShadow1.CheckedChanged += OnAnyChange;
rbShadow2.CheckedChanged += OnAnyChange;
rbShadow3.CheckedChanged += OnAnyChange;
rbShadow4.CheckedChanged += OnAnyChange;
bApply.Click += (_, _) => { WriteSettings(); bApply.Enabled = false; };
bOK.Click += (_, _) => { if (bApply.Enabled) { WriteSettings(); bApply.Enabled = false; } Close(); };
bCancel.Click += (_, _) => Close();
bLicense.Click += OnLicenseClick;
timer.Interval = 1000;
timer.Tick += TimerTimer;
FormClosing += OnFormClosing;
Shown += OnShown;
FormClosed += (_, _) => { if (ArchHelper.Is64Bit) ArchHelper.RevertWow64Redirection(); timer.Stop(); };
if (ArchHelper.Is64Bit) ArchHelper.DisableWow64Redirection();
ReadSettings();
_ready = true;
}
private void OnShown(object? sender, EventArgs e)
{
TimerTimer(sender, e); // immediate first tick
timer.Start();
}
// ── Layout ────────────────────────────────────────────────────────────────
private void BuildLayout()
{
Text = "RDP Wrapper Configuration";
ClientSize = new Size(540, 383);
FormBorderStyle = FormBorderStyle.FixedSingle;
MaximizeBox = false;
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Segoe UI", 9f);
// ── General GroupBox ──────────────────────────────────────────────────
gbGeneral.Text = "General";
gbGeneral.Location = new Point(8, 8);
gbGeneral.Size = new Size(260, 175);
cbAllowTSConnections.Text = "Allow Remote Desktop connections";
cbAllowTSConnections.Location = new Point(10, 22);
cbAllowTSConnections.AutoSize = true;
cbSingleSessionPerUser.Text = "Single session per user";
cbSingleSessionPerUser.Location = new Point(10, 46);
cbSingleSessionPerUser.AutoSize = true;
cbHideUsers.Text = "Hide users on logon screen";
cbHideUsers.Location = new Point(10, 70);
cbHideUsers.AutoSize = true;
cbCustomPrg.Text = "Custom program support (HonorLegacySettings)";
cbCustomPrg.Location = new Point(10, 94);
cbCustomPrg.AutoSize = true;
lRDPPort.Text = "RDP Port:";
lRDPPort.Location = new Point(10, 124);
lRDPPort.AutoSize = true;
seRDPPort.Location = new Point(80, 121);
seRDPPort.Minimum = 1;
seRDPPort.Maximum = 65535;
seRDPPort.Value = 3389;
seRDPPort.Width = 70;
gbGeneral.Controls.AddRange(new Control[]
{
cbAllowTSConnections, cbSingleSessionPerUser, cbHideUsers,
cbCustomPrg, lRDPPort, seRDPPort
});
// ── NLA GroupBox ──────────────────────────────────────────────────────
gbNLA.Text = "Network Level Authentication";
gbNLA.Location = new Point(276, 8);
gbNLA.Size = new Size(254, 88);
rbNLA0.Text = "No NLA";
rbNLA0.Location = new Point(10, 20);
rbNLA0.AutoSize = true;
rbNLA1.Text = "Negotiate (server-side NLA)";
rbNLA1.Location = new Point(10, 42);
rbNLA1.AutoSize = true;
rbNLA2.Text = "Require NLA (client + server)";
rbNLA2.Location = new Point(10, 64);
rbNLA2.AutoSize = true;
gbNLA.Controls.AddRange(new Control[] { rbNLA0, rbNLA1, rbNLA2 });
// ── Shadow GroupBox ───────────────────────────────────────────────────
gbShadow.Text = "Shadowing";
gbShadow.Location = new Point(276, 100);
gbShadow.Size = new Size(254, 135);
rbShadow0.Text = "Disabled";
rbShadow0.Location = new Point(10, 20); rbShadow0.AutoSize = true;
rbShadow1.Text = "Full Access (with confirmation)";
rbShadow1.Location = new Point(10, 42); rbShadow1.AutoSize = true;
rbShadow2.Text = "Full Access (no confirmation)";
rbShadow2.Location = new Point(10, 62); rbShadow2.AutoSize = true;
rbShadow3.Text = "View Only (with confirmation)";
rbShadow3.Location = new Point(10, 82); rbShadow3.AutoSize = true;
rbShadow4.Text = "View Only (no confirmation)";
rbShadow4.Location = new Point(10, 102); rbShadow4.AutoSize = true;
gbShadow.Controls.AddRange(new Control[]
{ rbShadow0, rbShadow1, rbShadow2, rbShadow3, rbShadow4 });
// ── Diagnostics GroupBox ──────────────────────────────────────────────
gbDiag.Text = "Diagnostics";
gbDiag.Location = new Point(8, 190);
gbDiag.Size = new Size(522, 140);
AddDiagRow(gbDiag, "Service:", lService, lsService, new Point(10, 22));
AddDiagRow(gbDiag, "Listener:", lListener, lsListener, new Point(10, 44));
AddDiagRow(gbDiag, "Wrapper:", lWrapper, lsWrapper, new Point(10, 66));
AddDiagRow(gbDiag, "TS version:", lTSVer, lsTSVer, new Point(10, 88));
AddDiagRow(gbDiag, "Wrapper version:", lWrapVer, lsWrapVer, new Point(10, 110));
lsSuppVer.Location = new Point(280, 88);
lsSuppVer.AutoSize = true;
lsSuppVer.Visible = false;
gbDiag.Controls.Add(lsSuppVer);
// ── Buttons ───────────────────────────────────────────────────────────
bLicense.Text = "License";
bLicense.Size = new Size(80, 26);
bLicense.Location = new Point(8, 345);
bApply.Text = "Apply";
bApply.Enabled = false;
bApply.Size = new Size(80, 26);
bApply.Location = new Point(276, 345);
bOK.Text = "OK";
bOK.Size = new Size(80, 26);
bOK.Location = new Point(364, 345);
bCancel.Text = "Cancel";
bCancel.Size = new Size(80, 26);
bCancel.Location = new Point(452, 345);
Controls.AddRange(new Control[]
{
gbGeneral, gbNLA, gbShadow, gbDiag,
bLicense, bApply, bOK, bCancel
});
}
private static void AddDiagRow(GroupBox parent, string labelText,
Label lbl, Label status, Point origin)
{
lbl.Text = labelText;
lbl.Location = new Point(origin.X, origin.Y);
lbl.AutoSize = true;
status.Text = "...";
status.Location = new Point(origin.X + 120, origin.Y);
status.AutoSize = true;
status.ForeColor = SystemColors.GrayText;
parent.Controls.Add(lbl);
parent.Controls.Add(status);
}
private void OnAnyChange(object? sender, EventArgs e)
{
if (_ready) bApply.Enabled = true;
}
// ── ReadSettings (mirrors Delphi ReadSettings) ────────────────────────────
private void ReadSettings()
{
const string tsKey = @"SYSTEM\CurrentControlSet\Control\Terminal Server";
const string rdpTcp = @"SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp";
const string policies = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System";
cbAllowTSConnections.Checked = !RegistryHelper.ReadBool(tsKey, "fDenyTSConnections", false);
cbSingleSessionPerUser.Checked = RegistryHelper.ReadBool(tsKey, "fSingleSessionPerUser", false);
cbCustomPrg.Checked = RegistryHelper.ReadBool(tsKey, "HonorLegacySettings", false);
int port = RegistryHelper.ReadInt(rdpTcp, "PortNumber", 3389);
seRDPPort.Value = Math.Clamp(port, 1, 65535);
_oldPort = port;
int secLayer = RegistryHelper.ReadInt(rdpTcp, "SecurityLayer", 0);
int userAuth = RegistryHelper.ReadInt(rdpTcp, "UserAuthentication", 0);
int shadow = RegistryHelper.ReadInt(rdpTcp, "Shadow", -1);
_ = (secLayer, userAuth) switch
{
(0, 0) => rbNLA0.Checked = true,
(1, 0) => rbNLA1.Checked = true,
(2, 1) => rbNLA2.Checked = true,
_ => rbNLA0.Checked = true
};
SetShadowRadio(shadow);
cbHideUsers.Checked = RegistryHelper.ReadBool(policies, "dontdisplaylastusername", false);
}
private void SetShadowRadio(int idx)
{
RadioButton[] rbs = { rbShadow0, rbShadow1, rbShadow2, rbShadow3, rbShadow4 };
if (idx >= 0 && idx < rbs.Length)
rbs[idx].Checked = true;
}
private int GetShadowIndex()
{
RadioButton[] rbs = { rbShadow0, rbShadow1, rbShadow2, rbShadow3, rbShadow4 };
for (int i = 0; i < rbs.Length; i++)
if (rbs[i].Checked) return i;
return -1;
}
// ── WriteSettings (mirrors Delphi WriteSettings) ──────────────────────────
private void WriteSettings()
{
const string tsKey = @"SYSTEM\CurrentControlSet\Control\Terminal Server";
const string rdpTcp = @"SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp";
const string policiesRdp = @"SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services";
const string policies = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System";
RegistryHelper.WriteBool(tsKey, "fDenyTSConnections", !cbAllowTSConnections.Checked);
RegistryHelper.WriteBool(tsKey, "fSingleSessionPerUser", cbSingleSessionPerUser.Checked);
RegistryHelper.WriteBool(tsKey, "HonorLegacySettings", cbCustomPrg.Checked);
int newPort = (int)seRDPPort.Value;
RegistryHelper.WriteInt(rdpTcp, "PortNumber", newPort);
if (_oldPort != newPort)
{
_oldPort = newPort;
ProcessHelper.ExecWait(
$"netsh advfirewall firewall set rule name=\"Remote Desktop\" new localport={newPort}");
}
// NLA
(int sl, int ua) = (rbNLA0.Checked, rbNLA1.Checked, rbNLA2.Checked) switch
{
(true, _, _ ) => (0, 0),
(_, true, _ ) => (1, 0),
(_, _, true) => (2, 1),
_ => (-1, -1)
};
if (sl >= 0)
{
RegistryHelper.WriteInt(rdpTcp, "SecurityLayer", sl);
RegistryHelper.WriteInt(rdpTcp, "UserAuthentication", ua);
}
// Shadow
int shadowIdx = GetShadowIndex();
if (shadowIdx >= 0)
{
RegistryHelper.WriteInt(rdpTcp, "Shadow", shadowIdx);
RegistryHelper.WriteInt(policiesRdp, "Shadow", shadowIdx);
}
RegistryHelper.WriteBool(policies, "dontdisplaylastusername", cbHideUsers.Checked);
}
// ── TimerTimer (mirrors Delphi TimerTimer) ────────────────────────────────
private void TimerTimer(object? sender, EventArgs e)
{
// ── Wrapper state ──
string wrapperPath = string.Empty;
int wrapState = IsWrapperInstalled(ref wrapperPath);
bool checkSupp = false;
string iniPath = string.Empty;
switch (wrapState)
{
case -1:
SetStatus(lsWrapper, "Unknown", SystemColors.GrayText);
break;
case 0:
SetStatus(lsWrapper, "Not installed", SystemColors.GrayText);
break;
case 1:
SetStatus(lsWrapper, "Installed", Color.Green);
iniPath = Path.Combine(
Path.GetDirectoryName(ArchHelper.ExpandPath(wrapperPath))!,
"rdpwrap.ini");
checkSupp = File.Exists(iniPath);
break;
case 2:
SetStatus(lsWrapper, "3rd-party", Color.Red);
break;
}
// ── Service state ──
int svcState = GetTermSrvState();
// dwCurrentState constants (mirrors NativeMethods SERVICE_* constants)
string svcText = svcState switch
{
1 => "Stopped",
2 => "Starting...",
3 => "Stopping...",
4 => "Running",
5 => "Resuming...",
6 => "Suspending...",
7 => "Suspended",
_ => "Unknown"
};
Color svcColor = svcState == 4 ? Color.Green // SERVICE_RUNNING
: svcState == 1 ? Color.Red // SERVICE_STOPPED
: SystemColors.GrayText;
SetStatus(lsService, svcText, svcColor);
// ── Listener ──
bool listening = IsListenerWorking();
SetStatus(lsListener,
listening ? "Listening" : "Not listening",
listening ? Color.Green : Color.Red);
// ── Wrapper version ──
string wrapExp = ArchHelper.ExpandPath(wrapperPath);
var wrapVer = string.IsNullOrEmpty(wrapperPath) ? null
: FileVersionHelper.GetVersion(wrapExp);
if (wrapVer is null)
SetStatus(lsWrapVer, "N/A", SystemColors.GrayText);
else
SetStatus(lsWrapVer, wrapVer.ToString(), SystemColors.WindowText);
// ── TS version ──
var tsVer = FileVersionHelper.GetVersionExpanded(@"%SystemRoot%\System32\termsrv.dll");
if (tsVer is null)
{
SetStatus(lsTSVer, "N/A", SystemColors.GrayText);
lsSuppVer.Visible = false;
return;
}
SetStatus(lsTSVer, tsVer.ToString(), SystemColors.WindowText);
lsSuppVer.Visible = checkSupp;
if (checkSupp)
{
string iniContent = IniHelper.LoadText(iniPath);
int level = IniHelper.CheckSupportLevel(iniContent, tsVer);
(string suppText, Color suppColor) = level switch
{
0 => ("[not supported]", Color.Red),
1 => ("[supported partially]", Color.Olive),
_ => ("[fully supported]", Color.Green)
};
lsSuppVer.Text = suppText;
lsSuppVer.ForeColor = suppColor;
}
}
// ── Helper: IsWrapperInstalled ────────────────────────────────────────────
/// <returns>-1=error, 0=not installed, 1=installed, 2=3rd-party</returns>
private static int IsWrapperInstalled(ref string wrapperPath)
{
wrapperPath = string.Empty;
var host = RegistryHelper.ReadString(
@"SYSTEM\CurrentControlSet\Services\TermService", "ImagePath") ?? string.Empty;
if (!host.Contains("svchost.exe", StringComparison.OrdinalIgnoreCase)) return 2;
var svcDll = RegistryHelper.ReadString(
@"SYSTEM\CurrentControlSet\Services\TermService\Parameters", "ServiceDll") ?? string.Empty;
if (svcDll.Length == 0) return -1;
if (!svcDll.Contains("termsrv.dll", StringComparison.OrdinalIgnoreCase) &&
!svcDll.Contains("rdpwrap.dll", StringComparison.OrdinalIgnoreCase))
return 2;
if (svcDll.Contains("rdpwrap.dll", StringComparison.OrdinalIgnoreCase))
{
wrapperPath = svcDll;
return 1;
}
return 0;
}
// ── Helper: GetTermSrvState (via SCM) ─────────────────────────────────────
private static int GetTermSrvState()
=> ServiceHelper.GetCurrentState("TermService");
// ── Helper: IsListenerWorking (via WinStationEnumerateW) ──────────────────
private static bool IsListenerWorking()
{
if (!NativeMethods.WinStationEnumerate(IntPtr.Zero,
out IntPtr pSessions, out uint count)) return false;
bool found = false;
try
{
// Each entry is { DWORD SessionId; WCHAR[34] Name; DWORD State }
// = 4 + 68 + 4 = 76 bytes on x64 (with natural alignment the struct is 76 bytes)
const int stride = 76; // sizeof(WTS_SESSION_INFOW) — matches Delphi packed array
for (uint i = 0; i < count; i++)
{
IntPtr entry = pSessions + (int)(i * stride);
// Name is at offset 4, WCHAR[34]
string name = Marshal.PtrToStringUni(entry + 4, 34).TrimEnd('\0');
if (name.Equals("RDP-Tcp", StringComparison.Ordinal))
{
found = true;
break;
}
}
}
finally
{
NativeMethods.WinStationFreeMemory(pSessions);
}
return found;
}
// ── Helper: SetStatus ─────────────────────────────────────────────────────
private static void SetStatus(Label lbl, string text, Color color)
{
lbl.Text = text;
lbl.ForeColor = color;
}
// ── OnFormClosing — unsaved-changes guard ─────────────────────────────────
private void OnFormClosing(object? sender, FormClosingEventArgs e)
{
if (bApply.Enabled)
{
var result = MessageBox.Show(
"Settings are not saved. Do you want to exit?",
"Warning",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
e.Cancel = result != DialogResult.Yes;
}
}
// ── License button ────────────────────────────────────────────────────────
private void OnLicenseClick(object? sender, EventArgs e)
{
var text = ResourceHelper.ReadText(
"RDPConf.Resources.license.txt",
System.Reflection.Assembly.GetExecutingAssembly());
text ??= ResourceHelper.ReadText("RDPWInst.Resources.license.txt") ?? "(license not found)";
using var dlg = new LicenseForm(text);
if (dlg.ShowDialog(this) != DialogResult.OK)
Application.Exit();
}
}