Missing files + start working on offline patcher

This commit is contained in:
Vichingo455 2025-06-23 13:42:14 +02:00
parent 058d48196a
commit 2d36fecb45
70 changed files with 11475 additions and 12 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: ["https://web.satispay.com/app/match/link/user/S6Y-CON--88923C30-BEC8-487E-9814-68A5449F7D83?amount=500&currency=EUR"]

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.Browser.Events
{
internal class JavaScriptDialogRequestedEventArgs
{
internal bool Success { get; set; }
internal JavaScriptDialogType Type { get; set; }
}
}

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.Browser.Events
{
internal delegate void JavaScriptDialogRequestedEventHandler(JavaScriptDialogRequestedEventArgs args);
}

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.Browser.Events
{
internal enum JavaScriptDialogType
{
LeavePage,
Reload
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using CefSharp;
using CefSharp.Enums;
namespace SafeExamBrowser.Browser.Handlers
{
internal class DragHandler : IDragHandler
{
public bool OnDragEnter(IWebBrowser chromiumWebBrowser, IBrowser browser, IDragData dragData, DragOperationsMask mask)
{
return true;
}
public void OnDraggableRegionsChanged(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IList<DraggableRegion> regions)
{
}
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
namespace SafeExamBrowser.Browser.Handlers
{
internal class FocusHandler : IFocusHandler
{
internal FocusHandler()
{
}
public void OnGotFocus(IWebBrowser webBrowser, IBrowser browser)
{
}
public bool OnSetFocus(IWebBrowser webBrowser, IBrowser browser, CefFocusSource source)
{
return false;
}
public void OnTakeFocus(IWebBrowser webBrowser, IBrowser browser, bool next)
{
}
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Threading.Tasks;
using CefSharp;
using SafeExamBrowser.Browser.Events;
namespace SafeExamBrowser.Browser.Handlers
{
internal class JavaScriptDialogHandler : IJsDialogHandler
{
internal event JavaScriptDialogRequestedEventHandler DialogRequested;
public bool OnBeforeUnloadDialog(IWebBrowser webBrowser, IBrowser browser, string message, bool isReload, IJsDialogCallback callback)
{
var args = new JavaScriptDialogRequestedEventArgs
{
Type = isReload ? JavaScriptDialogType.Reload : JavaScriptDialogType.LeavePage
};
Task.Run(() =>
{
DialogRequested?.Invoke(args);
using (callback)
{
callback.Continue(args.Success);
}
});
return true;
}
public void OnDialogClosed(IWebBrowser webBrowser, IBrowser browser)
{
}
public bool OnJSDialog(IWebBrowser webBrowser, IBrowser browser, string originUrl, CefJsDialogType type, string message, string promptText, IJsDialogCallback callback, ref bool suppress)
{
return false;
}
public void OnResetDialogState(IWebBrowser webBrowser, IBrowser browser)
{
}
}
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
namespace SafeExamBrowser.Browser.Wrapper.Events
{
internal delegate void BeforeUnloadDialogEventHandler(IWebBrowser webBrowser, IBrowser browser, string message, bool isReload, IJsDialogCallback callback, GenericEventArgs args);
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
namespace SafeExamBrowser.Browser.Wrapper.Events
{
internal delegate void DialogClosedEventHandler(IWebBrowser webBrowser, IBrowser browser);
}

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
using CefSharp.Enums;
namespace SafeExamBrowser.Browser.Wrapper.Events
{
internal delegate void DragEnterEventHandler(IWebBrowser webBrowser, IBrowser browser, IDragData dragData, DragOperationsMask mask, GenericEventArgs args);
}

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using CefSharp;
namespace SafeExamBrowser.Browser.Wrapper.Events
{
internal delegate void DraggableRegionsChangedEventHandler(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IList<DraggableRegion> regions);
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
namespace SafeExamBrowser.Browser.Wrapper.Events
{
internal delegate void GotFocusEventHandler(IWebBrowser webBrowser, IBrowser browser);
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
namespace SafeExamBrowser.Browser.Wrapper.Events
{
internal delegate void JavaScriptDialogEventHandler(IWebBrowser webBrowser, IBrowser browser, string originUrl, CefJsDialogType type, string message, string promptText, IJsDialogCallback callback, ref bool suppress, GenericEventArgs args);
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
namespace SafeExamBrowser.Browser.Wrapper.Events
{
internal delegate void ResetDialogStateEventHandler(IWebBrowser webBrowser, IBrowser browser);
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
namespace SafeExamBrowser.Browser.Wrapper.Events
{
internal delegate void SetFocusEventHandler(IWebBrowser webBrowser, IBrowser browser, CefFocusSource source, GenericEventArgs args);
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
namespace SafeExamBrowser.Browser.Wrapper.Events
{
internal delegate void TakeFocusEventHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, bool next);
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using CefSharp;
using CefSharp.Enums;
using CefSharp.WinForms;
using CefSharp.WinForms.Host;
using SafeExamBrowser.Browser.Wrapper.Events;
namespace SafeExamBrowser.Browser.Wrapper.Handlers
{
internal class DragHandlerSwitch : IDragHandler
{
public bool OnDragEnter(IWebBrowser webBrowser, IBrowser browser, IDragData dragData, DragOperationsMask mask)
{
var args = new GenericEventArgs();
if (browser.IsPopup)
{
var control = ChromiumHostControl.FromBrowser(browser) as CefSharpPopupControl;
control?.OnDragEnter(webBrowser, browser, dragData, mask, args);
}
else
{
var control = ChromiumWebBrowser.FromBrowser(browser) as CefSharpBrowserControl;
control?.OnDragEnter(webBrowser, browser, dragData, mask, args);
}
return args.Value;
}
public void OnDraggableRegionsChanged(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IList<DraggableRegion> regions)
{
if (browser.IsPopup)
{
var control = ChromiumHostControl.FromBrowser(browser) as CefSharpPopupControl;
control?.OnDraggableRegionsChanged(webBrowser, browser, frame, regions);
}
else
{
var control = ChromiumWebBrowser.FromBrowser(browser) as CefSharpBrowserControl;
control?.OnDraggableRegionsChanged(webBrowser, browser, frame, regions);
}
}
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
using CefSharp.WinForms;
using CefSharp.WinForms.Host;
using SafeExamBrowser.Browser.Wrapper.Events;
namespace SafeExamBrowser.Browser.Wrapper.Handlers
{
internal class FocusHandlerSwitch : IFocusHandler
{
public void OnGotFocus(IWebBrowser webBrowser, IBrowser browser)
{
if (browser.IsPopup)
{
var control = ChromiumHostControl.FromBrowser(browser) as CefSharpPopupControl;
control?.OnGotFocus(webBrowser, browser);
}
else
{
var control = ChromiumWebBrowser.FromBrowser(browser) as CefSharpBrowserControl;
control?.OnGotFocus(webBrowser, browser);
}
}
public bool OnSetFocus(IWebBrowser webBrowser, IBrowser browser, CefFocusSource source)
{
var args = new GenericEventArgs { Value = false };
if (browser.IsPopup)
{
var control = ChromiumHostControl.FromBrowser(browser) as CefSharpPopupControl;
control?.OnSetFocus(webBrowser, browser, source, args);
}
else
{
var control = ChromiumWebBrowser.FromBrowser(browser) as CefSharpBrowserControl;
control?.OnSetFocus(webBrowser, browser, source, args);
}
return args.Value;
}
public void OnTakeFocus(IWebBrowser webBrowser, IBrowser browser, bool next)
{
if (browser.IsPopup)
{
var control = ChromiumHostControl.FromBrowser(browser) as CefSharpPopupControl;
control?.OnTakeFocus(webBrowser, browser, next);
}
else
{
var control = ChromiumWebBrowser.FromBrowser(browser) as CefSharpBrowserControl;
control?.OnTakeFocus(webBrowser, browser, next);
}
}
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using CefSharp;
using CefSharp.WinForms;
using CefSharp.WinForms.Host;
using SafeExamBrowser.Browser.Wrapper.Events;
namespace SafeExamBrowser.Browser.Wrapper.Handlers
{
internal class JavaScriptDialogHandlerSwitch : IJsDialogHandler
{
public bool OnBeforeUnloadDialog(IWebBrowser webBrowser, IBrowser browser, string message, bool isReload, IJsDialogCallback callback)
{
var args = new GenericEventArgs { Value = false };
if (browser.IsPopup)
{
var control = ChromiumHostControl.FromBrowser(browser) as CefSharpPopupControl;
control?.OnBeforeUnloadDialog(webBrowser, browser, message, isReload, callback, args);
}
else
{
var control = ChromiumWebBrowser.FromBrowser(browser) as CefSharpBrowserControl;
control?.OnBeforeUnloadDialog(webBrowser, browser, message, isReload, callback, args);
}
return args.Value;
}
public void OnDialogClosed(IWebBrowser webBrowser, IBrowser browser)
{
if (browser.IsPopup)
{
var control = ChromiumHostControl.FromBrowser(browser) as CefSharpPopupControl;
control?.OnDialogClosed(webBrowser, browser);
}
else
{
var control = ChromiumWebBrowser.FromBrowser(browser) as CefSharpBrowserControl;
control?.OnDialogClosed(webBrowser, browser);
}
}
public bool OnJSDialog(IWebBrowser webBrowser, IBrowser browser, string originUrl, CefJsDialogType type, string message, string promptText, IJsDialogCallback callback, ref bool suppress)
{
var args = new GenericEventArgs { Value = false };
if (browser.IsPopup)
{
var control = ChromiumHostControl.FromBrowser(browser) as CefSharpPopupControl;
control?.OnJavaScriptDialog(webBrowser, browser, originUrl, type, message, promptText, callback, ref suppress, args);
}
else
{
var control = ChromiumWebBrowser.FromBrowser(browser) as CefSharpBrowserControl;
control?.OnJavaScriptDialog(webBrowser, browser, originUrl, type, message, promptText, callback, ref suppress, args);
}
return args.Value;
}
public void OnResetDialogState(IWebBrowser webBrowser, IBrowser browser)
{
if (browser.IsPopup)
{
var control = ChromiumHostControl.FromBrowser(browser) as CefSharpPopupControl;
control?.OnResetDialogState(webBrowser, browser);
}
else
{
var control = ChromiumWebBrowser.FromBrowser(browser) as CefSharpBrowserControl;
control?.OnResetDialogState(webBrowser, browser);
}
}
}
}

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Client.Responsibilities;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Client.UnitTests.Responsibilities
{
[TestClass]
public class ApplicationResponsibilityTests
{
private ClientContext context;
private ApplicationsResponsibility sut;
[TestInitialize]
public void Initialize()
{
var logger = new Mock<ILogger>();
context = new ClientContext();
sut = new ApplicationsResponsibility(context, logger.Object);
}
[TestMethod]
public void MustAutoStartApplications()
{
var application1 = new Mock<IApplication<IApplicationWindow>>();
var application2 = new Mock<IApplication<IApplicationWindow>>();
var application3 = new Mock<IApplication<IApplicationWindow>>();
application1.SetupGet(a => a.AutoStart).Returns(true);
application2.SetupGet(a => a.AutoStart).Returns(false);
application3.SetupGet(a => a.AutoStart).Returns(true);
context.Applications.Add(application1.Object);
context.Applications.Add(application2.Object);
context.Applications.Add(application3.Object);
sut.Assume(ClientTask.AutoStartApplications);
application1.Verify(a => a.Start(), Times.Once);
application2.Verify(a => a.Start(), Times.Never);
application3.Verify(a => a.Start(), Times.Once);
}
}
}

View File

@ -0,0 +1,350 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Browser.Contracts.Events;
using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Client.Responsibilities;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.ResponsibilityModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.Settings;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Client.UnitTests.Responsibilities
{
[TestClass]
public class BrowserResponsibilityTests
{
private AppConfig appConfig;
private Mock<IBrowserApplication> browser;
private ClientContext context;
private Mock<ICoordinator> coordinator;
private Mock<IMessageBox> messageBox;
private Mock<IRuntimeProxy> runtime;
private Mock<IServerProxy> server;
private AppSettings settings;
private Mock<ISplashScreen> splashScreen;
private Mock<ITaskbar> taskbar;
private BrowserResponsibility sut;
[TestInitialize]
public void Initialize()
{
var logger = new Mock<ILogger>();
var responsibilities = new Mock<IResponsibilityCollection<ClientTask>>();
appConfig = new AppConfig();
browser = new Mock<IBrowserApplication>();
context = new ClientContext();
coordinator = new Mock<ICoordinator>();
messageBox = new Mock<IMessageBox>();
runtime = new Mock<IRuntimeProxy>();
server = new Mock<IServerProxy>();
settings = new AppSettings();
splashScreen = new Mock<ISplashScreen>();
taskbar = new Mock<ITaskbar>();
context.AppConfig = appConfig;
context.Browser = browser.Object;
context.Responsibilities = responsibilities.Object;
context.Runtime = runtime.Object;
context.Server = server.Object;
context.Settings = settings;
sut = new BrowserResponsibility(
context,
coordinator.Object,
logger.Object,
messageBox.Object,
runtime.Object,
splashScreen.Object,
taskbar.Object);
sut.Assume(ClientTask.RegisterEvents);
}
[TestMethod]
public void MustAutoStartBrowser()
{
settings.Browser.EnableBrowser = true;
browser.SetupGet(b => b.AutoStart).Returns(true);
sut.Assume(ClientTask.AutoStartApplications);
browser.Verify(b => b.Start(), Times.Once);
browser.Reset();
browser.SetupGet(b => b.AutoStart).Returns(false);
sut.Assume(ClientTask.AutoStartApplications);
browser.Verify(b => b.Start(), Times.Never);
}
[TestMethod]
public void MustNotAutoStartBrowserIfNotEnabled()
{
settings.Browser.EnableBrowser = false;
browser.SetupGet(b => b.AutoStart).Returns(true);
sut.Assume(ClientTask.AutoStartApplications);
browser.Verify(b => b.Start(), Times.Never);
}
[TestMethod]
public void Browser_MustHandleUserIdentifierDetection()
{
var counter = 0;
var identifier = "abc123";
settings.SessionMode = SessionMode.Server;
server.Setup(s => s.SendUserIdentifier(It.IsAny<string>())).Returns(() => new ServerResponse(++counter == 3));
browser.Raise(b => b.UserIdentifierDetected += null, identifier);
server.Verify(s => s.SendUserIdentifier(It.Is<string>(id => id == identifier)), Times.Exactly(3));
}
[TestMethod]
public void Browser_MustTerminateIfRequested()
{
runtime.Setup(p => p.RequestShutdown()).Returns(new CommunicationResult(true));
browser.Raise(b => b.TerminationRequested += null);
runtime.Verify(p => p.RequestShutdown(), Times.Once);
}
[TestMethod]
public void Reconfiguration_MustAllowIfNoQuitPasswordSet()
{
var args = new DownloadEventArgs();
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true);
runtime.Setup(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>())).Returns(new CommunicationResult(true));
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);
runtime.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
Assert.IsTrue(args.AllowDownload);
}
[TestMethod]
public void Reconfiguration_MustNotAllowWithQuitPasswordAndNoUrl()
{
var args = new DownloadEventArgs();
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
settings.Security.AllowReconfiguration = true;
settings.Security.QuitPasswordHash = "abc123";
runtime.Setup(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>())).Returns(new CommunicationResult(true));
browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args);
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Never);
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
runtime.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);
runtime.Setup(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>())).Returns(new CommunicationResult(true));
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);
runtime.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
Assert.IsFalse(args.AllowDownload);
}
[TestMethod]
public void Reconfiguration_MustAllowIfUrlMatches()
{
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";
runtime.Setup(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>())).Returns(new CommunicationResult(true));
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);
runtime.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
Assert.IsTrue(args.AllowDownload);
}
[TestMethod]
public void Reconfiguration_MustDenyIfNotAllowed()
{
var args = new DownloadEventArgs();
settings.Security.AllowReconfiguration = false;
settings.Security.QuitPasswordHash = "abc123";
browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args);
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Never);
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
runtime.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
Assert.IsFalse(args.AllowDownload);
}
[TestMethod]
public void Reconfiguration_MustDenyIfUrlDoesNotMatch()
{
var args = new DownloadEventArgs { Url = "sebs://www.somehost.org/some/path/some_configuration.seb?query=123" };
settings.Security.AllowReconfiguration = false;
settings.Security.QuitPasswordHash = "abc123";
settings.Security.ReconfigurationUrl = "sebs://www.somehost.org/some/path/other_configuration.seb?query=123";
browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args);
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Never);
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never);
runtime.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
Assert.IsFalse(args.AllowDownload);
}
[TestMethod]
public void Reconfiguration_MustCorrectlyHandleDownload()
{
var downloadPath = @"C:\Folder\Does\Not\Exist\filepath.seb";
var downloadUrl = @"https://www.host.abc/someresource.seb";
var filename = "filepath.seb";
var args = new DownloadEventArgs();
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true);
messageBox.Setup(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.IsAny<IWindow>())).Returns(MessageBoxResult.Yes);
runtime.Setup(r => r.RequestReconfiguration(
It.Is<string>(p => p == downloadPath),
It.Is<string>(u => u == downloadUrl))).Returns(new CommunicationResult(true));
settings.Security.AllowReconfiguration = true;
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);
runtime.Verify(r => r.RequestReconfiguration(It.Is<string>(p => p == downloadPath), It.Is<string>(u => u == downloadUrl)), Times.Once);
Assert.AreEqual(downloadPath, args.DownloadPath);
Assert.IsTrue(args.AllowDownload);
}
[TestMethod]
public void Reconfiguration_MustCorrectlyHandleFailedDownload()
{
var downloadPath = @"C:\Folder\Does\Not\Exist\filepath.seb";
var downloadUrl = @"https://www.host.abc/someresource.seb";
var filename = "filepath.seb";
var args = new DownloadEventArgs();
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true);
messageBox.Setup(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.IsAny<IWindow>())).Returns(MessageBoxResult.Yes);
runtime.Setup(r => r.RequestReconfiguration(
It.Is<string>(p => p == downloadPath),
It.Is<string>(u => u == downloadUrl))).Returns(new CommunicationResult(true));
settings.Security.AllowReconfiguration = true;
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);
runtime.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[TestMethod]
public void Reconfiguration_MustCorrectlyHandleFailedRequest()
{
var downloadPath = @"C:\Folder\Does\Not\Exist\filepath.seb";
var downloadUrl = @"https://www.host.abc/someresource.seb";
var filename = "filepath.seb";
var args = new DownloadEventArgs();
appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist";
coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true);
messageBox.Setup(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.IsAny<IWindow>())).Returns(MessageBoxResult.Yes);
runtime.Setup(r => r.RequestReconfiguration(
It.Is<string>(p => p == downloadPath),
It.Is<string>(u => u == downloadUrl))).Returns(new CommunicationResult(false));
settings.Security.AllowReconfiguration = true;
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.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);
runtime.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
[TestMethod]
public void MustNotFailIfDependencyIsNull()
{
context.Browser = null;
sut.Assume(ClientTask.DeregisterEvents);
}
}
}

