Restore SEBPatch
This commit is contained in:
159
SafeExamBrowser.SystemComponents/Audio/Audio.cs
Normal file
159
SafeExamBrowser.SystemComponents/Audio/Audio.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
39
SafeExamBrowser.SystemComponents/FileSystem.cs
Normal file
39
SafeExamBrowser.SystemComponents/FileSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
105
SafeExamBrowser.SystemComponents/Keyboard/Keyboard.cs
Normal file
105
SafeExamBrowser.SystemComponents/Keyboard/Keyboard.cs
Normal 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()}]";
|
||||
}
|
||||
}
|
||||
}
|
35
SafeExamBrowser.SystemComponents/Keyboard/KeyboardLayout.cs
Normal file
35
SafeExamBrowser.SystemComponents/Keyboard/KeyboardLayout.cs
Normal 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}]";
|
||||
}
|
||||
}
|
||||
}
|
46
SafeExamBrowser.SystemComponents/Network/Extensions.cs
Normal file
46
SafeExamBrowser.SystemComponents/Network/Extensions.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
381
SafeExamBrowser.SystemComponents/Network/NetworkAdapter.cs
Normal file
381
SafeExamBrowser.SystemComponents/Network/NetworkAdapter.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
SafeExamBrowser.SystemComponents/Network/WirelessNetwork.cs
Normal file
22
SafeExamBrowser.SystemComponents/Network/WirelessNetwork.cs
Normal 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; }
|
||||
}
|
||||
}
|
93
SafeExamBrowser.SystemComponents/PowerSupply/PowerSupply.cs
Normal file
93
SafeExamBrowser.SystemComponents/PowerSupply/PowerSupply.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
33
SafeExamBrowser.SystemComponents/Properties/AssemblyInfo.cs
Normal file
33
SafeExamBrowser.SystemComponents/Properties/AssemblyInfo.cs
Normal 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")]
|
248
SafeExamBrowser.SystemComponents/Registry/Registry.cs
Normal file
248
SafeExamBrowser.SystemComponents/Registry/Registry.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
291
SafeExamBrowser.SystemComponents/SystemInfo.cs
Normal file
291
SafeExamBrowser.SystemComponents/SystemInfo.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
112
SafeExamBrowser.SystemComponents/UserInfo.cs
Normal file
112
SafeExamBrowser.SystemComponents/UserInfo.cs
Normal 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}$");
|
||||
}
|
||||
}
|
||||
}
|
11
SafeExamBrowser.SystemComponents/app.config
Normal file
11
SafeExamBrowser.SystemComponents/app.config
Normal 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>
|
Reference in New Issue
Block a user