Restore SEBPatch

This commit is contained in:
2025-06-01 11:44:20 +02:00
commit 8c656e3137
1297 changed files with 142172 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Linq;
using NAudio.CoreAudioApi;
using SafeExamBrowser.Settings.SystemComponents;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Audio;
using SafeExamBrowser.SystemComponents.Contracts.Audio.Events;
namespace SafeExamBrowser.SystemComponents.Audio
{
public class Audio : IAudio
{
private AudioSettings settings;
private MMDevice audioDevice;
private string audioDeviceFullName;
private string audioDeviceShortName;
private float originalVolume;
private ILogger logger;
public string DeviceFullName => audioDeviceFullName ?? string.Empty;
public string DeviceShortName => audioDeviceShortName ?? string.Empty;
public bool HasOutputDevice => audioDevice != default(MMDevice);
public bool OutputMuted => audioDevice?.AudioEndpointVolume.Mute == true;
public double OutputVolume => audioDevice?.AudioEndpointVolume.MasterVolumeLevelScalar ?? 0;
public event VolumeChangedEventHandler VolumeChanged;
public Audio(AudioSettings settings, ILogger logger)
{
this.settings = settings;
this.logger = logger;
}
public void Initialize()
{
if (TryLoadAudioDevice())
{
InitializeAudioDevice();
InitializeSettings();
}
else
{
logger.Warn("Could not find an active audio device!");
}
}
public void Mute()
{
if (audioDevice != default(MMDevice))
{
audioDevice.AudioEndpointVolume.Mute = true;
}
}
public void Unmute()
{
if (audioDevice != default(MMDevice))
{
audioDevice.AudioEndpointVolume.Mute = false;
}
}
public void SetVolume(double value)
{
if (audioDevice != default(MMDevice))
{
audioDevice.AudioEndpointVolume.MasterVolumeLevelScalar = (float) value;
}
}
public void Terminate()
{
if (audioDevice != default(MMDevice))
{
RevertSettings();
FinalizeAudioDevice();
}
}
private bool TryLoadAudioDevice()
{
using (var enumerator = new MMDeviceEnumerator())
{
if (enumerator.HasDefaultAudioEndpoint(DataFlow.Render, Role.Console))
{
audioDevice = enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Console);
}
else
{
audioDevice = enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).FirstOrDefault();
}
}
return audioDevice != default(MMDevice);
}
private void InitializeAudioDevice()
{
logger.Info($"Found '{audioDevice}' to be the active audio device.");
audioDevice.AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification;
audioDeviceFullName = audioDevice.FriendlyName;
audioDeviceShortName = audioDevice.FriendlyName.Length > 25 ? audioDevice.FriendlyName.Split(' ').First() : audioDevice.FriendlyName;
logger.Info("Started monitoring the audio device.");
}
private void FinalizeAudioDevice()
{
audioDevice.AudioEndpointVolume.OnVolumeNotification -= AudioEndpointVolume_OnVolumeNotification;
audioDevice.Dispose();
logger.Info("Stopped monitoring the audio device.");
}
private void InitializeSettings()
{
if (settings.InitializeVolume)
{
originalVolume = audioDevice.AudioEndpointVolume.MasterVolumeLevelScalar;
logger.Info($"Saved original volume of {Math.Round(originalVolume * 100)}%.");
audioDevice.AudioEndpointVolume.MasterVolumeLevelScalar = settings.InitialVolume / 100f;
logger.Info($"Set initial volume to {settings.InitialVolume}%.");
}
if (settings.MuteAudio)
{
audioDevice.AudioEndpointVolume.Mute = true;
logger.Info("Muted audio device.");
}
}
private void RevertSettings()
{
if (settings.InitializeVolume)
{
audioDevice.AudioEndpointVolume.MasterVolumeLevelScalar = originalVolume;
logger.Info($"Reverted volume to original value of {Math.Round(originalVolume * 100)}%.");
}
if (settings.MuteAudio)
{
audioDevice.AudioEndpointVolume.Mute = false;
logger.Info("Unmuted audio device.");
}
}
private void AudioEndpointVolume_OnVolumeNotification(AudioVolumeNotificationData data)
{
logger.Debug($"Volume is set to {data.MasterVolume * 100}%, audio device is {(data.Muted ? "muted" : "not muted")}.");
VolumeChanged?.Invoke(data.MasterVolume, data.Muted);
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.IO;
using SafeExamBrowser.SystemComponents.Contracts;
namespace SafeExamBrowser.SystemComponents
{
public class FileSystem : IFileSystem
{
public void CreateDirectory(string path)
{
Directory.CreateDirectory(path);
}
public void Delete(string path)
{
if (File.Exists(path))
{
File.Delete(path);
}
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
}
public void Save(string content, string path)
{
File.WriteAllText(path, content);
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Forms;
using System.Windows.Input;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Keyboard;
using SafeExamBrowser.SystemComponents.Contracts.Keyboard.Events;
namespace SafeExamBrowser.SystemComponents.Keyboard
{
public class Keyboard : IKeyboard
{
private readonly IList<KeyboardLayout> layouts;
private readonly ILogger logger;
private InputLanguage originalLanguage;
public event LayoutChangedEventHandler LayoutChanged;
public Keyboard(ILogger logger)
{
this.layouts = new List<KeyboardLayout>();
this.logger = logger;
}
public void ActivateLayout(Guid layoutId)
{
var layout = layouts.First(l => l.Id == layoutId);
logger.Info($"Changing keyboard layout to {layout}...");
InputLanguage.CurrentInputLanguage = layout.InputLanguage;
layout.IsCurrent = true;
LayoutChanged?.Invoke(layout);
}
public void Initialize()
{
originalLanguage = InputLanguage.CurrentInputLanguage;
logger.Info($"Saved current keyboard layout {ToString(originalLanguage)}.");
foreach (InputLanguage language in InputLanguage.InstalledInputLanguages)
{
var layout = new KeyboardLayout
{
CultureCode = language.Culture.ThreeLetterISOLanguageName.ToUpper(),
CultureName = language.Culture.NativeName,
InputLanguage = language,
IsCurrent = originalLanguage.Equals(language),
LayoutName = language.LayoutName
};
layouts.Add(layout);
logger.Info($"Detected keyboard layout {layout}.");
}
InputLanguageManager.Current.InputLanguageChanged += InputLanguageManager_InputLanguageChanged;
}
public IEnumerable<IKeyboardLayout> GetLayouts()
{
return new List<KeyboardLayout>(layouts.OrderBy(l => l.CultureName));
}
public void Terminate()
{
InputLanguageManager.Current.InputLanguageChanged -= InputLanguageManager_InputLanguageChanged;
if (originalLanguage != null)
{
InputLanguage.CurrentInputLanguage = originalLanguage;
logger.Info($"Restored original keyboard layout {ToString(originalLanguage)}.");
}
}
private void InputLanguageManager_InputLanguageChanged(object sender, InputLanguageEventArgs e)
{
var layout = layouts.First(l => l.InputLanguage.Culture.Equals(e.NewLanguage));
logger.Info($"Detected keyboard layout change from {ToString(e.PreviousLanguage)} to {ToString(e.NewLanguage)}.");
layout.IsCurrent = true;
LayoutChanged?.Invoke(layout);
}
private string ToString(InputLanguage language)
{
return $"'{language.Culture.NativeName}' [{language.Culture.ThreeLetterISOLanguageName.ToUpper()}, {language.LayoutName}]";
}
private string ToString(CultureInfo culture)
{
return $"'{culture.NativeName}' [{culture.ThreeLetterISOLanguageName.ToUpper()}]";
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Windows.Forms;
using SafeExamBrowser.SystemComponents.Contracts.Keyboard;
namespace SafeExamBrowser.SystemComponents.Keyboard
{
internal class KeyboardLayout : IKeyboardLayout
{
internal InputLanguage InputLanguage { get; set; }
public string CultureCode { get; set; }
public string CultureName { get; set; }
public Guid Id { get; }
public bool IsCurrent { get; set; }
public string LayoutName { get; set; }
public KeyboardLayout()
{
Id = Guid.NewGuid();
}
public override string ToString()
{
return $"'{CultureName}' [{CultureCode}, {LayoutName}]";
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using SafeExamBrowser.SystemComponents.Contracts.Network;
using Windows.Devices.WiFi;
using Windows.Networking.Connectivity;
namespace SafeExamBrowser.SystemComponents.Network
{
internal static class Extensions
{
internal static IOrderedEnumerable<WiFiAvailableNetwork> FilterAndOrder(this IReadOnlyList<WiFiAvailableNetwork> networks)
{
return networks.Where(n => !string.IsNullOrEmpty(n.Ssid)).GroupBy(n => n.Ssid).Select(g => g.First()).OrderBy(n => n.Ssid);
}
internal static bool IsOpen(this WiFiAvailableNetwork network)
{
return network.SecuritySettings.NetworkAuthenticationType == NetworkAuthenticationType.Open80211 && network.SecuritySettings.NetworkEncryptionType == NetworkEncryptionType.None;
}
internal static string ToLogString(this WiFiAvailableNetwork network)
{
return $"'{network.Ssid}' ({network.SecuritySettings.NetworkAuthenticationType}, {network.SecuritySettings.NetworkEncryptionType})";
}
internal static WirelessNetwork ToWirelessNetwork(this WiFiAvailableNetwork network)
{
return new WirelessNetwork
{
Name = network.Ssid,
Network = network,
SignalStrength = Convert.ToInt32(Math.Max(0, Math.Min(100, (network.NetworkRssiInDecibelMilliwatts + 100) * 2))),
Status = ConnectionStatus.Disconnected
};
}
}
}

View File

@@ -0,0 +1,381 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Network;
using SafeExamBrowser.SystemComponents.Contracts.Network.Events;
using SafeExamBrowser.WindowsApi.Contracts;
using Windows.Devices.Enumeration;
using Windows.Devices.WiFi;
using Windows.Foundation;
using Windows.Networking.Connectivity;
using Windows.Security.Credentials;
using Timer = System.Timers.Timer;
namespace SafeExamBrowser.SystemComponents.Network
{
public class NetworkAdapter : INetworkAdapter
{
private readonly object @lock = new object();
private readonly ConcurrentDictionary<string, object> attempts;
private readonly ILogger logger;
private readonly INativeMethods nativeMethods;
private readonly List<WirelessNetwork> wirelessNetworks;
private WiFiAdapter adapter;
private Timer timer;
private bool HasWirelessAdapter => adapter != default;
public ConnectionStatus Status { get; private set; }
public ConnectionType Type { get; private set; }
public event ChangedEventHandler Changed;
public event CredentialsRequiredEventHandler CredentialsRequired;
public NetworkAdapter(ILogger logger, INativeMethods nativeMethods)
{
this.attempts = new ConcurrentDictionary<string, object>();
this.logger = logger;
this.nativeMethods = nativeMethods;
this.wirelessNetworks = new List<WirelessNetwork>();
}
public void ConnectToWirelessNetwork(string name)
{
var isFirstAttempt = !attempts.TryGetValue(name, out _);
var network = default(WiFiAvailableNetwork);
lock (@lock)
{
network = wirelessNetworks.FirstOrDefault(n => n.Name == name)?.Network;
}
if (network != default)
{
if (isFirstAttempt || network.IsOpen())
{
ConnectAutomatically(network);
}
else
{
ConnectWithAuthentication(network);
}
Changed?.Invoke();
}
else
{
logger.Warn($"Could not find wireless network '{name}'!");
}
}
public IEnumerable<IWirelessNetwork> GetWirelessNetworks()
{
lock (@lock)
{
return new List<WirelessNetwork>(wirelessNetworks);
}
}
public void Initialize()
{
const int FIVE_SECONDS = 5000;
timer = new Timer(FIVE_SECONDS);
timer.Elapsed += (o, args) => Update();
timer.AutoReset = true;
InitializeAdapter();
NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged;
NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
NetworkInformation.NetworkStatusChanged += NetworkInformation_NetworkStatusChanged;
Update();
logger.Info("Started monitoring the network adapter.");
}
public void StartWirelessNetworkScanning()
{
timer?.Start();
if (HasWirelessAdapter)
{
_ = adapter.ScanAsync();
}
}
public void StopWirelessNetworkScanning()
{
timer?.Stop();
}
public void Terminate()
{
NetworkChange.NetworkAddressChanged -= NetworkChange_NetworkAddressChanged;
NetworkChange.NetworkAvailabilityChanged -= NetworkChange_NetworkAvailabilityChanged;
NetworkInformation.NetworkStatusChanged -= NetworkInformation_NetworkStatusChanged;
if (HasWirelessAdapter)
{
adapter.AvailableNetworksChanged -= Adapter_AvailableNetworksChanged;
}
if (timer != default)
{
timer.Stop();
}
logger.Info("Stopped monitoring the network adapter.");
}
private void Adapter_AvailableNetworksChanged(WiFiAdapter sender, object args)
{
Update(false);
}
private void Adapter_ConnectCompleted(WiFiAvailableNetwork network, IAsyncOperation<WiFiConnectionResult> operation, AsyncStatus status)
{
var connectionStatus = default(WiFiConnectionStatus?);
if (status == AsyncStatus.Completed)
{
connectionStatus = operation.GetResults()?.ConnectionStatus;
}
else
{
logger.Error($"Failed to complete connection operation! Status: {status}.");
}
if (connectionStatus == WiFiConnectionStatus.Success)
{
attempts.TryRemove(network.Ssid, out _);
logger.Info($"Successfully connected to wireless network {network.ToLogString()}.");
}
else if (connectionStatus == WiFiConnectionStatus.InvalidCredential)
{
attempts.TryAdd(network.Ssid, default);
logger.Info($"Failed to connect to wireless network {network.ToLogString()} due to invalid credentials. Retrying...");
Task.Run(() => ConnectToWirelessNetwork(network.Ssid));
}
else
{
Status = ConnectionStatus.Disconnected;
logger.Error($"Failed to connect to wireless network {network.ToLogString()}! Reason: {connectionStatus}.");
}
Update();
}
private void ConnectAutomatically(WiFiAvailableNetwork network)
{
logger.Info($"Attempting to automatically connect to {(network.IsOpen() ? "open" : "protected")} wireless network {network.ToLogString()}...");
adapter.ConnectAsync(network, WiFiReconnectionKind.Automatic).Completed = (o, s) => Adapter_ConnectCompleted(network, o, s);
Status = ConnectionStatus.Connecting;
}
private void ConnectWithAuthentication(WiFiAvailableNetwork network)
{
if (TryGetCredentials(network.Ssid, out var credentials))
{
logger.Info($"Attempting to connect to protected wirless network {network.ToLogString()}...");
adapter.ConnectAsync(network, WiFiReconnectionKind.Automatic, credentials).Completed = (o, s) => Adapter_ConnectCompleted(network, o, s);
Status = ConnectionStatus.Connecting;
}
else
{
Status = ConnectionStatus.Disconnected;
Update();
}
}
private void InitializeAdapter()
{
try
{
// Requesting access is required as of fall 2024 and must be granted manually by the user, otherwise all wireless functionality will
// be denied by the system (see also https://learn.microsoft.com/en-us/windows/win32/nativewifi/wi-fi-access-location-changes).
var task = WiFiAdapter.RequestAccessAsync().AsTask();
var status = task.GetAwaiter().GetResult();
if (status == WiFiAccessStatus.Allowed)
{
var findAll = DeviceInformation.FindAllAsync(WiFiAdapter.GetDeviceSelector()).AsTask();
var devices = findAll.GetAwaiter().GetResult();
if (devices.Any())
{
var id = devices.First().Id;
var getById = WiFiAdapter.FromIdAsync(id).AsTask();
logger.Debug($"Found {devices.Count()} wireless network adapter(s).");
adapter = getById.GetAwaiter().GetResult();
adapter.AvailableNetworksChanged += Adapter_AvailableNetworksChanged;
logger.Debug($"Successfully initialized wireless network adapter '{id}'.");
}
else
{
logger.Info("Could not find a wireless network adapter.");
}
}
else
{
logger.Error($"Access to the wireless network adapter has been denied ({status})!");
}
}
catch (Exception e)
{
logger.Error("Failed to initialize wireless network adapter!", e);
}
}
private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
{
logger.Debug("Network address changed.");
Update();
}
private void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
logger.Debug($"Network availability changed ({(e.IsAvailable ? "available" : "unavailable")}).");
Update();
}
private void NetworkInformation_NetworkStatusChanged(object sender)
{
logger.Debug("Network status changed.");
Update();
}
private bool TryGetCredentials(string network, out PasswordCredential credentials)
{
var args = new CredentialsRequiredEventArgs { NetworkName = network };
credentials = new PasswordCredential();
CredentialsRequired?.Invoke(args);
if (args.Success)
{
if (!string.IsNullOrEmpty(args.Password))
{
credentials.Password = args.Password;
}
if (!string.IsNullOrEmpty(args.Username))
{
credentials.UserName = args.Username;
}
}
return args.Success;
}
private bool TryGetCurrentWirelessNetwork(out string name)
{
name = default;
if (HasWirelessAdapter)
{
try
{
var getProfile = adapter.NetworkAdapter.GetConnectedProfileAsync().AsTask();
var profile = getProfile.GetAwaiter().GetResult();
if (profile?.IsWlanConnectionProfile == true)
{
name = profile.WlanConnectionProfileDetails.GetConnectedSsid();
}
}
catch
{
}
}
return name != default;
}
private void Update(bool rescan = true)
{
try
{
var currentNetwork = default(WirelessNetwork);
var hasConnection = nativeMethods.HasInternetConnection();
var isConnecting = Status == ConnectionStatus.Connecting;
var networks = new List<WirelessNetwork>();
var previousStatus = Status;
if (HasWirelessAdapter)
{
hasConnection &= TryGetCurrentWirelessNetwork(out var current);
foreach (var network in adapter.NetworkReport.AvailableNetworks.FilterAndOrder())
{
var wirelessNetwork = network.ToWirelessNetwork();
if (network.Ssid == current)
{
currentNetwork = wirelessNetwork;
wirelessNetwork.Status = ConnectionStatus.Connected;
}
networks.Add(wirelessNetwork);
}
if (rescan)
{
_ = adapter.ScanAsync();
}
}
lock (@lock)
{
wirelessNetworks.Clear();
wirelessNetworks.AddRange(networks);
}
Type = HasWirelessAdapter ? ConnectionType.Wireless : (hasConnection ? ConnectionType.Wired : ConnectionType.Undefined);
Status = hasConnection ? ConnectionStatus.Connected : (isConnecting ? ConnectionStatus.Connecting : ConnectionStatus.Disconnected);
LogNetworkChanges(previousStatus, currentNetwork);
}
catch (Exception e)
{
logger.Error("Failed to update network adapter!", e);
}
Changed?.Invoke();
}
private void LogNetworkChanges(ConnectionStatus previousStatus, WirelessNetwork currentNetwork = default)
{
if (previousStatus != ConnectionStatus.Connected && Status == ConnectionStatus.Connected)
{
logger.Info($"Connection established ({Type}{(currentNetwork != default ? $", {currentNetwork.Name}" : "")}).");
}
if (previousStatus != ConnectionStatus.Disconnected && Status == ConnectionStatus.Disconnected)
{
logger.Info("Connection lost.");
}
}
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using SafeExamBrowser.SystemComponents.Contracts.Network;
using Windows.Devices.WiFi;
namespace SafeExamBrowser.SystemComponents.Network
{
internal class WirelessNetwork : IWirelessNetwork
{
internal WiFiAvailableNetwork Network { get; set; }
public string Name { get; set; }
public int SignalStrength { get; set; }
public ConnectionStatus Status { get; set; }
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Timers;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.SystemComponents;
using SafeExamBrowser.SystemComponents.Contracts.PowerSupply;
using SafeExamBrowser.SystemComponents.Contracts.PowerSupply.Events;
using PowerLineStatus = System.Windows.Forms.PowerLineStatus;
using SystemInformation = System.Windows.Forms.SystemInformation;
namespace SafeExamBrowser.SystemComponents.PowerSupply
{
public class PowerSupply : IPowerSupply
{
private readonly ILogger logger;
private readonly PowerSupplySettings settings;
private double critical;
private double low;
private DateTime lastStatusLog;
private Timer timer;
public event StatusChangedEventHandler StatusChanged;
public PowerSupply(ILogger logger, PowerSupplySettings settings)
{
this.logger = logger;
this.settings = settings;
}
public IPowerSupplyStatus GetStatus()
{
var charge = SystemInformation.PowerStatus.BatteryLifePercent;
var hours = SystemInformation.PowerStatus.BatteryLifeRemaining / 3600;
var minutes = (SystemInformation.PowerStatus.BatteryLifeRemaining - (hours * 3600)) / 60;
var status = new PowerSupplyStatus();
status.BatteryCharge = charge;
status.BatteryChargeStatus = charge <= low ? (charge <= critical ? BatteryChargeStatus.Critical : BatteryChargeStatus.Low) : BatteryChargeStatus.Okay;
status.BatteryTimeRemaining = new TimeSpan(hours, minutes, 0);
status.IsOnline = SystemInformation.PowerStatus.PowerLineStatus == PowerLineStatus.Online;
if (lastStatusLog < DateTime.Now.AddMinutes(-1))
{
logger.Debug($"Power grid is {(status.IsOnline ? "" : "not ")}connected, battery charge at {charge * 100}%{(status.IsOnline ? "" : $" ({status.BatteryTimeRemaining})")}.");
lastStatusLog = DateTime.Now;
}
return status;
}
public void Initialize()
{
const int FIVE_SECONDS = 5000;
critical = SanitizeThreshold(settings.ChargeThresholdCritical);
low = SanitizeThreshold(settings.ChargeThresholdLow);
timer = new Timer(FIVE_SECONDS);
timer.Elapsed += Timer_Elapsed;
timer.AutoReset = true;
timer.Start();
logger.Info($"Started monitoring the power supply (battery charge thresholds: low = {low * 100}%, critical = {critical * 100}%).");
}
public void Terminate()
{
if (timer != null)
{
timer.Stop();
logger.Info("Stopped monitoring the power supply.");
}
}
private double SanitizeThreshold(double value)
{
return value < 0 ? 0 : (value > 1 ? 1 : value);
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
StatusChanged?.Invoke(GetStatus());
}
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.SystemComponents.Contracts.PowerSupply;
namespace SafeExamBrowser.SystemComponents.PowerSupply
{
internal class PowerSupplyStatus : IPowerSupplyStatus
{
public double BatteryCharge { get; set; }
public BatteryChargeStatus BatteryChargeStatus { get; set; }
public TimeSpan BatteryTimeRemaining { get; set; }
public bool IsOnline { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("SafeExamBrowser.SystemComponents")]
[assembly: AssemblyDescription("Safe Exam Browser")]
[assembly: AssemblyCompany("ETH Zürich")]
[assembly: AssemblyProduct("SafeExamBrowser.SystemComponents")]
[assembly: AssemblyCopyright("Copyright © 2024 ETH Zürich, IT Services")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("acee2ef1-14d2-4b52-8994-5c053055bb51")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0.0")]

View File

@@ -0,0 +1,248 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Timers;
using Microsoft.Win32;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Registry;
using SafeExamBrowser.SystemComponents.Contracts.Registry.Events;
namespace SafeExamBrowser.SystemComponents.Registry
{
public class Registry : IRegistry
{
private const int ONE_SECOND = 1000;
private readonly ILogger logger;
private readonly ConcurrentDictionary<(string key, string name), object> values;
private Timer timer;
public event RegistryValueChangedEventHandler ValueChanged;
public Registry(ILogger logger)
{
this.logger = logger;
this.values = new ConcurrentDictionary<(string key, string name), object>();
}
public void StartMonitoring(string key, string name)
{
if (timer?.Enabled != true)
{
timer = new Timer(ONE_SECOND);
timer.AutoReset = true;
timer.Elapsed += Timer_Elapsed;
timer.Start();
}
var success = TryRead(key, name, out var value);
values.TryAdd((key, name), value);
if (success)
{
logger.Debug($"Started monitoring value '{name}' from registry key '{key}'. Initial value: '{value}'.");
}
else
{
logger.Debug($"Started monitoring value '{name}' from registry key '{key}'. Value does currently not exist or initial read failed.");
}
}
public void StopMonitoring()
{
values.Clear();
if (timer != null)
{
timer.Stop();
logger.Debug("Stopped monitoring the registry.");
}
}
public void StopMonitoring(string key, string name)
{
values.TryRemove((key, name), out _);
}
public bool TryRead(string key, string name, out object value)
{
var defaultValue = new object();
value = default;
try
{
value = Microsoft.Win32.Registry.GetValue(key, name, defaultValue);
}
catch (Exception e)
{
logger.Error($"Failed to read value '{name}' from registry key '{key}'!", e);
}
return value != default && value != defaultValue;
}
public bool TryGetNames(string keyName, out IEnumerable<string> names)
{
names = default;
if (TryOpenKey(keyName, out var key))
{
using (key)
{
try
{
names = key.GetValueNames();
}
catch (Exception e)
{
logger.Error($"Failed to get registry value names for '{keyName}'!", e);
}
}
}
else
{
logger.Warn($"Failed to get names for '{keyName}'.");
}
return names != default;
}
public bool TryGetSubKeys(string keyName, out IEnumerable<string> subKeys)
{
subKeys = default;
if (TryOpenKey(keyName, out var key))
{
using (key)
{
try
{
subKeys = key.GetSubKeyNames();
}
catch (Exception e)
{
logger.Error($"Failed to get registry sub key names for '{keyName}'!", e);
}
}
}
else
{
logger.Warn($"Failed to get sub keys for '{keyName}'.");
}
return subKeys != default;
}
private bool Exists(string key, string name)
{
var defaultValue = new object();
var value = default(object);
try
{
value = Microsoft.Win32.Registry.GetValue(key, name, defaultValue);
}
catch (Exception e)
{
logger.Error($"Failed to read value '{name}' from registry key '{key}'!", e);
}
return value != default && value != defaultValue;
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
foreach (var item in values)
{
if (Exists(item.Key.key, item.Key.name))
{
if (TryRead(item.Key.key, item.Key.name, out var value))
{
if (!Equals(item.Value, value))
{
logger.Debug($"Value '{item.Key.name}' from registry key '{item.Key.key}' has changed from '{item.Value}' to '{value}'!");
ValueChanged?.Invoke(item.Key.key, item.Key.name, item.Value, value);
}
}
else
{
logger.Error($"Failed to monitor value '{item.Key.name}' from registry key '{item.Key.key}'!");
}
}
}
}
private bool TryOpenKey(string keyName, out RegistryKey key)
{
key = default;
try
{
if (TryGetHiveForKey(keyName, out var hive))
{
if (keyName == hive.Name)
{
key = hive;
}
else
{
key = hive.OpenSubKey(keyName.Replace($@"{hive.Name}\", ""));
}
}
else
{
logger.Warn($"Failed to get hive for key '{keyName}'!");
}
}
catch (Exception e)
{
logger.Error($"Failed to open registry key '{keyName}'!", e);
}
return key != default;
}
private bool TryGetHiveForKey(string keyName, out RegistryKey hive)
{
var length = keyName.IndexOf('\\');
var name = length != -1 ? keyName.Substring(0, length).ToUpperInvariant() : keyName.ToUpperInvariant();
hive = default;
switch (name)
{
case "HKEY_CLASSES_ROOT":
hive = Microsoft.Win32.Registry.ClassesRoot;
break;
case "HKEY_CURRENT_CONFIG":
hive = Microsoft.Win32.Registry.CurrentConfig;
break;
case "HKEY_CURRENT_USER":
hive = Microsoft.Win32.Registry.CurrentUser;
break;
case "HKEY_LOCAL_MACHINE":
hive = Microsoft.Win32.Registry.LocalMachine;
break;
case "HKEY_PERFORMANCE_DATA":
hive = Microsoft.Win32.Registry.PerformanceData;
break;
case "HKEY_USERS":
hive = Microsoft.Win32.Registry.Users;
break;
}
return hive != default;
}
}
}

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{ACEE2EF1-14D2-4B52-8994-5C053055BB51}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>SafeExamBrowser.SystemComponents</RootNamespace>
<AssemblyName>SafeExamBrowser.SystemComponents</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Management" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<Compile Include="Audio\Audio.cs" />
<Compile Include="FileSystem.cs" />
<Compile Include="Keyboard\KeyboardLayout.cs" />
<Compile Include="Keyboard\Keyboard.cs" />
<Compile Include="Network\Extensions.cs" />
<Compile Include="PowerSupply\PowerSupply.cs" />
<Compile Include="PowerSupply\PowerSupplyStatus.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Registry\Registry.cs" />
<Compile Include="SystemInfo.cs" />
<Compile Include="UserInfo.cs" />
<Compile Include="Network\NetworkAdapter.cs" />
<Compile Include="Network\WirelessNetwork.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SafeExamBrowser.Logging.Contracts\SafeExamBrowser.Logging.Contracts.csproj">
<Project>{64ea30fb-11d4-436a-9c2b-88566285363e}</Project>
<Name>SafeExamBrowser.Logging.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj">
<Project>{30b2d907-5861-4f39-abad-c4abf1b3470e}</Project>
<Name>SafeExamBrowser.Settings</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.SystemComponents.Contracts\SafeExamBrowser.SystemComponents.Contracts.csproj">
<Project>{903129c6-e236-493b-9ad6-c6a57f647a3a}</Project>
<Name>SafeExamBrowser.SystemComponents.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.WindowsApi.Contracts\SafeExamBrowser.WindowsApi.Contracts.csproj">
<Project>{7016f080-9aa5-41b2-a225-385ad877c171}</Project>
<Name>SafeExamBrowser.WindowsApi.Contracts</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Win32.Registry">
<Version>5.0.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.Contracts">
<Version>10.0.17134.1000</Version>
</PackageReference>
<PackageReference Include="NAudio">
<Version>2.2.1</Version>
</PackageReference>
<PackageReference Include="System.Security.AccessControl">
<Version>6.0.1</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -0,0 +1,291 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management;
using System.Windows.Forms;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Registry;
using BatteryChargeStatus = System.Windows.Forms.BatteryChargeStatus;
using OperatingSystem = SafeExamBrowser.SystemComponents.Contracts.OperatingSystem;
namespace SafeExamBrowser.SystemComponents
{
public class SystemInfo : ISystemInfo
{
private readonly IRegistry registry;
public string BiosInfo { get; private set; }
public string CpuName { get; private set; }
public bool HasBattery { get; private set; }
public string MacAddress { get; private set; }
public string Manufacturer { get; private set; }
public string Model { get; private set; }
public string Name { get; private set; }
public OperatingSystem OperatingSystem { get; private set; }
public string OperatingSystemInfo => $"{OperatingSystemName()}, {Environment.OSVersion.VersionString} ({Architecture()})";
public string[] PlugAndPlayDeviceIds { get; private set; }
public SystemInfo(IRegistry registry)
{
this.registry = registry;
InitializeBattery();
InitializeBiosInfo();
InitializeCpuName();
InitializeMacAddress();
InitializeMachineInfo();
InitializeOperatingSystem();
InitializePnPDevices();
}
public IEnumerable<DriveInfo> GetDrives()
{
var drives = DriveInfo.GetDrives();
registry.TryRead(RegistryValue.UserHive.NoDrives_Key, RegistryValue.UserHive.NoDrives_Name, out var value);
if (value is int noDrives && noDrives > 0)
{
drives = drives.Where(drive => (noDrives & (int) Math.Pow(2, drive.RootDirectory.ToString()[0] - 65)) == 0).ToArray();
}
return drives;
}
private void InitializeBattery()
{
var status = SystemInformation.PowerStatus.BatteryChargeStatus;
HasBattery = !status.HasFlag(BatteryChargeStatus.NoSystemBattery);
HasBattery &= !status.HasFlag(BatteryChargeStatus.Unknown);
}
private void InitializeBiosInfo()
{
var manufacturer = default(string);
var name = default(string);
try
{
using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_BIOS"))
using (var results = searcher.Get())
using (var bios = results.Cast<ManagementObject>().First())
{
foreach (var property in bios.Properties)
{
if (property.Name.Equals("Manufacturer"))
{
manufacturer = Convert.ToString(property.Value);
}
else if (property.Name.Equals("Name"))
{
name = Convert.ToString(property.Value);
}
}
}
BiosInfo = $"{manufacturer} {name}";
}
catch (Exception)
{
BiosInfo = "";
}
}
private void InitializeCpuName()
{
try
{
using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Processor"))
using (var results = searcher.Get())
{
foreach (var cpu in results)
{
using (cpu)
{
foreach (var property in cpu.Properties)
{
if (property.Name.Equals("Name"))
{
CpuName = Convert.ToString(property.Value);
}
}
}
}
}
}
catch (Exception)
{
CpuName = "";
}
}
private void InitializeMachineInfo()
{
var model = default(string);
var systemFamily = default(string);
try
{
using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_ComputerSystem"))
using (var results = searcher.Get())
using (var system = results.Cast<ManagementObject>().First())
{
foreach (var property in system.Properties)
{
if (property.Name.Equals("Manufacturer"))
{
Manufacturer = Convert.ToString(property.Value);
}
else if (property.Name.Equals("Model"))
{
model = Convert.ToString(property.Value);
}
else if (property.Name.Equals("Name"))
{
Name = Convert.ToString(property.Value);
}
else if (property.Name.Equals("SystemFamily"))
{
systemFamily = Convert.ToString(property.Value);
}
}
}
Model = $"{systemFamily} {model}";
}
catch (Exception)
{
Manufacturer = "";
Model = "";
Name = "";
}
}
private void InitializeOperatingSystem()
{
// IMPORTANT:
// In order to be able to retrieve the correct operating system version via System.Environment.OSVersion,
// the executing assembly needs to define an application manifest specifying all supported Windows versions!
var major = Environment.OSVersion.Version.Major;
var minor = Environment.OSVersion.Version.Minor;
var build = Environment.OSVersion.Version.Build;
// See https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions for mapping source...
if (major == 6)
{
if (minor == 1)
{
OperatingSystem = OperatingSystem.Windows7;
}
else if (minor == 2)
{
OperatingSystem = OperatingSystem.Windows8;
}
else if (minor == 3)
{
OperatingSystem = OperatingSystem.Windows8_1;
}
}
else if (major == 10)
{
if (build < 22000)
{
OperatingSystem = OperatingSystem.Windows10;
}
else
{
OperatingSystem = OperatingSystem.Windows11;
}
}
}
private string OperatingSystemName()
{
switch (OperatingSystem)
{
case OperatingSystem.Windows7:
return "Windows 7";
case OperatingSystem.Windows8:
return "Windows 8";
case OperatingSystem.Windows8_1:
return "Windows 8.1";
case OperatingSystem.Windows10:
return "Windows 10";
case OperatingSystem.Windows11:
return "Windows 11";
default:
return "Unknown Windows Version";
}
}
private string Architecture()
{
return Environment.Is64BitOperatingSystem ? "x64" : "x86";
}
private void InitializeMacAddress()
{
const string UNDEFINED = "000000000000";
try
{
using (var searcher = new ManagementObjectSearcher("SELECT MACAddress FROM Win32_NetworkAdapterConfiguration WHERE DNSHostName IS NOT NULL"))
using (var results = searcher.Get())
using (var networkAdapter = results.Cast<ManagementObject>().First())
{
foreach (var property in networkAdapter.Properties)
{
if (property.Name.Equals("MACAddress"))
{
MacAddress = Convert.ToString(property.Value).Replace(":", "").ToUpper();
}
}
}
}
finally
{
MacAddress = MacAddress ?? UNDEFINED;
}
}
private void InitializePnPDevices()
{
var deviceList = new List<string>();
try
{
using (var searcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT DeviceID FROM Win32_PnPEntity"))
using (var results = searcher.Get())
{
foreach (var device in results.Cast<ManagementObject>())
{
using (device)
{
foreach (var property in device.Properties)
{
if (property.Name.Equals("DeviceID"))
{
deviceList.Add(Convert.ToString(property.Value).ToLower());
}
}
}
}
}
}
finally
{
PlugAndPlayDeviceIds = deviceList.ToArray();
}
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Diagnostics;
using System.Security.Principal;
using System.Text.RegularExpressions;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts;
namespace SafeExamBrowser.SystemComponents
{
public class UserInfo : IUserInfo
{
private const string SID_REGEX_PATTERN = @"S-\d(-\d+)+";
private ILogger logger;
public UserInfo(ILogger logger)
{
this.logger = logger;
}
public string GetUserName()
{
return Environment.UserName;
}
public string GetUserSid()
{
return WindowsIdentity.GetCurrent().User.Value;
}
public bool TryGetSidForUser(string userName, out string sid)
{
var strategies = new Func<string, string>[] { NtAccount, Wmi };
var success = false;
sid = default(string);
foreach (var strategy in strategies)
{
try
{
sid = strategy.Invoke(userName);
if (IsValid(sid))
{
logger.Info($"Found SID '{sid}' via '{strategy.Method.Name}' for user name '{userName}'!");
success = true;
break;
}
logger.Warn($"Retrieved invalid SID '{sid}' via '{strategy.Method.Name}' for user name '{userName}'!");
}
catch (Exception e)
{
logger.Error($"Failed to get SID via '{strategy.Method.Name}' for user name '{userName}'!", e);
}
}
if (!success)
{
logger.Error($"All attempts to retrieve SID for user name '{userName}' failed!");
}
return success;
}
private string NtAccount(string userName)
{
var account = new NTAccount(userName);
if (account.IsValidTargetType(typeof(SecurityIdentifier)))
{
return account.Translate(typeof(SecurityIdentifier)).Value;
}
return null;
}
private string Wmi(string userName)
{
var process = new Process();
process.StartInfo.Arguments = $"/c \"wmic useraccount where name='{userName}' get sid\"";
process.StartInfo.CreateNoWindow = true;
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
process.Start();
process.WaitForExit(5000);
var output = process.StandardOutput.ReadToEnd();
var match = Regex.Match(output, SID_REGEX_PATTERN);
return match.Success ? match.Value : null;
}
private bool IsValid(string sid)
{
return !String.IsNullOrWhiteSpace(sid) && Regex.IsMatch(sid, $"^{SID_REGEX_PATTERN}$");
}
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Win32.Registry" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" /></startup></configuration>