Restore SEBPatch

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

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
<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>
<dependentAssembly>
<assemblyIdentity name="System.Security.Principal.Windows" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -0,0 +1,91 @@
/*
* 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.Threading;
using System.Threading.Tasks;
using System.Windows;
using SafeExamBrowser.Configuration.Contracts;
namespace SafeExamBrowser.Runtime
{
public class App : Application
{
private static readonly Mutex Mutex = new Mutex(true, AppConfig.RUNTIME_MUTEX_NAME);
private readonly CompositionRoot instances = new CompositionRoot();
[STAThread]
public static void Main()
{
try
{
StartApplication();
}
catch (Exception e)
{
MessageBox.Show(e.Message + "\n\n" + e.StackTrace, "Fatal Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
Mutex.Close();
}
}
private static void StartApplication()
{
if (NoInstanceRunning())
{
new App().Run();
}
else
{
MessageBox.Show("You can only run one instance of SEB at a time.", "Startup Not Allowed", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
private static bool NoInstanceRunning()
{
return Mutex.WaitOne(TimeSpan.Zero, true);
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
ShutdownMode = ShutdownMode.OnExplicitShutdown;
instances.BuildObjectGraph(Shutdown);
instances.LogStartupInformation();
Task.Run(new Action(TryStart));
}
private void TryStart()
{
var success = instances.RuntimeController.TryStart();
if (!success)
{
Shutdown();
}
}
public new void Shutdown()
{
Task.Run(new Action(ShutdownInternal));
}
private void ShutdownInternal()
{
instances.RuntimeController.Terminate();
instances.LogShutdownInformation();
Dispatcher.Invoke(base.Shutdown);
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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.Communication.Contracts;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Runtime.Communication
{
internal class RuntimeHost : BaseHost, IRuntimeHost
{
public bool AllowConnection { get; set; }
public Guid? AuthenticationToken { private get; set; }
public event CommunicationEventHandler ClientDisconnected;
public event CommunicationEventHandler ClientReady;
public event CommunicationEventHandler<ClientConfigurationEventArgs> ClientConfigurationNeeded;
public event CommunicationEventHandler<ExamSelectionReplyEventArgs> ExamSelectionReceived;
public event CommunicationEventHandler<MessageBoxReplyEventArgs> MessageBoxReplyReceived;
public event CommunicationEventHandler<PasswordReplyEventArgs> PasswordReceived;
public event CommunicationEventHandler<ReconfigurationEventArgs> ReconfigurationRequested;
public event CommunicationEventHandler<ServerFailureActionReplyEventArgs> ServerFailureActionReceived;
public event CommunicationEventHandler ShutdownRequested;
public RuntimeHost(string address, IHostObjectFactory factory, ILogger logger, int timeout_ms) : base(address, factory, logger, timeout_ms)
{
}
protected override bool OnConnect(Guid? token = null)
{
var authenticated = AuthenticationToken.HasValue && AuthenticationToken == token;
var accepted = AllowConnection && authenticated;
if (accepted)
{
AllowConnection = false;
}
return accepted;
}
protected override void OnDisconnect(Interlocutor interlocutor)
{
if (interlocutor == Interlocutor.Client)
{
ClientDisconnected?.Invoke();
}
}
protected override Response OnReceive(Message message)
{
switch (message)
{
case ExamSelectionReplyMessage m:
ExamSelectionReceived?.InvokeAsync(new ExamSelectionReplyEventArgs { RequestId = m.RequestId, SelectedExamId = m.SelectedExamId, Success = m.Success });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
case MessageBoxReplyMessage m:
MessageBoxReplyReceived?.InvokeAsync(new MessageBoxReplyEventArgs { RequestId = m.RequestId, Result = m.Result });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
case PasswordReplyMessage m:
PasswordReceived?.InvokeAsync(new PasswordReplyEventArgs { Password = m.Password, RequestId = m.RequestId, Success = m.Success });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
case ReconfigurationMessage m:
ReconfigurationRequested?.InvokeAsync(new ReconfigurationEventArgs { ConfigurationPath = m.ConfigurationPath, ResourceUrl = m.ResourceUrl });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
case ServerFailureActionReplyMessage m:
ServerFailureActionReceived?.InvokeAsync(new ServerFailureActionReplyEventArgs { Abort = m.Abort, Fallback = m.Fallback, RequestId = m.RequestId, Retry = m.Retry });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
}
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
}
protected override Response OnReceive(SimpleMessagePurport message)
{
switch (message)
{
case SimpleMessagePurport.ClientIsReady:
ClientReady?.Invoke();
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
case SimpleMessagePurport.ConfigurationNeeded:
return HandleConfigurationRequest();
case SimpleMessagePurport.RequestShutdown:
ShutdownRequested?.Invoke();
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
}
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
}
private Response HandleConfigurationRequest()
{
var args = new ClientConfigurationEventArgs();
ClientConfigurationNeeded?.Invoke(args);
return new ConfigurationResponse { Configuration = args.ClientConfiguration };
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* 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 SafeExamBrowser.Communication.Contracts;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Configuration;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Cryptography;
using SafeExamBrowser.Configuration.DataCompression;
using SafeExamBrowser.Configuration.DataFormats;
using SafeExamBrowser.Configuration.DataResources;
using SafeExamBrowser.Configuration.Integrity;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.OperationModel;
using SafeExamBrowser.Core.Operations;
using SafeExamBrowser.I18n;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring;
using SafeExamBrowser.Monitoring.Display;
using SafeExamBrowser.Monitoring.System;
using SafeExamBrowser.Runtime.Communication;
using SafeExamBrowser.Runtime.Operations;
using SafeExamBrowser.Server;
using SafeExamBrowser.Settings.Logging;
using SafeExamBrowser.SystemComponents;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.SystemComponents.Registry;
using SafeExamBrowser.UserInterface.Desktop;
using SafeExamBrowser.WindowsApi;
using SafeExamBrowser.WindowsApi.Desktops;
using SafeExamBrowser.WindowsApi.Processes;
namespace SafeExamBrowser.Runtime
{
internal class CompositionRoot
{
private AppConfig appConfig;
private IConfigurationRepository configuration;
private ILogger logger;
private ISystemInfo systemInfo;
private IText text;
internal RuntimeController RuntimeController { get; private set; }
internal void BuildObjectGraph(Action shutdown)
{
const int FIVE_SECONDS = 5000;
const int THIRTY_SECONDS = 30000;
logger = new Logger();
InitializeConfiguration();
InitializeLogging();
InitializeText();
var nativeMethods = new NativeMethods();
var registry = new Registry(ModuleLogger(nameof(Registry)));
var uiFactory = new UserInterfaceFactory(text);
var userInfo = new UserInfo(ModuleLogger(nameof(UserInfo)));
systemInfo = new SystemInfo(registry);
var args = Environment.GetCommandLineArgs();
var integrityModule = new IntegrityModule(appConfig, ModuleLogger(nameof(IntegrityModule)));
var desktopFactory = new DesktopFactory(ModuleLogger(nameof(DesktopFactory)));
var desktopMonitor = new DesktopMonitor(ModuleLogger(nameof(DesktopMonitor)));
var displayMonitor = new DisplayMonitor(ModuleLogger(nameof(DisplayMonitor)), nativeMethods, systemInfo);
var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods);
var fileSystem = new FileSystem();
var keyGenerator = new KeyGenerator(appConfig, integrityModule, ModuleLogger(nameof(KeyGenerator)));
var messageBox = new MessageBoxFactory(text);
var processFactory = new ProcessFactory(ModuleLogger(nameof(ProcessFactory)));
var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), ModuleLogger(nameof(ProxyFactory)));
var remoteSessionDetector = new RemoteSessionDetector(ModuleLogger(nameof(RemoteSessionDetector)));
var runtimeHost = new RuntimeHost(appConfig.RuntimeAddress, new HostObjectFactory(), ModuleLogger(nameof(RuntimeHost)), FIVE_SECONDS);
var runtimeWindow = uiFactory.CreateRuntimeWindow(appConfig);
var sentinel = new SystemSentinel(ModuleLogger(nameof(SystemSentinel)), nativeMethods, registry);
var server = new ServerProxy(appConfig, keyGenerator, ModuleLogger(nameof(ServerProxy)), systemInfo, userInfo);
var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), ModuleLogger(nameof(ServiceProxy)), Interlocutor.Runtime);
var sessionContext = new SessionContext();
var splashScreen = uiFactory.CreateSplashScreen(appConfig);
var vmDetector = new VirtualMachineDetector(ModuleLogger(nameof(VirtualMachineDetector)), registry, systemInfo);
var bootstrapOperations = new Queue<IOperation>();
var sessionOperations = new Queue<IRepeatableOperation>();
bootstrapOperations.Enqueue(new I18nOperation(logger, text));
bootstrapOperations.Enqueue(new CommunicationHostOperation(runtimeHost, logger));
bootstrapOperations.Enqueue(new ApplicationIntegrityOperation(integrityModule, logger));
sessionOperations.Enqueue(new SessionInitializationOperation(configuration, fileSystem, logger, runtimeHost, sessionContext));
sessionOperations.Enqueue(new ConfigurationOperation(args, configuration, new FileSystem(), new HashAlgorithm(), logger, sessionContext));
sessionOperations.Enqueue(new ServerOperation(args, configuration, fileSystem, logger, sessionContext, server));
sessionOperations.Enqueue(new VersionRestrictionOperation(logger, sessionContext, text));
sessionOperations.Enqueue(new DisclaimerOperation(logger, sessionContext));
sessionOperations.Enqueue(new RemoteSessionOperation(remoteSessionDetector, logger, sessionContext));
sessionOperations.Enqueue(new SessionIntegrityOperation(logger, sentinel, sessionContext));
sessionOperations.Enqueue(new VirtualMachineOperation(vmDetector, logger, sessionContext));
sessionOperations.Enqueue(new DisplayMonitorOperation(displayMonitor, logger, sessionContext, text));
sessionOperations.Enqueue(new ServiceOperation(logger, runtimeHost, serviceProxy, sessionContext, THIRTY_SECONDS, userInfo));
sessionOperations.Enqueue(new ClientTerminationOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS));
sessionOperations.Enqueue(new KioskModeOperation(desktopFactory, desktopMonitor, explorerShell, logger, processFactory, sessionContext));
sessionOperations.Enqueue(new ClientOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS));
sessionOperations.Enqueue(new SessionActivationOperation(logger, sessionContext));
var bootstrapSequence = new OperationSequence(logger, bootstrapOperations);
var sessionSequence = new RepeatableOperationSequence(logger, sessionOperations);
RuntimeController = new RuntimeController(
appConfig,
logger,
messageBox,
bootstrapSequence,
sessionSequence,
runtimeHost,
runtimeWindow,
serviceProxy,
sessionContext,
shutdown,
splashScreen,
text,
uiFactory);
}
internal void LogStartupInformation()
{
logger.Log($"/* {appConfig.ProgramTitle}, Version {appConfig.ProgramInformationalVersion}, Build {appConfig.ProgramBuildVersion}");
logger.Log($"/* {appConfig.ProgramCopyright}");
logger.Log($"/* ");
logger.Log($"/* Please visit https://www.github.com/SafeExamBrowser for more information.");
logger.Log(string.Empty);
logger.Log($"# Application started at {appConfig.ApplicationStartTime:yyyy-MM-dd HH:mm:ss.fff}");
logger.Log($"# Running on {systemInfo.OperatingSystemInfo}");
logger.Log($"# Computer '{systemInfo.Name}' is a {systemInfo.Model} manufactured by {systemInfo.Manufacturer}");
logger.Log($"# Runtime-ID: {appConfig.RuntimeId}");
logger.Log(string.Empty);
}
internal void LogShutdownInformation()
{
logger?.Log($"# Application terminated at {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
}
private void InitializeConfiguration()
{
var certificateStore = new CertificateStore(ModuleLogger(nameof(CertificateStore)));
var compressor = new GZipCompressor(ModuleLogger(nameof(GZipCompressor)));
var passwordEncryption = new PasswordEncryption(ModuleLogger(nameof(PasswordEncryption)));
var publicKeyEncryption = new PublicKeyEncryption(certificateStore, ModuleLogger(nameof(PublicKeyEncryption)));
var symmetricEncryption = new PublicKeySymmetricEncryption(certificateStore, ModuleLogger(nameof(PublicKeySymmetricEncryption)), passwordEncryption);
var repositoryLogger = ModuleLogger(nameof(ConfigurationRepository));
var xmlParser = new XmlParser(compressor, ModuleLogger(nameof(XmlParser)));
var xmlSerializer = new XmlSerializer(ModuleLogger(nameof(XmlSerializer)));
configuration = new ConfigurationRepository(certificateStore, repositoryLogger);
appConfig = configuration.InitializeAppConfig();
configuration.Register(new BinaryParser(
compressor,
new HashAlgorithm(),
ModuleLogger(nameof(BinaryParser)),
passwordEncryption,
publicKeyEncryption,
symmetricEncryption, xmlParser));
configuration.Register(new BinarySerializer(
compressor,
ModuleLogger(nameof(BinarySerializer)),
passwordEncryption,
publicKeyEncryption,
symmetricEncryption,
xmlSerializer));
configuration.Register(new XmlParser(compressor, ModuleLogger(nameof(XmlParser))));
configuration.Register(new XmlSerializer(ModuleLogger(nameof(XmlSerializer))));
configuration.Register(new FileResourceLoader(ModuleLogger(nameof(FileResourceLoader))));
configuration.Register(new FileResourceSaver(ModuleLogger(nameof(FileResourceSaver))));
configuration.Register(new NetworkResourceLoader(appConfig, new ModuleLogger(logger, nameof(NetworkResourceLoader))));
}
private void InitializeLogging()
{
var logFileWriter = new LogFileWriter(new DefaultLogFormatter(), appConfig.RuntimeLogFilePath);
logFileWriter.Initialize();
logger.LogLevel = LogLevel.Debug;
logger.Subscribe(logFileWriter);
}
private void InitializeText()
{
text = new Text(ModuleLogger(nameof(Text)));
}
private IModuleLogger ModuleLogger(string moduleInfo)
{
return new ModuleLogger(logger, moduleInfo);
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.Configuration.Contracts.Integrity;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Runtime.Operations
{
internal class ApplicationIntegrityOperation : IOperation
{
private readonly IIntegrityModule module;
private readonly ILogger logger;
public event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public event StatusChangedEventHandler StatusChanged;
public ApplicationIntegrityOperation(IIntegrityModule module, ILogger logger)
{
this.module = module;
this.logger = logger;
}
public OperationResult Perform()
{
logger.Info($"Attempting to verify application integrity...");
StatusChanged?.Invoke(TextKey.OperationStatus_VerifyApplicationIntegrity);
if (module.TryVerifyCodeSignature(out var isValid))
{
if (isValid)
{
logger.Info("Application integrity successfully verified.");
}
else
{
logger.Warn("Application integrity is compromised!");
}
}
else
{
logger.Warn("Failed to verify application integrity!");
}
return OperationResult.Success;
}
public OperationResult Revert()
{
return OperationResult.Success;
}
}
}

View File

@@ -0,0 +1,270 @@
/*
* 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.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.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
using SafeExamBrowser.WindowsApi.Contracts.Events;
namespace SafeExamBrowser.Runtime.Operations
{
internal class ClientOperation : SessionOperation
{
private readonly ILogger logger;
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 ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged;
public ClientOperation(
ILogger logger,
IProcessFactory processFactory,
IProxyFactory proxyFactory,
IRuntimeHost runtimeHost,
SessionContext sessionContext,
int timeout_ms) : base(sessionContext)
{
this.logger = logger;
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(Guid?);
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,44 @@
/*
* 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.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Runtime.Operations
{
internal class ClientTerminationOperation : ClientOperation
{
public ClientTerminationOperation(
ILogger logger,
IProcessFactory processFactory,
IProxyFactory proxyFactory,
IRuntimeHost runtimeHost,
SessionContext sessionContext,
int timeout_ms) : base(logger, processFactory, proxyFactory, runtimeHost, sessionContext, 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,91 @@
/*
* 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.Communication.Contracts.Data;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings;
namespace SafeExamBrowser.Runtime.Operations
{
internal abstract class ConfigurationBaseOperation : SessionOperation
{
protected string[] commandLineArgs;
protected IConfigurationRepository configuration;
protected string AppDataFilePath => Context.Next.AppConfig.AppDataFilePath;
protected string ProgramDataFilePath => Context.Next.AppConfig.ProgramDataFilePath;
public ConfigurationBaseOperation(string[] commandLineArgs, IConfigurationRepository configuration, SessionContext context) : base(context)
{
this.commandLineArgs = commandLineArgs;
this.configuration = configuration;
}
protected abstract void InvokeActionRequired(ActionRequiredEventArgs args);
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 = configuration.TryLoadSettings(uri, out settings, passwordParams);
if (status == LoadStatus.PasswordNeeded && currentPassword != default)
{
passwordParams.Password = currentPassword;
passwordParams.IsHash = true;
status = configuration.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 = configuration.TryLoadSettings(uri, out settings, passwordParams);
}
return status;
}
protected bool TryGetPassword(PasswordRequestPurpose purpose, out string password)
{
var args = new PasswordRequiredEventArgs { Purpose = purpose };
InvokeActionRequired(args);
password = args.Password;
return args.Success;
}
protected enum UriSource
{
Undefined,
AppData,
CommandLine,
ProgramData,
Reconfiguration,
Server
}
}
}

View File

@@ -0,0 +1,452 @@
/*
* 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.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.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.SystemComponents.Contracts;
namespace SafeExamBrowser.Runtime.Operations
{
internal class ConfigurationOperation : ConfigurationBaseOperation
{
private readonly IFileSystem fileSystem;
private readonly IHashAlgorithm hashAlgorithm;
private readonly ILogger logger;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public ConfigurationOperation(
string[] commandLineArgs,
IConfigurationRepository configuration,
IFileSystem fileSystem,
IHashAlgorithm hashAlgorithm,
ILogger logger,
SessionContext sessionContext) : base(commandLineArgs, configuration, sessionContext)
{
this.fileSystem = fileSystem;
this.hashAlgorithm = hashAlgorithm;
this.logger = logger;
}
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;
}
protected override void InvokeActionRequired(ActionRequiredEventArgs args)
{
ActionRequired?.Invoke(args);
}
private OperationResult LoadDefaultSettings()
{
logger.Info("No valid configuration resource specified and no local client configuration found - loading default settings...");
Context.Next.Settings = configuration.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.Display.AllowedDisplays = 10;
Context.Next.Settings.Display.IgnoreError = true;
Context.Next.Settings.Display.InternalDisplayOnly = false;
Context.Next.Settings.Browser.DeleteCacheOnShutdown = false;
Context.Next.Settings.Browser.DeleteCookiesOnShutdown = false;
Context.Next.Settings.Browser.StartUrl = uri.AbsoluteUri;
Context.Next.Settings.Security.AllowReconfiguration = true;
Context.Next.Settings.Security.VirtualMachinePolicy = VirtualMachinePolicy.Allow;
Context.Next.Settings.Service.IgnoreService = true;
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);
logger.Info("Starting client configuration...");
if (mustAuthenticate)
{
var authenticated = AuthenticateForClientConfiguration(currentPassword);
if (authenticated == true)
{
logger.Info("Authentication was successful.");
}
if (authenticated == false)
{
logger.Info("Authentication has failed!");
ActionRequired?.Invoke(new InvalidPasswordMessageArgs());
return false;
}
if (!authenticated.HasValue)
{
logger.Info("Authentication was aborted.");
return null;
}
}
else
{
logger.Info("Authentication is not required.");
}
var status = configuration.ConfigureClientWith(uri, passwordParams);
var success = status == SaveStatus.Success;
if (success)
{
logger.Info("Client configuration was successful.");
}
else
{
logger.Error($"Client configuration failed with status '{status}'!");
ActionRequired?.Invoke(new ClientConfigurationErrorMessageArgs());
}
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 = false;
for (var attempts = 0; attempts < 5 && !authenticated; attempts++)
{
var success = TryGetPassword(PasswordRequestPurpose.LocalAdministrator, out var password);
if (success)
{
authenticated = currentPassword.Equals(hashAlgorithm.GenerateHashFor(password), StringComparison.OrdinalIgnoreCase);
}
else
{
return null;
}
}
return authenticated;
}
private bool AbortAfterClientConfiguration()
{
var args = new ConfigurationCompletedEventArgs();
ActionRequired?.Invoke(args);
logger.Info($"The user chose to {(args.AbortStartup ? "abort" : "continue")} startup after successful client configuration.");
return args.AbortStartup;
}
private void ShowFailureMessage(LoadStatus status, Uri uri)
{
switch (status)
{
case LoadStatus.PasswordNeeded:
ActionRequired?.Invoke(new InvalidPasswordMessageArgs());
break;
case LoadStatus.InvalidData:
ActionRequired?.Invoke(new InvalidDataMessageArgs(uri.ToString()));
break;
case LoadStatus.NotSupported:
ActionRequired?.Invoke(new NotSupportedMessageArgs(uri.ToString()));
break;
case LoadStatus.UnexpectedError:
ActionRequired?.Invoke(new UnexpectedErrorMessageArgs(uri.ToString()));
break;
}
}
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,86 @@
/*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
{
internal class DisclaimerOperation : SessionOperation
{
private readonly ILogger logger;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public DisclaimerOperation(ILogger logger, SessionContext context) : base(context)
{
this.logger = logger;
}
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()
{
var args = new MessageEventArgs
{
Action = MessageBoxAction.OkCancel,
Icon = MessageBoxIcon.Information,
Message = TextKey.MessageBox_ScreenProctoringDisclaimer,
Title = TextKey.MessageBox_ScreenProctoringDisclaimerTitle
};
StatusChanged?.Invoke(TextKey.OperationStatus_WaitDisclaimerConfirmation);
ActionRequired?.Invoke(args);
if (args.Result == MessageBoxResult.Ok)
{
logger.Info("The user confirmed the screen proctoring disclaimer.");
return OperationResult.Success;
}
else
{
logger.Warn("The user did not confirm the screen proctoring disclaimer! Aborting session initialization...");
return OperationResult.Aborted;
}
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Display;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
{
internal class DisplayMonitorOperation : SessionOperation
{
private readonly IDisplayMonitor displayMonitor;
private readonly ILogger logger;
private readonly IText text;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public DisplayMonitorOperation(IDisplayMonitor displayMonitor, ILogger logger, SessionContext context, IText text) : base(context)
{
this.displayMonitor = displayMonitor;
this.logger = logger;
this.text = text;
}
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 result = OperationResult.Failed;
var validation = displayMonitor.ValidateConfiguration(Context.Next.Settings.Display);
if (validation.IsAllowed)
{
logger.Info("Display configuration is allowed.");
result = OperationResult.Success;
}
else
{
var args = new MessageEventArgs
{
Action = MessageBoxAction.Ok,
Icon = MessageBoxIcon.Error,
Message = TextKey.MessageBox_DisplayConfigurationError,
Title = TextKey.MessageBox_DisplayConfigurationErrorTitle
};
logger.Error("Display configuration is not allowed!");
args.MessagePlaceholders.Add("%%_ALLOWED_COUNT_%%", Convert.ToString(Context.Next.Settings.Display.AllowedDisplays));
args.MessagePlaceholders.Add("%%_TYPE_%%", Context.Next.Settings.Display.InternalDisplayOnly ? text.Get(TextKey.MessageBox_DisplayConfigurationInternal) : text.Get(TextKey.MessageBox_DisplayConfigurationInternalOrExternal));
args.MessagePlaceholders.Add("%%_EXTERNAL_COUNT_%%", Convert.ToString(validation.ExternalDisplays));
args.MessagePlaceholders.Add("%%_INTERNAL_COUNT_%%", Convert.ToString(validation.InternalDisplays));
ActionRequired?.Invoke(args);
}
return result;
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class ClientConfigurationErrorMessageArgs : MessageEventArgs
{
internal ClientConfigurationErrorMessageArgs()
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_ClientConfigurationError;
Title = TextKey.MessageBox_ClientConfigurationErrorTitle;
}
}
}

View File

@@ -0,0 +1,17 @@
/*
* 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.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class ConfigurationCompletedEventArgs : ActionRequiredEventArgs
{
public bool AbortStartup { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Server.Contracts.Data;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class ExamSelectionEventArgs : ActionRequiredEventArgs
{
internal IEnumerable<Exam> Exams { get; set; }
internal Exam SelectedExam { get; set; }
internal bool Success { get; set; }
internal ExamSelectionEventArgs(IEnumerable<Exam> exams)
{
Exams = exams;
}
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class InvalidDataMessageArgs : MessageEventArgs
{
internal InvalidDataMessageArgs(string uri)
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_InvalidConfigurationData;
MessagePlaceholders["%%URI%%"] = uri;
Title = TextKey.MessageBox_InvalidConfigurationDataTitle;
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class InvalidPasswordMessageArgs : MessageEventArgs
{
internal InvalidPasswordMessageArgs()
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_InvalidPasswordError;
Title = TextKey.MessageBox_InvalidPasswordErrorTitle;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class MessageEventArgs : ActionRequiredEventArgs
{
internal MessageBoxAction Action { get; set; }
internal MessageBoxIcon Icon { get; set; }
internal TextKey Message { get; set; }
internal MessageBoxResult Result { get; set; }
internal TextKey Title { get; set; }
internal Dictionary<string, string> MessagePlaceholders { get; private set; }
internal Dictionary<string, string> TitlePlaceholders { get; private set; }
public MessageEventArgs()
{
Action = MessageBoxAction.Ok;
MessagePlaceholders = new Dictionary<string, string>();
TitlePlaceholders = new Dictionary<string, string>();
}
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class NotSupportedMessageArgs : MessageEventArgs
{
internal NotSupportedMessageArgs(string uri)
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_NotSupportedConfigurationResource;
MessagePlaceholders["%%URI%%"] = uri;
Title = TextKey.MessageBox_NotSupportedConfigurationResourceTitle;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.Communication.Contracts.Data;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class PasswordRequiredEventArgs : ActionRequiredEventArgs
{
public string Password { get; set; }
public PasswordRequestPurpose Purpose { get; set; }
public bool Success { get; set; }
}
}

View File

@@ -0,0 +1,27 @@
/*
* 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.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class ServerFailureEventArgs : ActionRequiredEventArgs
{
public bool Abort { get; set; }
public bool Fallback { get; set; }
public string Message { get; set; }
public bool Retry { get; set; }
public bool ShowFallback { get; }
public ServerFailureEventArgs(string message, bool showFallback)
{
Message = message;
ShowFallback = showFallback;
}
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class UnexpectedErrorMessageArgs : MessageEventArgs
{
internal UnexpectedErrorMessageArgs(string uri)
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_UnexpectedConfigurationError;
MessagePlaceholders["%%URI%%"] = uri;
Title = TextKey.MessageBox_UnexpectedConfigurationErrorTitle;
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class VersionRestrictionMessageArgs : MessageEventArgs
{
internal VersionRestrictionMessageArgs(string version, string requiredVersions)
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_VersionRestrictionError;
MessagePlaceholders["%%_VERSION_%%"] = version;
MessagePlaceholders["%%_REQUIRED_VERSIONS_%%"] = requiredVersions;
Title = TextKey.MessageBox_VersionRestrictionErrorTitle;
}
}
}

View File

@@ -0,0 +1,183 @@
/*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Runtime.Operations
{
internal class KioskModeOperation : SessionOperation
{
private readonly IDesktopFactory desktopFactory;
private readonly IDesktopMonitor desktopMonitor;
private readonly IExplorerShell explorerShell;
private readonly ILogger logger;
private readonly IProcessFactory processFactory;
private KioskMode? activeMode;
private IDesktop customDesktop;
private IDesktop originalDesktop;
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged;
public KioskModeOperation(
IDesktopFactory desktopFactory,
IDesktopMonitor desktopMonitor,
IExplorerShell explorerShell,
ILogger logger,
IProcessFactory processFactory,
SessionContext sessionContext) : base(sessionContext)
{
this.desktopFactory = desktopFactory;
this.desktopMonitor = desktopMonitor;
this.explorerShell = explorerShell;
this.logger = logger;
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,71 @@
/*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
{
internal class RemoteSessionOperation : SessionOperation
{
private readonly IRemoteSessionDetector detector;
private readonly ILogger logger;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public RemoteSessionOperation(IRemoteSessionDetector detector, ILogger logger, SessionContext context) : base(context)
{
this.detector = detector;
this.logger = logger;
}
public override OperationResult Perform()
{
return ValidatePolicy();
}
public override OperationResult Repeat()
{
return ValidatePolicy();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult ValidatePolicy()
{
logger.Info($"Validating remote session policy...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateRemoteSessionPolicy);
if (Context.Next.Settings.Service.DisableRemoteConnections && detector.IsRemoteSession())
{
var args = new MessageEventArgs
{
Icon = MessageBoxIcon.Error,
Message = TextKey.MessageBox_RemoteSessionNotAllowed,
Title = TextKey.MessageBox_RemoteSessionNotAllowedTitle
};
logger.Error("Detected remote session while SEB is not allowed to be run in a remote session! Aborting...");
ActionRequired?.Invoke(args);
return OperationResult.Aborted;
}
return OperationResult.Success;
}
}
}

View File

@@ -0,0 +1,292 @@
/*
* 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.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.Settings;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
{
internal class ServerOperation : ConfigurationBaseOperation
{
private readonly IFileSystem fileSystem;
private readonly ILogger logger;
private readonly IServerProxy server;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public ServerOperation(
string[] commandLineArgs,
IConfigurationRepository configuration,
IFileSystem fileSystem,
ILogger logger,
SessionContext context,
IServerProxy server) : base(commandLineArgs, configuration, context)
{
this.fileSystem = fileSystem;
this.logger = logger;
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.");
}
}
return result;
}
public override OperationResult Repeat()
{
var result = OperationResult.Success;
if (Context.Current.Settings.SessionMode == SessionMode.Server && Context.Next.Settings.SessionMode == SessionMode.Server)
{
result = AbortServerReconfiguration();
}
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;
}
else
{
result = OperationResult.Failed;
}
}
return result;
}
protected override void InvokeActionRequired(ActionRequiredEventArgs args)
{
ActionRequired?.Invoke(args);
}
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 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.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)
{
var args = new ServerFailureEventArgs(message, Context.Next.Settings.Server.PerformFallback);
ActionRequired?.Invoke(args);
abort = args.Abort;
fallback = args.Fallback;
if (args.Retry)
{
logger.Debug("The user chose to retry the current server request.");
}
return args.Retry;
}
private bool TrySelectExam(IEnumerable<Exam> exams, out Exam exam)
{
var success = true;
if (string.IsNullOrWhiteSpace(Context.Next.Settings.Server.ExamId))
{
var args = new ExamSelectionEventArgs(exams);
ActionRequired?.Invoke(args);
exam = args.SelectedExam;
success = args.Success;
}
else
{
exam = exams.First();
logger.Info("Automatically selected exam as defined in configuration.");
}
return success;
}
private OperationResult AbortServerReconfiguration()
{
var args = new MessageEventArgs
{
Action = MessageBoxAction.Ok,
Icon = MessageBoxIcon.Warning,
Message = TextKey.MessageBox_ServerReconfigurationWarning,
Title = TextKey.MessageBox_ServerReconfigurationWarningTitle
};
logger.Warn("Server reconfiguration is currently not supported, aborting...");
ActionRequired?.Invoke(args);
return OperationResult.Aborted;
}
}
}

View File

@@ -0,0 +1,293 @@
/*
* 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.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.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings.Service;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
{
internal class ServiceOperation : SessionOperation
{
private ILogger logger;
private IRuntimeHost runtimeHost;
private IServiceProxy service;
private string serviceEventName;
private Guid? sessionId;
private int timeout_ms;
private IUserInfo userInfo;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public ServiceOperation(
ILogger logger,
IRuntimeHost runtimeHost,
IServiceProxy service,
SessionContext sessionContext,
int timeout_ms,
IUserInfo userInfo) : base(sessionContext)
{
this.logger = logger;
this.runtimeHost = runtimeHost;
this.service = service;
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 && service.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 (service.IsConnected)
{
if (sessionId.HasValue)
{
success = TryStopSession();
}
if (success && IgnoreService())
{
success = TryTerminateConnection();
}
}
else
{
success = IgnoreService() || TryInitializeConnection();
}
if (success && service.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 (service.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 = service.Connect();
var success = connected || !mandatory;
if (success)
{
logger.Info($"The service is {(mandatory ? "mandatory" : "optional")} and {(connected ? "connected." : "not connected.")}");
if (!connected && warn)
{
ActionRequired?.Invoke(new MessageEventArgs
{
Icon = MessageBoxIcon.Warning,
Message = TextKey.MessageBox_ServiceUnavailableWarning,
Title = TextKey.MessageBox_ServiceUnavailableWarningTitle
});
}
}
else
{
logger.Error("The service is mandatory but no connection could be established!");
ActionRequired?.Invoke(new MessageEventArgs
{
Icon = MessageBoxIcon.Error,
Message = TextKey.MessageBox_ServiceUnavailableError,
Title = TextKey.MessageBox_ServiceUnavailableErrorTitle
});
}
return success;
}
private bool TryTerminateConnection()
{
var disconnected = service.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 = service.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 = service.StopSession(sessionId.Value);
if (communication.Success)
{
success = TryWaitForServiceEvent(serviceEventName);
if (success)
{
sessionId = default(Guid?);
serviceEventName = default(string);
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 = service.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,77 @@
/*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Runtime.Operations
{
internal class SessionActivationOperation : SessionOperation
{
private ILogger logger;
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged { add { } remove { } }
public SessionActivationOperation(ILogger logger, SessionContext sessionContext) : base(sessionContext)
{
this.logger = logger;
}
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 == null;
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 = null;
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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.Communication.Contracts.Hosts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts;
namespace SafeExamBrowser.Runtime.Operations
{
internal class SessionInitializationOperation : SessionOperation
{
private IConfigurationRepository configuration;
private IFileSystem fileSystem;
private ILogger logger;
private IRuntimeHost runtimeHost;
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged;
public SessionInitializationOperation(
IConfigurationRepository configuration,
IFileSystem fileSystem,
ILogger logger,
IRuntimeHost runtimeHost,
SessionContext sessionContext) : base(sessionContext)
{
this.configuration = configuration;
this.fileSystem = fileSystem;
this.logger = logger;
this.runtimeHost = runtimeHost;
}
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 = configuration.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,136 @@
/*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.System;
namespace SafeExamBrowser.Runtime.Operations
{
internal class SessionIntegrityOperation : SessionOperation
{
private readonly ILogger logger;
private readonly ISystemSentinel sentinel;
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged;
public SessionIntegrityOperation(ILogger logger, ISystemSentinel sentinel, SessionContext context) : base(context)
{
this.logger = logger;
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,33 @@
/*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations
{
/// <summary>
/// The base implementation to be used for all operations in the session operation sequence.
/// </summary>
internal abstract class SessionOperation : IRepeatableOperation
{
protected SessionContext Context { get; private set; }
public abstract event ActionRequiredEventHandler ActionRequired;
public abstract event StatusChangedEventHandler StatusChanged;
public SessionOperation(SessionContext context)
{
Context = context;
}
public abstract OperationResult Perform();
public abstract OperationResult Repeat();
public abstract OperationResult Revert();
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.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.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings.Security;
namespace SafeExamBrowser.Runtime.Operations
{
internal class VersionRestrictionOperation : SessionOperation
{
private readonly ILogger logger;
private readonly IText text;
private IList<VersionRestriction> Restrictions => Context.Next.Settings.Security.VersionRestrictions;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public VersionRestrictionOperation(ILogger logger, SessionContext context, IText text) : base(context)
{
this.logger = logger;
this.text = text;
}
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}.");
ActionRequired?.Invoke(new VersionRestrictionMessageArgs(version, BuildRequiredVersions()));
}
}
else
{
logger.Info($"There are no version restrictions for the configuration.");
}
return result;
}
private bool IsFulfilled(VersionRestriction restriction)
{
var isFulfilled = true;
var (major, minor, patch, build, isAllianceEdition) = GetVersion();
if (restriction.IsMinimumRestriction)
{
isFulfilled &= restriction.Major <= major;
if (restriction.Major == major)
{
isFulfilled &= restriction.Minor <= minor;
if (restriction.Minor == minor)
{
isFulfilled &= !restriction.Patch.HasValue || restriction.Patch <= patch;
if (restriction.Patch == patch)
{
isFulfilled &= !restriction.Build.HasValue || restriction.Build <= build;
}
}
}
isFulfilled &= !restriction.RequiresAllianceEdition || isAllianceEdition;
}
else
{
isFulfilled &= restriction.Major == major;
isFulfilled &= restriction.Minor == minor;
isFulfilled &= !restriction.Patch.HasValue || restriction.Patch == patch;
isFulfilled &= !restriction.Build.HasValue || restriction.Build == build;
isFulfilled &= !restriction.RequiresAllianceEdition || isAllianceEdition;
}
return isFulfilled;
}
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();
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
{
internal class VirtualMachineOperation : SessionOperation
{
private readonly IVirtualMachineDetector detector;
private readonly ILogger logger;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public VirtualMachineOperation(IVirtualMachineDetector detector, ILogger logger, SessionContext context) : base(context)
{
this.detector = detector;
this.logger = logger;
}
public override OperationResult Perform()
{
return ValidatePolicy();
}
public override OperationResult Repeat()
{
return ValidatePolicy();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult ValidatePolicy()
{
logger.Info($"Validating virtual machine policy...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateVirtualMachinePolicy);
if (Context.Next.Settings.Security.VirtualMachinePolicy == VirtualMachinePolicy.Deny && detector.IsVirtualMachine())
{
var args = new MessageEventArgs
{
Icon = MessageBoxIcon.Error,
Message = TextKey.MessageBox_VirtualMachineNotAllowed,
Title = TextKey.MessageBox_VirtualMachineNotAllowedTitle
};
logger.Error("Detected virtual machine while SEB is not allowed to be run in a virtual machine! Aborting...");
ActionRequired?.Invoke(args);
return OperationResult.Aborted;
}
return OperationResult.Success;
}
}
}

View File

@@ -0,0 +1,53 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// 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("Safe Exam Browser")]
[assembly: AssemblyDescription("Safe Exam Browser")]
[assembly: AssemblyCompany("ETH Zürich")]
[assembly: AssemblyProduct("Safe Exam Browser")]
[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)]
[assembly: InternalsVisibleTo("SafeExamBrowser.Runtime.UnitTests")]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> to en-US. Then uncomment
//the NeutralResourceLanguage attribute below. Update the "en-US" in
//the line below to match the UICulture setting in the project file.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
// 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("3.8.0.742")]
[assembly: AssemblyFileVersion("3.8.0.742")]
[assembly: AssemblyInformationalVersion("3.8.0.742")]

View File

@@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace SafeExamBrowser.Runtime.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SafeExamBrowser.Runtime.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace SafeExamBrowser.Runtime.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.6.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@@ -0,0 +1,737 @@
/*
* 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.IO;
using System.Linq;
using System.Threading;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Communication.Contracts.Events;
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.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.Settings.Service;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime
{
internal class RuntimeController
{
private readonly AppConfig appConfig;
private readonly ILogger logger;
private readonly IMessageBox messageBox;
private readonly IOperationSequence bootstrapSequence;
private readonly IRepeatableOperationSequence sessionSequence;
private readonly IRuntimeHost runtimeHost;
private readonly IRuntimeWindow runtimeWindow;
private readonly IServiceProxy service;
private readonly SessionContext sessionContext;
private readonly ISplashScreen splashScreen;
private readonly Action shutdown;
private readonly IText text;
private readonly IUserInterfaceFactory uiFactory;
private SessionConfiguration Session
{
get { return sessionContext.Current; }
}
private bool SessionIsRunning
{
get { return Session != null; }
}
internal RuntimeController(
AppConfig appConfig,
ILogger logger,
IMessageBox messageBox,
IOperationSequence bootstrapSequence,
IRepeatableOperationSequence sessionSequence,
IRuntimeHost runtimeHost,
IRuntimeWindow runtimeWindow,
IServiceProxy service,
SessionContext sessionContext,
Action shutdown,
ISplashScreen splashScreen,
IText text,
IUserInterfaceFactory uiFactory)
{
this.appConfig = appConfig;
this.bootstrapSequence = bootstrapSequence;
this.logger = logger;
this.messageBox = messageBox;
this.runtimeHost = runtimeHost;
this.runtimeWindow = runtimeWindow;
this.sessionSequence = sessionSequence;
this.service = service;
this.sessionContext = sessionContext;
this.shutdown = shutdown;
this.splashScreen = splashScreen;
this.text = text;
this.uiFactory = uiFactory;
}
internal bool TryStart()
{
logger.Info("Initiating startup procedure...");
bootstrapSequence.ProgressChanged += BootstrapSequence_ProgressChanged;
bootstrapSequence.StatusChanged += BootstrapSequence_StatusChanged;
sessionSequence.ActionRequired += SessionSequence_ActionRequired;
sessionSequence.ProgressChanged += SessionSequence_ProgressChanged;
sessionSequence.StatusChanged += SessionSequence_StatusChanged;
// We need to show the runtime window here already, this way implicitly setting it as the runtime application's main window.
// Otherwise, the splash screen is considered as the main window and thus the operating system and/or WPF does not correctly
// activate the runtime window once bootstrapping has finished, which in turn leads to undesired UI behavior.
runtimeWindow.Show();
runtimeWindow.BringToForeground();
runtimeWindow.SetIndeterminate();
splashScreen.Show();
splashScreen.BringToForeground();
var initialized = bootstrapSequence.TryPerform() == OperationResult.Success;
if (initialized)
{
RegisterEvents();
logger.Info("Application successfully initialized.");
logger.Log(string.Empty);
logger.Subscribe(runtimeWindow);
splashScreen.Hide();
StartSession();
}
else
{
logger.Info("Application startup aborted!");
logger.Log(string.Empty);
messageBox.Show(AppendLogFilePaths(TextKey.MessageBox_StartupError), text.Get(TextKey.MessageBox_StartupErrorTitle), icon: MessageBoxIcon.Error, parent: splashScreen);
}
return initialized && SessionIsRunning;
}
internal void Terminate()
{
DeregisterEvents();
if (SessionIsRunning)
{
StopSession();
}
logger.Unsubscribe(runtimeWindow);
runtimeWindow.Close();
splashScreen.Show();
splashScreen.BringToForeground();
logger.Log(string.Empty);
logger.Info("Initiating shutdown procedure...");
var success = bootstrapSequence.TryRevert() == OperationResult.Success;
if (success)
{
logger.Info("Application successfully finalized.");
logger.Log(string.Empty);
}
else
{
logger.Info("Shutdown procedure failed!");
logger.Log(string.Empty);
messageBox.Show(AppendLogFilePaths(TextKey.MessageBox_ShutdownError), text.Get(TextKey.MessageBox_ShutdownErrorTitle), icon: MessageBoxIcon.Error, parent: splashScreen);
}
splashScreen.Close();
}
private void StartSession()
{
runtimeWindow.Show();
runtimeWindow.BringToForeground();
runtimeWindow.ShowProgressBar = true;
logger.Info(AppendDivider("Session Start Procedure"));
if (SessionIsRunning)
{
DeregisterSessionEvents();
}
var result = SessionIsRunning ? sessionSequence.TryRepeat() : sessionSequence.TryPerform();
if (result == OperationResult.Success)
{
logger.Info(AppendDivider("Session Running"));
HandleSessionStartSuccess();
}
else if (result == OperationResult.Failed)
{
logger.Info(AppendDivider("Session Start Failed"));
HandleSessionStartFailure();
}
else if (result == OperationResult.Aborted)
{
logger.Info(AppendDivider("Session Start Aborted"));
HandleSessionStartAbortion();
}
}
private void HandleSessionStartSuccess()
{
RegisterSessionEvents();
runtimeWindow.ShowProgressBar = false;
runtimeWindow.ShowLog = Session.Settings.Security.AllowApplicationLogAccess;
runtimeWindow.TopMost = false;
runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_ApplicationRunning);
runtimeWindow.Hide();
}
private void HandleSessionStartFailure()
{
if (SessionIsRunning)
{
StopSession();
messageBox.Show(AppendLogFilePaths(TextKey.MessageBox_SessionStartError), text.Get(TextKey.MessageBox_SessionStartErrorTitle), icon: MessageBoxIcon.Error, parent: runtimeWindow);
logger.Info("Terminating application...");
shutdown.Invoke();
}
else
{
messageBox.Show(AppendLogFilePaths(TextKey.MessageBox_SessionStartError), text.Get(TextKey.MessageBox_SessionStartErrorTitle), icon: MessageBoxIcon.Error, parent: runtimeWindow);
}
}
private void HandleSessionStartAbortion()
{
if (SessionIsRunning)
{
RegisterSessionEvents();
runtimeWindow.ShowProgressBar = false;
runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_ApplicationRunning);
runtimeWindow.TopMost = Session.Settings.Security.KioskMode != KioskMode.None;
if (Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell)
{
runtimeWindow.Hide();
}
sessionContext.ClientProxy.InformReconfigurationAborted();
}
}
private void StopSession()
{
runtimeWindow.Show();
runtimeWindow.BringToForeground();
runtimeWindow.ShowProgressBar = true;
logger.Info(AppendDivider("Session Stop Procedure"));
DeregisterSessionEvents();
var success = sessionSequence.TryRevert() == OperationResult.Success;
if (success)
{
logger.Info(AppendDivider("Session Terminated"));
}
else
{
logger.Info(AppendDivider("Session Stop Failed"));
}
}
private void RegisterEvents()
{
runtimeHost.ClientConfigurationNeeded += RuntimeHost_ClientConfigurationNeeded;
runtimeHost.ReconfigurationRequested += RuntimeHost_ReconfigurationRequested;
runtimeHost.ShutdownRequested += RuntimeHost_ShutdownRequested;
}
private void DeregisterEvents()
{
runtimeHost.ClientConfigurationNeeded -= RuntimeHost_ClientConfigurationNeeded;
runtimeHost.ReconfigurationRequested -= RuntimeHost_ReconfigurationRequested;
runtimeHost.ShutdownRequested -= RuntimeHost_ShutdownRequested;
}
private void RegisterSessionEvents()
{
service.ConnectionLost += ServiceProxy_ConnectionLost;
sessionContext.ClientProcess.Terminated += ClientProcess_Terminated;
sessionContext.ClientProxy.ConnectionLost += ClientProxy_ConnectionLost;
}
private void DeregisterSessionEvents()
{
service.ConnectionLost -= ServiceProxy_ConnectionLost;
if (sessionContext.ClientProcess != null)
{
sessionContext.ClientProcess.Terminated -= ClientProcess_Terminated;
}
if (sessionContext.ClientProxy != null)
{
sessionContext.ClientProxy.ConnectionLost -= ClientProxy_ConnectionLost;
}
}
private void BootstrapSequence_ProgressChanged(ProgressChangedEventArgs args)
{
MapProgress(splashScreen, args);
}
private void BootstrapSequence_StatusChanged(TextKey status)
{
splashScreen.UpdateStatus(status, true);
}
private void ClientProcess_Terminated(int exitCode)
{
logger.Error($"Client application has unexpectedly terminated with exit code {exitCode}!");
if (SessionIsRunning)
{
StopSession();
}
//messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error, parent: runtimeWindow);
shutdown.Invoke();
}
private void ClientProxy_ConnectionLost()
{
logger.Error("Lost connection to the client application!");
if (SessionIsRunning)
{
StopSession();
}
messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error, parent: runtimeWindow);
shutdown.Invoke();
}
private void RuntimeHost_ClientConfigurationNeeded(ClientConfigurationEventArgs args)
{
args.ClientConfiguration = new ClientConfiguration
{
AppConfig = sessionContext.Next.AppConfig,
SessionId = sessionContext.Next.SessionId,
Settings = sessionContext.Next.Settings
};
}
private void RuntimeHost_ReconfigurationRequested(ReconfigurationEventArgs args)
{
logger.Info($"Accepted request for reconfiguration with '{args.ConfigurationPath}'.");
sessionContext.ReconfigurationFilePath = args.ConfigurationPath;
sessionContext.ReconfigurationUrl = args.ResourceUrl;
StartSession();
}
private void RuntimeHost_ShutdownRequested()
{
logger.Info("Received shutdown request from the client application.");
shutdown.Invoke();
}
private void ServiceProxy_ConnectionLost()
{
if (SessionIsRunning && Session.Settings.Service.Policy == ServicePolicy.Mandatory)
{
logger.Error("Lost connection to the service component!");
StopSession();
messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error, parent: runtimeWindow);
shutdown.Invoke();
}
else
{
logger.Warn("Lost connection to the service component!");
}
}
private void SessionSequence_ActionRequired(ActionRequiredEventArgs args)
{
switch (args)
{
case ConfigurationCompletedEventArgs a:
AskIfConfigurationSufficient(a);
break;
case ExamSelectionEventArgs a:
AskForExamSelection(a);
break;
case MessageEventArgs m:
ShowMessageBox(m);
break;
case PasswordRequiredEventArgs p:
AskForPassword(p);
break;
case ServerFailureEventArgs a:
AskForServerFailureAction(a);
break;
}
}
private void AskForExamSelection(ExamSelectionEventArgs args)
{
var isStartup = !SessionIsRunning;
var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell;
if (isStartup || isRunningOnDefaultDesktop)
{
TryAskForExamSelectionViaDialog(args);
}
else
{
TryAskForExamSelectionViaClient(args);
}
}
private void AskForServerFailureAction(ServerFailureEventArgs args)
{
var isStartup = !SessionIsRunning;
var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell;
if (isStartup || isRunningOnDefaultDesktop)
{
TryAskForServerFailureActionViaDialog(args);
}
else
{
TryAskForServerFailureActionViaClient(args);
}
}
private void AskIfConfigurationSufficient(ConfigurationCompletedEventArgs args)
{
var message = TextKey.MessageBox_ClientConfigurationQuestion;
var title = TextKey.MessageBox_ClientConfigurationQuestionTitle;
var result = messageBox.Show(message, title, MessageBoxAction.YesNo, MessageBoxIcon.Question, runtimeWindow);
args.AbortStartup = result == MessageBoxResult.Yes;
}
private void AskForPassword(PasswordRequiredEventArgs args)
{
var isStartup = !SessionIsRunning;
var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell;
if (isStartup || isRunningOnDefaultDesktop)
{
TryGetPasswordViaDialog(args);
}
else
{
TryGetPasswordViaClient(args);
}
}
private void ShowMessageBox(MessageEventArgs args)
{
var isStartup = !SessionIsRunning;
var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell;
var message = text.Get(args.Message);
var title = text.Get(args.Title);
foreach (var placeholder in args.MessagePlaceholders)
{
message = message.Replace(placeholder.Key, placeholder.Value);
}
foreach (var placeholder in args.TitlePlaceholders)
{
title = title.Replace(placeholder.Key, placeholder.Value);
}
if (isStartup || isRunningOnDefaultDesktop)
{
args.Result = messageBox.Show(message, title, args.Action, args.Icon, runtimeWindow);
}
else
{
args.Result = ShowMessageBoxViaClient(message, title, args.Action, args.Icon);
}
}
private MessageBoxResult ShowMessageBoxViaClient(string message, string title, MessageBoxAction action, MessageBoxIcon icon)
{
var requestId = Guid.NewGuid();
var result = MessageBoxResult.None;
var response = default(MessageBoxReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<MessageBoxReplyEventArgs>((args) =>
{
if (args.RequestId == requestId)
{
response = args;
responseEvent.Set();
}
});
runtimeHost.MessageBoxReplyReceived += responseEventHandler;
var communication = sessionContext.ClientProxy.ShowMessage(message, title, (int) action, (int) icon, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
result = (MessageBoxResult) response.Result;
}
runtimeHost.MessageBoxReplyReceived -= responseEventHandler;
return result;
}
private void TryAskForExamSelectionViaDialog(ExamSelectionEventArgs args)
{
var dialog = uiFactory.CreateExamSelectionDialog(args.Exams);
var result = dialog.Show(runtimeWindow);
args.SelectedExam = result.SelectedExam;
args.Success = result.Success;
}
private void TryAskForExamSelectionViaClient(ExamSelectionEventArgs args)
{
var exams = args.Exams.Select(e => (e.Id, e.LmsName, e.Name, e.Url));
var requestId = Guid.NewGuid();
var response = default(ExamSelectionReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<ExamSelectionReplyEventArgs>((a) =>
{
if (a.RequestId == requestId)
{
response = a;
responseEvent.Set();
}
});
runtimeHost.ExamSelectionReceived += responseEventHandler;
var communication = sessionContext.ClientProxy.RequestExamSelection(exams, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
args.SelectedExam = args.Exams.First(e => e.Id == response.SelectedExamId);
args.Success = response.Success;
}
else
{
args.SelectedExam = default(Exam);
args.Success = false;
}
runtimeHost.ExamSelectionReceived -= responseEventHandler;
}
private void TryAskForServerFailureActionViaDialog(ServerFailureEventArgs args)
{
var dialog = uiFactory.CreateServerFailureDialog(args.Message, args.ShowFallback);
var result = dialog.Show(runtimeWindow);
args.Abort = result.Abort;
args.Fallback = result.Fallback;
args.Retry = result.Retry;
}
private void TryAskForServerFailureActionViaClient(ServerFailureEventArgs args)
{
var requestId = Guid.NewGuid();
var response = default(ServerFailureActionReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<ServerFailureActionReplyEventArgs>((a) =>
{
if (a.RequestId == requestId)
{
response = a;
responseEvent.Set();
}
});
runtimeHost.ServerFailureActionReceived += responseEventHandler;
var communication = sessionContext.ClientProxy.RequestServerFailureAction(args.Message, args.ShowFallback, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
args.Abort = response.Abort;
args.Fallback = response.Fallback;
args.Retry = response.Retry;
}
else
{
args.Abort = true;
args.Fallback = false;
args.Retry = false;
}
runtimeHost.ServerFailureActionReceived -= responseEventHandler;
}
private void TryGetPasswordViaDialog(PasswordRequiredEventArgs args)
{
var message = default(TextKey);
var title = default(TextKey);
switch (args.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;
}
var dialog = uiFactory.CreatePasswordDialog(text.Get(message), text.Get(title));
var result = dialog.Show(runtimeWindow);
args.Password = result.Password;
args.Success = result.Success;
}
private void TryGetPasswordViaClient(PasswordRequiredEventArgs args)
{
var requestId = Guid.NewGuid();
var response = default(PasswordReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<PasswordReplyEventArgs>((a) =>
{
if (a.RequestId == requestId)
{
response = a;
responseEvent.Set();
}
});
runtimeHost.PasswordReceived += responseEventHandler;
var communication = sessionContext.ClientProxy.RequestPassword(args.Purpose, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
args.Password = response.Password;
args.Success = response.Success;
}
else
{
args.Password = default(string);
args.Success = false;
}
runtimeHost.PasswordReceived -= responseEventHandler;
}
private void SessionSequence_ProgressChanged(ProgressChangedEventArgs args)
{
MapProgress(runtimeWindow, args);
}
private void SessionSequence_StatusChanged(TextKey status)
{
runtimeWindow?.UpdateStatus(status, true);
}
private void MapProgress(IProgressIndicator progressIndicator, ProgressChangedEventArgs args)
{
if (args.CurrentValue.HasValue)
{
progressIndicator?.SetValue(args.CurrentValue.Value);
}
if (args.IsIndeterminate == true)
{
progressIndicator?.SetIndeterminate();
}
if (args.MaxValue.HasValue)
{
progressIndicator?.SetMaxValue(args.MaxValue.Value);
}
if (args.Progress == true)
{
progressIndicator?.Progress();
}
if (args.Regress == true)
{
progressIndicator?.Regress();
}
}
private string AppendDivider(string message)
{
var dashesLeft = new String('-', 48 - message.Length / 2 - message.Length % 2);
var dashesRight = new String('-', 48 - message.Length / 2);
return $"### {dashesLeft} {message} {dashesRight} ###";
}
private string AppendLogFilePaths(TextKey key)
{
var message = text.Get(key);
if (File.Exists(appConfig.BrowserLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.BrowserLogFilePath}";
}
if (File.Exists(appConfig.ClientLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.ClientLogFilePath}";
}
if (File.Exists(appConfig.RuntimeLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.RuntimeLogFilePath}";
}
if (File.Exists(appConfig.ServiceLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.ServiceLogFilePath}";
}
return message;
}
}
}

View File

@@ -0,0 +1,282 @@
<?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>{E3AED2F8-B5DF-45D1-AC19-48066923D6D8}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>SafeExamBrowser.Runtime</RootNamespace>
<AssemblyName>SafeExamBrowser</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>3.0.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup>
<StartupObject>SafeExamBrowser.Runtime.App</StartupObject>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>SafeExamBrowser.ico</ApplicationIcon>
</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>
<Prefer32Bit>true</Prefer32Bit>
<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>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</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>
<Prefer32Bit>true</Prefer32Bit>
</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>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Xaml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<Compile Include="App.cs" />
<Compile Include="Operations\ClientOperation.cs" />
<Compile Include="Operations\ClientTerminationOperation.cs" />
<Compile Include="Operations\ConfigurationBaseOperation.cs" />
<Compile Include="Operations\ConfigurationOperation.cs" />
<Compile Include="Operations\DisclaimerOperation.cs" />
<Compile Include="Operations\DisplayMonitorOperation.cs" />
<Compile Include="Operations\Events\ClientConfigurationErrorMessageArgs.cs" />
<Compile Include="Operations\Events\ConfigurationCompletedEventArgs.cs" />
<Compile Include="Operations\Events\ExamSelectionEventArgs.cs" />
<Compile Include="Operations\Events\InvalidDataMessageArgs.cs" />
<Compile Include="Operations\Events\InvalidPasswordMessageArgs.cs" />
<Compile Include="Operations\Events\MessageEventArgs.cs" />
<Compile Include="Operations\Events\NotSupportedMessageArgs.cs" />
<Compile Include="Operations\Events\PasswordRequiredEventArgs.cs" />
<Compile Include="Operations\Events\ServerFailureEventArgs.cs" />
<Compile Include="Operations\Events\UnexpectedErrorMessageArgs.cs" />
<Compile Include="Operations\ApplicationIntegrityOperation.cs" />
<Compile Include="Operations\Events\VersionRestrictionMessageArgs.cs" />
<Compile Include="Operations\KioskModeOperation.cs" />
<Compile Include="Operations\RemoteSessionOperation.cs" />
<Compile Include="Operations\ServerOperation.cs" />
<Compile Include="Operations\ServiceOperation.cs" />
<Compile Include="Operations\SessionActivationOperation.cs" />
<Compile Include="Operations\SessionIntegrityOperation.cs" />
<Compile Include="Operations\SessionOperation.cs" />
<Compile Include="Operations\SessionInitializationOperation.cs" />
<Compile Include="Communication\RuntimeHost.cs" />
<Compile Include="CompositionRoot.cs" />
<Compile Include="Operations\VersionRestrictionOperation.cs" />
<Compile Include="Operations\VirtualMachineOperation.cs" />
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<Compile Include="RuntimeController.cs" />
<Compile Include="SessionContext.cs" />
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="app.manifest" />
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SafeExamBrowser.Communication.Contracts\SafeExamBrowser.Communication.Contracts.csproj">
<Project>{0cd2c5fe-711a-4c32-afe0-bb804fe8b220}</Project>
<Name>SafeExamBrowser.Communication.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Communication\SafeExamBrowser.Communication.csproj">
<Project>{c9416a62-0623-4d38-96aa-92516b32f02f}</Project>
<Name>SafeExamBrowser.Communication</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Configuration.Contracts\SafeExamBrowser.Configuration.Contracts.csproj">
<Project>{7d74555e-63e1-4c46-bd0a-8580552368c8}</Project>
<Name>SafeExamBrowser.Configuration.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Configuration\SafeExamBrowser.Configuration.csproj">
<Project>{c388c4dd-a159-457d-af92-89f7ad185109}</Project>
<Name>SafeExamBrowser.Configuration</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Core.Contracts\SafeExamBrowser.Core.Contracts.csproj">
<Project>{fe0e1224-b447-4b14-81e7-ed7d84822aa0}</Project>
<Name>SafeExamBrowser.Core.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Core\SafeExamBrowser.Core.csproj">
<Project>{3d6fdbb6-a4af-4626-bb2b-bf329d44f9cc}</Project>
<Name>SafeExamBrowser.Core</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.I18n.Contracts\SafeExamBrowser.I18n.Contracts.csproj">
<Project>{1858ddf3-bc2a-4bff-b663-4ce2ffeb8b7d}</Project>
<Name>SafeExamBrowser.I18n.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.I18n\SafeExamBrowser.I18n.csproj">
<Project>{10c62628-8e6a-45aa-9d97-339b119ad21d}</Project>
<Name>SafeExamBrowser.I18n</Name>
</ProjectReference>
<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.Logging\SafeExamBrowser.Logging.csproj">
<Project>{e107026c-2011-4552-a7d8-3a0d37881df6}</Project>
<Name>SafeExamBrowser.Logging</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Monitoring.Contracts\SafeExamBrowser.Monitoring.Contracts.csproj">
<Project>{6d563a30-366d-4c35-815b-2c9e6872278b}</Project>
<Name>SafeExamBrowser.Monitoring.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Monitoring\SafeExamBrowser.Monitoring.csproj">
<Project>{ef563531-4eb5-44b9-a5ec-d6d6f204469b}</Project>
<Name>SafeExamBrowser.Monitoring</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Server.Contracts\SafeExamBrowser.Server.Contracts.csproj">
<Project>{db701e6f-bddc-4cec-b662-335a9dc11809}</Project>
<Name>SafeExamBrowser.Server.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Server\SafeExamBrowser.Server.csproj">
<Project>{46edbde0-58b4-4725-9783-0c55c3d49c0c}</Project>
<Name>SafeExamBrowser.Server</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.SystemComponents\SafeExamBrowser.SystemComponents.csproj">
<Project>{acee2ef1-14d2-4b52-8994-5c053055bb51}</Project>
<Name>SafeExamBrowser.SystemComponents</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.UserInterface.Contracts\SafeExamBrowser.UserInterface.Contracts.csproj">
<Project>{c7889e97-6ff6-4a58-b7cb-521ed276b316}</Project>
<Name>SafeExamBrowser.UserInterface.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.UserInterface.Desktop\SafeExamBrowser.UserInterface.Desktop.csproj">
<Project>{a502df54-7169-4647-94bd-18b192924866}</Project>
<Name>SafeExamBrowser.UserInterface.Desktop</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.WindowsApi.Contracts\SafeExamBrowser.WindowsApi.Contracts.csproj">
<Project>{7016f080-9aa5-41b2-a225-385ad877c171}</Project>
<Name>SafeExamBrowser.WindowsApi.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.WindowsApi\SafeExamBrowser.WindowsApi.csproj">
<Project>{73724659-4150-4792-A94E-42F5F3C1B696}</Project>
<Name>SafeExamBrowser.WindowsApi</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.5.2">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.5.2 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<Resource Include="SafeExamBrowser.ico" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>IF EXIST "C:\SEB\seb_$(PlatformName).dll" (
robocopy "C:\SEB" "$(TargetDir)\" "seb_$(PlatformName).dll" /np
IF %25ERRORLEVEL%25 GEQ 8 (
EXIT 1
)
) ELSE (
ECHO WARNING: Integrity module not included!
)
EXIT 0</PostBuildEvent>
</PropertyGroup>
<PropertyGroup>
<PreBuildEvent>
</PreBuildEvent>
</PropertyGroup>
<Target Name="AfterClean" AfterTargets="Clean">
<RemoveDir Directories="$(TargetDir)" />
</Target>
<Import Project="..\packages\OpenCover.4.7.1221\build\OpenCover.targets" Condition="Exists('..\packages\OpenCover.4.7.1221\build\OpenCover.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\OpenCover.4.7.1221\build\OpenCover.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\OpenCover.4.7.1221\build\OpenCover.targets'))" />
</Target>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -0,0 +1,50 @@
/*
* 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.Communication.Contracts.Proxies;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Runtime
{
/// <summary>
/// Holds all configuration and runtime data required for the session handling.
/// </summary>
internal class SessionContext
{
/// <summary>
/// The currently running client process.
/// </summary>
internal IProcess ClientProcess { get; set; }
/// <summary>
/// The communication proxy for the currently running client process.
/// </summary>
internal IClientProxy ClientProxy { get; set; }
/// <summary>
/// The configuration of the currently active session.
/// </summary>
internal SessionConfiguration Current { get; set; }
/// <summary>
/// The configuration of the next session to be activated.
/// </summary>
internal SessionConfiguration Next { get; set; }
/// <summary>
/// The path of the configuration file to be used for reconfiguration.
/// </summary>
internal string ReconfigurationFilePath { get; set; }
/// <summary>
/// The original URL from where the configuration file was downloaded.
/// </summary>
internal string ReconfigurationUrl { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on and is
is designed to work with. Uncomment the appropriate elements and Windows will
automatically selected the most compatible environment. -->
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="OpenCover" version="4.7.1221" targetFramework="net48" />
</packages>