SEBWIN-771: Implemented reconfiguration safeguard and refactored and moved reconfiguration and session locking to new coordinator module.
This commit is contained in:
parent
f3a9030505
commit
b48ef21708
9 changed files with 364 additions and 41 deletions
|
@ -14,6 +14,7 @@ using Moq;
|
|||
using SafeExamBrowser.Applications.Contracts;
|
||||
using SafeExamBrowser.Browser.Contracts;
|
||||
using SafeExamBrowser.Browser.Contracts.Events;
|
||||
using SafeExamBrowser.Client.Contracts;
|
||||
using SafeExamBrowser.Client.Operations.Events;
|
||||
using SafeExamBrowser.Communication.Contracts.Data;
|
||||
using SafeExamBrowser.Communication.Contracts.Events;
|
||||
|
@ -56,6 +57,7 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
private Mock<IBrowserApplication> browser;
|
||||
private Mock<IClientHost> clientHost;
|
||||
private ClientContext context;
|
||||
private Mock<ICoordinator> coordinator;
|
||||
private Mock<IDisplayMonitor> displayMonitor;
|
||||
private Mock<IExplorerShell> explorerShell;
|
||||
private Mock<IFileSystemDialog> fileSystemDialog;
|
||||
|
@ -90,6 +92,7 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
browser = new Mock<IBrowserApplication>();
|
||||
clientHost = new Mock<IClientHost>();
|
||||
context = new ClientContext();
|
||||
coordinator = new Mock<ICoordinator>();
|
||||
displayMonitor = new Mock<IDisplayMonitor>();
|
||||
explorerShell = new Mock<IExplorerShell>();
|
||||
fileSystemDialog = new Mock<IFileSystemDialog>();
|
||||
|
@ -120,6 +123,7 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
actionCenter.Object,
|
||||
applicationMonitor.Object,
|
||||
context,
|
||||
coordinator.Object,
|
||||
displayMonitor.Object,
|
||||
explorerShell.Object,
|
||||
fileSystemDialog.Object,
|
||||
|
@ -729,13 +733,17 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
var args = new DownloadEventArgs();
|
||||
|
||||
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
|
||||
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true);
|
||||
runtimeProxy.Setup(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>())).Returns(new CommunicationResult(true));
|
||||
|
||||
sut.TryStart();
|
||||
browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args);
|
||||
args.Callback(true, string.Empty);
|
||||
|
||||
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once);
|
||||
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
|
||||
|
||||
Assert.IsTrue(args.AllowDownload);
|
||||
}
|
||||
|
||||
|
@ -752,7 +760,30 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
sut.TryStart();
|
||||
browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args);
|
||||
|
||||
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Never);
|
||||
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
|
||||
Assert.IsFalse(args.AllowDownload);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Reconfiguration_MustNotAllowConcurrentExecution()
|
||||
{
|
||||
var args = new DownloadEventArgs();
|
||||
|
||||
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
|
||||
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(false);
|
||||
runtimeProxy.Setup(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>())).Returns(new CommunicationResult(true));
|
||||
|
||||
sut.TryStart();
|
||||
browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args);
|
||||
args.Callback?.Invoke(true, string.Empty);
|
||||
|
||||
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once);
|
||||
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
|
||||
Assert.IsFalse(args.AllowDownload);
|
||||
}
|
||||
|
||||
|
@ -762,6 +793,7 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
var args = new DownloadEventArgs { Url = "sebs://www.somehost.org/some/path/some_configuration.seb?query=123" };
|
||||
|
||||
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
|
||||
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true);
|
||||
settings.Security.AllowReconfiguration = true;
|
||||
settings.Security.QuitPasswordHash = "abc123";
|
||||
settings.Security.ReconfigurationUrl = "sebs://www.somehost.org/some/path/*.seb?query=123";
|
||||
|
@ -771,7 +803,10 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args);
|
||||
args.Callback(true, string.Empty);
|
||||
|
||||
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once);
|
||||
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
|
||||
|
||||
Assert.IsTrue(args.AllowDownload);
|
||||
}
|
||||
|
||||
|
@ -786,7 +821,10 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
sut.TryStart();
|
||||
browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args);
|
||||
|
||||
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Never);
|
||||
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
|
||||
Assert.IsFalse(args.AllowDownload);
|
||||
}
|
||||
|
||||
|
@ -802,7 +840,10 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
sut.TryStart();
|
||||
browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args);
|
||||
|
||||
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Never);
|
||||
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
|
||||
Assert.IsFalse(args.AllowDownload);
|
||||
}
|
||||
|
||||
|
@ -815,7 +856,7 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
var args = new DownloadEventArgs();
|
||||
|
||||
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
|
||||
settings.Security.AllowReconfiguration = true;
|
||||
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true);
|
||||
messageBox.Setup(m => m.Show(
|
||||
It.IsAny<TextKey>(),
|
||||
It.IsAny<TextKey>(),
|
||||
|
@ -825,11 +866,14 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
runtimeProxy.Setup(r => r.RequestReconfiguration(
|
||||
It.Is<string>(p => p == downloadPath),
|
||||
It.Is<string>(u => u == downloadUrl))).Returns(new CommunicationResult(true));
|
||||
settings.Security.AllowReconfiguration = true;
|
||||
|
||||
sut.TryStart();
|
||||
browser.Raise(b => b.ConfigurationDownloadRequested += null, filename, args);
|
||||
args.Callback(true, downloadUrl, downloadPath);
|
||||
|
||||
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once);
|
||||
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.Is<string>(p => p == downloadPath), It.Is<string>(u => u == downloadUrl)), Times.Once);
|
||||
|
||||
Assert.AreEqual(downloadPath, args.DownloadPath);
|
||||
|
@ -845,7 +889,7 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
var args = new DownloadEventArgs();
|
||||
|
||||
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
|
||||
settings.Security.AllowReconfiguration = true;
|
||||
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true);
|
||||
messageBox.Setup(m => m.Show(
|
||||
It.IsAny<TextKey>(),
|
||||
It.IsAny<TextKey>(),
|
||||
|
@ -855,11 +899,14 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
runtimeProxy.Setup(r => r.RequestReconfiguration(
|
||||
It.Is<string>(p => p == downloadPath),
|
||||
It.Is<string>(u => u == downloadUrl))).Returns(new CommunicationResult(true));
|
||||
settings.Security.AllowReconfiguration = true;
|
||||
|
||||
sut.TryStart();
|
||||
browser.Raise(b => b.ConfigurationDownloadRequested += null, filename, args);
|
||||
args.Callback(false, downloadPath);
|
||||
|
||||
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once);
|
||||
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Once);
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
}
|
||||
|
||||
|
@ -872,7 +919,7 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
var args = new DownloadEventArgs();
|
||||
|
||||
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
|
||||
settings.Security.AllowReconfiguration = true;
|
||||
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true);
|
||||
messageBox.Setup(m => m.Show(
|
||||
It.IsAny<TextKey>(),
|
||||
It.IsAny<TextKey>(),
|
||||
|
@ -882,18 +929,21 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
runtimeProxy.Setup(r => r.RequestReconfiguration(
|
||||
It.Is<string>(p => p == downloadPath),
|
||||
It.Is<string>(u => u == downloadUrl))).Returns(new CommunicationResult(false));
|
||||
settings.Security.AllowReconfiguration = true;
|
||||
|
||||
sut.TryStart();
|
||||
browser.Raise(b => b.ConfigurationDownloadRequested += null, filename, args);
|
||||
args.Callback(true, downloadUrl, downloadPath);
|
||||
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
|
||||
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once);
|
||||
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Once);
|
||||
messageBox.Verify(m => m.Show(
|
||||
It.IsAny<TextKey>(),
|
||||
It.IsAny<TextKey>(),
|
||||
It.IsAny<MessageBoxAction>(),
|
||||
It.Is<MessageBoxIcon>(i => i == MessageBoxIcon.Error),
|
||||
It.IsAny<IWindow>()), Times.Once);
|
||||
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
@ -1231,8 +1281,9 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
{
|
||||
var lockScreen = new Mock<ILockScreen>();
|
||||
|
||||
settings.Service.IgnoreService = true;
|
||||
coordinator.Setup(c => c.RequestSessionLock()).Returns(true);
|
||||
lockScreen.Setup(l => l.WaitForResult()).Returns(new LockScreenResult());
|
||||
settings.Service.IgnoreService = true;
|
||||
uiFactory
|
||||
.Setup(f => f.CreateLockScreen(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
|
||||
.Returns(lockScreen.Object);
|
||||
|
@ -1240,6 +1291,8 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
sut.TryStart();
|
||||
systemMonitor.Raise(m => m.SessionChanged += null);
|
||||
|
||||
coordinator.Verify(c => c.RequestSessionLock(), Times.Once);
|
||||
coordinator.Verify(c => c.ReleaseSessionLock(), Times.Once);
|
||||
lockScreen.Verify(l => l.Show(), Times.Once);
|
||||
}
|
||||
|
||||
|
@ -1249,9 +1302,10 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
var lockScreen = new Mock<ILockScreen>();
|
||||
var result = new LockScreenResult();
|
||||
|
||||
settings.Service.IgnoreService = true;
|
||||
coordinator.Setup(c => c.RequestSessionLock()).Returns(true);
|
||||
lockScreen.Setup(l => l.WaitForResult()).Returns(result);
|
||||
runtimeProxy.Setup(r => r.RequestShutdown()).Returns(new CommunicationResult(true));
|
||||
settings.Service.IgnoreService = true;
|
||||
uiFactory
|
||||
.Setup(f => f.CreateLockScreen(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
|
||||
.Callback(new Action<string, string, IEnumerable<LockScreenOption>, LockScreenSettings>((message, title, options, settings) => result.OptionId = options.Last().Id))
|
||||
|
@ -1260,6 +1314,8 @@ namespace SafeExamBrowser.Client.UnitTests
|
|||
sut.TryStart();
|
||||
systemMonitor.Raise(m => m.SessionChanged += null);
|
||||
|
||||
coordinator.Verify(c => c.RequestSessionLock(), Times.Once);
|
||||
coordinator.Verify(c => c.ReleaseSessionLock(), Times.Once);
|
||||
lockScreen.Verify(l => l.Show(), Times.Once);
|
||||
runtimeProxy.Verify(p => p.RequestShutdown(), Times.Once);
|
||||
}
|
||||
|
|
118
SafeExamBrowser.Client.UnitTests/CoordinatorTests.cs
Normal file
118
SafeExamBrowser.Client.UnitTests/CoordinatorTests.cs
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace SafeExamBrowser.Client.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class CoordinatorTests
|
||||
{
|
||||
private Coordinator sut;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
sut = new Coordinator();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReconfigurationLock_MustWorkCorrectly()
|
||||
{
|
||||
Assert.IsFalse(sut.IsReconfigurationLocked());
|
||||
|
||||
sut.RequestReconfigurationLock();
|
||||
|
||||
var result = Parallel.For(1, 1000, (_) =>
|
||||
{
|
||||
Assert.IsTrue(sut.IsReconfigurationLocked());
|
||||
Assert.IsFalse(sut.RequestReconfigurationLock());
|
||||
});
|
||||
|
||||
Assert.IsTrue(result.IsCompleted);
|
||||
|
||||
result = Parallel.For(1, 1000, (_) =>
|
||||
{
|
||||
sut.ReleaseReconfigurationLock();
|
||||
});
|
||||
|
||||
Assert.IsFalse(sut.IsReconfigurationLocked());
|
||||
Assert.IsTrue(result.IsCompleted);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RequestReconfigurationLock_MustOnlyAllowLockingOnce()
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
Assert.IsFalse(sut.IsReconfigurationLocked());
|
||||
|
||||
var result = Parallel.For(1, 1000, (_) =>
|
||||
{
|
||||
var acquired = sut.RequestReconfigurationLock();
|
||||
|
||||
if (acquired)
|
||||
{
|
||||
Interlocked.Increment(ref count);
|
||||
}
|
||||
});
|
||||
|
||||
Assert.AreEqual(1, count);
|
||||
Assert.IsTrue(sut.IsReconfigurationLocked());
|
||||
Assert.IsTrue(result.IsCompleted);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RequestSessionLock_MustOnlyAllowLockingOnce()
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
Assert.IsFalse(sut.IsSessionLocked());
|
||||
|
||||
var result = Parallel.For(1, 1000, (_) =>
|
||||
{
|
||||
var acquired = sut.RequestSessionLock();
|
||||
|
||||
if (acquired)
|
||||
{
|
||||
Interlocked.Increment(ref count);
|
||||
}
|
||||
});
|
||||
|
||||
Assert.AreEqual(1, count);
|
||||
Assert.IsTrue(sut.IsSessionLocked());
|
||||
Assert.IsTrue(result.IsCompleted);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SessionLock_MustWorkCorrectly()
|
||||
{
|
||||
Assert.IsFalse(sut.IsSessionLocked());
|
||||
|
||||
sut.RequestSessionLock();
|
||||
|
||||
var result = Parallel.For(1, 1000, (_) =>
|
||||
{
|
||||
Assert.IsTrue(sut.IsSessionLocked());
|
||||
Assert.IsFalse(sut.RequestSessionLock());
|
||||
});
|
||||
|
||||
Assert.IsTrue(result.IsCompleted);
|
||||
|
||||
result = Parallel.For(1, 1000, (_) =>
|
||||
{
|
||||
sut.ReleaseSessionLock();
|
||||
});
|
||||
|
||||
Assert.IsFalse(sut.IsSessionLocked());
|
||||
Assert.IsTrue(result.IsCompleted);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -166,6 +166,7 @@
|
|||
<Compile Include="Operations\SystemMonitorOperationTests.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ClientControllerTests.cs" />
|
||||
<Compile Include="CoordinatorTests.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config">
|
||||
|
|
|
@ -16,6 +16,7 @@ using System.Threading.Tasks;
|
|||
using SafeExamBrowser.Applications.Contracts;
|
||||
using SafeExamBrowser.Browser.Contracts;
|
||||
using SafeExamBrowser.Browser.Contracts.Events;
|
||||
using SafeExamBrowser.Client.Contracts;
|
||||
using SafeExamBrowser.Client.Operations.Events;
|
||||
using SafeExamBrowser.Communication.Contracts.Data;
|
||||
using SafeExamBrowser.Communication.Contracts.Events;
|
||||
|
@ -53,6 +54,7 @@ namespace SafeExamBrowser.Client
|
|||
private readonly IActionCenter actionCenter;
|
||||
private readonly IApplicationMonitor applicationMonitor;
|
||||
private readonly ClientContext context;
|
||||
private readonly ICoordinator coordinator;
|
||||
private readonly IDisplayMonitor displayMonitor;
|
||||
private readonly IExplorerShell explorerShell;
|
||||
private readonly IFileSystemDialog fileSystemDialog;
|
||||
|
@ -78,12 +80,12 @@ namespace SafeExamBrowser.Client
|
|||
private AppSettings Settings => context.Settings;
|
||||
|
||||
private ILockScreen lockScreen;
|
||||
private bool sessionLocked;
|
||||
|
||||
internal ClientController(
|
||||
IActionCenter actionCenter,
|
||||
IApplicationMonitor applicationMonitor,
|
||||
ClientContext context,
|
||||
ICoordinator coordinator,
|
||||
IDisplayMonitor displayMonitor,
|
||||
IExplorerShell explorerShell,
|
||||
IFileSystemDialog fileSystemDialog,
|
||||
|
@ -104,6 +106,7 @@ namespace SafeExamBrowser.Client
|
|||
this.actionCenter = actionCenter;
|
||||
this.applicationMonitor = applicationMonitor;
|
||||
this.context = context;
|
||||
this.coordinator = coordinator;
|
||||
this.displayMonitor = displayMonitor;
|
||||
this.explorerShell = explorerShell;
|
||||
this.fileSystemDialog = fileSystemDialog;
|
||||
|
@ -488,31 +491,11 @@ namespace SafeExamBrowser.Client
|
|||
|
||||
private void Browser_ConfigurationDownloadRequested(string fileName, DownloadEventArgs args)
|
||||
{
|
||||
var allow = false;
|
||||
var hasQuitPassword = !string.IsNullOrWhiteSpace(Settings.Security.QuitPasswordHash);
|
||||
var hasUrl = !string.IsNullOrWhiteSpace(Settings.Security.ReconfigurationUrl);
|
||||
args.AllowDownload = false;
|
||||
|
||||
if (hasQuitPassword)
|
||||
if (IsAllowedToReconfigure(args.Url))
|
||||
{
|
||||
if (hasUrl)
|
||||
{
|
||||
var expression = Regex.Escape(Settings.Security.ReconfigurationUrl).Replace(@"\*", ".*");
|
||||
var regex = new Regex($"^{expression}$", RegexOptions.IgnoreCase);
|
||||
var sebUrl = args.Url.Replace(Uri.UriSchemeHttps, context.AppConfig.SebUriSchemeSecure).Replace(Uri.UriSchemeHttp, context.AppConfig.SebUriScheme);
|
||||
|
||||
allow = Settings.Security.AllowReconfiguration && (regex.IsMatch(args.Url) || regex.IsMatch(sebUrl));
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("The active configuration does not contain a valid reconfiguration URL!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
allow = Settings.ConfigurationMode == ConfigurationMode.ConfigureClient || Settings.Security.AllowReconfiguration;
|
||||
}
|
||||
|
||||
if (allow)
|
||||
if (coordinator.RequestReconfigurationLock())
|
||||
{
|
||||
args.AllowDownload = true;
|
||||
args.Callback = Browser_ConfigurationDownloadFinished;
|
||||
|
@ -527,10 +510,43 @@ namespace SafeExamBrowser.Client
|
|||
}
|
||||
else
|
||||
{
|
||||
args.AllowDownload = false;
|
||||
logger.Info($"Denied download request for configuration file '{fileName}'.");
|
||||
logger.Warn($"A reconfiguration is already in progress, denied download request for configuration file '{fileName}'!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info($"Reconfiguration is not allowed, denied download request for configuration file '{fileName}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsAllowedToReconfigure(string url)
|
||||
{
|
||||
var allow = false;
|
||||
var hasQuitPassword = !string.IsNullOrWhiteSpace(Settings.Security.QuitPasswordHash);
|
||||
var hasUrl = !string.IsNullOrWhiteSpace(Settings.Security.ReconfigurationUrl);
|
||||
|
||||
if (hasQuitPassword)
|
||||
{
|
||||
if (hasUrl)
|
||||
{
|
||||
var expression = Regex.Escape(Settings.Security.ReconfigurationUrl).Replace(@"\*", ".*");
|
||||
var regex = new Regex($"^{expression}$", RegexOptions.IgnoreCase);
|
||||
var sebUrl = url.Replace(Uri.UriSchemeHttps, context.AppConfig.SebUriSchemeSecure).Replace(Uri.UriSchemeHttp, context.AppConfig.SebUriScheme);
|
||||
|
||||
allow = Settings.Security.AllowReconfiguration && (regex.IsMatch(url) || regex.IsMatch(sebUrl));
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("The active configuration does not contain a valid reconfiguration URL!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
allow = Settings.ConfigurationMode == ConfigurationMode.ConfigureClient || Settings.Security.AllowReconfiguration;
|
||||
}
|
||||
|
||||
return allow;
|
||||
}
|
||||
|
||||
private void Browser_ConfigurationDownloadFinished(bool success, string url, string filePath = null)
|
||||
{
|
||||
|
@ -547,15 +563,19 @@ namespace SafeExamBrowser.Client
|
|||
else
|
||||
{
|
||||
logger.Error($"Failed to communicate reconfiguration request for '{filePath}'!");
|
||||
|
||||
messageBox.Show(TextKey.MessageBox_ReconfigurationError, TextKey.MessageBox_ReconfigurationErrorTitle, icon: MessageBoxIcon.Error, parent: splashScreen);
|
||||
splashScreen.Hide();
|
||||
coordinator.ReleaseReconfigurationLock();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"Failed to download configuration file '{filePath}'!");
|
||||
|
||||
messageBox.Show(TextKey.MessageBox_ConfigurationDownloadError, TextKey.MessageBox_ConfigurationDownloadErrorTitle, icon: MessageBoxIcon.Error, parent: splashScreen);
|
||||
splashScreen.Hide();
|
||||
coordinator.ReleaseReconfigurationLock();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -643,6 +663,7 @@ namespace SafeExamBrowser.Client
|
|||
{
|
||||
logger.Info("The reconfiguration was aborted by the runtime.");
|
||||
splashScreen.Hide();
|
||||
coordinator.ReleaseReconfigurationLock();
|
||||
}
|
||||
|
||||
private void ClientHost_ReconfigurationDenied(ReconfigurationEventArgs args)
|
||||
|
@ -650,6 +671,7 @@ namespace SafeExamBrowser.Client
|
|||
logger.Info($"The reconfiguration request for '{args.ConfigurationPath}' was denied by the runtime!");
|
||||
messageBox.Show(TextKey.MessageBox_ReconfigurationDenied, TextKey.MessageBox_ReconfigurationDeniedTitle, parent: splashScreen);
|
||||
splashScreen.Hide();
|
||||
coordinator.ReleaseReconfigurationLock();
|
||||
}
|
||||
|
||||
private void ClientHost_ServerFailureActionRequested(ServerFailureActionRequestEventArgs args)
|
||||
|
@ -776,14 +798,13 @@ namespace SafeExamBrowser.Client
|
|||
{
|
||||
logger.Warn($@"The cursor registry value '{key}\{name}' has changed from '{oldValue}' to '{newValue}'! Attempting to show lock screen...");
|
||||
|
||||
if (!sessionLocked)
|
||||
if (coordinator.RequestSessionLock())
|
||||
{
|
||||
var message = text.Get(TextKey.LockScreen_CursorMessage);
|
||||
var title = text.Get(TextKey.LockScreen_Title);
|
||||
var continueOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_CursorContinueOption) };
|
||||
var terminateOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_CursorTerminateOption) };
|
||||
|
||||
sessionLocked = true;
|
||||
registry.StopMonitoring(key, name);
|
||||
|
||||
var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption });
|
||||
|
@ -798,7 +819,7 @@ namespace SafeExamBrowser.Client
|
|||
TryRequestShutdown();
|
||||
}
|
||||
|
||||
sessionLocked = false;
|
||||
coordinator.ReleaseSessionLock();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -810,14 +831,13 @@ namespace SafeExamBrowser.Client
|
|||
{
|
||||
logger.Warn($@"The ease of access registry value '{key}\{name}' has changed from '{oldValue}' to '{newValue}'! Attempting to show lock screen...");
|
||||
|
||||
if (!sessionLocked)
|
||||
if (coordinator.RequestSessionLock())
|
||||
{
|
||||
var message = text.Get(TextKey.LockScreen_EaseOfAccessMessage);
|
||||
var title = text.Get(TextKey.LockScreen_Title);
|
||||
var continueOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_EaseOfAccessContinueOption) };
|
||||
var terminateOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_EaseOfAccessTerminateOption) };
|
||||
|
||||
sessionLocked = true;
|
||||
registry.StopMonitoring(key, name);
|
||||
|
||||
var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption });
|
||||
|
@ -832,7 +852,7 @@ namespace SafeExamBrowser.Client
|
|||
TryRequestShutdown();
|
||||
}
|
||||
|
||||
sessionLocked = false;
|
||||
coordinator.ReleaseSessionLock();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -843,8 +863,8 @@ namespace SafeExamBrowser.Client
|
|||
private void Runtime_ConnectionLost()
|
||||
{
|
||||
logger.Error("Lost connection to the runtime!");
|
||||
messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error);
|
||||
|
||||
messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error);
|
||||
shutdown.Invoke();
|
||||
}
|
||||
|
||||
|
@ -858,11 +878,10 @@ namespace SafeExamBrowser.Client
|
|||
{
|
||||
logger.Info("Attempting to show lock screen as requested by the server...");
|
||||
|
||||
if (!sessionLocked)
|
||||
if (coordinator.RequestSessionLock())
|
||||
{
|
||||
sessionLocked = true;
|
||||
ShowLockScreen(message, text.Get(TextKey.LockScreen_Title), Enumerable.Empty<LockScreenOption>());
|
||||
sessionLocked = false;
|
||||
coordinator.ReleaseSessionLock();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -900,10 +919,8 @@ namespace SafeExamBrowser.Client
|
|||
{
|
||||
logger.Warn("Detected user session change!");
|
||||
|
||||
if (!sessionLocked)
|
||||
if (coordinator.RequestSessionLock())
|
||||
{
|
||||
sessionLocked = true;
|
||||
|
||||
var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption });
|
||||
|
||||
if (result.OptionId == terminateOption.Id)
|
||||
|
@ -912,7 +929,7 @@ namespace SafeExamBrowser.Client
|
|||
TryRequestShutdown();
|
||||
}
|
||||
|
||||
sessionLocked = false;
|
||||
coordinator.ReleaseSessionLock();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -114,6 +114,7 @@ namespace SafeExamBrowser.Client
|
|||
|
||||
var applicationFactory = new ApplicationFactory(applicationMonitor, ModuleLogger(nameof(ApplicationFactory)), nativeMethods, processFactory, new Registry(ModuleLogger(nameof(Registry))));
|
||||
var clipboard = new Clipboard(ModuleLogger(nameof(Clipboard)), nativeMethods);
|
||||
var coordinator = new Coordinator();
|
||||
var displayMonitor = new DisplayMonitor(ModuleLogger(nameof(DisplayMonitor)), nativeMethods, systemInfo);
|
||||
var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods);
|
||||
var fileSystemDialog = BuildFileSystemDialog();
|
||||
|
@ -148,6 +149,7 @@ namespace SafeExamBrowser.Client
|
|||
actionCenter,
|
||||
applicationMonitor,
|
||||
context,
|
||||
coordinator,
|
||||
displayMonitor,
|
||||
explorerShell,
|
||||
fileSystemDialog,
|
||||
|
|
46
SafeExamBrowser.Client/Contracts/ICoordinator.cs
Normal file
46
SafeExamBrowser.Client/Contracts/ICoordinator.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Client.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Coordinates concurrent operations of the client application.
|
||||
/// </summary>
|
||||
internal interface ICoordinator
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the reconfiguration lock is currently occupied.
|
||||
/// </summary>
|
||||
bool IsReconfigurationLocked();
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the session lock is currently occupied.
|
||||
/// </summary>
|
||||
bool IsSessionLocked();
|
||||
|
||||
/// <summary>
|
||||
/// Releases the reconfiguration lock.
|
||||
/// </summary>
|
||||
void ReleaseReconfigurationLock();
|
||||
|
||||
/// <summary>
|
||||
/// Releases the session lock.
|
||||
/// </summary>
|
||||
void ReleaseSessionLock();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to acquire the unique reconfiguration lock. Returns <c>true</c> if successful, otherwise <c>false</c>.
|
||||
/// </summary>
|
||||
bool RequestReconfigurationLock();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to acquire the unique session lock. Returns <c>true</c> if successful, otherwise <c>false</c>.
|
||||
/// </summary>
|
||||
bool RequestSessionLock();
|
||||
}
|
||||
}
|
78
SafeExamBrowser.Client/Coordinator.cs
Normal file
78
SafeExamBrowser.Client/Coordinator.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using SafeExamBrowser.Client.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Client
|
||||
{
|
||||
internal class Coordinator : ICoordinator
|
||||
{
|
||||
private readonly ConcurrentBag<Guid> reconfiguration;
|
||||
private readonly ConcurrentBag<Guid> session;
|
||||
|
||||
internal Coordinator()
|
||||
{
|
||||
reconfiguration = new ConcurrentBag<Guid>();
|
||||
session = new ConcurrentBag<Guid>();
|
||||
}
|
||||
|
||||
public bool IsReconfigurationLocked()
|
||||
{
|
||||
return !reconfiguration.IsEmpty;
|
||||
}
|
||||
|
||||
public bool IsSessionLocked()
|
||||
{
|
||||
return !session.IsEmpty;
|
||||
}
|
||||
|
||||
public void ReleaseReconfigurationLock()
|
||||
{
|
||||
reconfiguration.TryTake(out _);
|
||||
}
|
||||
|
||||
public void ReleaseSessionLock()
|
||||
{
|
||||
session.TryTake(out _);
|
||||
}
|
||||
|
||||
public bool RequestReconfigurationLock()
|
||||
{
|
||||
var acquired = false;
|
||||
|
||||
lock (reconfiguration)
|
||||
{
|
||||
if (reconfiguration.IsEmpty)
|
||||
{
|
||||
reconfiguration.Add(Guid.NewGuid());
|
||||
acquired = true;
|
||||
}
|
||||
}
|
||||
|
||||
return acquired;
|
||||
}
|
||||
|
||||
public bool RequestSessionLock()
|
||||
{
|
||||
var acquired = false;
|
||||
|
||||
lock (session)
|
||||
{
|
||||
if (session.IsEmpty)
|
||||
{
|
||||
session.Add(Guid.NewGuid());
|
||||
acquired = true;
|
||||
}
|
||||
}
|
||||
|
||||
return acquired;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,9 @@ using System.Windows;
|
|||
// 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)]
|
||||
|
||||
// Required for mocking internal contracts with Moq
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
[assembly: InternalsVisibleTo("SafeExamBrowser.Client.UnitTests")]
|
||||
|
||||
//In order to begin building localizable applications, set
|
||||
|
|
|
@ -74,6 +74,8 @@
|
|||
<Compile Include="App.cs" />
|
||||
<Compile Include="ClientContext.cs" />
|
||||
<Compile Include="ClientController.cs" />
|
||||
<Compile Include="Contracts\ICoordinator.cs" />
|
||||
<Compile Include="Coordinator.cs" />
|
||||
<Compile Include="Operations\ClientHostDisconnectionOperation.cs" />
|
||||
<Compile Include="Operations\ClientOperation.cs" />
|
||||
<Compile Include="Operations\ConfigurationOperation.cs" />
|
||||
|
|
Loading…
Reference in a new issue