From de6cb5e75cc91e69cef38ca772504d90ebc36b0c Mon Sep 17 00:00:00 2001 From: dbuechel Date: Wed, 9 Oct 2019 14:04:27 +0200 Subject: [PATCH] SEBWIN-313: Finished blacklist monitoring. --- SafeExamBrowser.Client/ClientController.cs | 10 ++ SafeExamBrowser.Client/CompositionRoot.cs | 3 +- .../Operations/ApplicationOperation.cs | 13 +- .../Events/ExplorerStartedEventHandler.cs | 2 +- .../Events/TerminationFailedEventHandler.cs | 17 ++ .../Applications/IApplicationMonitor.cs | 5 + ...afeExamBrowser.Monitoring.Contracts.csproj | 1 + .../Applications/ApplicationMonitor.cs | 163 +++++++++++------- .../Operations/ClientOperationTests.cs | 10 +- .../Operations/ClientOperation.cs | 2 +- 10 files changed, 151 insertions(+), 75 deletions(-) create mode 100644 SafeExamBrowser.Monitoring.Contracts/Applications/Events/TerminationFailedEventHandler.cs diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index 52522e44..3d87c78d 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -7,6 +7,7 @@ */ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using SafeExamBrowser.Browser.Contracts; @@ -170,6 +171,7 @@ namespace SafeExamBrowser.Client { actionCenter.QuitButtonClicked += Shell_QuitButtonClicked; applicationMonitor.ExplorerStarted += ApplicationMonitor_ExplorerStarted; + applicationMonitor.TerminationFailed += ApplicationMonitor_TerminationFailed; Browser.ConfigurationDownloadRequested += Browser_ConfigurationDownloadRequested; ClientHost.MessageBoxRequested += ClientHost_MessageBoxRequested; ClientHost.PasswordRequested += ClientHost_PasswordRequested; @@ -185,6 +187,7 @@ namespace SafeExamBrowser.Client { actionCenter.QuitButtonClicked -= Shell_QuitButtonClicked; applicationMonitor.ExplorerStarted -= ApplicationMonitor_ExplorerStarted; + applicationMonitor.TerminationFailed -= ApplicationMonitor_TerminationFailed; displayMonitor.DisplayChanged -= DisplayMonitor_DisplaySettingsChanged; runtime.ConnectionLost -= Runtime_ConnectionLost; taskbar.QuitButtonClicked -= Shell_QuitButtonClicked; @@ -230,6 +233,13 @@ namespace SafeExamBrowser.Client logger.Info("Desktop successfully restored."); } + private void ApplicationMonitor_TerminationFailed(IEnumerable applications) + { + // foreach actionCenterActivator -> Pause + // TODO: Show lock screen! + // foreach actionCenterActivator -> Resume + } + private void Browser_ConfigurationDownloadRequested(string fileName, DownloadEventArgs args) { if (Settings.ConfigurationMode == ConfigurationMode.ConfigureClient) diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index 4277044b..b9fce267 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -52,6 +52,7 @@ namespace SafeExamBrowser.Client { internal class CompositionRoot { + private const int TWO_SECONDS = 2000; private const int FIVE_SECONDS = 5000; private Guid authenticationToken; @@ -94,7 +95,7 @@ namespace SafeExamBrowser.Client taskbar = BuildTaskbar(); terminationActivator = new TerminationActivator(ModuleLogger(nameof(TerminationActivator))); - var applicationMonitor = new ApplicationMonitor(FIVE_SECONDS, ModuleLogger(nameof(ApplicationMonitor)), nativeMethods, new ProcessFactory(ModuleLogger(nameof(ProcessFactory)))); + var applicationMonitor = new ApplicationMonitor(TWO_SECONDS, ModuleLogger(nameof(ApplicationMonitor)), nativeMethods, new ProcessFactory(ModuleLogger(nameof(ProcessFactory)))); var displayMonitor = new DisplayMonitor(ModuleLogger(nameof(DisplayMonitor)), nativeMethods, systemInfo); var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods); var hashAlgorithm = new HashAlgorithm(); diff --git a/SafeExamBrowser.Client/Operations/ApplicationOperation.cs b/SafeExamBrowser.Client/Operations/ApplicationOperation.cs index 46b1baf8..eba09ab2 100644 --- a/SafeExamBrowser.Client/Operations/ApplicationOperation.cs +++ b/SafeExamBrowser.Client/Operations/ApplicationOperation.cs @@ -14,6 +14,7 @@ using SafeExamBrowser.Core.Contracts.OperationModel.Events; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Monitoring.Contracts.Applications; +using SafeExamBrowser.Settings; using SafeExamBrowser.Settings.Applications; namespace SafeExamBrowser.Client.Operations @@ -109,18 +110,18 @@ namespace SafeExamBrowser.Client.Operations private void StartMonitor() { - //TODO: if (Context.Settings.KioskMode != KioskMode.None) - //{ + if (Context.Settings.KioskMode != KioskMode.None) + { applicationMonitor.Start(); - //} + } } private void StopMonitor() { - //TODO: if (Context.Settings.KioskMode != KioskMode.None) - //{ + if (Context.Settings.KioskMode != KioskMode.None) + { applicationMonitor.Stop(); - //} + } } private OperationResult TryTerminate(IEnumerable runningApplications) diff --git a/SafeExamBrowser.Monitoring.Contracts/Applications/Events/ExplorerStartedEventHandler.cs b/SafeExamBrowser.Monitoring.Contracts/Applications/Events/ExplorerStartedEventHandler.cs index fea16098..cddbddd1 100644 --- a/SafeExamBrowser.Monitoring.Contracts/Applications/Events/ExplorerStartedEventHandler.cs +++ b/SafeExamBrowser.Monitoring.Contracts/Applications/Events/ExplorerStartedEventHandler.cs @@ -9,7 +9,7 @@ namespace SafeExamBrowser.Monitoring.Contracts.Applications.Events { /// - /// Indicates that the Windows explorer process has started. + /// Indicates that the Windows Explorer has been started. /// public delegate void ExplorerStartedEventHandler(); } diff --git a/SafeExamBrowser.Monitoring.Contracts/Applications/Events/TerminationFailedEventHandler.cs b/SafeExamBrowser.Monitoring.Contracts/Applications/Events/TerminationFailedEventHandler.cs new file mode 100644 index 00000000..5fec11d1 --- /dev/null +++ b/SafeExamBrowser.Monitoring.Contracts/Applications/Events/TerminationFailedEventHandler.cs @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * 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; + +namespace SafeExamBrowser.Monitoring.Contracts.Applications.Events +{ + /// + /// Indicates that the given blacklisted applications could not be terminated. + /// + public delegate void TerminationFailedEventHandler(IEnumerable applications); +} diff --git a/SafeExamBrowser.Monitoring.Contracts/Applications/IApplicationMonitor.cs b/SafeExamBrowser.Monitoring.Contracts/Applications/IApplicationMonitor.cs index ec2a6815..bd0062dd 100644 --- a/SafeExamBrowser.Monitoring.Contracts/Applications/IApplicationMonitor.cs +++ b/SafeExamBrowser.Monitoring.Contracts/Applications/IApplicationMonitor.cs @@ -21,6 +21,11 @@ namespace SafeExamBrowser.Monitoring.Contracts.Applications /// event ExplorerStartedEventHandler ExplorerStarted; + /// + /// Event fired when the automatic termination of a blacklisted application failed. + /// + event TerminationFailedEventHandler TerminationFailed; + /// /// Initializes the application monitor. /// diff --git a/SafeExamBrowser.Monitoring.Contracts/SafeExamBrowser.Monitoring.Contracts.csproj b/SafeExamBrowser.Monitoring.Contracts/SafeExamBrowser.Monitoring.Contracts.csproj index 07472042..1e1860e9 100644 --- a/SafeExamBrowser.Monitoring.Contracts/SafeExamBrowser.Monitoring.Contracts.csproj +++ b/SafeExamBrowser.Monitoring.Contracts/SafeExamBrowser.Monitoring.Contracts.csproj @@ -53,6 +53,7 @@ + diff --git a/SafeExamBrowser.Monitoring/Applications/ApplicationMonitor.cs b/SafeExamBrowser.Monitoring/Applications/ApplicationMonitor.cs index 71fbbf78..55d2711b 100644 --- a/SafeExamBrowser.Monitoring/Applications/ApplicationMonitor.cs +++ b/SafeExamBrowser.Monitoring/Applications/ApplicationMonitor.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using System.Timers; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Monitoring.Contracts.Applications; @@ -32,6 +33,7 @@ namespace SafeExamBrowser.Monitoring.Applications private IList whitelist; public event ExplorerStartedEventHandler ExplorerStarted; + public event TerminationFailedEventHandler TerminationFailed; public ApplicationMonitor(int interval_ms, ILogger logger, INativeMethods nativeMethods, IProcessFactory processFactory) { @@ -133,6 +135,74 @@ namespace SafeExamBrowser.Monitoring.Applications return success; } + private void SystemEvent_WindowChanged(IntPtr window) + { + if (window != IntPtr.Zero && activeWindow != window) + { + logger.Debug($"Window has changed from {activeWindow} to {window}."); + activeWindow = window; + + Task.Run(() => + { + if (!IsAllowed(window) && !TryHide(window)) + { + Close(window); + } + }); + } + } + + private void Timer_Elapsed(object sender, ElapsedEventArgs e) + { + var failed = new List(); + var running = processFactory.GetAllRunning(); + var started = running.Where(r => processes.All(p => p.Id != r.Id)).ToList(); + var terminated = processes.Where(p => running.All(r => r.Id != p.Id)).ToList(); + + foreach (var process in started) + { + logger.Debug($"Process {process} has been started."); + processes.Add(process); + + if (process.Name == "explorer") + { + HandleExplorerStart(process); + } + else if (!IsAllowed(process) && !TryTerminate(process)) + { + AddFailed(process, failed); + } + } + + foreach (var process in terminated) + { + logger.Debug($"Process {process} has been terminated."); + processes.Remove(process); + } + + if (failed.Any()) + { + logger.Warn($"Failed to terminate these blacklisted applications: {string.Join(", ", failed.Select(a => a.Name))}."); + TerminationFailed?.Invoke(failed); + } + + timer.Start(); + } + + private void AddFailed(IProcess process, List failed) + { + var name = blacklist.First(a => BelongsToApplication(process, a)).ExecutableName; + var application = failed.FirstOrDefault(a => a.Name == name); + + if (application == default(RunningApplication)) + { + application = new RunningApplication(name); + failed.Add(application); + } + + application.Processes.Add(process); + } + private void AddFailed(string name, IProcess process, InitializationResult result) { var application = result.FailedAutoTerminations.FirstOrDefault(a => a.Name == name); @@ -169,21 +239,6 @@ namespace SafeExamBrowser.Monitoring.Applications return sameName || sameOriginalName; } - private void Check(IntPtr window) - { - var allowed = IsAllowed(window); - - if (!allowed) - { - var success = TryHide(window); - - if (!success) - { - Close(window); - } - } - } - private void Close(IntPtr window) { var title = nativeMethods.GetWindowTitle(window); @@ -192,6 +247,23 @@ namespace SafeExamBrowser.Monitoring.Applications logger.Info($"Sent close message to window '{title}' with handle = {window}."); } + private void HandleExplorerStart(IProcess process) + { + logger.Warn($"A new instance of Windows Explorer {process} has been started!"); + + if (!TryTerminate(process)) + { + var application = new RunningApplication("Windows Explorer"); + + logger.Error("Failed to terminate new Windows Explorer instance!"); + application.Processes.Add(process); + + Task.Run(() => TerminationFailed?.Invoke(new[] { application })); + } + + Task.Run(() => ExplorerStarted?.Invoke()); + } + private bool IsAllowed(IntPtr window) { var processId = nativeMethods.GetProcessIdFor(window); @@ -213,6 +285,21 @@ namespace SafeExamBrowser.Monitoring.Applications return true; } + private bool IsAllowed(IProcess process) + { + foreach (var application in blacklist) + { + if (BelongsToApplication(process, application)) + { + logger.Warn($"Process {process} belongs to blacklisted application '{application.ExecutableName}'!"); + + return false; + } + } + + return true; + } + private bool TryHide(IntPtr window) { var title = nativeMethods.GetWindowTitle(window); @@ -265,51 +352,5 @@ namespace SafeExamBrowser.Monitoring.Applications return process.HasTerminated; } - - private void Timer_Elapsed(object sender, ElapsedEventArgs e) - { - var running = processFactory.GetAllRunning(); - var started = running.Where(r => processes.All(p => p.Id != r.Id)).ToList(); - var terminated = processes.Where(p => running.All(r => r.Id != p.Id)).ToList(); - - foreach (var process in started) - { - logger.Debug($"Process {process} has been started."); - processes.Add(process); - - foreach (var application in blacklist) - { - if (BelongsToApplication(process, application)) - { - logger.Warn($"Process {process} belongs to blacklisted application '{application.ExecutableName}'! Attempting termination..."); - - var success = TryTerminate(process); - - if (!success) - { - // TODO: Invoke event -> Show lock screen! - } - } - } - } - - foreach (var process in terminated) - { - logger.Debug($"Process {process} has been terminated."); - processes.Remove(process); - } - - timer.Start(); - } - - private void SystemEvent_WindowChanged(IntPtr window) - { - if (window != IntPtr.Zero && activeWindow != window) - { - logger.Debug($"Window has changed from {activeWindow} to {window}."); - activeWindow = window; - Check(window); - } - } } } diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/ClientOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/ClientOperationTests.cs index 86e6de2c..1e120631 100644 --- a/SafeExamBrowser.Runtime.UnitTests/Operations/ClientOperationTests.cs +++ b/SafeExamBrowser.Runtime.UnitTests/Operations/ClientOperationTests.cs @@ -172,7 +172,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations proxy.Verify(p => p.InitiateShutdown(), Times.Once); proxy.Verify(p => p.Disconnect(), Times.Once); - process.Verify(p => p.TryKill(default(int)), Times.Never); + process.Verify(p => p.TryKill(It.IsAny()), Times.Never); Assert.IsNull(sessionContext.ClientProcess); Assert.IsNull(sessionContext.ClientProxy); @@ -181,12 +181,12 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations [TestMethod] public void Revert_MustKillClientIfStoppingFailed() { - process.Setup(p => p.TryKill(default(int))).Callback(() => process.SetupGet(p => p.HasTerminated).Returns(true)); + process.Setup(p => p.TryKill(It.IsAny())).Callback(() => process.SetupGet(p => p.HasTerminated).Returns(true)); PerformNormally(); sut.Revert(); - process.Verify(p => p.TryKill(default(int)), Times.AtLeastOnce); + process.Verify(p => p.TryKill(It.IsAny()), Times.AtLeastOnce); Assert.IsNull(sessionContext.ClientProcess); Assert.IsNull(sessionContext.ClientProxy); @@ -198,7 +198,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations PerformNormally(); sut.Revert(); - process.Verify(p => p.TryKill(default(int)), Times.Exactly(5)); + process.Verify(p => p.TryKill(It.IsAny()), Times.Exactly(5)); Assert.IsNotNull(sessionContext.ClientProcess); Assert.IsNotNull(sessionContext.ClientProxy); @@ -213,7 +213,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations proxy.Verify(p => p.InitiateShutdown(), Times.Never); proxy.Verify(p => p.Disconnect(), Times.Never); - process.Verify(p => p.TryKill(default(int)), Times.Never); + process.Verify(p => p.TryKill(It.IsAny()), Times.Never); Assert.IsNull(sessionContext.ClientProcess); Assert.IsNull(sessionContext.ClientProxy); diff --git a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs index 8696f5fd..5eb551ff 100644 --- a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs @@ -248,7 +248,7 @@ namespace SafeExamBrowser.Runtime.Operations { logger.Info($"Attempt {attempt}/{MAX_ATTEMPTS} to kill client process with ID = {ClientProcess.Id}."); - if (ClientProcess.TryKill()) + if (ClientProcess.TryKill(500)) { break; }