Update Safe Exam Browser Patch to 3.10.0.826

This commit is contained in:
2025-09-16 16:32:31 +02:00
parent 4827ae1afc
commit dd82d45ed8
320 changed files with 8445 additions and 5295 deletions

View File

@@ -0,0 +1,265 @@
/*
* Copyright (c) 2025 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.Text;
using System.Threading;
using SafeExamBrowser.Communication.Contracts;
using SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
using SafeExamBrowser.WindowsApi.Contracts.Events;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ClientOperation : SessionOperation
{
private readonly IProcessFactory processFactory;
private readonly IProxyFactory proxyFactory;
private readonly IRuntimeHost runtimeHost;
private readonly int timeout_ms;
private IProcess ClientProcess
{
get { return Context.ClientProcess; }
set { Context.ClientProcess = value; }
}
private IClientProxy ClientProxy
{
get { return Context.ClientProxy; }
set { Context.ClientProxy = value; }
}
public override event StatusChangedEventHandler StatusChanged;
public ClientOperation(
Dependencies dependencies,
IProcessFactory processFactory,
IProxyFactory proxyFactory,
IRuntimeHost runtimeHost,
int timeout_ms) : base(dependencies)
{
this.processFactory = processFactory;
this.proxyFactory = proxyFactory;
this.runtimeHost = runtimeHost;
this.timeout_ms = timeout_ms;
}
public override OperationResult Perform()
{
StatusChanged?.Invoke(TextKey.OperationStatus_StartClient);
var success = TryStartClient();
if (success)
{
Logger.Info("Successfully started new client instance.");
}
else
{
Logger.Error("Failed to start new client instance! Aborting procedure...");
}
return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Repeat()
{
return Perform();
}
public override OperationResult Revert()
{
var success = true;
if (ClientProcess != null && !ClientProcess.HasTerminated)
{
StatusChanged?.Invoke(TextKey.OperationStatus_StopClient);
success = TryStopClient();
}
return success ? OperationResult.Success : OperationResult.Failed;
}
private bool TryStartClient()
{
var authenticationToken = Context.Next.ClientAuthenticationToken.ToString("D");
var executablePath = Context.Next.AppConfig.ClientExecutablePath;
var logFilePath = $"{'"' + Convert.ToBase64String(Encoding.UTF8.GetBytes(Context.Next.AppConfig.ClientLogFilePath)) + '"'}";
var logLevel = Context.Next.Settings.LogLevel.ToString();
var runtimeHostUri = Context.Next.AppConfig.RuntimeAddress;
var uiMode = Context.Next.Settings.UserInterface.Mode.ToString();
var clientReady = false;
var clientReadyEvent = new AutoResetEvent(false);
var clientReadyEventHandler = new CommunicationEventHandler(() => clientReadyEvent.Set());
var clientTerminated = false;
var clientTerminatedEventHandler = new ProcessTerminatedEventHandler(_ => { clientTerminated = true; clientReadyEvent.Set(); });
Logger.Info("Starting new client process...");
runtimeHost.AllowConnection = true;
runtimeHost.AuthenticationToken = Context.Next.ClientAuthenticationToken;
runtimeHost.ClientReady += clientReadyEventHandler;
ClientProcess = processFactory.StartNew(executablePath, logFilePath, logLevel, runtimeHostUri, authenticationToken, uiMode);
ClientProcess.Terminated += clientTerminatedEventHandler;
Logger.Info("Waiting for client to complete initialization...");
clientReady = clientReadyEvent.WaitOne();
runtimeHost.AllowConnection = false;
runtimeHost.AuthenticationToken = default;
runtimeHost.ClientReady -= clientReadyEventHandler;
ClientProcess.Terminated -= clientTerminatedEventHandler;
if (clientReady && !clientTerminated)
{
return TryStartCommunication();
}
if (!clientReady)
{
Logger.Error($"Failed to start client!");
}
if (clientTerminated)
{
Logger.Error("Client instance terminated unexpectedly during initialization!");
}
return false;
}
private bool TryStartCommunication()
{
var success = false;
Logger.Info("Client has been successfully started and initialized. Creating communication proxy for client host...");
ClientProxy = proxyFactory.CreateClientProxy(Context.Next.AppConfig.ClientAddress, Interlocutor.Runtime);
if (ClientProxy.Connect(Context.Next.ClientAuthenticationToken))
{
Logger.Info("Connection with client has been established. Requesting authentication...");
var communication = ClientProxy.RequestAuthentication();
var response = communication.Value;
success = communication.Success && ClientProcess.Id == response?.ProcessId;
if (success)
{
Logger.Info("Authentication of client has been successful, client is ready to operate.");
}
else
{
Logger.Error("Failed to verify client integrity!");
}
}
else
{
Logger.Error("Failed to connect to client!");
}
return success;
}
private bool TryStopClient()
{
var success = false;
var disconnected = false;
var disconnectedEvent = new AutoResetEvent(false);
var disconnectedEventHandler = new CommunicationEventHandler(() => disconnectedEvent.Set());
var terminated = false;
var terminatedEvent = new AutoResetEvent(false);
var terminatedEventHandler = new ProcessTerminatedEventHandler((_) => terminatedEvent.Set());
if (ClientProxy != null)
{
runtimeHost.ClientDisconnected += disconnectedEventHandler;
ClientProcess.Terminated += terminatedEventHandler;
Logger.Info("Instructing client to initiate shutdown procedure.");
ClientProxy.InitiateShutdown();
Logger.Info("Disconnecting from client communication host.");
ClientProxy.Disconnect();
Logger.Info("Waiting for client to disconnect from runtime communication host...");
disconnected = disconnectedEvent.WaitOne(timeout_ms / 2);
if (!disconnected)
{
Logger.Error($"Client failed to disconnect within {timeout_ms / 2 / 1000} seconds!");
}
Logger.Info("Waiting for client process to terminate...");
terminated = terminatedEvent.WaitOne(timeout_ms / 2);
if (!terminated)
{
Logger.Error($"Client failed to terminate within {timeout_ms / 2 / 1000} seconds!");
}
runtimeHost.ClientDisconnected -= disconnectedEventHandler;
ClientProcess.Terminated -= terminatedEventHandler;
}
if (disconnected && terminated)
{
Logger.Info("Client has been successfully terminated.");
success = true;
}
else
{
Logger.Warn("Attempting to kill client process since graceful termination failed!");
success = TryKillClient();
}
if (success)
{
ClientProcess = null;
ClientProxy = null;
}
return success;
}
private bool TryKillClient()
{
const int MAX_ATTEMPTS = 5;
for (var attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)
{
Logger.Info($"Attempt {attempt}/{MAX_ATTEMPTS} to kill client process with ID = {ClientProcess.Id}.");
if (ClientProcess.TryKill(500))
{
break;
}
}
if (ClientProcess.HasTerminated)
{
Logger.Info("Client process has terminated.");
}
else
{
Logger.Error($"Failed to kill client process within {MAX_ATTEMPTS} attempts!");
}
return ClientProcess.HasTerminated;
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 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.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ClientTerminationOperation : ClientOperation
{
public ClientTerminationOperation(
Dependencies dependencies,
IProcessFactory processFactory,
IProxyFactory proxyFactory,
IRuntimeHost runtimeHost,
int timeout_ms) : base(dependencies, processFactory, proxyFactory, runtimeHost, timeout_ms)
{
}
public override OperationResult Perform()
{
return OperationResult.Success;
}
public override OperationResult Repeat()
{
return base.Revert();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (c) 2025 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.Communication.Contracts.Data;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Runtime.Communication;
using SafeExamBrowser.Settings;
using SafeExamBrowser.UserInterface.Contracts;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal abstract class ConfigurationBaseOperation : SessionOperation
{
protected readonly IConfigurationRepository repository;
protected readonly IUserInterfaceFactory uiFactory;
protected string AppDataFilePath => Context.Next.AppConfig.AppDataFilePath;
protected string ProgramDataFilePath => Context.Next.AppConfig.ProgramDataFilePath;
public ConfigurationBaseOperation(
Dependencies dependencies,
IConfigurationRepository repository,
IUserInterfaceFactory uiFactory) : base(dependencies)
{
this.repository = repository;
this.uiFactory = uiFactory;
}
protected LoadStatus? TryLoadSettings(Uri uri, UriSource source, out PasswordParameters passwordParams, out AppSettings settings, string currentPassword = default)
{
passwordParams = new PasswordParameters { Password = string.Empty, IsHash = true };
var status = repository.TryLoadSettings(uri, out settings, passwordParams);
if (status == LoadStatus.PasswordNeeded && currentPassword != default)
{
passwordParams.Password = currentPassword;
passwordParams.IsHash = true;
status = repository.TryLoadSettings(uri, out settings, passwordParams);
}
for (var attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++)
{
var isLocalConfig = source == UriSource.AppData || source == UriSource.ProgramData;
var purpose = isLocalConfig ? PasswordRequestPurpose.LocalSettings : PasswordRequestPurpose.Settings;
var success = TryGetPassword(purpose, out var password);
if (success)
{
passwordParams.Password = password;
passwordParams.IsHash = false;
}
else
{
return null;
}
status = repository.TryLoadSettings(uri, out settings, passwordParams);
}
return status;
}
protected bool TryGetPassword(PasswordRequestPurpose purpose, out string password)
{
var success = false;
if (ClientBridge.IsRequired())
{
ClientBridge.TryGetPassword(purpose, out password);
}
else
{
var (message, title) = GetTextKeysFor(purpose);
var dialog = uiFactory.CreatePasswordDialog(Text.Get(message), Text.Get(title));
var result = dialog.Show(RuntimeWindow);
password = result.Password;
success = result.Success;
}
return success;
}
private (TextKey message, TextKey title) GetTextKeysFor(PasswordRequestPurpose purpose)
{
var message = default(TextKey);
var title = default(TextKey);
switch (purpose)
{
case PasswordRequestPurpose.LocalAdministrator:
message = TextKey.PasswordDialog_LocalAdminPasswordRequired;
title = TextKey.PasswordDialog_LocalAdminPasswordRequiredTitle;
break;
case PasswordRequestPurpose.LocalSettings:
message = TextKey.PasswordDialog_LocalSettingsPasswordRequired;
title = TextKey.PasswordDialog_LocalSettingsPasswordRequiredTitle;
break;
case PasswordRequestPurpose.Settings:
message = TextKey.PasswordDialog_SettingsPasswordRequired;
title = TextKey.PasswordDialog_SettingsPasswordRequiredTitle;
break;
}
return (message, title);
}
protected enum UriSource
{
Undefined,
AppData,
CommandLine,
ProgramData,
Reconfiguration,
Server
}
}
}

View File

@@ -0,0 +1,466 @@
/*
* Copyright (c) 2025 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 SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Settings;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ConfigurationOperation : ConfigurationBaseOperation
{
private readonly string[] commandLineArgs;
private readonly IFileSystem fileSystem;
private readonly IHashAlgorithm hashAlgorithm;
public override event StatusChangedEventHandler StatusChanged;
public ConfigurationOperation(
string[] commandLineArgs,
Dependencies dependencies,
IFileSystem fileSystem,
IHashAlgorithm hashAlgorithm,
IConfigurationRepository repository,
IUserInterfaceFactory uiFactory) : base(dependencies, repository, uiFactory)
{
this.commandLineArgs = commandLineArgs;
this.fileSystem = fileSystem;
this.hashAlgorithm = hashAlgorithm;
}
public override OperationResult Perform()
{
Logger.Info("Initializing application configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeConfiguration);
var result = OperationResult.Failed;
var isValidUri = TryInitializeSettingsUri(out var uri, out var source);
if (isValidUri)
{
result = LoadSettingsForStartup(uri, source);
}
else
{
result = LoadDefaultSettings();
}
LogOperationResult(result);
return result;
}
public override OperationResult Repeat()
{
Logger.Info("Initializing new application configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeConfiguration);
var result = OperationResult.Failed;
var isValidUri = TryValidateSettingsUri(Context.ReconfigurationFilePath, out var uri);
if (isValidUri)
{
result = LoadSettingsForReconfiguration(uri);
}
else
{
Logger.Warn($"The resource specified for reconfiguration does not exist or is not valid!");
}
LogOperationResult(result);
return result;
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult LoadDefaultSettings()
{
Logger.Info("No valid configuration resource specified and no local client configuration found - loading default settings...");
Context.Next.Settings = repository.LoadDefaultSettings();
return OperationResult.Success;
}
private OperationResult LoadSettingsForStartup(Uri uri, UriSource source)
{
var currentPassword = default(string);
var passwordParams = default(PasswordParameters);
var settings = default(AppSettings);
var status = default(LoadStatus?);
if (source == UriSource.CommandLine)
{
var hasAppDataFile = File.Exists(AppDataFilePath);
var hasProgramDataFile = File.Exists(ProgramDataFilePath);
if (hasProgramDataFile)
{
status = TryLoadSettings(new Uri(ProgramDataFilePath, UriKind.Absolute), UriSource.ProgramData, out _, out settings);
}
else if (hasAppDataFile)
{
status = TryLoadSettings(new Uri(AppDataFilePath, UriKind.Absolute), UriSource.AppData, out _, out settings);
}
if ((!hasProgramDataFile && !hasAppDataFile) || status == LoadStatus.Success)
{
currentPassword = settings?.Security.AdminPasswordHash;
status = TryLoadSettings(uri, source, out passwordParams, out settings, currentPassword);
}
}
else
{
status = TryLoadSettings(uri, source, out passwordParams, out settings);
}
if (status.HasValue)
{
return DetermineLoadResult(uri, source, settings, status.Value, passwordParams, currentPassword);
}
else
{
return OperationResult.Aborted;
}
}
private OperationResult LoadSettingsForReconfiguration(Uri uri)
{
var currentPassword = Context.Current.Settings.Security.AdminPasswordHash;
var source = UriSource.Reconfiguration;
var status = TryLoadSettings(uri, source, out var passwordParams, out var settings, currentPassword);
var result = OperationResult.Failed;
if (status.HasValue)
{
result = DetermineLoadResult(uri, source, settings, status.Value, passwordParams, currentPassword);
}
else
{
result = OperationResult.Aborted;
}
if (result == OperationResult.Success && Context.Current.IsBrowserResource)
{
HandleReconfigurationByBrowserResource();
}
fileSystem.Delete(uri.LocalPath);
Logger.Info($"Deleted temporary configuration file '{uri}'.");
return result;
}
private OperationResult DetermineLoadResult(Uri uri, UriSource source, AppSettings settings, LoadStatus status, PasswordParameters passwordParams, string currentPassword = default)
{
var result = OperationResult.Failed;
if (status == LoadStatus.LoadWithBrowser || status == LoadStatus.Success)
{
var isNewConfiguration = source == UriSource.CommandLine || source == UriSource.Reconfiguration;
Context.Next.Settings = settings;
if (status == LoadStatus.LoadWithBrowser)
{
result = HandleBrowserResource(uri);
}
else if (isNewConfiguration && settings.ConfigurationMode == ConfigurationMode.ConfigureClient)
{
result = HandleClientConfiguration(uri, passwordParams, currentPassword);
}
else
{
result = OperationResult.Success;
}
HandleStartUrlQuery(uri, source);
}
else
{
ShowFailureMessage(status, uri);
}
return result;
}
private OperationResult HandleBrowserResource(Uri uri)
{
Context.Next.IsBrowserResource = true;
Context.Next.Settings.Applications.Blacklist.Clear();
Context.Next.Settings.Applications.Whitelist.Clear();
Context.Next.Settings.Browser.DeleteCacheOnShutdown = false;
Context.Next.Settings.Browser.DeleteCookiesOnShutdown = false;
Context.Next.Settings.Browser.StartUrl = uri.AbsoluteUri;
Context.Next.Settings.Display.AllowedDisplays = 10;
Context.Next.Settings.Display.IgnoreError = true;
Context.Next.Settings.Display.InternalDisplayOnly = false;
Context.Next.Settings.Security.AllowReconfiguration = true;
Context.Next.Settings.Security.VirtualMachinePolicy = VirtualMachinePolicy.Allow;
Context.Next.Settings.Service.IgnoreService = true;
Context.Next.Settings.UserInterface.ActionCenter.EnableActionCenter = false;
Logger.Info($"The configuration resource needs authentication or is a webpage, using '{uri}' as start URL for the browser.");
return OperationResult.Success;
}
private OperationResult HandleClientConfiguration(Uri uri, PasswordParameters passwordParams, string currentPassword = default)
{
var isFirstSession = Context.Current == null;
var success = TryConfigureClient(uri, passwordParams, currentPassword);
var result = OperationResult.Failed;
if (!success.HasValue || (success == true && isFirstSession && AbortAfterClientConfiguration()))
{
result = OperationResult.Aborted;
}
else if (success == true)
{
result = OperationResult.Success;
}
return result;
}
private void HandleReconfigurationByBrowserResource()
{
Context.Next.Settings.Browser.DeleteCookiesOnStartup = false;
Logger.Info("Some browser settings were overridden in order to retain a potential LMS / web application session.");
}
private void HandleStartUrlQuery(Uri uri, UriSource source)
{
if (source == UriSource.Reconfiguration && Uri.TryCreate(Context.ReconfigurationUrl, UriKind.Absolute, out var reconfigurationUri))
{
uri = reconfigurationUri;
}
if (uri != default && uri.Query.LastIndexOf('?') > 0)
{
Context.Next.Settings.Browser.StartUrlQuery = uri.Query.Substring(uri.Query.LastIndexOf('?'));
}
}
private bool? TryConfigureClient(Uri uri, PasswordParameters passwordParams, string currentPassword = default)
{
var mustAuthenticate = IsRequiredToAuthenticateForClientConfiguration(passwordParams, currentPassword);
var success = new bool?(true);
Logger.Info("Starting client configuration...");
if (mustAuthenticate)
{
success = AuthenticateForClientConfiguration(currentPassword);
}
else
{
Logger.Info("Authentication is not required.");
}
if (success == true)
{
var status = repository.ConfigureClientWith(uri, passwordParams);
success = status == SaveStatus.Success;
if (success == true)
{
Logger.Info("Client configuration was successful.");
}
else
{
Logger.Error($"Client configuration failed with status '{status}'!");
ShowMessageBox(TextKey.MessageBox_ClientConfigurationError, TextKey.MessageBox_ClientConfigurationErrorTitle, icon: MessageBoxIcon.Error);
}
}
return success;
}
private bool IsRequiredToAuthenticateForClientConfiguration(PasswordParameters passwordParams, string currentPassword = default)
{
var mustAuthenticate = currentPassword != default;
if (mustAuthenticate)
{
var nextPassword = Context.Next.Settings.Security.AdminPasswordHash;
var hasSettingsPassword = passwordParams.Password != null;
var sameAdminPassword = currentPassword.Equals(nextPassword, StringComparison.OrdinalIgnoreCase);
if (sameAdminPassword)
{
mustAuthenticate = false;
}
else if (hasSettingsPassword)
{
var settingsPassword = passwordParams.IsHash ? passwordParams.Password : hashAlgorithm.GenerateHashFor(passwordParams.Password);
var knowsAdminPassword = currentPassword.Equals(settingsPassword, StringComparison.OrdinalIgnoreCase);
mustAuthenticate = !knowsAdminPassword;
}
}
return mustAuthenticate;
}
private bool? AuthenticateForClientConfiguration(string currentPassword)
{
var authenticated = default(bool?);
for (var attempts = 0; attempts < 5 && authenticated != true; attempts++)
{
var success = TryGetPassword(PasswordRequestPurpose.LocalAdministrator, out var password);
if (success)
{
authenticated = currentPassword.Equals(hashAlgorithm.GenerateHashFor(password), StringComparison.OrdinalIgnoreCase);
}
else
{
authenticated = default;
break;
}
}
if (authenticated == true)
{
Logger.Info("Authentication was successful.");
}
if (authenticated == false)
{
Logger.Info("Authentication has failed!");
ShowMessageBox(TextKey.MessageBox_InvalidPasswordError, TextKey.MessageBox_InvalidPasswordErrorTitle, icon: MessageBoxIcon.Error);
}
if (authenticated == default)
{
Logger.Info("Authentication was aborted.");
}
return authenticated;
}
private bool AbortAfterClientConfiguration()
{
var message = TextKey.MessageBox_ClientConfigurationQuestion;
var title = TextKey.MessageBox_ClientConfigurationQuestionTitle;
var result = ShowMessageBox(message, title, MessageBoxAction.YesNo, MessageBoxIcon.Question);
var abort = result == MessageBoxResult.Yes;
Logger.Info($"The user chose to {(abort ? "abort" : "continue")} startup after successful client configuration.");
return abort;
}
private void ShowFailureMessage(LoadStatus status, Uri uri)
{
var error = MessageBoxIcon.Error;
var message = default(TextKey);
var placeholders = new Dictionary<string, string>();
var title = default(TextKey);
switch (status)
{
case LoadStatus.PasswordNeeded:
message = TextKey.MessageBox_InvalidPasswordError;
title = TextKey.MessageBox_InvalidPasswordErrorTitle;
break;
case LoadStatus.InvalidData:
message = TextKey.MessageBox_InvalidConfigurationData;
placeholders["%%URI%%"] = uri.ToString();
title = TextKey.MessageBox_InvalidConfigurationDataTitle;
break;
case LoadStatus.NotSupported:
message = TextKey.MessageBox_NotSupportedConfigurationResource;
placeholders["%%URI%%"] = uri.ToString();
title = TextKey.MessageBox_NotSupportedConfigurationResourceTitle;
break;
case LoadStatus.UnexpectedError:
message = TextKey.MessageBox_UnexpectedConfigurationError;
placeholders["%%URI%%"] = uri.ToString();
title = TextKey.MessageBox_UnexpectedConfigurationErrorTitle;
break;
}
ShowMessageBox(message, title, icon: error, messagePlaceholders: placeholders);
}
private bool TryInitializeSettingsUri(out Uri uri, out UriSource source)
{
var isValidUri = false;
uri = default;
source = default;
if (commandLineArgs?.Length > 1)
{
isValidUri = Uri.TryCreate(commandLineArgs[1], UriKind.Absolute, out uri);
source = UriSource.CommandLine;
Logger.Info($"Found command-line argument for configuration resource: '{uri}', the URI is {(isValidUri ? "valid" : "invalid")}.");
}
if (!isValidUri && File.Exists(ProgramDataFilePath))
{
isValidUri = Uri.TryCreate(ProgramDataFilePath, UriKind.Absolute, out uri);
source = UriSource.ProgramData;
Logger.Info($"Found configuration file in program data directory: '{uri}'.");
}
if (!isValidUri && File.Exists(AppDataFilePath))
{
isValidUri = Uri.TryCreate(AppDataFilePath, UriKind.Absolute, out uri);
source = UriSource.AppData;
Logger.Info($"Found configuration file in app data directory: '{uri}'.");
}
return isValidUri;
}
private bool TryValidateSettingsUri(string path, out Uri uri)
{
var isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri);
isValidUri &= uri != null && uri.IsFile;
isValidUri &= File.Exists(path);
return isValidUri;
}
private void LogOperationResult(OperationResult result)
{
switch (result)
{
case OperationResult.Aborted:
Logger.Info("The configuration was aborted by the user.");
break;
case OperationResult.Failed:
Logger.Warn("The configuration has failed!");
break;
case OperationResult.Success:
Logger.Info("The configuration was successful.");
break;
}
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 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.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Communication;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class Dependencies
{
public ClientBridge ClientBridge { get; }
internal ILogger Logger { get; }
internal IMessageBox MessageBox { get; }
internal RuntimeContext RuntimeContext { get; }
internal IRuntimeWindow RuntimeWindow { get; }
internal IText Text { get; }
internal Dependencies(
ClientBridge clientBridge,
ILogger logger,
IMessageBox messageBox,
IRuntimeWindow runtimeWindow,
RuntimeContext runtimeContext,
IText text)
{
ClientBridge = clientBridge ?? throw new ArgumentNullException(nameof(clientBridge));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
MessageBox = messageBox ?? throw new ArgumentNullException(nameof(messageBox));
RuntimeWindow = runtimeWindow ?? throw new ArgumentNullException(nameof(runtimeWindow));
RuntimeContext = runtimeContext ?? throw new ArgumentNullException(nameof(runtimeContext));
Text = text ?? throw new ArgumentNullException(nameof(text));
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2025 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class DisclaimerOperation : SessionOperation
{
public override event StatusChangedEventHandler StatusChanged;
public DisclaimerOperation(Dependencies dependencies) : base(dependencies)
{
}
public override OperationResult Perform()
{
var result = OperationResult.Success;
if (Context.Next.Settings.Proctoring.ScreenProctoring.Enabled)
{
result = ShowScreenProctoringDisclaimer();
}
return result;
}
public override OperationResult Repeat()
{
var result = OperationResult.Success;
if (Context.Next.Settings.Proctoring.ScreenProctoring.Enabled)
{
result = ShowScreenProctoringDisclaimer();
}
return result;
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult ShowScreenProctoringDisclaimer()
{
StatusChanged?.Invoke(TextKey.OperationStatus_WaitDisclaimerConfirmation);
var message = TextKey.MessageBox_ScreenProctoringDisclaimer;
var title = TextKey.MessageBox_ScreenProctoringDisclaimerTitle;
var result = ShowMessageBox(message, title, action: MessageBoxAction.OkCancel);
var operationResult = result == MessageBoxResult.Ok ? OperationResult.Success : OperationResult.Aborted;
if (result == MessageBoxResult.Ok)
{
Logger.Info("The user confirmed the screen proctoring disclaimer.");
}
else
{
Logger.Warn("The user did not confirm the screen proctoring disclaimer! Aborting session initialization...");
}
return operationResult;
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (c) 2025 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 SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Display;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class DisplayMonitorOperation : SessionOperation
{
private readonly IDisplayMonitor displayMonitor;
public override event StatusChangedEventHandler StatusChanged;
public DisplayMonitorOperation(Dependencies dependencies, IDisplayMonitor displayMonitor) : base(dependencies)
{
this.displayMonitor = displayMonitor;
}
public override OperationResult Perform()
{
return CheckDisplayConfiguration();
}
public override OperationResult Repeat()
{
return CheckDisplayConfiguration();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult CheckDisplayConfiguration()
{
Logger.Info("Validating display configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateDisplayConfiguration);
var validation = displayMonitor.ValidateConfiguration(Context.Next.Settings.Display);
var result = validation.IsAllowed ? OperationResult.Success : OperationResult.Failed;
if (validation.IsAllowed)
{
Logger.Info("Display configuration is allowed.");
}
else
{
Logger.Error("Display configuration is not allowed!");
ShowError(validation);
}
return result;
}
private void ShowError(ValidationResult validation)
{
var internalOnly = Text.Get(TextKey.MessageBox_DisplayConfigurationInternal);
var internalOrExternal = Text.Get(TextKey.MessageBox_DisplayConfigurationInternalOrExternal);
var message = TextKey.MessageBox_DisplayConfigurationError;
var title = TextKey.MessageBox_DisplayConfigurationErrorTitle;
var placeholders = new Dictionary<string, string>
{
{ "%%_ALLOWED_COUNT_%%", Convert.ToString(Context.Next.Settings.Display.AllowedDisplays) },
{ "%%_EXTERNAL_COUNT_%%", Convert.ToString(validation.ExternalDisplays) },
{ "%%_INTERNAL_COUNT_%%", Convert.ToString(validation.InternalDisplays) },
{ "%%_TYPE_%%", Context.Next.Settings.Display.InternalDisplayOnly ? internalOnly : internalOrExternal }
};
ShowMessageBox(message, title, icon: MessageBoxIcon.Error, messagePlaceholders: placeholders);
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright (c) 2025 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class KioskModeOperation : SessionOperation
{
private readonly IDesktopFactory desktopFactory;
private readonly IDesktopMonitor desktopMonitor;
private readonly IExplorerShell explorerShell;
private readonly IProcessFactory processFactory;
private KioskMode? activeMode;
private IDesktop customDesktop;
private IDesktop originalDesktop;
public override event StatusChangedEventHandler StatusChanged;
public KioskModeOperation(
Dependencies dependencies,
IDesktopFactory desktopFactory,
IDesktopMonitor desktopMonitor,
IExplorerShell explorerShell,
IProcessFactory processFactory) : base(dependencies)
{
this.desktopFactory = desktopFactory;
this.desktopMonitor = desktopMonitor;
this.explorerShell = explorerShell;
this.processFactory = processFactory;
}
public override OperationResult Perform()
{
Logger.Info($"Initializing kiosk mode '{Context.Next.Settings.Security.KioskMode}'...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeKioskMode);
activeMode = Context.Next.Settings.Security.KioskMode;
switch (Context.Next.Settings.Security.KioskMode)
{
case KioskMode.CreateNewDesktop:
CreateCustomDesktop();
break;
case KioskMode.DisableExplorerShell:
TerminateExplorerShell();
break;
}
return OperationResult.Success;
}
public override OperationResult Repeat()
{
var newMode = Context.Next.Settings.Security.KioskMode;
if (activeMode == newMode)
{
Logger.Info($"New kiosk mode '{newMode}' is the same as the currently active mode, skipping re-initialization...");
}
else
{
Logger.Info($"Switching from kiosk mode '{activeMode}' to '{newMode}'...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeKioskMode);
switch (activeMode)
{
case KioskMode.CreateNewDesktop:
CloseCustomDesktop();
break;
case KioskMode.DisableExplorerShell:
RestartExplorerShell();
break;
}
activeMode = newMode;
switch (newMode)
{
case KioskMode.CreateNewDesktop:
CreateCustomDesktop();
break;
case KioskMode.DisableExplorerShell:
TerminateExplorerShell();
break;
}
}
return OperationResult.Success;
}
public override OperationResult Revert()
{
Logger.Info($"Reverting kiosk mode '{activeMode}'...");
StatusChanged?.Invoke(TextKey.OperationStatus_RevertKioskMode);
switch (activeMode)
{
case KioskMode.CreateNewDesktop:
CloseCustomDesktop();
break;
case KioskMode.DisableExplorerShell:
RestartExplorerShell();
break;
}
return OperationResult.Success;
}
private void CreateCustomDesktop()
{
originalDesktop = desktopFactory.GetCurrent();
Logger.Info($"Current desktop is {originalDesktop}.");
customDesktop = desktopFactory.CreateRandom();
Logger.Info($"Created custom desktop {customDesktop}.");
customDesktop.Activate();
processFactory.StartupDesktop = customDesktop;
Logger.Info("Successfully activated custom desktop.");
desktopMonitor.Start(customDesktop);
}
private void CloseCustomDesktop()
{
desktopMonitor.Stop();
if (originalDesktop != default)
{
originalDesktop.Activate();
processFactory.StartupDesktop = originalDesktop;
Logger.Info($"Switched back to original desktop {originalDesktop}.");
}
else
{
Logger.Warn($"No original desktop found to activate!");
}
if (customDesktop != default)
{
customDesktop.Close();
Logger.Info($"Closed custom desktop {customDesktop}.");
}
else
{
Logger.Warn($"No custom desktop found to close!");
}
}
private void TerminateExplorerShell()
{
StatusChanged?.Invoke(TextKey.OperationStatus_WaitExplorerTermination);
explorerShell.HideAllWindows();
explorerShell.Terminate();
}
private void RestartExplorerShell()
{
StatusChanged?.Invoke(TextKey.OperationStatus_WaitExplorerStartup);
explorerShell.Start();
explorerShell.RestoreAllWindows();
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2025 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Monitoring.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class RemoteSessionOperation : SessionOperation
{
private readonly IRemoteSessionDetector detector;
public override event StatusChangedEventHandler StatusChanged;
public RemoteSessionOperation(Dependencies dependencies, IRemoteSessionDetector detector) : base(dependencies)
{
this.detector = detector;
}
public override OperationResult Perform()
{
return ValidatePolicy();
}
public override OperationResult Repeat()
{
return ValidatePolicy();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult ValidatePolicy()
{
var result = OperationResult.Success;
Logger.Info($"Validating remote session policy...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateRemoteSessionPolicy);
if (Context.Next.Settings.Service.DisableRemoteConnections && detector.IsRemoteSession())
{
result = OperationResult.Aborted;
Logger.Error("Detected remote session while SEB is not allowed to be run in a remote session! Aborting...");
ShowMessageBox(TextKey.MessageBox_RemoteSessionNotAllowed, TextKey.MessageBox_RemoteSessionNotAllowedTitle, icon: MessageBoxIcon.Error);
}
return result;
}
}
}

View File

@@ -0,0 +1,316 @@
/*
* Copyright (c) 2025 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.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.Settings;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ServerOperation : ConfigurationBaseOperation
{
private readonly IFileSystem fileSystem;
private readonly IServerProxy server;
public override event StatusChangedEventHandler StatusChanged;
public ServerOperation(
Dependencies dependencies,
IFileSystem fileSystem,
IConfigurationRepository repository,
IServerProxy server,
IUserInterfaceFactory uiFactory) : base(dependencies, repository, uiFactory)
{
this.fileSystem = fileSystem;
this.server = server;
}
public override OperationResult Perform()
{
var result = OperationResult.Success;
if (Context.Next.Settings.SessionMode == SessionMode.Server)
{
var browserExamKey = default(string);
var exam = default(Exam);
var exams = default(IEnumerable<Exam>);
var uri = default(Uri);
Logger.Info("Initializing server...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServer);
server.Initialize(Context.Next.Settings.Server);
var (abort, fallback, success) = TryPerformWithFallback(() => server.Connect());
if (success)
{
(abort, fallback, success) = TryPerformWithFallback(() => server.GetAvailableExams(Context.Next.Settings.Server.ExamId), out exams);
}
if (success)
{
success = TrySelectExam(exams, out exam);
}
if (success)
{
(abort, fallback, success) = TryPerformWithFallback(() => server.GetConfigurationFor(exam), out uri);
}
if (success)
{
result = TryLoadServerSettings(exam, uri);
}
if (success && result == OperationResult.Success)
{
(abort, fallback, success) = TryPerformWithFallback(() => server.SendSelectedExam(exam), out browserExamKey);
}
if (browserExamKey != default)
{
Context.Next.Settings.Browser.CustomBrowserExamKey = browserExamKey;
}
if (abort)
{
result = OperationResult.Aborted;
Logger.Info("The user aborted the server operation.");
}
if (fallback)
{
Context.Next.Settings.SessionMode = SessionMode.Normal;
result = OperationResult.Success;
Logger.Info("The user chose to fallback and start a normal session.");
}
if (result == OperationResult.Success)
{
Logger.Info("Successfully initialized server.");
}
else if (result == OperationResult.Failed)
{
Logger.Error("Failed to initialize server!");
}
}
return result;
}
public override OperationResult Repeat()
{
var result = OperationResult.Success;
if (Context.Current.Settings.SessionMode == SessionMode.Server && Context.Next.Settings.SessionMode == SessionMode.Server)
{
result = Revert();
if (result == OperationResult.Success)
{
result = Perform();
}
else
{
Logger.Error($"Cannot start new server session due to failed finalization of current server session! Terminating...");
}
}
else if (Context.Current.Settings.SessionMode == SessionMode.Server)
{
result = Revert();
}
else if (Context.Next.Settings.SessionMode == SessionMode.Server)
{
result = Perform();
}
return result;
}
public override OperationResult Revert()
{
var result = OperationResult.Success;
if (Context.Current?.Settings.SessionMode == SessionMode.Server || Context.Next?.Settings.SessionMode == SessionMode.Server)
{
Logger.Info("Finalizing server...");
StatusChanged?.Invoke(TextKey.OperationStatus_FinalizeServer);
var disconnect = server.Disconnect();
if (disconnect.Success)
{
result = OperationResult.Success;
Logger.Info("Successfully finalized server.");
}
else
{
result = OperationResult.Failed;
Logger.Error("Failed to finalize server!");
}
}
return result;
}
private OperationResult TryLoadServerSettings(Exam exam, Uri uri)
{
var info = server.GetConnectionInfo();
var result = OperationResult.Failed;
var status = TryLoadSettings(uri, UriSource.Server, out _, out var settings);
fileSystem.Delete(uri.LocalPath);
if (status == LoadStatus.Success)
{
var browserSettings = Context.Next.Settings.Browser;
var invigilationSettings = settings.Server.Invigilation;
var serverSettings = Context.Next.Settings.Server;
Context.Next.AppConfig.ServerApi = info.Api;
Context.Next.AppConfig.ServerConnectionToken = info.ConnectionToken;
Context.Next.AppConfig.ServerExamId = exam.Id;
Context.Next.AppConfig.ServerOauth2Token = info.Oauth2Token;
Context.Next.Settings = settings;
Context.Next.Settings.Browser.StartUrl = exam.Url;
Context.Next.Settings.Browser.StartUrlQuery = browserSettings.StartUrlQuery;
Context.Next.Settings.Server = serverSettings;
Context.Next.Settings.Server.Invigilation = invigilationSettings;
Context.Next.Settings.SessionMode = SessionMode.Server;
result = OperationResult.Success;
}
return result;
}
private (bool abort, bool fallback, bool success) TryPerformWithFallback(Func<ServerResponse> request)
{
var abort = false;
var fallback = false;
var success = false;
while (!success)
{
var response = request();
success = response.Success;
if (!success && !Retry(response.Message, out abort, out fallback))
{
break;
}
}
return (abort, fallback, success);
}
private (bool abort, bool fallback, bool success) TryPerformWithFallback<T>(Func<ServerResponse<T>> request, out T value)
{
var abort = false;
var fallback = false;
var success = false;
value = default;
while (!success)
{
var response = request();
success = response.Success;
value = response.Value;
if (!success && !Retry(response.Message, out abort, out fallback))
{
break;
}
}
return (abort, fallback, success);
}
private bool Retry(string message, out bool abort, out bool fallback)
{
AskForServerFailureAction(message, out abort, out fallback, out var retry);
if (retry)
{
Logger.Debug("The user chose to retry the current server request.");
}
return retry;
}
private void AskForServerFailureAction(string message, out bool abort, out bool fallback, out bool retry)
{
var showFallback = Context.Next.Settings.Server.PerformFallback;
if (ClientBridge.IsRequired())
{
ClientBridge.TryAskForServerFailureAction(message, showFallback, out abort, out fallback, out retry);
}
else
{
var dialog = uiFactory.CreateServerFailureDialog(message, showFallback);
var result = dialog.Show(RuntimeWindow);
abort = result.Abort;
fallback = result.Fallback;
retry = result.Retry;
}
}
private bool TrySelectExam(IEnumerable<Exam> exams, out Exam exam)
{
var success = true;
if (string.IsNullOrWhiteSpace(Context.Next.Settings.Server.ExamId))
{
success = TryAskForExamSelection(exams, out exam);
}
else
{
exam = exams.First();
Logger.Info("Automatically selected exam as defined in configuration.");
}
return success;
}
private bool TryAskForExamSelection(IEnumerable<Exam> exams, out Exam exam)
{
var success = false;
if (ClientBridge.IsRequired())
{
success = ClientBridge.TryAskForExamSelection(exams, out exam);
}
else
{
var dialog = uiFactory.CreateExamSelectionDialog(exams);
var result = dialog.Show(RuntimeWindow);
exam = result.SelectedExam;
success = result.Success;
}
return success;
}
}
}

View File

@@ -0,0 +1,278 @@
/*
* Copyright (c) 2025 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.Security.AccessControl;
using System.Threading;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Settings.Service;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ServiceOperation : SessionOperation
{
private readonly IRuntimeHost runtimeHost;
private readonly IServiceProxy serviceProxy;
private readonly int timeout_ms;
private readonly IUserInfo userInfo;
private string serviceEventName;
private Guid? sessionId;
public override event StatusChangedEventHandler StatusChanged;
public ServiceOperation(
Dependencies dependencies,
IRuntimeHost runtimeHost,
IServiceProxy serviceProxy,
int timeout_ms,
IUserInfo userInfo) : base(dependencies)
{
this.runtimeHost = runtimeHost;
this.serviceProxy = serviceProxy;
this.timeout_ms = timeout_ms;
this.userInfo = userInfo;
}
public override OperationResult Perform()
{
Logger.Info($"Initializing service...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServiceSession);
var success = IgnoreService() || TryInitializeConnection();
if (success && serviceProxy.IsConnected)
{
success = TryStartSession();
}
return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Repeat()
{
Logger.Info($"Initializing service...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServiceSession);
var success = true;
if (serviceProxy.IsConnected)
{
if (sessionId.HasValue)
{
success = TryStopSession();
}
if (success && IgnoreService())
{
success = TryTerminateConnection();
}
}
else
{
success = IgnoreService() || TryInitializeConnection();
}
if (success && serviceProxy.IsConnected)
{
success = TryStartSession();
}
return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Revert()
{
Logger.Info("Finalizing service...");
StatusChanged?.Invoke(TextKey.OperationStatus_FinalizeServiceSession);
var success = true;
if (serviceProxy.IsConnected)
{
if (sessionId.HasValue)
{
success = TryStopSession(true);
}
success &= TryTerminateConnection();
}
return success ? OperationResult.Success : OperationResult.Failed;
}
private bool IgnoreService()
{
if (Context.Next.Settings.Service.IgnoreService)
{
Logger.Info("The service will be ignored for the next session.");
return true;
}
return false;
}
private bool TryInitializeConnection()
{
var mandatory = Context.Next.Settings.Service.Policy == ServicePolicy.Mandatory;
var warn = Context.Next.Settings.Service.Policy == ServicePolicy.Warn;
var connected = serviceProxy.Connect();
var success = connected || !mandatory;
if (success)
{
Logger.Info($"The service is {(mandatory ? "mandatory" : "optional")} and {(connected ? "connected." : "not connected.")}");
if (!connected && warn)
{
ShowMessageBox(TextKey.MessageBox_ServiceUnavailableWarning, TextKey.MessageBox_ServiceUnavailableWarningTitle, icon: MessageBoxIcon.Warning);
}
}
else
{
Logger.Error("The service is mandatory but no connection could be established!");
ShowMessageBox(TextKey.MessageBox_ServiceUnavailableError, TextKey.MessageBox_ServiceUnavailableErrorTitle, icon: MessageBoxIcon.Error);
}
return success;
}
private bool TryTerminateConnection()
{
var disconnected = serviceProxy.Disconnect();
if (disconnected)
{
Logger.Info("Successfully disconnected from service.");
}
else
{
Logger.Error("Failed to disconnect from service!");
}
return disconnected;
}
private bool TryStartSession()
{
var configuration = new ServiceConfiguration
{
AppConfig = Context.Next.AppConfig,
SessionId = Context.Next.SessionId,
Settings = Context.Next.Settings,
UserName = userInfo.GetUserName(),
UserSid = userInfo.GetUserSid()
};
var started = false;
Logger.Info("Starting new service session...");
var communication = serviceProxy.StartSession(configuration);
if (communication.Success)
{
started = TryWaitForServiceEvent(Context.Next.AppConfig.ServiceEventName);
if (started)
{
sessionId = Context.Next.SessionId;
serviceEventName = Context.Next.AppConfig.ServiceEventName;
Logger.Info("Successfully started new service session.");
}
else
{
Logger.Error($"Failed to start new service session within {timeout_ms / 1000} seconds!");
}
}
else
{
Logger.Error("Failed to communicate session start command to service!");
}
return started;
}
private bool TryStopSession(bool isFinalSession = false)
{
var success = false;
Logger.Info("Stopping current service session...");
var communication = serviceProxy.StopSession(sessionId.Value);
if (communication.Success)
{
success = TryWaitForServiceEvent(serviceEventName);
if (success)
{
sessionId = default;
serviceEventName = default;
Logger.Info("Successfully stopped service session.");
}
else
{
Logger.Error($"Failed to stop service session within {timeout_ms / 1000} seconds!");
}
}
else
{
Logger.Error("Failed to communicate session stop command to service!");
}
if (success && isFinalSession)
{
communication = serviceProxy.RunSystemConfigurationUpdate();
success = communication.Success;
if (communication.Success)
{
Logger.Info("Instructed service to perform system configuration update.");
}
else
{
Logger.Error("Failed to communicate system configuration update command to service!");
}
}
return success;
}
private bool TryWaitForServiceEvent(string eventName)
{
var serviceEvent = default(EventWaitHandle);
var startTime = DateTime.Now;
do
{
if (EventWaitHandle.TryOpenExisting(eventName, EventWaitHandleRights.Synchronize, out serviceEvent))
{
break;
}
} while (startTime.AddMilliseconds(timeout_ms) > DateTime.Now);
if (serviceEvent != default(EventWaitHandle))
{
using (serviceEvent)
{
return serviceEvent.WaitOne(timeout_ms);
}
}
return false;
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (c) 2025 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class SessionActivationOperation : SessionOperation
{
public override event StatusChangedEventHandler StatusChanged { add { } remove { } }
public SessionActivationOperation(Dependencies dependencies) : base(dependencies)
{
}
public override OperationResult Perform()
{
SwitchLogSeverity();
ActivateNewSession();
return OperationResult.Success;
}
public override OperationResult Repeat()
{
SwitchLogSeverity();
ActivateNewSession();
return OperationResult.Success;
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private void SwitchLogSeverity()
{
if (Logger.LogLevel != Context.Next.Settings.LogLevel)
{
var current = Logger.LogLevel.ToString().ToUpper();
var next = Context.Next.Settings.LogLevel.ToString().ToUpper();
Logger.Info($"Switching from log severity '{current}' to '{next}' for new session.");
Logger.LogLevel = Context.Next.Settings.LogLevel;
}
}
private void ActivateNewSession()
{
var isFirstSession = Context.Current == default;
if (isFirstSession)
{
Logger.Info($"Successfully activated first session '{Context.Next.SessionId}'.");
}
else
{
Logger.Info($"Successfully terminated old session '{Context.Current.SessionId}' and activated new session '{Context.Next.SessionId}'.");
}
Context.Current = Context.Next;
Context.Next = default;
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2025 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.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.SystemComponents.Contracts;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class SessionInitializationOperation : SessionOperation
{
private readonly IConfigurationRepository repository;
private readonly IFileSystem fileSystem;
public override event StatusChangedEventHandler StatusChanged;
public SessionInitializationOperation(
Dependencies dependencies,
IFileSystem fileSystem,
IConfigurationRepository repository) : base(dependencies)
{
this.fileSystem = fileSystem;
this.repository = repository;
}
public override OperationResult Perform()
{
InitializeSessionConfiguration();
return OperationResult.Success;
}
public override OperationResult Repeat()
{
InitializeSessionConfiguration();
return OperationResult.Success;
}
public override OperationResult Revert()
{
FinalizeSessionConfiguration();
return OperationResult.Success;
}
private void InitializeSessionConfiguration()
{
Logger.Info("Initializing new session configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeSession);
Context.Next = repository.InitializeSessionConfiguration();
Logger.Info($" -> Client-ID: {Context.Next.AppConfig.ClientId}");
Logger.Info($" -> Runtime-ID: {Context.Next.AppConfig.RuntimeId}");
Logger.Info($" -> Session-ID: {Context.Next.SessionId}");
fileSystem.CreateDirectory(Context.Next.AppConfig.TemporaryDirectory);
}
private void FinalizeSessionConfiguration()
{
Context.Current = null;
Context.Next = null;
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright (c) 2025 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Monitoring.Contracts.System;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class SessionIntegrityOperation : SessionOperation
{
private readonly ISystemSentinel sentinel;
public override event StatusChangedEventHandler StatusChanged;
public SessionIntegrityOperation(Dependencies dependencies, ISystemSentinel sentinel) : base(dependencies)
{
this.sentinel = sentinel;
}
public override OperationResult Perform()
{
var success = true;
StatusChanged?.Invoke(TextKey.OperationStatus_VerifySessionIntegrity);
success &= InitializeStickyKeys();
success &= VerifyCursors();
success &= VerifyEaseOfAccess();
LogResult(success);
return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Repeat()
{
var success = true;
StatusChanged?.Invoke(TextKey.OperationStatus_VerifySessionIntegrity);
success &= InitializeStickyKeys();
success &= VerifyCursors();
success &= VerifyEaseOfAccess();
LogResult(success);
return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Revert()
{
FinalizeStickyKeys();
return OperationResult.Success;
}
private void FinalizeStickyKeys()
{
sentinel.RevertStickyKeys();
}
private bool InitializeStickyKeys()
{
var success = true;
sentinel.RevertStickyKeys();
if (!Context.Next.Settings.Security.AllowStickyKeys)
{
success = sentinel.DisableStickyKeys();
}
return success;
}
private void LogResult(bool success)
{
if (success)
{
Logger.Info("Successfully ensured session integrity.");
}
else
{
Logger.Error("Failed to ensure session integrity! Aborting session initialization...");
}
}
private bool VerifyCursors()
{
var success = true;
if (Context.Next.Settings.Security.VerifyCursorConfiguration)
{
success = sentinel.VerifyCursors();
}
else
{
Logger.Debug("Verification of cursor configuration is disabled.");
}
return success;
}
private bool VerifyEaseOfAccess()
{
var success = sentinel.VerifyEaseOfAccess();
if (!success)
{
if (Context.Current?.Settings.Service.IgnoreService == false)
{
Logger.Info($"Ease of access configuration is compromised but service was active in the current session.");
success = true;
}
else if (!Context.Next.Settings.Service.IgnoreService)
{
Logger.Info($"Ease of access configuration is compromised but service will be active in the next session.");
success = true;
}
}
return success;
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 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.Collections.Generic;
using System.Linq;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Communication;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Operations.Session
{
/// <summary>
/// The base implementation to be used for all operations in the session operation sequence.
/// </summary>
internal abstract class SessionOperation : IRepeatableOperation
{
private readonly IMessageBox messageBox;
protected ClientBridge ClientBridge { get; }
protected RuntimeContext Context { get; }
protected ILogger Logger { get; }
protected IRuntimeWindow RuntimeWindow { get; }
protected IText Text { get; }
public abstract event StatusChangedEventHandler StatusChanged;
internal SessionOperation(Dependencies dependencies)
{
ClientBridge = dependencies.ClientBridge;
Context = dependencies.RuntimeContext;
Logger = dependencies.Logger;
messageBox = dependencies.MessageBox;
RuntimeWindow = dependencies.RuntimeWindow;
Text = dependencies.Text;
}
public abstract OperationResult Perform();
public abstract OperationResult Repeat();
public abstract OperationResult Revert();
/// <summary>
/// Shows a message box either directly or (if required) via the currently running client application component. All session operations
/// should always use this method instead of using <see cref="IMessageBox"/> directly!
/// </summary>
protected MessageBoxResult ShowMessageBox(
TextKey messageKey,
TextKey titleKey,
MessageBoxAction action = MessageBoxAction.Ok,
MessageBoxIcon icon = MessageBoxIcon.Information,
IDictionary<string, string> messagePlaceholders = default,
IDictionary<string, string> titlePlaceholders = default)
{
var message = Text.Get(messageKey);
var result = default(MessageBoxResult);
var title = Text.Get(titleKey);
foreach (var placeholder in messagePlaceholders ?? Enumerable.Empty<KeyValuePair<string, string>>())
{
message = message.Replace(placeholder.Key, placeholder.Value);
}
foreach (var placeholder in titlePlaceholders ?? Enumerable.Empty<KeyValuePair<string, string>>())
{
title = title.Replace(placeholder.Key, placeholder.Value);
}
if (ClientBridge.IsRequired())
{
result = ClientBridge.ShowMessageBox(message, title, action, icon);
}
else
{
result = messageBox.Show(message, title, action, icon, RuntimeWindow);
}
return result;
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (c) 2025 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.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Core.OperationModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class SessionOperationSequence : RepeatableOperationSequence<SessionOperation>
{
private readonly IRuntimeWindow runtimeWindow;
internal SessionOperationSequence(ILogger logger, IEnumerable<SessionOperation> operations, IRuntimeWindow runtimeWindow) : base(logger, operations)
{
this.runtimeWindow = runtimeWindow;
ProgressChanged += SessionSequence_ProgressChanged;
StatusChanged += SessionSequence_StatusChanged;
}
private void SessionSequence_ProgressChanged(ProgressChangedEventArgs args)
{
if (args.CurrentValue.HasValue)
{
runtimeWindow?.SetValue(args.CurrentValue.Value);
}
if (args.IsIndeterminate == true)
{
runtimeWindow?.SetIndeterminate();
}
if (args.MaxValue.HasValue)
{
runtimeWindow?.SetMaxValue(args.MaxValue.Value);
}
if (args.Progress == true)
{
runtimeWindow?.Progress();
}
if (args.Regress == true)
{
runtimeWindow?.Regress();
}
}
private void SessionSequence_StatusChanged(TextKey status)
{
runtimeWindow?.UpdateStatus(status, true);
}
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright (c) 2025 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.Collections.Generic;
using System.Linq;
using System.Text;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class VersionRestrictionOperation : SessionOperation
{
private IList<VersionRestriction> Restrictions => Context.Next.Settings.Security.VersionRestrictions;
public override event StatusChangedEventHandler StatusChanged;
public VersionRestrictionOperation(Dependencies dependencies) : base(dependencies)
{
}
public override OperationResult Perform()
{
return ValidateRestrictions();
}
public override OperationResult Repeat()
{
return ValidateRestrictions();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult ValidateRestrictions()
{
var result = OperationResult.Success;
Logger.Info("Validating version restrictions...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateVersionRestrictions);
if (Restrictions.Any())
{
var requiredVersions = $"'{string.Join("', '", Restrictions)}'";
var version = Context.Next.AppConfig.ProgramInformationalVersion;
if (Restrictions.Any(r => IsFulfilled(r)))
{
Logger.Info($"The installed SEB version '{version}' complies with the version restrictions: {requiredVersions}.");
}
else
{
result = OperationResult.Aborted;
Logger.Error($"The installed SEB version '{version}' does not comply with the version restrictions: {requiredVersions}.");
ShowErrorMessage(version);
}
}
else
{
Logger.Info($"There are no version restrictions for the configuration.");
}
return result;
}
private bool IsFulfilled(VersionRestriction restriction)
{
var fulfilled = true;
var (major, minor, patch, build, isAllianceEdition) = GetVersion();
if (restriction.IsMinimumRestriction)
{
fulfilled &= restriction.Major <= major;
if (restriction.Major == major)
{
fulfilled &= restriction.Minor <= minor;
if (restriction.Minor == minor)
{
fulfilled &= !restriction.Patch.HasValue || restriction.Patch <= patch;
if (restriction.Patch == patch)
{
fulfilled &= !restriction.Build.HasValue || restriction.Build <= build;
}
}
}
fulfilled &= !restriction.RequiresAllianceEdition || isAllianceEdition;
}
else
{
fulfilled &= restriction.Major == major;
fulfilled &= restriction.Minor == minor;
fulfilled &= !restriction.Patch.HasValue || restriction.Patch == patch;
fulfilled &= !restriction.Build.HasValue || restriction.Build == build;
fulfilled &= !restriction.RequiresAllianceEdition || isAllianceEdition;
}
return fulfilled;
}
private (int major, int minor, int patch, int build, bool isAllianceEdition) GetVersion()
{
var parts = Context.Next.AppConfig.ProgramBuildVersion.Split('.');
var major = int.Parse(parts[0]);
var minor = int.Parse(parts[1]);
var patch = int.Parse(parts[2]);
var build = int.Parse(parts[3]);
var isAllianceEdition = Context.Next.AppConfig.ProgramInformationalVersion.Contains("Alliance Edition");
return (major, minor, patch, build, isAllianceEdition);
}
private string BuildRequiredVersions()
{
var info = new StringBuilder();
var minimumVersionText = Text.Get(TextKey.MessageBox_VersionRestrictionMinimum);
info.AppendLine();
info.AppendLine();
foreach (var restriction in Restrictions)
{
var build = restriction.Build.HasValue ? $".{restriction.Build}" : "";
var patch = restriction.Patch.HasValue ? $".{restriction.Patch}" : "";
var allianceEdition = restriction.RequiresAllianceEdition ? " Alliance Edition" : "";
var version = $"{restriction.Major}.{restriction.Minor}{patch}{build}{allianceEdition}";
if (restriction.IsMinimumRestriction)
{
info.AppendLine(minimumVersionText.Replace("%%_VERSION_%%", version));
}
else
{
info.AppendLine($"SEB {version}");
}
}
info.AppendLine();
return info.ToString();
}
private void ShowErrorMessage(string version)
{
var message = TextKey.MessageBox_VersionRestrictionError;
var title = TextKey.MessageBox_VersionRestrictionErrorTitle;
var placeholders = new Dictionary<string, string>()
{
{"%%_VERSION_%%", version },
{ "%%_REQUIRED_VERSIONS_%%", BuildRequiredVersions() }
};
ShowMessageBox(message, title, icon: MessageBoxIcon.Error, messagePlaceholders: placeholders);
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2025 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Monitoring.Contracts;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class VirtualMachineOperation : SessionOperation
{
private readonly IVirtualMachineDetector detector;
public override event StatusChangedEventHandler StatusChanged;
public VirtualMachineOperation(Dependencies dependencies, IVirtualMachineDetector detector) : base(dependencies)
{
this.detector = detector;
}
public override OperationResult Perform()
{
return ValidatePolicy();
}
public override OperationResult Repeat()
{
return ValidatePolicy();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult ValidatePolicy()
{
var result = OperationResult.Success;
Logger.Info($"Validating virtual machine policy...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateVirtualMachinePolicy);
if (Context.Next.Settings.Security.VirtualMachinePolicy == VirtualMachinePolicy.Deny && detector.IsVirtualMachine())
{
result = OperationResult.Aborted;
Logger.Error("Detected virtual machine while SEB is not allowed to be run in a virtual machine! Aborting...");
ShowMessageBox(TextKey.MessageBox_VirtualMachineNotAllowed, TextKey.MessageBox_VirtualMachineNotAllowedTitle, icon: MessageBoxIcon.Error);
}
return result;
}
}
}