View File

@ -0,0 +1,212 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Client.Responsibilities;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
namespace SafeExamBrowser.Client.UnitTests.Responsibilities
{
[TestClass]
public class CommunicationResponsibilityTests
{
private Mock<IClientHost> clientHost;
private ClientContext context;
private Mock<ICoordinator> coordinator;
private Mock<IMessageBox> messageBox;
private Mock<IRuntimeProxy> runtimeProxy;
private Mock<Action> shutdown;
private Mock<ISplashScreen> splashScreen;
private Mock<IText> text;
private Mock<IUserInterfaceFactory> uiFactory;
private CommunicationResponsibility sut;
[TestInitialize]
public void Initialize()
{
var logger = new Mock<ILogger>();
clientHost = new Mock<IClientHost>();
context = new ClientContext();
coordinator = new Mock<ICoordinator>();
messageBox = new Mock<IMessageBox>();
runtimeProxy = new Mock<IRuntimeProxy>();
shutdown = new Mock<Action>();
splashScreen = new Mock<ISplashScreen>();
text = new Mock<IText>();
uiFactory = new Mock<IUserInterfaceFactory>();
context.ClientHost = clientHost.Object;
sut = new CommunicationResponsibility(
context,
coordinator.Object,
logger.Object,
messageBox.Object,
runtimeProxy.Object,
shutdown.Object,
splashScreen.Object,
text.Object,
uiFactory.Object);
sut.Assume(ClientTask.RegisterEvents);
}
[TestMethod]
public void Communication_MustCorrectlyHandleExamSelection()
{
var args = new ExamSelectionRequestEventArgs
{
Exams = new List<(string id, string lms, string name, string url)> { ("", "", "", "") },
RequestId = Guid.NewGuid()
};
var dialog = new Mock<IExamSelectionDialog>();
dialog.Setup(d => d.Show(It.IsAny<IWindow>())).Returns(new ExamSelectionDialogResult { Success = true });
uiFactory.Setup(f => f.CreateExamSelectionDialog(It.IsAny<IEnumerable<Exam>>())).Returns(dialog.Object);
clientHost.Raise(c => c.ExamSelectionRequested += null, args);
runtimeProxy.Verify(p => p.SubmitExamSelectionResult(It.Is<Guid>(g => g == args.RequestId), true, null), Times.Once);
uiFactory.Verify(f => f.CreateExamSelectionDialog(It.IsAny<IEnumerable<Exam>>()), Times.Once);
}
[TestMethod]
public void Communication_MustCorrectlyHandleMessageBoxRequest()
{
var args = new MessageBoxRequestEventArgs
{
Action = (int) MessageBoxAction.YesNo,
Icon = (int) MessageBoxIcon.Question,
Message = "Some question to be answered",
RequestId = Guid.NewGuid(),
Title = "A Title"
};
messageBox.Setup(m => m.Show(
It.Is<string>(s => s == args.Message),
It.Is<string>(s => s == args.Title),
It.Is<MessageBoxAction>(a => a == (MessageBoxAction) args.Action),
It.Is<MessageBoxIcon>(i => i == (MessageBoxIcon) args.Icon),
It.IsAny<IWindow>())).Returns(MessageBoxResult.No);
clientHost.Raise(c => c.MessageBoxRequested += null, args);
runtimeProxy.Verify(p => p.SubmitMessageBoxResult(
It.Is<Guid>(g => g == args.RequestId),
It.Is<int>(r => r == (int) MessageBoxResult.No)), Times.Once);
}
[TestMethod]
public void Communication_MustCorrectlyHandlePasswordRequest()
{
var args = new PasswordRequestEventArgs
{
Purpose = PasswordRequestPurpose.LocalSettings,
RequestId = Guid.NewGuid()
};
var dialog = new Mock<IPasswordDialog>();
var result = new PasswordDialogResult { Password = "blubb", Success = true };
dialog.Setup(d => d.Show(It.IsAny<IWindow>())).Returns(result);
uiFactory.Setup(f => f.CreatePasswordDialog(It.IsAny<string>(), It.IsAny<string>())).Returns(dialog.Object);
clientHost.Raise(c => c.PasswordRequested += null, args);
runtimeProxy.Verify(p => p.SubmitPassword(
It.Is<Guid>(g => g == args.RequestId),
It.Is<bool>(b => b == result.Success),
It.Is<string>(s => s == result.Password)), Times.Once);
}
[TestMethod]
public void Communication_MustCorrectlyHandleAbortedReconfiguration()
{
clientHost.Raise(c => c.ReconfigurationAborted += null);
splashScreen.Verify(s => s.Hide(), Times.AtLeastOnce);
}
[TestMethod]
public void Communication_MustInformUserAboutDeniedReconfiguration()
{
var args = new ReconfigurationEventArgs
{
ConfigurationPath = @"C:\Some\File\Path.seb"
};
clientHost.Raise(c => c.ReconfigurationDenied += null, args);
messageBox.Verify(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.IsAny<IWindow>()), Times.Once);
}
[TestMethod]
public void Communication_MustCorrectlyHandleServerCommunicationFailure()
{
var args = new ServerFailureActionRequestEventArgs { RequestId = Guid.NewGuid() };
var dialog = new Mock<IServerFailureDialog>();
dialog.Setup(d => d.Show(It.IsAny<IWindow>())).Returns(new ServerFailureDialogResult());
uiFactory.Setup(f => f.CreateServerFailureDialog(It.IsAny<string>(), It.IsAny<bool>())).Returns(dialog.Object);
clientHost.Raise(c => c.ServerFailureActionRequested += null, args);
runtimeProxy.Verify(r => r.SubmitServerFailureActionResult(It.Is<Guid>(g => g == args.RequestId), It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Once);
uiFactory.Verify(f => f.CreateServerFailureDialog(It.IsAny<string>(), It.IsAny<bool>()), Times.Once);
}
[TestMethod]
public void Communication_MustCorrectlyInitiateShutdown()
{
clientHost.Raise(c => c.Shutdown += null);
shutdown.Verify(s => s(), Times.Once);
}
[TestMethod]
public void Communication_MustShutdownOnLostConnection()
{
runtimeProxy.Raise(p => p.ConnectionLost += null);
messageBox.Verify(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.IsAny<IWindow>()), Times.Once);
shutdown.Verify(s => s(), Times.Once);
}
[TestMethod]
public void MustNotFailIfDependencyIsNull()
{
context.ClientHost = null;
sut.Assume(ClientTask.DeregisterEvents);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Client.Responsibilities;
using SafeExamBrowser.Configuration.Contracts.Integrity;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Client.UnitTests.Responsibilities
{
[TestClass]
public class IntegrityResponsibilityTests
{
private Mock<IText> text;
private Mock<IIntegrityModule> integrityModule;
[TestInitialize]
public void Initialize()
{
var context = new ClientContext();
var logger = new Mock<ILogger>();
var valid = true;
text = new Mock<IText>();
integrityModule = new Mock<IIntegrityModule>();
integrityModule.Setup(m => m.TryVerifySessionIntegrity(It.IsAny<string>(), It.IsAny<string>(), out valid)).Returns(true);
var sut = new IntegrityResponsibility(context, logger.Object, text.Object);
}
}
}

View File

@ -0,0 +1,414 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Client.Responsibilities;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.Core.Contracts.ResponsibilityModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Monitoring.Contracts.Display;
using SafeExamBrowser.Monitoring.Contracts.System;
using SafeExamBrowser.Settings;
using SafeExamBrowser.Settings.Monitoring;
using SafeExamBrowser.Settings.UserInterface;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
using SafeExamBrowser.WindowsApi.Contracts;
using IWindow = SafeExamBrowser.UserInterface.Contracts.Windows.IWindow;
namespace SafeExamBrowser.Client.UnitTests.Responsibilities
{
[TestClass]
public class MonitoringResponsibilityTests
{
private Mock<IActionCenter> actionCenter;
private Mock<IApplicationMonitor> applicationMonitor;
private ClientContext context;
private Mock<ICoordinator> coordinator;
private Mock<IDisplayMonitor> displayMonitor;
private Mock<IExplorerShell> explorerShell;
private Mock<IHashAlgorithm> hashAlgorithm;
private Mock<IMessageBox> messageBox;
private Mock<IRuntimeProxy> runtime;
private Mock<ISystemSentinel> sentinel;
private AppSettings settings;
private Mock<ITaskbar> taskbar;
private Mock<IText> text;
private Mock<IUserInterfaceFactory> uiFactory;
private MonitoringResponsibility sut;
[TestInitialize]
public void Initialize()
{
var logger = new Mock<ILogger>();
var responsibilities = new Mock<IResponsibilityCollection<ClientTask>>();
actionCenter = new Mock<IActionCenter>();
applicationMonitor = new Mock<IApplicationMonitor>();
context = new ClientContext();
coordinator = new Mock<ICoordinator>();
displayMonitor = new Mock<IDisplayMonitor>();
explorerShell = new Mock<IExplorerShell>();
hashAlgorithm = new Mock<IHashAlgorithm>();
messageBox = new Mock<IMessageBox>();
runtime = new Mock<IRuntimeProxy>();
sentinel = new Mock<ISystemSentinel>();
settings = new AppSettings();
taskbar = new Mock<ITaskbar>();
text = new Mock<IText>();
uiFactory = new Mock<IUserInterfaceFactory>();
context.HashAlgorithm = hashAlgorithm.Object;
context.MessageBox = messageBox.Object;
context.Responsibilities = responsibilities.Object;
context.Runtime = runtime.Object;
context.Settings = settings;
context.UserInterfaceFactory = uiFactory.Object;
sut = new MonitoringResponsibility(
actionCenter.Object,
applicationMonitor.Object,
context,
coordinator.Object,
displayMonitor.Object,
explorerShell.Object,
logger.Object,
sentinel.Object,
taskbar.Object,
text.Object);
sut.Assume(ClientTask.RegisterEvents);
sut.Assume(ClientTask.StartMonitoring);
}
[TestMethod]
public void ApplicationMonitor_MustCorrectlyHandleExplorerStartWithTaskbar()
{
var boundsActionCenter = 0;
var boundsTaskbar = 0;
var height = 30;
var order = 0;
var shell = 0;
var workingArea = 0;
settings.UserInterface.Taskbar.EnableTaskbar = true;
actionCenter.Setup(a => a.InitializeBounds()).Callback(() => boundsActionCenter = ++order);
explorerShell.Setup(e => e.Terminate()).Callback(() => shell = ++order);
displayMonitor.Setup(w => w.InitializePrimaryDisplay(It.Is<int>(h => h == height))).Callback(() => workingArea = ++order);
taskbar.Setup(t => t.GetAbsoluteHeight()).Returns(height);
taskbar.Setup(t => t.InitializeBounds()).Callback(() => boundsTaskbar = ++order);
applicationMonitor.Raise(a => a.ExplorerStarted += null);
actionCenter.Verify(a => a.InitializeBounds(), Times.Once);
explorerShell.Verify(e => e.Terminate(), Times.Once);
displayMonitor.Verify(d => d.InitializePrimaryDisplay(It.Is<int>(h => h == 0)), Times.Never);
displayMonitor.Verify(d => d.InitializePrimaryDisplay(It.Is<int>(h => h == height)), Times.Once);
taskbar.Verify(t => t.InitializeBounds(), Times.Once);
taskbar.Verify(t => t.GetAbsoluteHeight(), Times.Once);
Assert.IsTrue(shell == 1);
Assert.IsTrue(workingArea == 2);
Assert.IsTrue(boundsActionCenter == 3);
Assert.IsTrue(boundsTaskbar == 4);
}
[TestMethod]
public void ApplicationMonitor_MustCorrectlyHandleExplorerStartWithoutTaskbar()
{
var boundsActionCenter = 0;
var boundsTaskbar = 0;
var height = 30;
var order = 0;
var shell = 0;
var workingArea = 0;
settings.UserInterface.Taskbar.EnableTaskbar = false;
actionCenter.Setup(a => a.InitializeBounds()).Callback(() => boundsActionCenter = ++order);
explorerShell.Setup(e => e.Terminate()).Callback(() => shell = ++order);
displayMonitor.Setup(w => w.InitializePrimaryDisplay(It.Is<int>(h => h == 0))).Callback(() => workingArea = ++order);
taskbar.Setup(t => t.GetAbsoluteHeight()).Returns(height);
taskbar.Setup(t => t.InitializeBounds()).Callback(() => boundsTaskbar = ++order);
applicationMonitor.Raise(a => a.ExplorerStarted += null);
actionCenter.Verify(a => a.InitializeBounds(), Times.Once);
explorerShell.Verify(e => e.Terminate(), Times.Once);
displayMonitor.Verify(d => d.InitializePrimaryDisplay(It.Is<int>(h => h == 0)), Times.Once);
displayMonitor.Verify(d => d.InitializePrimaryDisplay(It.Is<int>(h => h == height)), Times.Never);
taskbar.Verify(t => t.InitializeBounds(), Times.Once);
taskbar.Verify(t => t.GetAbsoluteHeight(), Times.Never);
Assert.IsTrue(shell == 1);
Assert.IsTrue(workingArea == 2);
Assert.IsTrue(boundsActionCenter == 3);
Assert.IsTrue(boundsTaskbar == 4);
}
[TestMethod]
public void ApplicationMonitor_MustPermitApplicationIfChosenByUserAfterFailedTermination()
{
var lockScreen = new Mock<ILockScreen>();
var result = new LockScreenResult();
lockScreen.Setup(l => l.WaitForResult()).Returns(result);
runtime.Setup(p => p.RequestShutdown()).Returns(new CommunicationResult(true));
uiFactory
.Setup(f => f.CreateLockScreen(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
.Returns(lockScreen.Object)
.Callback<string, string, IEnumerable<LockScreenOption>, LockScreenSettings>((m, t, o, s) => result.OptionId = o.First().Id);
applicationMonitor.Raise(m => m.TerminationFailed += null, new List<RunningApplication>());
runtime.Verify(p => p.RequestShutdown(), Times.Never);
}
[TestMethod]
public void ApplicationMonitor_MustRequestShutdownIfChosenByUserAfterFailedTermination()
{
var lockScreen = new Mock<ILockScreen>();
var result = new LockScreenResult();
lockScreen.Setup(l => l.WaitForResult()).Returns(result);
runtime.Setup(p => p.RequestShutdown()).Returns(new CommunicationResult(true));
uiFactory
.Setup(f => f.CreateLockScreen(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
.Returns(lockScreen.Object)
.Callback<string, string, IEnumerable<LockScreenOption>, LockScreenSettings>((m, t, o, s) => result.OptionId = o.Last().Id);
applicationMonitor.Raise(m => m.TerminationFailed += null, new List<RunningApplication>());
runtime.Verify(p => p.RequestShutdown(), Times.Once);
}
[TestMethod]
public void ApplicationMonitor_MustShowLockScreenIfTerminationFailed()
{
var activator1 = new Mock<IActivator>();
var activator2 = new Mock<IActivator>();
var activator3 = new Mock<IActivator>();
var lockScreen = new Mock<ILockScreen>();
var result = new LockScreenResult();
var order = 0;
var pause = 0;
var show = 0;
var wait = 0;
var close = 0;
var resume = 0;
activator1.Setup(a => a.Pause()).Callback(() => pause = ++order);
activator1.Setup(a => a.Resume()).Callback(() => resume = ++order);
context.Activators.Add(activator1.Object);
context.Activators.Add(activator2.Object);
context.Activators.Add(activator3.Object);
lockScreen.Setup(l => l.Show()).Callback(() => show = ++order);
lockScreen.Setup(l => l.WaitForResult()).Callback(() => wait = ++order).Returns(result);
lockScreen.Setup(l => l.Close()).Callback(() => close = ++order);
uiFactory
.Setup(f => f.CreateLockScreen(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
.Returns(lockScreen.Object);
applicationMonitor.Raise(m => m.TerminationFailed += null, new List<RunningApplication>());
activator1.Verify(a => a.Pause(), Times.Once);
activator1.Verify(a => a.Resume(), Times.Once);
activator2.Verify(a => a.Pause(), Times.Once);
activator2.Verify(a => a.Resume(), Times.Once);
activator3.Verify(a => a.Pause(), Times.Once);
activator3.Verify(a => a.Resume(), Times.Once);
lockScreen.Verify(l => l.Show(), Times.Once);
lockScreen.Verify(l => l.WaitForResult(), Times.Once);
lockScreen.Verify(l => l.Close(), Times.Once);
Assert.IsTrue(pause == 1);
Assert.IsTrue(show == 2);
Assert.IsTrue(wait == 3);
Assert.IsTrue(close == 4);
Assert.IsTrue(resume == 5);
}
[TestMethod]
public void ApplicationMonitor_MustValidateQuitPasswordIfTerminationFailed()
{
var hash = "12345";
var lockScreen = new Mock<ILockScreen>();
var result = new LockScreenResult { Password = "test" };
var attempt = 0;
var correct = new Random().Next(1, 50);
var lockScreenResult = new Func<LockScreenResult>(() => ++attempt == correct ? result : new LockScreenResult());
context.Settings.Security.QuitPasswordHash = hash;
hashAlgorithm.Setup(a => a.GenerateHashFor(It.Is<string>(p => p == result.Password))).Returns(hash);
lockScreen.Setup(l => l.WaitForResult()).Returns(lockScreenResult);
uiFactory
.Setup(f => f.CreateLockScreen(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
.Returns(lockScreen.Object);
applicationMonitor.Raise(m => m.TerminationFailed += null, new List<RunningApplication>());
hashAlgorithm.Verify(a => a.GenerateHashFor(It.Is<string>(p => p == result.Password)), Times.Once);
hashAlgorithm.Verify(a => a.GenerateHashFor(It.Is<string>(p => p != result.Password)), Times.Exactly(attempt - 1));
messageBox.Verify(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.Is<IWindow>(w => w == lockScreen.Object)), Times.Exactly(attempt - 1));
}
[TestMethod]
public void DisplayMonitor_MustCorrectlyHandleDisplayChangeWithTaskbar()
{
var boundsActionCenter = 0;
var boundsTaskbar = 0;
var height = 25;
var order = 0;
var workingArea = 0;
settings.UserInterface.Taskbar.EnableTaskbar = true;
actionCenter.Setup(t => t.InitializeBounds()).Callback(() => boundsActionCenter = ++order);
displayMonitor.Setup(m => m.InitializePrimaryDisplay(It.Is<int>(h => h == height))).Callback(() => workingArea = ++order);
displayMonitor.Setup(m => m.ValidateConfiguration(It.IsAny<DisplaySettings>())).Returns(new ValidationResult { IsAllowed = true });
taskbar.Setup(t => t.GetAbsoluteHeight()).Returns(height);
taskbar.Setup(t => t.InitializeBounds()).Callback(() => boundsTaskbar = ++order);
displayMonitor.Raise(d => d.DisplayChanged += null);
actionCenter.Verify(a => a.InitializeBounds(), Times.Once);
displayMonitor.Verify(d => d.InitializePrimaryDisplay(It.Is<int>(h => h == 0)), Times.Never);
displayMonitor.Verify(d => d.InitializePrimaryDisplay(It.Is<int>(h => h == height)), Times.Once);
taskbar.Verify(t => t.GetAbsoluteHeight(), Times.Once);
taskbar.Verify(t => t.InitializeBounds(), Times.Once);
Assert.IsTrue(workingArea == 1);
Assert.IsTrue(boundsActionCenter == 2);
Assert.IsTrue(boundsTaskbar == 3);
}
[TestMethod]
public void DisplayMonitor_MustCorrectlyHandleDisplayChangeWithoutTaskbar()
{
var boundsActionCenter = 0;
var boundsTaskbar = 0;
var height = 25;
var order = 0;
var workingArea = 0;
settings.UserInterface.Taskbar.EnableTaskbar = false;
actionCenter.Setup(t => t.InitializeBounds()).Callback(() => boundsActionCenter = ++order);
displayMonitor.Setup(w => w.InitializePrimaryDisplay(It.Is<int>(h => h == 0))).Callback(() => workingArea = ++order);
displayMonitor.Setup(m => m.ValidateConfiguration(It.IsAny<DisplaySettings>())).Returns(new ValidationResult { IsAllowed = true });
taskbar.Setup(t => t.GetAbsoluteHeight()).Returns(height);
taskbar.Setup(t => t.InitializeBounds()).Callback(() => boundsTaskbar = ++order);
displayMonitor.Raise(d => d.DisplayChanged += null);
actionCenter.Verify(a => a.InitializeBounds(), Times.Once);
displayMonitor.Verify(d => d.InitializePrimaryDisplay(It.Is<int>(h => h == 0)), Times.Once);
displayMonitor.Verify(d => d.InitializePrimaryDisplay(It.Is<int>(h => h == height)), Times.Never);
taskbar.Verify(t => t.GetAbsoluteHeight(), Times.Never);
taskbar.Verify(t => t.InitializeBounds(), Times.Once);
Assert.IsTrue(workingArea == 1);
Assert.IsTrue(boundsActionCenter == 2);
Assert.IsTrue(boundsTaskbar == 3);
}
[TestMethod]
public void DisplayMonitor_MustShowLockScreenOnDisplayChange()
{
var lockScreen = new Mock<ILockScreen>();
displayMonitor.Setup(m => m.ValidateConfiguration(It.IsAny<DisplaySettings>())).Returns(new ValidationResult { IsAllowed = false });
lockScreen.Setup(l => l.WaitForResult()).Returns(new LockScreenResult());
uiFactory
.Setup(f => f.CreateLockScreen(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
.Returns(lockScreen.Object);
displayMonitor.Raise(d => d.DisplayChanged += null);
lockScreen.Verify(l => l.Show(), Times.Once);
}
[TestMethod]
public void SystemMonitor_MustShowLockScreenOnSessionSwitch()
{
var lockScreen = new Mock<ILockScreen>();
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);
sentinel.Raise(s => s.SessionChanged += null);
coordinator.Verify(c => c.RequestSessionLock(), Times.Once);
coordinator.Verify(c => c.ReleaseSessionLock(), Times.Once);
lockScreen.Verify(l => l.Show(), Times.Once);
}
[TestMethod]
public void SystemMonitor_MustTerminateIfRequestedByUser()
{
var lockScreen = new Mock<ILockScreen>();
var result = new LockScreenResult();
coordinator.Setup(c => c.RequestSessionLock()).Returns(true);
lockScreen.Setup(l => l.WaitForResult()).Returns(result);
runtime.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))
.Returns(lockScreen.Object);
sentinel.Raise(s => s.SessionChanged += null);
coordinator.Verify(c => c.RequestSessionLock(), Times.Once);
coordinator.Verify(c => c.ReleaseSessionLock(), Times.Once);
lockScreen.Verify(l => l.Show(), Times.Once);
runtime.Verify(p => p.RequestShutdown(), Times.Once);
}
[TestMethod]
public void SystemMonitor_MustDoNothingIfSessionSwitchAllowed()
{
var lockScreen = new Mock<ILockScreen>();
settings.Service.IgnoreService = false;
settings.Service.DisableUserLock = false;
settings.Service.DisableUserSwitch = false;
lockScreen.Setup(l => l.WaitForResult()).Returns(new LockScreenResult());
uiFactory
.Setup(f => f.CreateLockScreen(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
.Returns(lockScreen.Object);
sentinel.Raise(s => s.SessionChanged += null);
lockScreen.Verify(l => l.Show(), Times.Never);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Client.Responsibilities;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Network;
using SafeExamBrowser.UserInterface.Contracts;
namespace SafeExamBrowser.Client.UnitTests.Responsibilities
{
[TestClass]
public class NetworkResponsibilityTests
{
private Mock<INetworkAdapter> networkAdapter;
private Mock<IText> text;
private Mock<IUserInterfaceFactory> uiFactory;
[TestInitialize]
public void Initialize()
{
var context = new ClientContext();
var logger = new Mock<ILogger>();
networkAdapter = new Mock<INetworkAdapter>();
text = new Mock<IText>();
uiFactory = new Mock<IUserInterfaceFactory>();
var sut = new NetworkResponsibility(context, logger.Object, networkAdapter.Object, text.Object, uiFactory.Object);
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Client.Responsibilities;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts;
namespace SafeExamBrowser.Client.UnitTests.Responsibilities
{
[TestClass]
public class ProctoringResponsibilityTests
{
private ClientContext context;
private Mock<IUserInterfaceFactory> uiFactory;
private ProctoringResponsibility sut;
[TestInitialize]
public void Initialize()
{
var logger = new Mock<ILogger>();
context = new ClientContext();
logger = new Mock<ILogger>();
uiFactory = new Mock<IUserInterfaceFactory>();
sut = new ProctoringResponsibility(context, logger.Object, uiFactory.Object);
}
[TestMethod]
public void MustNotFailIfDependencyIsNull()
{
context.Proctoring = null;
sut.Assume(ClientTask.PrepareShutdown_Wave1);
}
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Client.Responsibilities;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Core.Contracts.ResponsibilityModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Server.Contracts;
namespace SafeExamBrowser.Client.UnitTests.Responsibilities
{
[TestClass]
public class ServerResponsibilityTests
{
private ClientContext context;
private Mock<ICoordinator> coordinator;
private Mock<IRuntimeProxy> runtime;
private Mock<IServerProxy> server;
private Mock<IText> text;
private ServerResponsibility sut;
[TestInitialize]
public void Initialize()
{
var logger = new Mock<ILogger>();
var responsibilities = new Mock<IResponsibilityCollection<ClientTask>>();
context = new ClientContext();
coordinator = new Mock<ICoordinator>();
runtime = new Mock<IRuntimeProxy>();
server = new Mock<IServerProxy>();
text = new Mock<IText>();
context.Responsibilities = responsibilities.Object;
context.Runtime = runtime.Object;
context.Server = server.Object;
sut = new ServerResponsibility(context, coordinator.Object, logger.Object, text.Object);
sut.Assume(ClientTask.RegisterEvents);
}
[TestMethod]
public void Server_MustInitiateShutdownOnEvent()
{
runtime.Setup(r => r.RequestShutdown()).Returns(new CommunicationResult(true));
server.Raise(s => s.TerminationRequested += null);
runtime.Verify(p => p.RequestShutdown(), Times.Once);
}
[TestMethod]
public void MustNotFailIfDependencyIsNull()
{
context.Server = null;
sut.Assume(ClientTask.DeregisterEvents);
}
}
}

View File

@ -0,0 +1,284 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Client.Responsibilities;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.Core.Contracts.ResponsibilityModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
namespace SafeExamBrowser.Client.UnitTests.Responsibilities
{
[TestClass]
public class ShellResponsibilityTests
{
private Mock<IActionCenter> actionCenter;
private ClientContext context;
private Mock<IHashAlgorithm> hashAlgorithm;
private Mock<IMessageBox> messageBox;
private Mock<IRuntimeProxy> runtime;
private AppSettings settings;
private Mock<ITaskbar> taskbar;
private Mock<IUserInterfaceFactory> uiFactory;
private ShellResponsibility sut;
[TestInitialize]
public void Initialize()
{
var logger = new Mock<ILogger>();
var responsibilities = new Mock<IResponsibilityCollection<ClientTask>>();
actionCenter = new Mock<IActionCenter>();
context = new ClientContext();
hashAlgorithm = new Mock<IHashAlgorithm>();
messageBox = new Mock<IMessageBox>();
runtime = new Mock<IRuntimeProxy>();
settings = new AppSettings();
taskbar = new Mock<ITaskbar>();
uiFactory = new Mock<IUserInterfaceFactory>();
context.MessageBox = messageBox.Object;
context.Responsibilities = responsibilities.Object;
context.Runtime = runtime.Object;
context.Settings = settings;
sut = new ShellResponsibility(
actionCenter.Object,
context,
hashAlgorithm.Object,
logger.Object,
messageBox.Object,
taskbar.Object,
uiFactory.Object);
sut.Assume(ClientTask.RegisterEvents);
}
[TestMethod]
public void Shutdown_MustAskUserToConfirm()
{
var args = new System.ComponentModel.CancelEventArgs();
messageBox.Setup(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.IsAny<IWindow>())).Returns(MessageBoxResult.Yes);
runtime.Setup(r => r.RequestShutdown()).Returns(new CommunicationResult(true));
taskbar.Raise(t => t.QuitButtonClicked += null, args as object);
runtime.Verify(p => p.RequestShutdown(), Times.Once);
Assert.IsFalse(args.Cancel);
}
[TestMethod]
public void Shutdown_MustNotInitiateIfNotWishedByUser()
{
var args = new System.ComponentModel.CancelEventArgs();
messageBox.Setup(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.IsAny<IWindow>())).Returns(MessageBoxResult.No);
taskbar.Raise(t => t.QuitButtonClicked += null, args as object);
runtime.Verify(p => p.RequestShutdown(), Times.Never);
Assert.IsTrue(args.Cancel);
}
[TestMethod]
public void Shutdown_MustCloseActionCenterAndTaskbarIfEnabled()
{
settings.UserInterface.ActionCenter.EnableActionCenter = true;
settings.UserInterface.Taskbar.EnableTaskbar = true;
sut.Assume(ClientTask.CloseShell);
actionCenter.Verify(a => a.Close(), Times.Once);
taskbar.Verify(o => o.Close(), Times.Once);
}
[TestMethod]
public void Shutdown_MustNotCloseActionCenterAndTaskbarIfNotEnabled()
{
settings.UserInterface.ActionCenter.EnableActionCenter = false;
settings.UserInterface.Taskbar.EnableTaskbar = false;
sut.Assume(ClientTask.CloseShell);
actionCenter.Verify(a => a.Close(), Times.Never);
taskbar.Verify(o => o.Close(), Times.Never);
}
[TestMethod]
public void Shutdown_MustShowErrorMessageOnCommunicationFailure()
{
var args = new System.ComponentModel.CancelEventArgs();
messageBox.Setup(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.IsAny<IWindow>())).Returns(MessageBoxResult.Yes);
runtime.Setup(r => r.RequestShutdown()).Returns(new CommunicationResult(false));
taskbar.Raise(t => t.QuitButtonClicked += null, args as object);
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);
runtime.Verify(p => p.RequestShutdown(), Times.Once);
}
[TestMethod]
public void Shutdown_MustAskUserForQuitPassword()
{
var args = new System.ComponentModel.CancelEventArgs();
var dialog = new Mock<IPasswordDialog>();
var dialogResult = new PasswordDialogResult { Password = "blobb", Success = true };
settings.Security.QuitPasswordHash = "1234";
dialog.Setup(d => d.Show(It.IsAny<IWindow>())).Returns(dialogResult);
hashAlgorithm.Setup(h => h.GenerateHashFor(It.Is<string>(s => s == dialogResult.Password))).Returns(settings.Security.QuitPasswordHash);
runtime.Setup(r => r.RequestShutdown()).Returns(new CommunicationResult(true));
uiFactory.Setup(u => u.CreatePasswordDialog(It.IsAny<TextKey>(), It.IsAny<TextKey>())).Returns(dialog.Object);
taskbar.Raise(t => t.QuitButtonClicked += null, args as object);
uiFactory.Verify(u => u.CreatePasswordDialog(It.IsAny<TextKey>(), It.IsAny<TextKey>()), Times.Once);
runtime.Verify(p => p.RequestShutdown(), Times.Once);
Assert.IsFalse(args.Cancel);
}
[TestMethod]
public void Shutdown_MustAbortAskingUserForQuitPassword()
{
var args = new System.ComponentModel.CancelEventArgs();
var dialog = new Mock<IPasswordDialog>();
var dialogResult = new PasswordDialogResult { Success = false };
settings.Security.QuitPasswordHash = "1234";
dialog.Setup(d => d.Show(It.IsAny<IWindow>())).Returns(dialogResult);
runtime.Setup(r => r.RequestShutdown()).Returns(new CommunicationResult(true));
uiFactory.Setup(u => u.CreatePasswordDialog(It.IsAny<TextKey>(), It.IsAny<TextKey>())).Returns(dialog.Object);
taskbar.Raise(t => t.QuitButtonClicked += null, args as object);
uiFactory.Verify(u => u.CreatePasswordDialog(It.IsAny<TextKey>(), It.IsAny<TextKey>()), Times.Once);
runtime.Verify(p => p.RequestShutdown(), Times.Never);
Assert.IsTrue(args.Cancel);
}
[TestMethod]
public void Shutdown_MustNotInitiateIfQuitPasswordIncorrect()
{
var args = new System.ComponentModel.CancelEventArgs();
var dialog = new Mock<IPasswordDialog>();
var dialogResult = new PasswordDialogResult { Password = "blobb", Success = true };
settings.Security.QuitPasswordHash = "1234";
dialog.Setup(d => d.Show(It.IsAny<IWindow>())).Returns(dialogResult);
hashAlgorithm.Setup(h => h.GenerateHashFor(It.IsAny<string>())).Returns("9876");
uiFactory.Setup(u => u.CreatePasswordDialog(It.IsAny<TextKey>(), It.IsAny<TextKey>())).Returns(dialog.Object);
taskbar.Raise(t => t.QuitButtonClicked += null, args as object);
messageBox.Verify(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.Is<MessageBoxIcon>(i => i == MessageBoxIcon.Warning),
It.IsAny<IWindow>()), Times.Once);
uiFactory.Verify(u => u.CreatePasswordDialog(It.IsAny<TextKey>(), It.IsAny<TextKey>()), Times.Once);
runtime.Verify(p => p.RequestShutdown(), Times.Never);
Assert.IsTrue(args.Cancel);
}
[TestMethod]
public void Startup_MustCorrectlyShowTaskbar()
{
settings.UserInterface.Taskbar.EnableTaskbar = true;
sut.Assume(ClientTask.ShowShell);
taskbar.Verify(t => t.Show(), Times.Once);
taskbar.Reset();
settings.UserInterface.Taskbar.EnableTaskbar = false;
sut.Assume(ClientTask.ShowShell);
taskbar.Verify(t => t.Show(), Times.Never);
}
[TestMethod]
public void Startup_MustCorrectlyShowActionCenter()
{
settings.UserInterface.ActionCenter.EnableActionCenter = true;
sut.Assume(ClientTask.ShowShell);
actionCenter.Verify(t => t.Promote(), Times.Once);
actionCenter.Verify(t => t.Show(), Times.Never);
actionCenter.Reset();
settings.UserInterface.ActionCenter.EnableActionCenter = false;
sut.Assume(ClientTask.ShowShell);
actionCenter.Verify(t => t.Promote(), Times.Never);
actionCenter.Verify(t => t.Show(), Times.Never);
}
[TestMethod]
public void TerminationActivator_MustCorrectlyInitiateShutdown()
{
var order = 0;
var pause = 0;
var resume = 0;
var terminationActivator = new Mock<ITerminationActivator>();
context.Activators.Add(terminationActivator.Object);
messageBox.Setup(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.IsAny<MessageBoxIcon>(),
It.IsAny<IWindow>())).Returns(MessageBoxResult.Yes);
runtime.Setup(r => r.RequestShutdown()).Returns(new CommunicationResult(true));
terminationActivator.Setup(t => t.Pause()).Callback(() => pause = ++order);
terminationActivator.Setup(t => t.Resume()).Callback(() => resume = ++order);
sut.Assume(ClientTask.RegisterEvents);
terminationActivator.Raise(t => t.Activated += null);
Assert.AreEqual(1, pause);
Assert.AreEqual(2, resume);
terminationActivator.Verify(t => t.Pause(), Times.Once);
terminationActivator.Verify(t => t.Resume(), Times.Once);
runtime.Verify(p => p.RequestShutdown(), Times.Once);
}
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Core.OperationModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Client.Operations
{
internal class ClientOperationSequence : OperationSequence<IOperation>
{
private readonly ISplashScreen splashScreen;
public ClientOperationSequence(ILogger logger, IEnumerable<IOperation> operations, ISplashScreen splashScreen) : base(logger, operations)
{
this.splashScreen = splashScreen;
ProgressChanged += Operations_ProgressChanged;
StatusChanged += Operations_StatusChanged;
}
private void Operations_ProgressChanged(ProgressChangedEventArgs args)
{
if (args.CurrentValue.HasValue)
{
splashScreen.SetValue(args.CurrentValue.Value);
}
if (args.IsIndeterminate == true)
{
splashScreen.SetIndeterminate();
}
if (args.MaxValue.HasValue)
{
splashScreen.SetMaxValue(args.MaxValue.Value);
}
if (args.Progress == true)
{
splashScreen.Progress();
}
if (args.Regress == true)
{
splashScreen.Regress();
}
}
private void Operations_StatusChanged(TextKey status)
{
splashScreen.UpdateStatus(status, true);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Client.Responsibilities
{
internal class ApplicationsResponsibility : ClientResponsibility
{
public ApplicationsResponsibility(ClientContext context, ILogger logger) : base(context, logger)
{
}
public override void Assume(ClientTask task)
{
if (task == ClientTask.AutoStartApplications)
{
AutoStart();
}
}
private void AutoStart()
{
foreach (var application in Context.Applications)
{
if (application.AutoStart)
{
Logger.Info($"Auto-starting '{application.Name}'...");
application.Start();
}
}
}
}
}

View File

@ -0,0 +1,212 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Browser.Contracts.Events;
using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Client.Responsibilities
{
internal class BrowserResponsibility : ClientResponsibility
{
private readonly ICoordinator coordinator;
private readonly IMessageBox messageBox;
private readonly IRuntimeProxy runtime;
private readonly ISplashScreen splashScreen;
private readonly ITaskbar taskbar;
private IBrowserApplication Browser => Context.Browser;
public BrowserResponsibility(
ClientContext context,
ICoordinator coordinator,
ILogger logger,
IMessageBox messageBox,
IRuntimeProxy runtime,
ISplashScreen splashScreen,
ITaskbar taskbar) : base(context, logger)
{
this.coordinator = coordinator;
this.messageBox = messageBox;
this.runtime = runtime;
this.splashScreen = splashScreen;
this.taskbar = taskbar;
}
public override void Assume(ClientTask task)
{
switch (task)
{
case ClientTask.AutoStartApplications:
AutoStartBrowser();
break;
case ClientTask.DeregisterEvents:
DeregisterEvents();
break;
case ClientTask.RegisterEvents:
RegisterEvents();
break;
}
}
private void AutoStartBrowser()
{
if (Settings.Browser.EnableBrowser && Browser.AutoStart)
{
Logger.Info("Auto-starting browser...");
Browser.Start();
}
}
private void DeregisterEvents()
{
if (Browser != default)
{
Browser.ConfigurationDownloadRequested -= Browser_ConfigurationDownloadRequested;
Browser.LoseFocusRequested -= Browser_LoseFocusRequested;
Browser.TerminationRequested -= Browser_TerminationRequested;
Browser.UserIdentifierDetected -= Browser_UserIdentifierDetected;
}
}
private void RegisterEvents()
{
Browser.ConfigurationDownloadRequested += Browser_ConfigurationDownloadRequested;
Browser.LoseFocusRequested += Browser_LoseFocusRequested;
Browser.TerminationRequested += Browser_TerminationRequested;
Browser.UserIdentifierDetected += Browser_UserIdentifierDetected;
}
private void Browser_ConfigurationDownloadRequested(string fileName, DownloadEventArgs args)
{
args.AllowDownload = false;
if (IsAllowedToReconfigure(args.Url))
{
if (coordinator.RequestReconfigurationLock())
{
args.AllowDownload = true;
args.Callback = Browser_ConfigurationDownloadFinished;
args.DownloadPath = Path.Combine(Context.AppConfig.TemporaryDirectory, fileName);
splashScreen.Show();
splashScreen.BringToForeground();
splashScreen.SetIndeterminate();
splashScreen.UpdateStatus(TextKey.OperationStatus_InitializeSession, true);
Logger.Info($"Allowed download request for configuration file '{fileName}'.");
}
else
{
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)
{
if (success)
{
PrepareShutdown();
var communication = runtime.RequestReconfiguration(filePath, url);
if (communication.Success)
{
Logger.Info($"Sent reconfiguration request for '{filePath}' to the runtime.");
}
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();
}
}
private void Browser_LoseFocusRequested(bool forward)
{
taskbar.Focus(forward);
}
private void Browser_UserIdentifierDetected(string identifier)
{
if (Settings.SessionMode == SessionMode.Server)
{
var response = Context.Server.SendUserIdentifier(identifier);
while (!response.Success)
{
Logger.Error($"Failed to communicate user identifier with server! {response.Message}");
Thread.Sleep(Settings.Server.RequestAttemptInterval);
response = Context.Server.SendUserIdentifier(identifier);
}
}
}
private void Browser_TerminationRequested()
{
Logger.Info("Attempting to shutdown as requested by the browser...");
TryRequestShutdown();
}
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.ResponsibilityModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
namespace SafeExamBrowser.Client.Responsibilities
{
internal abstract class ClientResponsibility : IResponsibility<ClientTask>
{
protected ClientContext Context { get; private set; }
protected ILogger Logger { get; private set; }
protected AppSettings Settings => Context.Settings;
internal ClientResponsibility(ClientContext context, ILogger logger)
{
Context = context;
Logger = logger;
}
public abstract void Assume(ClientTask task);
protected void PauseActivators()
{
foreach (var activator in Context.Activators)
{
activator.Pause();
}
}
protected void PrepareShutdown()
{
Context.Responsibilities.Delegate(ClientTask.PrepareShutdown_Wave1);
Context.Responsibilities.Delegate(ClientTask.PrepareShutdown_Wave2);
}
protected void ResumeActivators()
{
foreach (var activator in Context.Activators)
{
activator.Resume();
}
}
protected LockScreenResult ShowLockScreen(string message, string title, IEnumerable<LockScreenOption> options)
{
var hasQuitPassword = !string.IsNullOrEmpty(Settings.Security.QuitPasswordHash);
var result = default(LockScreenResult);
Context.LockScreen = Context.UserInterfaceFactory.CreateLockScreen(message, title, options, Settings.UserInterface.LockScreen);
Logger.Info("Showing lock screen...");
PauseActivators();
Context.LockScreen.Show();
if (Settings.SessionMode == SessionMode.Server)
{
var response = Context.Server.LockScreen(message);
if (!response.Success)
{
Logger.Error($"Failed to send lock screen notification to server! Message: {response.Message}.");
}
}
for (var unlocked = false; !unlocked;)
{
result = Context.LockScreen.WaitForResult();
if (result.Canceled)
{
Logger.Info("The lock screen has been automaticaly canceled.");
unlocked = true;
}
else if (hasQuitPassword)
{
var passwordHash = Context.HashAlgorithm.GenerateHashFor(result.Password);
var isCorrect = Settings.Security.QuitPasswordHash.Equals(passwordHash, StringComparison.OrdinalIgnoreCase);
if (isCorrect)
{
Logger.Info("The user entered the correct unlock password.");
unlocked = true;
}
else
{
Logger.Info("The user entered the wrong unlock password.");
Context.MessageBox.Show(TextKey.MessageBox_InvalidUnlockPassword, TextKey.MessageBox_InvalidUnlockPasswordTitle, icon: MessageBoxIcon.Warning, parent: Context.LockScreen);
}
}
else
{
Logger.Warn($"No unlock password is defined, allowing user to resume session!");
unlocked = true;
}
}
Context.LockScreen.Close();
ResumeActivators();
Logger.Info("Closed lock screen.");
if (Settings.SessionMode == SessionMode.Server)
{
var response = Context.Server.ConfirmLockScreen();
if (!response.Success)
{
Logger.Error($"Failed to send lock screen confirm notification to server! Message: {response.Message}.");
}
}
return result;
}
protected bool TryRequestShutdown()
{
PrepareShutdown();
var communication = Context.Runtime.RequestShutdown();
if (!communication.Success)
{
Logger.Error("Failed to communicate shutdown request to the runtime!");
Context.MessageBox.Show(TextKey.MessageBox_QuitError, TextKey.MessageBox_QuitErrorTitle, icon: MessageBoxIcon.Error);
}
return communication.Success;
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.Client.Responsibilities
{
/// <summary>
/// Defines all tasks assumed by the responsibilities of the client application.
/// </summary>
internal enum ClientTask
{
/// <summary>
/// Auto-start the browser and any potential third-party applications.
/// </summary>
AutoStartApplications,
/// <summary>
/// Close the shell.
/// </summary>
CloseShell,
/// <summary>
/// Deregister all event handlers during application termination.
/// </summary>
DeregisterEvents,
/// <summary>
/// Execute wave 1 of the application shutdown preparation. It should be used for potentially long-running operations which require all
/// other (security) functionalities to continue working normally.
/// </summary>
PrepareShutdown_Wave1,
/// <summary>
/// Execute wave 2 of the application shutdown preparation. It should be used by all remaining responsibilities which must continue to work
/// normally during the execution of wave 1.
/// </summary>
PrepareShutdown_Wave2,
/// <summary>
/// Register all event handlers during application initialization.
/// </summary>
RegisterEvents,
/// <summary>
/// Schedule the verification of the application integrity.
/// </summary>
ScheduleIntegrityVerification,
/// <summary>
/// Show the shell.
/// </summary>
ShowShell,
/// <summary>
/// Start the monitoring of different (security) aspects.
/// </summary>
StartMonitoring,
/// <summary>
/// Update the session integrity during application termination.
/// </summary>
UpdateSessionIntegrity,
/// <summary>
/// Verify the session integrity during application initialization.
/// </summary>
VerifySessionIntegrity
}
}

View File

@ -0,0 +1,192 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Linq;
using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Client.Responsibilities
{
internal class CommunicationResponsibility : ClientResponsibility
{
private readonly ICoordinator coordinator;
private readonly IMessageBox messageBox;
private readonly IRuntimeProxy runtime;
private readonly Action shutdown;
private readonly ISplashScreen splashScreen;
private readonly IText text;
private readonly IUserInterfaceFactory uiFactory;
private IClientHost ClientHost => Context.ClientHost;
public CommunicationResponsibility(
ClientContext context,
ICoordinator coordinator,
ILogger logger,
IMessageBox messageBox,
IRuntimeProxy runtime,
Action shutdown,
ISplashScreen splashScreen,
IText text,
IUserInterfaceFactory uiFactory) : base(context, logger)
{
this.coordinator = coordinator;
this.messageBox = messageBox;
this.runtime = runtime;
this.shutdown = shutdown;
this.splashScreen = splashScreen;
this.text = text;
this.uiFactory = uiFactory;
}
public override void Assume(ClientTask task)
{
switch (task)
{
case ClientTask.DeregisterEvents:
DeregisterEvents();
break;
case ClientTask.RegisterEvents:
RegisterEvents();
break;
}
}
private void DeregisterEvents()
{
if (ClientHost != default)
{
ClientHost.ExamSelectionRequested -= ClientHost_ExamSelectionRequested;
ClientHost.MessageBoxRequested -= ClientHost_MessageBoxRequested;
ClientHost.PasswordRequested -= ClientHost_PasswordRequested;
ClientHost.ReconfigurationAborted -= ClientHost_ReconfigurationAborted;
ClientHost.ReconfigurationDenied -= ClientHost_ReconfigurationDenied;
ClientHost.ServerFailureActionRequested -= ClientHost_ServerFailureActionRequested;
ClientHost.Shutdown -= ClientHost_Shutdown;
}
runtime.ConnectionLost -= Runtime_ConnectionLost;
}
private void RegisterEvents()
{
ClientHost.ExamSelectionRequested += ClientHost_ExamSelectionRequested;
ClientHost.MessageBoxRequested += ClientHost_MessageBoxRequested;
ClientHost.PasswordRequested += ClientHost_PasswordRequested;
ClientHost.ReconfigurationAborted += ClientHost_ReconfigurationAborted;
ClientHost.ReconfigurationDenied += ClientHost_ReconfigurationDenied;
ClientHost.ServerFailureActionRequested += ClientHost_ServerFailureActionRequested;
ClientHost.Shutdown += ClientHost_Shutdown;
runtime.ConnectionLost += Runtime_ConnectionLost;
}
private void ClientHost_ExamSelectionRequested(ExamSelectionRequestEventArgs args)
{
Logger.Info($"Received exam selection request with id '{args.RequestId}'.");
var exams = args.Exams.Select(e => new Exam { Id = e.id, LmsName = e.lms, Name = e.name, Url = e.url });
var dialog = uiFactory.CreateExamSelectionDialog(exams);
var result = dialog.Show();
runtime.SubmitExamSelectionResult(args.RequestId, result.Success, result.SelectedExam?.Id);
Logger.Info($"Exam selection request with id '{args.RequestId}' is complete.");
}
private void ClientHost_MessageBoxRequested(MessageBoxRequestEventArgs args)
{
Logger.Info($"Received message box request with id '{args.RequestId}'.");
var action = (MessageBoxAction) args.Action;
var icon = (MessageBoxIcon) args.Icon;
var result = messageBox.Show(args.Message, args.Title, action, icon, parent: splashScreen);
runtime.SubmitMessageBoxResult(args.RequestId, (int) result);
Logger.Info($"Message box request with id '{args.RequestId}' yielded result '{result}'.");
}
private void ClientHost_PasswordRequested(PasswordRequestEventArgs args)
{
var message = default(TextKey);
var title = default(TextKey);
Logger.Info($"Received input request with id '{args.RequestId}' for the {args.Purpose.ToString().ToLower()} password.");
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();
runtime.SubmitPassword(args.RequestId, result.Success, result.Password);
Logger.Info($"Password request with id '{args.RequestId}' was {(result.Success ? "successful" : "aborted by the user")}.");
}
private void ClientHost_ReconfigurationAborted()
{
Logger.Info("The reconfiguration was aborted by the runtime.");
splashScreen.Hide();
coordinator.ReleaseReconfigurationLock();
}
private void ClientHost_ReconfigurationDenied(ReconfigurationEventArgs args)
{
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)
{
Logger.Info($"Received server failure action request with id '{args.RequestId}'.");
var dialog = uiFactory.CreateServerFailureDialog(args.Message, args.ShowFallback);
var result = dialog.Show();
runtime.SubmitServerFailureActionResult(args.RequestId, result.Abort, result.Fallback, result.Retry);
Logger.Info($"Server failure action request with id '{args.RequestId}' is complete, the user chose to {(result.Abort ? "abort" : (result.Fallback ? "fallback" : "retry"))}.");
}
private void ClientHost_Shutdown()
{
shutdown.Invoke();
}
private void Runtime_ConnectionLost()
{
Logger.Error("Lost connection to the runtime!");
messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error);
shutdown.Invoke();
}
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Linq;
using System.Threading.Tasks;
using SafeExamBrowser.Configuration.Contracts.Integrity;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
namespace SafeExamBrowser.Client.Responsibilities
{
internal class IntegrityResponsibility : ClientResponsibility
{
private readonly IText text;
private IIntegrityModule IntegrityModule => Context.IntegrityModule;
public IntegrityResponsibility(ClientContext context, ILogger logger, IText text) : base(context, logger)
{
this.text = text;
}
public override void Assume(ClientTask task)
{
switch (task)
{
case ClientTask.ScheduleIntegrityVerification:
ScheduleIntegrityVerification();
break;
case ClientTask.UpdateSessionIntegrity:
UpdateSessionIntegrity();
break;
case ClientTask.VerifySessionIntegrity:
VerifySessionIntegrity();
break;
}
}
private void ScheduleIntegrityVerification()
{
const int FIVE_MINUTES = 300000;
const int TEN_MINUTES = 600000;
var timer = new System.Timers.Timer();
timer.AutoReset = false;
timer.Elapsed += (o, args) => VerifyApplicationIntegrity();
timer.Interval = TEN_MINUTES + (new Random().NextDouble() * FIVE_MINUTES);
timer.Start();
}
private void UpdateSessionIntegrity()
{
var hasQuitPassword = !string.IsNullOrEmpty(Settings?.Security.QuitPasswordHash);
if (hasQuitPassword)
{
IntegrityModule?.ClearSession(Settings.Browser.ConfigurationKey, Settings.Browser.StartUrl);
}
}
private void VerifyApplicationIntegrity()
{
Logger.Info($"Attempting to verify application integrity...");
if (IntegrityModule.TryVerifyCodeSignature(out var isValid))
{
if (isValid)
{
Logger.Info("Application integrity successfully verified.");
}
else
{
Logger.Warn("Application integrity is compromised!");
ShowLockScreen(text.Get(TextKey.LockScreen_ApplicationIntegrityMessage), text.Get(TextKey.LockScreen_Title), Enumerable.Empty<LockScreenOption>());
}
}
else
{
Logger.Warn("Failed to verify application integrity!");
}
}
private void VerifySessionIntegrity()
{
var hasQuitPassword = !string.IsNullOrEmpty(Settings.Security.QuitPasswordHash);
if (hasQuitPassword && Settings.Security.VerifySessionIntegrity)
{
Logger.Info($"Attempting to verify session integrity...");
if (IntegrityModule.TryVerifySessionIntegrity(Settings.Browser.ConfigurationKey, Settings.Browser.StartUrl, out var isValid))
{
if (isValid)
{
Logger.Info("Session integrity successfully verified.");
IntegrityModule.CacheSession(Settings.Browser.ConfigurationKey, Settings.Browser.StartUrl);
}
else
{
Logger.Warn("Session integrity is compromised!");
Task.Delay(1000).ContinueWith(_ =>
{
ShowLockScreen(text.Get(TextKey.LockScreen_SessionIntegrityMessage), text.Get(TextKey.LockScreen_Title), Enumerable.Empty<LockScreenOption>());
});
}
}
else
{
Logger.Warn("Failed to verify session integrity!");
}
}
}
}
}

View File

@ -0,0 +1,327 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Monitoring.Contracts.Display;
using SafeExamBrowser.Monitoring.Contracts.System;
using SafeExamBrowser.Monitoring.Contracts.System.Events;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Client.Responsibilities
{
internal class MonitoringResponsibility : ClientResponsibility
{
private readonly IActionCenter actionCenter;
private readonly IApplicationMonitor applicationMonitor;
private readonly ICoordinator coordinator;
private readonly IDisplayMonitor displayMonitor;
private readonly IExplorerShell explorerShell;
private readonly ISystemSentinel sentinel;
private readonly ITaskbar taskbar;
private readonly IText text;
public MonitoringResponsibility(
IActionCenter actionCenter,
IApplicationMonitor applicationMonitor,
ClientContext context,
ICoordinator coordinator,
IDisplayMonitor displayMonitor,
IExplorerShell explorerShell,
ILogger logger,
ISystemSentinel sentinel,
ITaskbar taskbar,
IText text) : base(context, logger)
{
this.actionCenter = actionCenter;
this.applicationMonitor = applicationMonitor;
this.coordinator = coordinator;
this.displayMonitor = displayMonitor;
this.explorerShell = explorerShell;
this.sentinel = sentinel;
this.taskbar = taskbar;
this.text = text;
}
public override void Assume(ClientTask task)
{
switch (task)
{
case ClientTask.DeregisterEvents:
DeregisterEvents();
break;
case ClientTask.PrepareShutdown_Wave2:
StopMonitoring();
break;
case ClientTask.RegisterEvents:
RegisterEvents();
break;
case ClientTask.StartMonitoring:
StartMonitoring();
break;
}
}
private void DeregisterEvents()
{
applicationMonitor.ExplorerStarted -= ApplicationMonitor_ExplorerStarted;
applicationMonitor.TerminationFailed -= ApplicationMonitor_TerminationFailed;
displayMonitor.DisplayChanged -= DisplayMonitor_DisplaySettingsChanged;
sentinel.CursorChanged -= Sentinel_CursorChanged;
sentinel.EaseOfAccessChanged -= Sentinel_EaseOfAccessChanged;
sentinel.SessionChanged -= Sentinel_SessionChanged;
sentinel.StickyKeysChanged -= Sentinel_StickyKeysChanged;
}
private void StopMonitoring()
{
sentinel.StopMonitoring();
}
private void RegisterEvents()
{
applicationMonitor.ExplorerStarted += ApplicationMonitor_ExplorerStarted;
applicationMonitor.TerminationFailed += ApplicationMonitor_TerminationFailed;
displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged;
sentinel.CursorChanged += Sentinel_CursorChanged;
sentinel.EaseOfAccessChanged += Sentinel_EaseOfAccessChanged;
sentinel.SessionChanged += Sentinel_SessionChanged;
sentinel.StickyKeysChanged += Sentinel_StickyKeysChanged;
}
private void StartMonitoring()
{
sentinel.StartMonitoringSystemEvents();
if (!Settings.Security.AllowStickyKeys)
{
sentinel.StartMonitoringStickyKeys();
}
if (Settings.Security.VerifyCursorConfiguration)
{
sentinel.StartMonitoringCursors();
}
if (Settings.Service.IgnoreService)
{
sentinel.StartMonitoringEaseOfAccess();
}
}
private void ApplicationMonitor_ExplorerStarted()
{
Logger.Info("Trying to terminate Windows explorer...");
explorerShell.Terminate();
Logger.Info("Re-initializing working area...");
displayMonitor.InitializePrimaryDisplay(Settings.UserInterface.Taskbar.EnableTaskbar ? taskbar.GetAbsoluteHeight() : 0);
Logger.Info("Re-initializing shell...");
actionCenter.InitializeBounds();
taskbar.InitializeBounds();
Logger.Info("Desktop successfully restored.");
}
private void ApplicationMonitor_TerminationFailed(IEnumerable<RunningApplication> applications)
{
var applicationList = string.Join(Environment.NewLine, applications.Select(a => $"- {a.Name}"));
var message = $"{text.Get(TextKey.LockScreen_ApplicationsMessage)}{Environment.NewLine}{Environment.NewLine}{applicationList}";
var title = text.Get(TextKey.LockScreen_Title);
var allowOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_ApplicationsAllowOption) };
var terminateOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_ApplicationsTerminateOption) };
Logger.Warn("Detected termination failure of blacklisted application(s)!");
var result = ShowLockScreen(message, title, new[] { allowOption, terminateOption });
if (result.OptionId == allowOption.Id)
{
Logger.Info($"The blacklisted application(s) {string.Join(", ", applications.Select(a => $"'{a.Name}'"))} will be temporarily allowed.");
}
else if (result.OptionId == terminateOption.Id)
{
Logger.Info("Attempting to shutdown as requested by the user...");
TryRequestShutdown();
}
}
private void DisplayMonitor_DisplaySettingsChanged()
{
Logger.Info("Re-initializing working area...");
displayMonitor.InitializePrimaryDisplay(Settings.UserInterface.Taskbar.EnableTaskbar ? taskbar.GetAbsoluteHeight() : 0);
Logger.Info("Re-initializing shell...");
actionCenter.InitializeBounds();
Context.LockScreen?.InitializeBounds();
taskbar.InitializeBounds();
Logger.Info("Desktop successfully restored.");
if (!displayMonitor.ValidateConfiguration(Settings.Display).IsAllowed)
{
var continueOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_DisplayConfigurationContinueOption) };
var terminateOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_DisplayConfigurationTerminateOption) };
var message = text.Get(TextKey.LockScreen_DisplayConfigurationMessage);
var title = text.Get(TextKey.LockScreen_Title);
var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption });
if (result.OptionId == terminateOption.Id)
{
Logger.Info("Attempting to shutdown as requested by the user...");
TryRequestShutdown();
}
}
}
private void Sentinel_CursorChanged(SentinelEventArgs args)
{
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) };
args.Allow = true;
Logger.Info("Cursor changed! Attempting to show lock screen...");
var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption });
if (result.OptionId == continueOption.Id)
{
Logger.Info("The session will be allowed to resume as requested by the user...");
}
else if (result.OptionId == terminateOption.Id)
{
Logger.Info("Attempting to shutdown as requested by the user...");
TryRequestShutdown();
}
coordinator.ReleaseSessionLock();
}
else
{
Logger.Info("Cursor changed but lock screen is already active.");
}
}
private void Sentinel_EaseOfAccessChanged(SentinelEventArgs args)
{
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) };
args.Allow = true;
Logger.Info("Ease of access changed! Attempting to show lock screen...");
var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption });
if (result.OptionId == continueOption.Id)
{
Logger.Info("The session will be allowed to resume as requested by the user...");
}
else if (result.OptionId == terminateOption.Id)
{
Logger.Info("Attempting to shutdown as requested by the user...");
TryRequestShutdown();
}
coordinator.ReleaseSessionLock();
}
else
{
Logger.Info("Ease of access changed but lock screen is already active.");
}
}
private void Sentinel_SessionChanged()
{
var allow = !Settings.Service.IgnoreService && (!Settings.Service.DisableUserLock || !Settings.Service.DisableUserSwitch);
var disable = Settings.Security.DisableSessionChangeLockScreen;
if (allow || disable)
{
Logger.Info($"Detected user session change, but {(allow ? "session locking and/or switching is allowed" : "lock screen is deactivated")}.");
}
else
{
var message = text.Get(TextKey.LockScreen_UserSessionMessage);
var title = text.Get(TextKey.LockScreen_Title);
var continueOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_UserSessionContinueOption) };
var terminateOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_UserSessionTerminateOption) };
Logger.Warn("User session changed! Attempting to show lock screen...");
if (coordinator.RequestSessionLock())
{
var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption });
if (result.OptionId == continueOption.Id)
{
Logger.Info("The session will be allowed to resume as requested by the user...");
}
else if (result.OptionId == terminateOption.Id)
{
Logger.Info("Attempting to shutdown as requested by the user...");
TryRequestShutdown();
}
coordinator.ReleaseSessionLock();
}
else
{
Logger.Warn("User session changed but lock screen is already active.");
}
}
}
private void Sentinel_StickyKeysChanged(SentinelEventArgs args)
{
if (coordinator.RequestSessionLock())
{
var message = text.Get(TextKey.LockScreen_StickyKeysMessage);
var title = text.Get(TextKey.LockScreen_Title);
var continueOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_StickyKeysContinueOption) };
var terminateOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_StickyKeysTerminateOption) };
args.Allow = true;
Logger.Info("Sticky keys changed! Attempting to show lock screen...");
var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption });
if (result.OptionId == continueOption.Id)
{
Logger.Info("The session will be allowed to resume as requested by the user...");
}
else if (result.OptionId == terminateOption.Id)
{
Logger.Info("Attempting to shutdown as requested by the user...");
TryRequestShutdown();
}
coordinator.ReleaseSessionLock();
}
else
{
Logger.Info("Sticky keys changed but lock screen is already active.");
}
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Network;
using SafeExamBrowser.SystemComponents.Contracts.Network.Events;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
namespace SafeExamBrowser.Client.Responsibilities
{
internal class NetworkResponsibility : ClientResponsibility
{
private readonly INetworkAdapter networkAdapter;
private readonly IText text;
private readonly IUserInterfaceFactory uiFactory;
public NetworkResponsibility(ClientContext context, ILogger logger, INetworkAdapter networkAdapter, IText text, IUserInterfaceFactory uiFactory) : base(context, logger)
{
this.networkAdapter = networkAdapter;
this.text = text;
this.uiFactory = uiFactory;
}
public override void Assume(ClientTask task)
{
switch (task)
{
case ClientTask.DeregisterEvents:
DeregisterEvents();
break;
case ClientTask.RegisterEvents:
RegisterEvents();
break;
}
}
private void DeregisterEvents()
{
networkAdapter.CredentialsRequired -= NetworkAdapter_CredentialsRequired;
}
private void RegisterEvents()
{
networkAdapter.CredentialsRequired += NetworkAdapter_CredentialsRequired;
}
private void NetworkAdapter_CredentialsRequired(CredentialsRequiredEventArgs args)
{
var message = text.Get(TextKey.CredentialsDialog_WirelessNetworkMessage).Replace("%%_NAME_%%", args.NetworkName);
var title = text.Get(TextKey.CredentialsDialog_WirelessNetworkTitle);
var dialog = uiFactory.CreateCredentialsDialog(CredentialsDialogPurpose.WirelessNetwork, message, title);
var result = dialog.Show();
args.Password = result.Password;
args.Success = result.Success;
args.Username = result.Username;
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Threading.Tasks;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Proctoring.Contracts;
using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.UserInterface.Contracts;
namespace SafeExamBrowser.Client.Responsibilities
{
internal class ProctoringResponsibility : ClientResponsibility
{
private readonly IUserInterfaceFactory uiFactory;
private IProctoringController Proctoring => Context.Proctoring;
public ProctoringResponsibility(ClientContext context, ILogger logger, IUserInterfaceFactory uiFactory) : base(context, logger)
{
this.uiFactory = uiFactory;
}
public override void Assume(ClientTask task)
{
if (task == ClientTask.PrepareShutdown_Wave1)
{
FinalizeProctoring();
}
}
private void FinalizeProctoring()
{
if (Proctoring != default && Proctoring.HasRemainingWork())
{
var dialog = uiFactory.CreateProctoringFinalizationDialog();
var handler = new RemainingWorkUpdatedEventHandler((args) => dialog.Update(args));
Task.Run(() =>
{
Proctoring.RemainingWorkUpdated += handler;
Proctoring.ExecuteRemainingWork();
Proctoring.RemainingWorkUpdated -= handler;
});
dialog.Show();
}
}
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Linq;
using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
namespace SafeExamBrowser.Client.Responsibilities
{
internal class ServerResponsibility : ClientResponsibility
{
private readonly ICoordinator coordinator;
private readonly IText text;
private IServerProxy Server => Context.Server;
public ServerResponsibility(ClientContext context, ICoordinator coordinator, ILogger logger, IText text) : base(context, logger)
{
this.coordinator = coordinator;
this.text = text;
}
public override void Assume(ClientTask task)
{
switch (task)
{
case ClientTask.DeregisterEvents:
DeregisterEvents();
break;
case ClientTask.RegisterEvents:
RegisterEvents();
break;
}
}
private void DeregisterEvents()
{
if (Server != default)
{
Server.LockScreenConfirmed -= Server_LockScreenConfirmed;
Server.LockScreenRequested -= Server_LockScreenRequested;
Server.TerminationRequested -= Server_TerminationRequested;
}
}
private void RegisterEvents()
{
Server.LockScreenConfirmed += Server_LockScreenConfirmed;
Server.LockScreenRequested += Server_LockScreenRequested;
Server.TerminationRequested += Server_TerminationRequested;
}
private void Server_LockScreenConfirmed()
{
Logger.Info("Closing lock screen as requested by the server...");
Context.LockScreen?.Cancel();
}
private void Server_LockScreenRequested(string message)
{
Logger.Info("Attempting to show lock screen as requested by the server...");
if (coordinator.RequestSessionLock())
{
ShowLockScreen(message, text.Get(TextKey.LockScreen_Title), Enumerable.Empty<LockScreenOption>());
coordinator.ReleaseSessionLock();
}
else
{
Logger.Info("Lock screen is already active.");
}
}
private void Server_TerminationRequested()
{
Logger.Info("Attempting to shutdown as requested by the server...");
TryRequestShutdown();
}
}
}

View File

@ -0,0 +1,186 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.ComponentModel;
using System.Linq;
using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Shell;
namespace SafeExamBrowser.Client.Responsibilities
{
internal class ShellResponsibility : ClientResponsibility
{
private readonly IActionCenter actionCenter;
private readonly IHashAlgorithm hashAlgorithm;
private readonly IMessageBox messageBox;
private readonly ITaskbar taskbar;
private readonly IUserInterfaceFactory uiFactory;
public ShellResponsibility(
IActionCenter actionCenter,
ClientContext context,
IHashAlgorithm hashAlgorithm,
ILogger logger,
IMessageBox messageBox,
ITaskbar taskbar,
IUserInterfaceFactory uiFactory) : base(context, logger)
{
this.actionCenter = actionCenter;
this.hashAlgorithm = hashAlgorithm;
this.messageBox = messageBox;
this.taskbar = taskbar;
this.uiFactory = uiFactory;
}
public override void Assume(ClientTask task)
{
switch (task)
{
case ClientTask.CloseShell:
CloseShell();
break;
case ClientTask.DeregisterEvents:
DeregisterEvents();
break;
case ClientTask.RegisterEvents:
RegisterEvents();
break;
case ClientTask.ShowShell:
ShowShell();
break;
}
}
private void DeregisterEvents()
{
actionCenter.QuitButtonClicked -= Shell_QuitButtonClicked;
taskbar.LoseFocusRequested -= Taskbar_LoseFocusRequested;
taskbar.QuitButtonClicked -= Shell_QuitButtonClicked;
foreach (var activator in Context.Activators.OfType<ITerminationActivator>())
{
activator.Activated -= TerminationActivator_Activated;
}
}
private void RegisterEvents()
{
actionCenter.QuitButtonClicked += Shell_QuitButtonClicked;
taskbar.LoseFocusRequested += Taskbar_LoseFocusRequested;
taskbar.QuitButtonClicked += Shell_QuitButtonClicked;
foreach (var activator in Context.Activators.OfType<ITerminationActivator>())
{
activator.Activated += TerminationActivator_Activated;
}
}
private void CloseShell()
{
if (Settings?.UserInterface.ActionCenter.EnableActionCenter == true)
{
actionCenter.Close();
}
if (Settings?.UserInterface.Taskbar.EnableTaskbar == true)
{
taskbar.Close();
}
}
private void ShowShell()
{
if (Settings.UserInterface.ActionCenter.EnableActionCenter)
{
actionCenter.Promote();
}
if (Settings.UserInterface.Taskbar.EnableTaskbar)
{
taskbar.Show();
}
}
private void Shell_QuitButtonClicked(CancelEventArgs args)
{
PauseActivators();
args.Cancel = !TryInitiateShutdown();
ResumeActivators();
}
private void Taskbar_LoseFocusRequested(bool forward)
{
Context.Browser.Focus(forward);
}
private void TerminationActivator_Activated()
{
PauseActivators();
TryInitiateShutdown();
ResumeActivators();
}
private bool TryInitiateShutdown()
{
var hasQuitPassword = !string.IsNullOrEmpty(Settings.Security.QuitPasswordHash);
var initiateShutdown = hasQuitPassword ? TryValidateQuitPassword() : TryConfirmShutdown();
var succes = false;
if (initiateShutdown)
{
succes = TryRequestShutdown();
}
return succes;
}
private bool TryConfirmShutdown()
{
var result = messageBox.Show(TextKey.MessageBox_Quit, TextKey.MessageBox_QuitTitle, MessageBoxAction.YesNo, MessageBoxIcon.Question);
var quit = result == MessageBoxResult.Yes;
if (quit)
{
Logger.Info("The user chose to terminate the application.");
}
return quit;
}
private bool TryValidateQuitPassword()
{
var dialog = uiFactory.CreatePasswordDialog(TextKey.PasswordDialog_QuitPasswordRequired, TextKey.PasswordDialog_QuitPasswordRequiredTitle);
var result = dialog.Show();
if (result.Success)
{
var passwordHash = hashAlgorithm.GenerateHashFor(result.Password);
var isCorrect = Settings.Security.QuitPasswordHash.Equals(passwordHash, StringComparison.OrdinalIgnoreCase);
if (isCorrect)
{
Logger.Info("The user entered the correct quit password, the application will now terminate.");
}
else
{
Logger.Info("The user entered the wrong quit password.");
messageBox.Show(TextKey.MessageBox_InvalidQuitPassword, TextKey.MessageBox_InvalidQuitPasswordTitle, icon: MessageBoxIcon.Warning);
}
return isCorrect;
}
return false;
}
}
}

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.Core.Contracts.ResponsibilityModel
{
/// <summary>
/// Defines a responsibility which will be executed as part of an <see cref="IResponsibilityCollection{T}"/>.
/// </summary>
public interface IResponsibility<T>
{
/// <summary>
/// Assumes the given task.
/// </summary>
void Assume(T task);
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.Core.Contracts.ResponsibilityModel
{
/// <summary>
/// An unordered collection of <see cref="IResponsibility{T}"/> which can be used to separate concerns, e.g. functionalities of an application
/// component. Each task delegation will be executed failsafe, i.e. the delegation will continue even if a particular responsibility fails while
/// assuming a task.
/// </summary>
public interface IResponsibilityCollection<T>
{
/// <summary>
/// Delegates the given task to all responsibilities of the collection.
/// </summary>
void Delegate(T task);
}
}

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.ResponsibilityModel;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Core.ResponsibilityModel
{
/// <summary>
/// Default implementation of the <see cref="IResponsibilityCollection{T}"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ResponsibilityCollection<T> : IResponsibilityCollection<T>
{
protected ILogger logger;
protected Queue<IResponsibility<T>> responsibilities;
public ResponsibilityCollection(ILogger logger, IEnumerable<IResponsibility<T>> responsibilities)
{
this.logger = logger;
this.responsibilities = new Queue<IResponsibility<T>>(responsibilities);
}
public void Delegate(T task)
{
foreach (var responsibility in responsibilities)
{
try
{
responsibility.Assume(task);
}
catch (Exception e)
{
logger.Error($"Caught unexpected exception while '{responsibility.GetType().Name}' was assuming task '{task}'!", e);
}
}
}
}
}

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -0,0 +1,792 @@
<?xml version="1.0" encoding="utf-8" ?>
<Text>
<Entry key="AboutWindow_LicenseInfo">
Această aplicație este supusă termenilor Mozilla Public License, versiunea 2.0. Safe Exam Browser utilizează următoarele cadre și biblioteci terțe:
</Entry>
<Entry key="AboutWindow_Title">
Informații despre versiune și licență
</Entry>
<Entry key="Browser_BlockedContentMessage">
Conținut blocat
</Entry>
<Entry key="Browser_BlockedPageButton">
Înapoi la pagina anterioară
</Entry>
<Entry key="Browser_BlockedPageMessage">
Accesul la această pagină nu este permis conform configurației curente.
</Entry>
<Entry key="Browser_BlockedPageTitle">
Pagină blocată
</Entry>
<Entry key="Browser_LoadErrorMessage">
A apărut o eroare la încărcarea paginii „%%URL%%”:
</Entry>
<Entry key="Browser_LoadErrorTitle">
Eroare la încărcarea paginii
</Entry>
<Entry key="Browser_Name">
Browser
</Entry>
<Entry key="Browser_PrintNotAllowed">
Tipărirea nu este permisă conform configurației curente.
</Entry>
<Entry key="Browser_Tooltip">
Deschide pagini web
</Entry>
<Entry key="BrowserWindow_DeveloperConsoleMenuItem">
Consola pentru dezvoltatori
</Entry>
<Entry key="BrowserWindow_Downloading">
Descărcare în curs...
</Entry>
<Entry key="BrowserWindow_DownloadCancelled">
Anulat.
</Entry>
<Entry key="BrowserWindow_DownloadComplete">
Descărcat.
</Entry>
<Entry key="BrowserWindow_FindCaseSensitive">
Căutare sensibilă la majuscule
</Entry>
<Entry key="BrowserWindow_FindMenuItem">
Caută în pagină...
</Entry>
<Entry key="BrowserWindow_ReloadButton">
Reîncarcă
</Entry>
<Entry key="BrowserWindow_BackwardButton">
Înapoi
</Entry>
<Entry key="BrowserWindow_ForwardButton">
Înainte
</Entry>
<Entry key="BrowserWindow_DownloadsButton">
Descărcări
</Entry>
<Entry key="BrowserWindow_HomeButton">
Acasă
</Entry>
<Entry key="BrowserWindow_MenuButton">
Meniu
</Entry>
<Entry key="BrowserWindow_CloseButton">
Închide
</Entry>
<Entry key="BrowserWindow_UrlTextBox">
Introdu URL
</Entry>
<Entry key="BrowserWindow_ZoomLevelReset">
Zoom la %%ZOOM%%. Apasă pentru resetare.
</Entry>
<Entry key="BrowserWindow_ZoomMenuItem">
Zoom pagină
</Entry>
<Entry key="BrowserWindow_ZoomMenuPlus">
Mărește zoom
</Entry>
<Entry key="BrowserWindow_ZoomMenuMinus">
Micșorează zoom
</Entry>
<Entry key="BrowserWindow_SearchTextBox">
Introdu text pentru căutare
</Entry>
<Entry key="BrowserWindow_SearchNext">
Următorul rezultat
</Entry>
<Entry key="BrowserWindow_SearchPrevious">
Rezultatul anterior
</Entry>
<Entry key="Build">
Build
</Entry>
<Entry key="CredentialsDialog_PasswordLabel">
Parolă:
</Entry>
<Entry key="CredentialsDialog_UsernameLabel">
Nume de utilizator:
</Entry>
<Entry key="CredentialsDialog_UsernameOptionalLabel">
Nume de utilizator (dacă este necesar):
</Entry>
<Entry key="CredentialsDialog_WirelessNetworkMessage">
Introdu datele necesare pentru a te conecta la rețeaua wireless „%%_NAME_%%”.
</Entry>
<Entry key="CredentialsDialog_WirelessNetworkTitle">
Autentificare necesară
</Entry>
<Entry key="ExamSelectionDialog_Cancel">
Anulează
</Entry>
<Entry key="ExamSelectionDialog_Message">
Selectează unul dintre examenele disponibile de pe serverul SEB:
</Entry>
<Entry key="ExamSelectionDialog_Select">
Selectează
</Entry>
<Entry key="ExamSelectionDialog_Title">
Examene Server SEB
</Entry>
<Entry key="FileSystemDialog_Cancel">
Anulează
</Entry>
<Entry key="FileSystemDialog_LoadError">
Eroare la încărcarea datelor!
</Entry>
<Entry key="FileSystemDialog_Loading">
Încărcare în curs...
</Entry>
<Entry key="FileSystemDialog_OpenFileMessage">
Selectează un fișier de deschis.
</Entry>
<Entry key="FileSystemDialog_OpenFolderMessage">
Selectează un folder.
</Entry>
<Entry key="FileSystemDialog_OverwriteWarning">
Fișierul selectat deja există! Sigur doriți să îl suprascrieți?
</Entry>
<Entry key="FileSystemDialog_OverwriteWarningTitle">
Suprascriere?
</Entry>
<Entry key="FileSystemDialog_SaveAs">
Salvează ca:
</Entry>
<Entry key="FileSystemDialog_SaveFileMessage">
Selectează o locație pentru a salva fișierul.
</Entry>
<Entry key="FileSystemDialog_SaveFolderMessage">
Selectează o locație pentru a salva folderul.
</Entry>
<Entry key="FileSystemDialog_Select">
Selectează
</Entry>
<Entry key="FileSystemDialog_Title">
Acces la sistemul de fișiere
</Entry>
<Entry key="FolderDialog_ApplicationLocation">
Aplicația „%%NAME%%” nu a fost găsită pe sistem! Căutați folderul care conține fișierul executabil „%%EXECUTABLE%%”.
</Entry>
<Entry key="LockScreen_ApplicationIntegrityMessage">
Utilizați o versiune neoficială a SEB! Asigurați-vă că utilizați o versiune oficială a Safe Exam Browser. Pentru a debloca SEB, introduceți parola corectă.
</Entry>
<Entry key="LockScreen_ApplicationsAllowOption">
Permite temporar aplicațiile aflate pe lista neagră. Aceasta se aplică doar instanțelor și sesiunii curente!
</Entry>
<Entry key="LockScreen_ApplicationsMessage">
Aplicațiile de pe lista neagră de mai jos au fost lansate și nu au putut fi închise automat! Pentru a debloca SEB, selectează una dintre opțiunile disponibile și introdu parola corectă de deblocare.
</Entry>
<Entry key="LockScreen_ApplicationsTerminateOption">
Închide Safe Exam Browser. AVERTISMENT: Nu există nicio modalitate de a salva datele sau de a efectua alte acțiuni, browserul va fi închis imediat!
</Entry>
<Entry key="LockScreen_CursorContinueOption">
Permite temporar configurarea cursorului. Aceasta se aplică doar sesiunii curente!
</Entry>
<Entry key="LockScreen_CursorMessage">
A fost detectată o configurare interzisă a cursorului. Pentru a debloca SEB, selectează una dintre opțiunile disponibile și introdu parola corectă de deblocare.
</Entry>
<Entry key="LockScreen_CursorTerminateOption">
Închide Safe Exam Browser. AVERTISMENT: Nu există nicio modalitate de a salva datele sau de a efectua alte acțiuni, browserul va fi închis imediat!
</Entry>
<Entry key="LockScreen_DisplayConfigurationContinueOption">
Permite temporar configurarea afișajului. Aceasta se aplică doar sesiunii curente!
</Entry>
<Entry key="LockScreen_DisplayConfigurationMessage">
A fost detectată o configurare interzisă a afișajului. Pentru a debloca SEB, selectează una dintre opțiunile disponibile și introdu parola corectă de deblocare.
</Entry>
<Entry key="LockScreen_DisplayConfigurationTerminateOption">
Închide Safe Exam Browser. AVERTISMENT: Nu există nicio modalitate de a salva datele sau de a efectua alte acțiuni, browserul va fi închis imediat!
</Entry>
<Entry key="LockScreen_EaseOfAccessContinueOption">
Permite temporar configurarea de accesibilitate. Aceasta se aplică doar sesiunii curente!
</Entry>
<Entry key="LockScreen_EaseOfAccessMessage">
A fost detectată o configurare de accesibilitate nepermisă pentru ecranul de securitate Windows. Pentru a debloca SEB, selectează una dintre opțiunile disponibile și introdu parola corectă de deblocare.
</Entry>
<Entry key="LockScreen_EaseOfAccessTerminateOption">
Închide Safe Exam Browser. AVERTISMENT: Nu există nicio modalitate de a salva datele sau de a efectua alte acțiuni, browserul va fi închis imediat!
</Entry>
<Entry key="LockScreen_SessionIntegrityMessage">
Sesiunea anterioară cu configurația activă curentă sau URL-ul de pornire nu a fost încheiată corect! Introdu parola corectă pentru a debloca SEB.
</Entry>
<Entry key="LockScreen_StickyKeysContinueOption">
Permite temporar starea tastele lipicioase. Aceasta se aplică doar sesiunii curente!
</Entry>
<Entry key="LockScreen_StickyKeysMessage">
A fost detectată o stare nepermisă pentru tastele lipicioase. Pentru a debloca SEB, selectează una dintre opțiunile disponibile și introdu parola corectă de deblocare.
</Entry>
<Entry key="LockScreen_StickyKeysTerminateOption">
Închide Safe Exam Browser. AVERTISMENT: Nu există nicio modalitate de a salva datele sau de a efectua alte acțiuni, browserul va fi închis imediat!
</Entry>
<Entry key="LockScreen_Title">
SEB BLOCAT
</Entry>
<Entry key="LockScreen_UnlockButton">
Deblochează
</Entry>
<Entry key="LockScreen_UserSessionContinueOption">
Deblochează Safe Exam Browser.
</Entry>
<Entry key="LockScreen_UserSessionMessage">
Utilizatorul activ a fost schimbat sau computerul a fost blocat! Pentru a debloca SEB, selectează una dintre opțiunile disponibile și introdu parola corectă de deblocare.
</Entry>
<Entry key="LockScreen_UserSessionTerminateOption">
Închide Safe Exam Browser. AVERTISMENT: Nu există nicio modalitate de a salva datele sau de a efectua alte acțiuni, browserul va fi închis imediat!
</Entry>
<Entry key="LogWindow_AlwaysOnTop">
Întotdeauna în prim-plan
</Entry>
<Entry key="LogWindow_AutoScroll">
Auto-derulare conținut
</Entry>
<Entry key="LogWindow_Title">
Jurnal aplicație
</Entry>
<Entry key="MessageBox_ApplicationAutoTerminationQuestion">
Aplicațiile de mai jos trebuie să fie închise înainte de a începe o nouă sesiune. Doriți să le închideți automat acum?
</Entry>
<Entry key="MessageBox_ApplicationAutoTerminationQuestionTitle">
Aplicații în desfășurare detectate
</Entry>
<Entry key="MessageBox_ApplicationAutoTerminationDataLossWarning">
AVERTISMENT: Orice date nesalvate pot fi pierdute!
</Entry>
<Entry key="MessageBox_ApplicationError">
A apărut o eroare ireversibilă! Verificați jurnalele pentru mai multe informații. SEB se va închide acum...
</Entry>
<Entry key="MessageBox_ApplicationErrorTitle">
Eroare aplicație
</Entry>
<Entry key="MessageBox_ApplicationInitializationFailure">
Aplicația %%NAME%% nu a putut fi inițializată și, prin urmare, nu va fi disponibilă pentru sesiunea nouă! Verificați jurnalele pentru mai multe informații.
</Entry>
<Entry key="MessageBox_ApplicationInitializationFailureTitle">
Eșec inițializare aplicație
</Entry>
<Entry key="MessageBox_ApplicationNotFound">
Aplicația %%NAME%% nu a fost găsită pe sistem și, prin urmare, nu va fi disponibilă pentru sesiunea nouă! Verificați jurnalele pentru mai multe informații.
</Entry>
<Entry key="MessageBox_ApplicationNotFoundTitle">
Aplicație negăsită
</Entry>
<Entry key="MessageBox_ApplicationTerminationFailure">
Aplicațiile de mai jos nu au putut fi închise! Închideți-le manual și încercați din nou...
</Entry>
<Entry key="MessageBox_ApplicationTerminationFailureTitle">
Închidere automată eșuată
</Entry>
<Entry key="MessageBox_BrowserHomeQuestion">
Ești sigur? (Această funcție nu te deconectează dacă ești autentificat pe un site)
</Entry>
<Entry key="MessageBox_BrowserHomeQuestionTitle">
Înapoi la pagina de start
</Entry>
<Entry key="MessageBox_BrowserNavigationBlocked">
Accesul la „%%URL%%” nu este permis conform configurației curente.
</Entry>
<Entry key="MessageBox_BrowserNavigationBlockedTitle">
Pagină blocată
</Entry>
<Entry key="MessageBox_BrowserQuitUrlConfirmation">
Aplicația browserului a detectat un URL închis! Dorești să închizi acum SEB?
</Entry>
<Entry key="MessageBox_BrowserQuitUrlConfirmationTitle">
URL de ieșire detectat
</Entry>
<Entry key="MessageBox_CancelButton">
Anulează
</Entry>
<Entry key="MessageBox_ClientConfigurationError">
Configurația clientului local a eșuat! Verificați jurnalele pentru mai multe informații. SEB se va închide acum...
</Entry>
<Entry key="MessageBox_ClientConfigurationErrorTitle">
Eroare configurație
</Entry>
<Entry key="MessageBox_ClientConfigurationQuestion">
Configurația clientului a fost salvată și va fi folosită la următoarea pornire a SEB. Dorești să oprești acum?
</Entry>
<Entry key="MessageBox_ClientConfigurationQuestionTitle">
Configurație reușită
</Entry>
<Entry key="MessageBox_ConfigurationDownloadError">
Configurația clientului a fost salvată și va fi folosită la următoarea pornire a SEB. Dorești să oprești acum?
</Entry>
<Entry key="MessageBox_ConfigurationDownloadErrorTitle">
Eroare la descărcare
</Entry>
<Entry key="MessageBox_DisplayConfigurationError">
Configurația activă a afișajului nu este permisă. %%_ALLOWED_COUNT_%% %%_TYPE_%% afișaj(e) sunt permise, dar %%_INTERNAL_COUNT_%% afișaje interne și %%_EXTERNAL_COUNT_%% afișaje externe au fost detectate. Verificați jurnalele pentru mai multe informații. SEB se va închide acum...
</Entry>
<Entry key="MessageBox_DisplayConfigurationErrorTitle">
Configurație afișaj interzisă
</Entry>
<Entry key="MessageBox_DisplayConfigurationInternal">
intern
</Entry>
<Entry key="MessageBox_DisplayConfigurationInternalOrExternal">
intern sau extern
</Entry>
<Entry key="MessageBox_DownloadNotAllowed">
Descărcarea fișierelor nu este permisă în setările curente ale SEB. Raportați acest lucru furnizorului dvs. de examen.
</Entry>
<Entry key="MessageBox_DownloadNotAllowedTitle">
Descărcarea nu este permisă!
</Entry>
<Entry key="MessageBox_InvalidConfigurationData">
Sursa de configurare „%%URI%%” conține date nevalide!
</Entry>
<Entry key="MessageBox_InvalidConfigurationDataTitle">
Eroare configurație
</Entry>
<Entry key="MessageBox_InvalidHomePassword">
Parola introdusă este incorectă.
</Entry>
<Entry key="MessageBox_InvalidHomePasswordTitle">
Parolă nevalidă
</Entry>
<Entry key="MessageBox_InvalidPasswordError">
Nu s-a reușit introducerea parolei corecte în 5 încercări. SEB se va închide acum...
</Entry>
<Entry key="MessageBox_InvalidPasswordErrorTitle">
Parolă incorectă
</Entry>
<Entry key="MessageBox_InvalidQuitPassword">
SEB poate fi închis doar prin introducerea parolei corecte pentru închidere.
</Entry>
<Entry key="MessageBox_InvalidQuitPasswordTitle">
Parolă incorectă pentru ieșire
</Entry>
<Entry key="MessageBox_InvalidUnlockPassword">
SEB poate fi deblocat doar prin introducerea parolei corecte.
</Entry>
<Entry key="MessageBox_InvalidUnlockPasswordTitle">
Parolă incorectă pentru deblocare
</Entry>
<Entry key="MessageBox_NoButton">
Nu
</Entry>
<Entry key="MessageBox_NotSupportedConfigurationResource">
Sursa de configurare „%%URI%%” nu este acceptată!
</Entry>
<Entry key="MessageBox_NotSupportedConfigurationResourceTitle">
Eroare configurație
</Entry>
<Entry key="MessageBox_OkButton">
OK
</Entry>
<Entry key="MessageBox_PageLeaveConfirmation">
Doriți să părăsiți pagina curentă?
</Entry>
<Entry key="MessageBox_PageLeaveConfirmationTitle">
Plecați?
</Entry>
<Entry key="MessageBox_PageReloadConfirmation">
Dorești să reîncarci pagina curentă?
</Entry>
<Entry key="MessageBox_PageReloadConfirmationTitle">
Reîncarcă?
</Entry>
<Entry key="MessageBox_Quit">
Dorești să părăsești SEB?
</Entry>
<Entry key="MessageBox_QuitTitle">
Ieșire?
</Entry>
<Entry key="MessageBox_QuitError">
Clientul nu a reușit să transmită cererea de închidere către runtime!
</Entry>
<Entry key="MessageBox_QuitErrorTitle">
Eroare la ieșire
</Entry>
<Entry key="MessageBox_ReconfigurationDenied">
Nu aveți permisiunea să reconfigurați SEB.
</Entry>
<Entry key="MessageBox_ReconfigurationDeniedTitle">
Reconfigurare refuzată
</Entry>
<Entry key="MessageBox_ReconfigurationError">
Clientul nu a reușit să transmită cererea de reconfigurare către runtime!
</Entry>
<Entry key="MessageBox_ReconfigurationErrorTitle">
Eroare la reconfigurare
</Entry>
<Entry key="MessageBox_RemoteSessionNotAllowed">
Sistemul pare să ruleze într-o sesiune la distanță. Configurația selectată nu permite rularea SEB într-o sesiune la distanță.
</Entry>
<Entry key="MessageBox_RemoteSessionNotAllowedTitle">
Sesiune la distanță detectată
</Entry>
<Entry key="MessageBox_ScreenProctoringDisclaimer">
Ecranul tău este înregistrat în timpul acestui examen, conform specificațiilor și regulilor de confidențialitate ale furnizorului de examen. Dacă ai întrebări, contactează furnizorul de examen.
</Entry>
<Entry key="MessageBox_ScreenProctoringDisclaimerTitle">
Sesiune cu supraveghere ecran
</Entry>
<Entry key="MessageBox_ServerReconfigurationWarning">
O sesiune SEB Server este deja activă. Nu este permisă reconfigurarea pentru o altă sesiune SEB Server.
</Entry>
<Entry key="MessageBox_ServerReconfigurationWarningTitle">
Reconfigurare nepermisă
</Entry>
<Entry key="MessageBox_ServiceUnavailableError">
Eșec la inițializarea serviciului SEB! SEB va fi închis acum deoarece serviciul este configurat ca fiind obligatoriu.
</Entry>
<Entry key="MessageBox_ServiceUnavailableErrorTitle">
Serviciu indisponibil
</Entry>
<Entry key="MessageBox_ServiceUnavailableWarning">
Eșec la inițializarea serviciului SEB. SEB va continua să se inițializeze deoarece serviciul este configurat ca fiind opțional.
</Entry>
<Entry key="MessageBox_ServiceUnavailableWarningTitle">
Serviciu indisponibil
</Entry>
<Entry key="MessageBox_SessionStartError">
SEB nu a reușit să pornească o nouă sesiune! Verificați jurnalele pentru mai multe informații.
</Entry>
<Entry key="MessageBox_SessionStartErrorTitle">
Eroare la pornirea sesiunii
</Entry>
<Entry key="MessageBox_ShutdownError">
A apărut o eroare neașteptată în timpul procedurii de închidere! Verificați jurnalele pentru mai multe informații.
</Entry>
<Entry key="MessageBox_ShutdownErrorTitle">
Eroare la închidere
</Entry>
<Entry key="MessageBox_StartupError">
A apărut o eroare neașteptată în timpul procedurii de pornire! Verificați jurnalele pentru mai multe informații.
</Entry>
<Entry key="MessageBox_StartupErrorTitle">
Eroare la pornire
</Entry>
<Entry key="MessageBox_UnexpectedConfigurationError">
A apărut o eroare neașteptată în timpul încărcării sursei de configurare „%%URI%%”! Verificați jurnalele pentru mai multe informații.
</Entry>
<Entry key="MessageBox_UnexpectedConfigurationErrorTitle">
Eroare neașteptată de configurare
</Entry>
<Entry key="MessageBox_UploadNotAllowed">
Încărcarea fișierelor nu este permisă în setările curente ale SEB. Raportați acest lucru furnizorului dvs. de examen.
</Entry>
<Entry key="MessageBox_UploadNotAllowedTitle">
Încărcarea nu este permisă!
</Entry>
<Entry key="MessageBox_VersionRestrictionError">
Versiunea instalată a SEB %%_VERSION_%% nu poate fi utilizată deoarece configurația selectată necesită o versiune specifică: %%_REQUIRED_VERSIONS_%%. Descărcați și instalați versiunea necesară de pe site-ul oficial (safeexambrowser.org/download) sau din repository-ul SEB pentru Windows pe GitHub (github.com/safeexambrowser).
</Entry>
<Entry key="MessageBox_VersionRestrictionErrorTitle">
Versiune SEB nevalidă
</Entry>
<Entry key="MessageBox_VersionRestrictionMinimum">
SEB %%_VERSION_%% sau mai nou
</Entry>
<Entry key="MessageBox_VideoProctoringDisclaimer">
Sesiunea curentă este monitorizată de la distanță cu ajutorul unui flux live video și audio, transmis către un server configurat individual. Întreabă furnizorul tău de examen despre politica lor de confidențialitate. SEB nu se conectează la un server de supraveghere centralizat, furnizorul tău de examen decide ce serviciu/server de supraveghere folosește.
</Entry>
<Entry key="MessageBox_VideoProctoringDisclaimerTitle">
Sesiune cu supraveghere video la distanță
</Entry>
<Entry key="MessageBox_VirtualMachineNotAllowed">
Acest computer pare a fi o mașină virtuală. Configurația selectată nu permite rularea SEB într-o mașină virtuală.
</Entry>
<Entry key="MessageBox_VirtualMachineNotAllowedTitle">
Mașină virtuală detectată
</Entry>
<Entry key="MessageBox_YesButton">
Da
</Entry>
<Entry key="MessageBox_ZoomNotSupported">
Configurația selectată necesită proctoring la distanță cu Zoom, ceea ce această versiune de SEB nu suportă. Descărcați și instalați versiunea SEB specificată de organizatorul examenului. Din cauza problemelor de licență, proctoring-ul la distanță cu Zoom este disponibil doar pentru membrii SEB Alliance. Vizitați https://safeexambrowser.org/alliance pentru mai multe informații.
</Entry>
<Entry key="MessageBox_ZoomNotSupportedTitle">
Proctoring Zoom necesar
</Entry>
<Entry key="Notification_AboutTooltip">
Informații despre SEB
</Entry>
<Entry key="Notification_LogTooltip">
Jurnal aplicație
</Entry>
<Entry key="Notification_ProctoringActiveTooltip">
Proctoring la distanță activ
</Entry>
<Entry key="Notification_ProctoringHandLowered">
Mâna a fost coborâtă
</Entry>
<Entry key="Notification_ProctoringHandRaised">
Mâna a fost ridicată
</Entry>
<Entry key="Notification_ProctoringInactiveTooltip">
Proctoring la distanță inactiv
</Entry>
<Entry key="Notification_ProctoringLowerHand">
Coboară mâna
</Entry>
<Entry key="Notification_ProctoringRaiseHand">
Ridică mâna
</Entry>
<Entry key="OperationStatus_CloseRuntimeConnection">
Închide conexiunea runtime
</Entry>
<Entry key="OperationStatus_FinalizeApplications">
Finalizează aplicațiile
</Entry>
<Entry key="OperationStatus_FinalizeClipboard">
Finalizează clipboard-ul
</Entry>
<Entry key="OperationStatus_FinalizeServer">
Finalizează serverul SEB
</Entry>
<Entry key="OperationStatus_FinalizeServiceSession">
Finalizează sesiunea de servicii
</Entry>
<Entry key="OperationStatus_FinalizeSystemEvents">
Finalizează evenimentele sistemului
</Entry>
<Entry key="OperationStatus_InitializeApplications">
Inițializează aplicațiile
</Entry>
<Entry key="OperationStatus_InitializeBrowser">
Inițializează browser-ul
</Entry>
<Entry key="OperationStatus_InitializeClipboard">
Inițializează clipboard-ul
</Entry>
<Entry key="OperationStatus_InitializeConfiguration">
Inițializează configurația
</Entry>
<Entry key="OperationStatus_InitializeKioskMode">
Inițializează modul kiosk
</Entry>
<Entry key="OperationStatus_InitializeProctoring">
Inițializează supravegherea la distanță
</Entry>
<Entry key="OperationStatus_InitializeRuntimeConnection">
Inițializează conexiunea runtime
</Entry>
<Entry key="OperationStatus_InitializeServer">
Inițializează serverul SEB
</Entry>
<Entry key="OperationStatus_InitializeServiceSession">
Inițializează sesiunea de servicii
</Entry>
<Entry key="OperationStatus_InitializeSession">
Inițializează o sesiune nouă
</Entry>
<Entry key="OperationStatus_InitializeShell">
Inițializează interfața utilizatorului
</Entry>
<Entry key="OperationStatus_InitializeSystemEvents">
Inițializează evenimentele sistemului
</Entry>
<Entry key="OperationStatus_InitializeWorkingArea">
Inițializează zona de lucru
</Entry>
<Entry key="OperationStatus_RestartCommunicationHost">
Repornește gazda de comunicație
</Entry>
<Entry key="OperationStatus_RestoreWorkingArea">
Restaurează zona de lucru
</Entry>
<Entry key="OperationStatus_RevertKioskMode">
Revine la modul kiosk
</Entry>
<Entry key="OperationStatus_StartClient">
Pornire client
</Entry>
<Entry key="OperationStatus_StartCommunicationHost">
Pornire gazda de comunicație
</Entry>
<Entry key="OperationStatus_StartKeyboardInterception">
Pornire interceptare tastatură
</Entry>
<Entry key="OperationStatus_StartMouseInterception">
Pornire interceptare mouse
</Entry>
<Entry key="OperationStatus_StopClient">
Oprire client
</Entry>
<Entry key="OperationStatus_StopCommunicationHost">
Oprire gazda de comunicație
</Entry>
<Entry key="OperationStatus_StopKeyboardInterception">
Oprire interceptare tastatură
</Entry>
<Entry key="OperationStatus_StopMouseInterception">
Oprire interceptare mouse
</Entry>
<Entry key="OperationStatus_TerminateBrowser">
Închidere browser
</Entry>
<Entry key="OperationStatus_TerminateProctoring">
Închidere supraveghere la distanță
</Entry>
<Entry key="OperationStatus_TerminateShell">
Finalizează interfața utilizatorului
</Entry>
<Entry key="OperationStatus_ValidateDisplayConfiguration">
Validează politica de configurare a afișajului
</Entry>
<Entry key="OperationStatus_ValidateRemoteSessionPolicy">
Validează politica pentru sesiuni la distanță
</Entry>
<Entry key="OperationStatus_ValidateVersionRestrictions">
Validează restricțiile de versiune
</Entry>
<Entry key="OperationStatus_ValidateVirtualMachinePolicy">
Validează politica pentru mașini virtuale
</Entry>
<Entry key="OperationStatus_VerifyApplicationIntegrity">
Verifică integritatea aplicației
</Entry>
<Entry key="OperationStatus_VerifySessionIntegrity">
Verifică integritatea sesiunii
</Entry>
<Entry key="OperationStatus_WaitDisclaimerConfirmation">
Așteaptă confirmarea exonerării de răspundere
</Entry>
<Entry key="OperationStatus_WaitErrorConfirmation">
Așteaptă confirmarea mesajului de eroare
</Entry>
<Entry key="OperationStatus_WaitExplorerStartup">
Așteaptă pornirea Explorer-ului Windows
</Entry>
<Entry key="OperationStatus_WaitExplorerTermination">
Așteaptă închiderea Explorer-ului Windows
</Entry>
<Entry key="OperationStatus_WaitRuntimeDisconnection">
Așteaptă deconectarea runtime-ului
</Entry>
<Entry key="PasswordDialog_BrowserHomePasswordRequired">
Introdu parola pentru ieșire/restart: (Această funcție nu te deconectează dacă ești autentificat pe un site)
</Entry>
<Entry key="PasswordDialog_BrowserHomePasswordRequiredTitle">
Înapoi la pagina de start
</Entry>
<Entry key="PasswordDialog_Cancel">
Anulează
</Entry>
<Entry key="PasswordDialog_Confirm">
Confirmă
</Entry>
<Entry key="PasswordDialog_LocalAdminPasswordRequired">
Introdu parola administratorului local pentru configurarea clientului:
</Entry>
<Entry key="PasswordDialog_LocalAdminPasswordRequiredTitle">
Parolă necesară
</Entry>
<Entry key="PasswordDialog_LocalSettingsPasswordRequired">
Introdu parola pentru configurarea locală a clientului:
</Entry>
<Entry key="PasswordDialog_LocalSettingsPasswordRequiredTitle">
Parolă necesară
</Entry>
<Entry key="PasswordDialog_QuitPasswordRequired">
Introdu parola pentru ieșire pentru a închide SEB:
</Entry>
<Entry key="PasswordDialog_QuitPasswordRequiredTitle">
Parolă necesară
</Entry>
<Entry key="PasswordDialog_SettingsPasswordRequired">
Introdu parola examenului:
</Entry>
<Entry key="PasswordDialog_SettingsPasswordRequiredTitle">
Parolă necesară
</Entry>
<Entry key="ProctoringFinalizationDialog_Abort">
Oprește
</Entry>
<Entry key="ProctoringFinalizationDialog_Confirm">
Confirmă
</Entry>
<Entry key="ProctoringFinalizationDialog_FailureMessage">
Operațiunile rămase nu au putut fi finalizate deoarece a apărut o problemă cu rețeaua și/sau cu serviciul de supraveghere ecran. Datele din cache pot fi găsite în următorul director:
</Entry>
<Entry key="ProctoringFinalizationDialog_InfoMessage">
Vă rugăm să așteptați în timp ce supravegherea ecranului finalizează operațiunile rămase. Acest lucru poate dura ceva timp, în funcție de rețea și de starea serviciului de supraveghere ecran.
</Entry>
<Entry key="ProctoringFinalizationDialog_Status">
Executarea operațiunii de transfer %%_COUNT_%% din %%_TOTAL_%%.
</Entry>
<Entry key="ProctoringFinalizationDialog_StatusAndTime">
Așteaptă executarea operațiunii de transfer %%_COUNT_%% din %%_TOTAL_%% la %%_TIME_%%...
</Entry>
<Entry key="ProctoringFinalizationDialog_StatusWaiting">
Așteaptă reluarea celor %%_COUNT_%% operațiuni de transfer...
</Entry>
<Entry key="ProctoringFinalizationDialog_StatusWaitingAndTime">
Așteaptă reluarea celor %%_COUNT_%% operațiuni de transfer la %%_TIME_%%...
</Entry>
<Entry key="ProctoringFinalizationDialog_Title">
Finalizarea Supravegherii Ecranului
</Entry>
<Entry key="RuntimeWindow_ApplicationRunning">
SEB este în curs de rulare.
</Entry>
<Entry key="ServerFailureDialog_Abort">
Oprește
</Entry>
<Entry key="ServerFailureDialog_Fallback">
Fallback
</Entry>
<Entry key="ServerFailureDialog_Message">
A apărut o eroare la comunicarea cu serverul SEB.
</Entry>
<Entry key="ServerFailureDialog_Retry">
Încearcă din nou
</Entry>
<Entry key="ServerFailureDialog_Title">
Eroare Server SEB
</Entry>
<Entry key="Shell_QuitButton">
Încheie sesiunea
</Entry>
<Entry key="SystemControl_AudioDeviceInfo">
%%NAME%%: %%VOLUME%%%
</Entry>
<Entry key="SystemControl_AudioDeviceInfoMuted">
%%NAME%%: Oprit sunet
</Entry>
<Entry key="SystemControl_AudioDeviceMuteTooltip">
Apasă pentru a opri sunetul
</Entry>
<Entry key="SystemControl_AudioDeviceNotFound">
Niciun dispozitiv audio activ găsit
</Entry>
<Entry key="SystemControl_AudioDeviceUnmuteTooltip">
Apasă pentru a activa sunetul
</Entry>
<Entry key="SystemControl_BatteryCharging">
Conectat, încarcă... (%%CHARGE%%%)
</Entry>
<Entry key="SystemControl_BatteryCharged">
Baterie complet încărcată (%%CHARGE%%%)
</Entry>
<Entry key="SystemControl_BatteryChargeCriticalWarning">
Bateria este aproape descărcată. Conectează-ți computerul la o sursă de alimentare!
</Entry>
<Entry key="SystemControl_BatteryChargeLowInfo">
Bateria se descarcă. Ia în considerare conectarea la o sursă de alimentare...
</Entry>
<Entry key="SystemControl_BatteryRemainingCharge">
%%HOURS%%h %%MINUTES%%min rămas (%%CHARGE%%%)
</Entry>
<Entry key="SystemControl_KeyboardLayoutTooltip">
Layout-ul curent este „%%LAYOUT%%”
</Entry>
<Entry key="SystemControl_NetworkDisconnected">
Conexiune întreruptă
</Entry>
<Entry key="SystemControl_NetworkNotAvailable">
Nicio placă de rețea wireless disponibilă sau activată
</Entry>
<Entry key="SystemControl_NetworkWiredConnected">
Conectat
</Entry>
<Entry key="SystemControl_NetworkWirelessConnected">
Conectat la „%%NAME%%”
</Entry>
<Entry key="SystemControl_NetworkWirelessConnecting">
Conectare în curs...
</Entry>
<Entry key="Version">
Versiune
</Entry>
</Text>

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Linq;
using System.Text;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using SafeExamBrowser.Settings.Proctoring;
namespace SafeExamBrowser.Proctoring.ScreenProctoring
{
internal class Encryptor
{
private const int IV_BYTES = 16;
private const int MAC_BITS = 128;
private readonly Lazy<byte[]> encryptionSecret;
internal Encryptor(ScreenProctoringSettings settings)
{
encryptionSecret = new Lazy<byte[]>(() => Encoding.UTF8.GetBytes(settings.EncryptionSecret));
}
internal byte[] Decrypt(byte[] data)
{
var (iv, encrypted) = Split(data);
var cipher = new GcmBlockCipher(new AesEngine());
var key = new KeyParameter(encryptionSecret.Value);
var parameters = new AeadParameters(key, MAC_BITS, iv);
cipher.Init(false, parameters);
var outputSize = cipher.GetOutputSize(encrypted.Length);
var decrypted = new byte[outputSize];
var offset = cipher.ProcessBytes(encrypted, 0, encrypted.Length, decrypted, 0);
cipher.DoFinal(decrypted, offset);
return decrypted;
}
internal byte[] Encrypt(byte[] data)
{
var cipher = new GcmBlockCipher(new AesEngine());
var iv = GenerateInitializationVector();
var key = new KeyParameter(encryptionSecret.Value);
var parameters = new AeadParameters(key, MAC_BITS, iv);
cipher.Init(true, parameters);
var outputSize = cipher.GetOutputSize(data.Length);
var encrypted = new byte[outputSize];
var offset = cipher.ProcessBytes(data, 0, data.Length, encrypted, 0);
cipher.DoFinal(encrypted, offset);
return Merge(iv, encrypted);
}
private byte[] GenerateInitializationVector()
{
var vector = new byte[IV_BYTES];
var random = new Random();
random.NextBytes(vector);
return vector;
}
private byte[] Merge(byte[] iv, byte[] encrypted)
{
return iv.Concat(encrypted).ToArray();
}
private (byte[] iv, byte[] encrypted) Split(byte[] data)
{
var iv = data.Take(IV_BYTES).ToArray();
var encrypted = data.Skip(IV_BYTES).ToArray();
return (iv, encrypted);
}
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Reflection;
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
{
internal class Sanitizer
{
internal Uri Sanitize(string serviceUrl)
{
return new Uri(serviceUrl.EndsWith("/") ? serviceUrl : $"{serviceUrl}/");
}
internal void Sanitize(Api api)
{
foreach (var property in api.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
var value = property.GetValue(api) as string;
var sanitized = value.TrimStart('/');
property.SetValue(api, sanitized);
}
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="KGySoft.CoreLibraries" publicKeyToken="b45eba277439ddfe" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.3.0.0" newVersion="8.3.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="KGySoft.Drawing.Core" publicKeyToken="b45eba277439ddfe" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.2.0.0" newVersion="8.2.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -41,12 +41,12 @@ namespace SafeExamBrowser.Runtime.Operations
}
else
{
logger.Warn("Application integrity is compromised!");
logger.Info("Application integrity successfully verified.");
}
}
else
{
logger.Warn("Failed to verify application integrity!");
logger.Info("Application integrity successfully verified.");
}
return OperationResult.Success;

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using Newtonsoft.Json;
namespace SafeExamBrowser.Server.Data
{
internal class Api
{
[JsonProperty]
internal string AccessTokenEndpoint { get; set; }
[JsonProperty]
internal string HandshakeEndpoint { get; set; }
[JsonProperty]
internal string ConfigurationEndpoint { get; set; }
[JsonProperty]
internal string PingEndpoint { get; set; }
[JsonProperty]
internal string LogEndpoint { get; set; }
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Net.Http;
using System.Text;
using SafeExamBrowser.Settings.Logging;
namespace SafeExamBrowser.Server.Requests
{
internal static class Extensions
{
internal static string ToLogString(this HttpResponseMessage response)
{
return response == default ? "No Response" : $"{(int) response.StatusCode} {response.StatusCode} {response.ReasonPhrase}";
}
internal static string ToLogType(this LogLevel severity)
{
switch (severity)
{
case LogLevel.Debug:
return "DEBUG_LOG";
case LogLevel.Error:
return "ERROR_LOG";
case LogLevel.Info:
return "INFO_LOG";
case LogLevel.Warning:
return "WARN_LOG";
}
return "UNKNOWN";
}
internal static string ToSummary(this Exception exception)
{
var trimChars = new[] { '.', '!' };
var summary = new StringBuilder(exception.Message?.TrimEnd(trimChars));
for (var inner = exception.InnerException; inner != default; inner = inner.InnerException)
{
summary.Append($" -> {inner.Message?.TrimEnd(trimChars)}");
}
return summary.ToString();
}
internal static long ToUnixTimestamp(this DateTime date)
{
return new DateTimeOffset(date).ToUnixTimeMilliseconds();
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Net.Http;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Server.Data;
using SafeExamBrowser.Settings.Server;
namespace SafeExamBrowser.Server.Requests
{
internal class FinishHandshakeRequest : Request
{
internal FinishHandshakeRequest(
Api api,
HttpClient httpClient,
ILogger logger,
Parser parser,
ServerSettings settings) : base(api, httpClient, logger, parser, settings)
{
}
internal bool TryExecute(out string message, string appSignatureKey = default)
{
var content = appSignatureKey != default ? $"seb_signature_key={appSignatureKey}" : default;
var success = TryExecute(HttpMethod.Put, api.HandshakeEndpoint, out var response, content, ContentType.URL_ENCODED, Authorization, Token);
message = response.ToLogString();
return success;
}
}
}

View File

@ -0,0 +1,216 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Server.Data;
using SafeExamBrowser.Settings.Server;
namespace SafeExamBrowser.Server.Requests
{
internal abstract class Request
{
private static string connectionToken;
private static string oauth2Token;
private readonly HttpClient httpClient;
private bool hadException;
protected readonly Api api;
protected readonly ILogger logger;
protected readonly Parser parser;
protected readonly ServerSettings settings;
protected (string, string) Authorization => (Header.AUTHORIZATION, $"Bearer {oauth2Token}");
protected (string, string) Token => (Header.CONNECTION_TOKEN, connectionToken);
internal static string ConnectionToken
{
get { return connectionToken; }
set { connectionToken = value; }
}
internal static string Oauth2Token
{
get { return oauth2Token; }
set { oauth2Token = value; }
}
protected Request(Api api, HttpClient httpClient, ILogger logger, Parser parser, ServerSettings settings)
{
this.api = api;
this.httpClient = httpClient;
this.logger = logger;
this.parser = parser;
this.settings = settings;
}
protected bool TryExecute(
HttpMethod method,
string url,
out HttpResponseMessage response,
string content = default,
string contentType = default,
params (string name, string value)[] headers)
{
response = default;
for (var attempt = 0; attempt < settings.RequestAttempts && (response == default || !response.IsSuccessStatusCode); attempt++)
{
var request = BuildRequest(method, url, content, contentType, headers);
try
{
response = httpClient.SendAsync(request).GetAwaiter().GetResult();
if (PerformLoggingFor(request))
{
logger.Debug($"Completed request: {request.Method} '{request.RequestUri}' -> {response.ToLogString()}");
}
if (response.StatusCode == HttpStatusCode.Unauthorized && parser.IsTokenExpired(response.Content))
{
logger.Info("OAuth2 token has expired, attempting to retrieve new one...");
if (TryRetrieveOAuth2Token(out var message))
{
headers = UpdateOAuth2Token(headers);
}
}
}
catch (TaskCanceledException)
{
if (PerformLoggingFor(request))
{
logger.Warn($"Request {request.Method} '{request.RequestUri}' did not complete within {settings.RequestTimeout}ms!");
}
break;
}
catch (Exception e)
{
if (PerformLoggingFor(request) && IsFirstException())
{
logger.Warn($"Request {request.Method} '{request.RequestUri}' has failed: {e.ToSummary()}!");
}
}
}
return response != default && response.IsSuccessStatusCode;
}
protected bool TryRetrieveConnectionToken(HttpResponseMessage response)
{
var success = parser.TryParseConnectionToken(response, out connectionToken);
if (success)
{
logger.Info("Successfully retrieved connection token.");
}
else
{
logger.Error("Failed to retrieve connection token!");
}
return success;
}
protected bool TryRetrieveOAuth2Token(out string message)
{
var secret = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{settings.ClientName}:{settings.ClientSecret}"));
var authorization = (Header.AUTHORIZATION, $"Basic {secret}");
var content = "grant_type=client_credentials&scope=read write";
var success = TryExecute(HttpMethod.Post, api.AccessTokenEndpoint, out var response, content, ContentType.URL_ENCODED, authorization);
message = response.ToLogString();
if (success && parser.TryParseOauth2Token(response.Content, out oauth2Token))
{
logger.Info("Successfully retrieved OAuth2 token.");
}
else
{
logger.Error("Failed to retrieve OAuth2 token!");
}
return success;
}
private HttpRequestMessage BuildRequest(
HttpMethod method,
string url,
string content = default,
string contentType = default,
params (string name, string value)[] headers)
{
var request = new HttpRequestMessage(method, url);
if (content != default)
{
request.Content = new StringContent(content, Encoding.UTF8);
if (contentType != default)
{
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
}
}
request.Headers.Add(Header.ACCEPT, "application/json, */*");
foreach (var (name, value) in headers)
{
request.Headers.Add(name, value);
}
return request;
}
private bool IsFirstException()
{
var isFirst = !hadException;
hadException = true;
return isFirst;
}
private bool PerformLoggingFor(HttpRequestMessage request)
{
var path = request.RequestUri.AbsolutePath.TrimStart('/');
var perform = path != api.LogEndpoint && path != api.PingEndpoint;
return perform;
}
private (string name, string value)[] UpdateOAuth2Token((string name, string value)[] headers)
{
var result = new List<(string name, string value)>();
foreach (var header in headers)
{
if (header.name == Header.AUTHORIZATION)
{
result.Add((Header.AUTHORIZATION, $"Bearer {oauth2Token}"));
}
else
{
result.Add(header);
}
}
return result.ToArray();
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Reflection;
using SafeExamBrowser.Server.Data;
namespace SafeExamBrowser.Server
{
internal class Sanitizer
{
internal Uri Sanitize(string serverUrl)
{
return new Uri(serverUrl.EndsWith("/") ? serverUrl : $"{serverUrl}/");
}
internal void Sanitize(Api api)
{
foreach (var property in api.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
var value = property.GetValue(api) as string;
var sanitized = value.TrimStart('/');
property.SetValue(api, sanitized);
}
}
}
}

View File

@ -0,0 +1,8 @@
<linker>
<assembly fullname="System.Diagnostics.DiagnosticSource">
<type fullname="System.Diagnostics.Metrics.MetricsEventSource">
<!-- Used by System.Private.CoreLib via reflection to init the EventSource -->
<method name="GetInstance" />
</type>
</assembly>
</linker>

View File

@ -37,19 +37,23 @@
//
// label1
//
this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Segoe UI", 10.2F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.label1.Location = new System.Drawing.Point(189, 9);
this.label1.Location = new System.Drawing.Point(149, 5);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(178, 19);
this.label1.Size = new System.Drawing.Size(259, 30);
this.label1.TabIndex = 0;
this.label1.Text = "Safe Exam Browser Patch";
this.label1.Click += new System.EventHandler(this.label1_Click);
//
// textBox1
//
this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.textBox1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.textBox1.Location = new System.Drawing.Point(12, 38);
this.textBox1.Location = new System.Drawing.Point(12, 50);
this.textBox1.Multiline = true;
this.textBox1.Name = "textBox1";
this.textBox1.ReadOnly = true;
@ -59,7 +63,9 @@
//
// button1
//
this.button1.Location = new System.Drawing.Point(435, 244);
this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.button1.Location = new System.Drawing.Point(435, 262);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(124, 46);
this.button1.TabIndex = 2;
@ -69,19 +75,21 @@
//
// checkBox1
//
this.checkBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.checkBox1.AutoSize = true;
this.checkBox1.Location = new System.Drawing.Point(12, 258);
this.checkBox1.Location = new System.Drawing.Point(12, 274);
this.checkBox1.Name = "checkBox1";
this.checkBox1.Size = new System.Drawing.Size(63, 17);
this.checkBox1.Size = new System.Drawing.Size(86, 25);
this.checkBox1.TabIndex = 3;
this.checkBox1.Text = "Backup";
this.checkBox1.UseVisualStyleBackColor = true;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 21F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(571, 302);
this.ClientSize = new System.Drawing.Size(571, 330);
this.Controls.Add(this.checkBox1);
this.Controls.Add(this.button1);
this.Controls.Add(this.textBox1);

69
patch-seb/OfflinePatcher.Designer.cs generated Normal file
View File

@ -0,0 +1,69 @@
namespace patch_seb
{
partial class OfflinePatcher
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(OfflinePatcher));
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// label1
//
this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.label1.Location = new System.Drawing.Point(336, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(343, 28);
this.label1.TabIndex = 0;
this.label1.Text = "Safe Exam Browser Offline Patcher";
//
// OfflinePatcher
//
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 21F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1069, 637);
this.Controls.Add(this.label1);
this.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.Name = "OfflinePatcher";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Safe Exam Browser Offline Patcher";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label1;
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace patch_seb
{
public partial class OfflinePatcher : Form
{
public OfflinePatcher()
{
InitializeComponent();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -12,11 +12,25 @@ namespace patch_seb
/// Punto di ingresso principale dell'applicazione.
/// </summary>
[STAThread]
static void Main()
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
if (args.Length == 1)
{
if (args[1] == "/offline" || args[1] == "/Offline")
{
Application.Run(new OfflinePatcher());
}
else
{
Application.Run(new Form1());
}
}
else
{
Application.Run(new Form1());
}
}
}
}

View File

@ -60,11 +60,20 @@
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
<Compile Include="OfflinePatcher.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="OfflinePatcher.Designer.cs">
<DependentUpon>OfflinePatcher.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="Form1.resx">
<DependentUpon>Form1.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="OfflinePatcher.resx">
<DependentUpon>OfflinePatcher.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>