diff --git a/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs b/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs index 7295bbb1..33eaf00c 100644 --- a/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs +++ b/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs @@ -145,7 +145,7 @@ namespace SafeExamBrowser.Lockdown.UnitTests Thread.Sleep(25); sut.Stop(); - backup.Verify(b => b.GetAllConfigurations(), Times.Exactly(counter)); + backup.Verify(b => b.GetAllConfigurations(), Times.Between(counter, counter + 1, Range.Inclusive)); } [TestMethod] diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurationFactory.cs b/SafeExamBrowser.Lockdown/FeatureConfigurationFactory.cs index 6c8590ac..4c05055b 100644 --- a/SafeExamBrowser.Lockdown/FeatureConfigurationFactory.cs +++ b/SafeExamBrowser.Lockdown/FeatureConfigurationFactory.cs @@ -9,9 +9,9 @@ using System; using SafeExamBrowser.Contracts.Lockdown; using SafeExamBrowser.Contracts.Logging; -using SafeExamBrowser.Lockdown.FeatureConfigurations; using SafeExamBrowser.Lockdown.FeatureConfigurations.RegistryConfigurations.MachineHive; using SafeExamBrowser.Lockdown.FeatureConfigurations.RegistryConfigurations.UserHive; +using SafeExamBrowser.Lockdown.FeatureConfigurations.ServiceConfigurations; namespace SafeExamBrowser.Lockdown { diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceConfiguration.cs b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceConfiguration.cs new file mode 100644 index 00000000..5cbc312f --- /dev/null +++ b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceConfiguration.cs @@ -0,0 +1,288 @@ +/* + * 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; +using System.Collections.Generic; +using System.Linq; +using System.ServiceProcess; +using SafeExamBrowser.Contracts.Lockdown; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Lockdown.FeatureConfigurations.ServiceConfigurations +{ + [Serializable] + internal abstract class ServiceConfiguration : FeatureConfiguration + { + private static readonly TimeSpan FIVE_SECONDS = TimeSpan.FromSeconds(5); + private IList originalItems; + + protected abstract IEnumerable Items { get; } + + public ServiceConfiguration(Guid groupId, ILogger logger) : base(groupId, logger) + { + originalItems = new List(); + } + + public override bool DisableFeature() + { + var success = true; + + foreach (var item in Items) + { + success &= TrySet(new ServiceDataItem { Name = item.Name, Status = item.Disabled }); + } + + if (success) + { + logger.Info("Successfully disabled feature."); + } + else + { + logger.Warn("Failed to disable feature!"); + } + + return success; + } + + public override bool EnableFeature() + { + var success = true; + + foreach (var item in Items) + { + success &= TrySet(new ServiceDataItem { Name = item.Name, Status = item.Enabled }); + } + + if (success) + { + logger.Info("Successfully enabled feature."); + } + else + { + logger.Warn("Failed to enable feature!"); + } + + return success; + } + + public override FeatureConfigurationStatus GetStatus() + { + var status = FeatureConfigurationStatus.Undefined; + + foreach (var item in Items) + { + var current = ReadService(item.Name); + + if (current.Status == item.Disabled && status != FeatureConfigurationStatus.Enabled) + { + status = FeatureConfigurationStatus.Disabled; + } + else if (current.Status == item.Enabled && status != FeatureConfigurationStatus.Disabled) + { + status = FeatureConfigurationStatus.Enabled; + } + else + { + status = FeatureConfigurationStatus.Undefined; + + break; + } + } + + return status; + } + + public override void Initialize() + { + foreach (var item in Items) + { + var original = ReadService(item.Name); + + if (original.Status != ServiceStatus.NotAvailable) + { + originalItems.Add(original); + } + } + } + + public override bool Restore() + { + foreach (var item in new List(originalItems)) + { + if (TrySet(item)) + { + originalItems.Remove(item); + } + } + + var success = !originalItems.Any(); + + if (success) + { + logger.Info("Successfully restored feature."); + } + else + { + logger.Warn("Failed to restore feature!"); + } + + return success; + } + + private ServiceDataItem ReadService(string name) + { + var item = new ServiceDataItem { Name = name, Status = ServiceStatus.NotAvailable }; + + try + { + using (var service = new ServiceController(name)) + { + item.Status = ToStatus(service.Status); + } + } + catch (Exception e) + { + logger.Error($"Failed to retrieve status of service '{name}'!", e); + } + + return item; + } + + private bool TrySet(ServiceDataItem item) + { + var success = false; + + try + { + if (IsAvailable(item)) + { + using (var service = new ServiceController(item.Name)) + { + if (item.Status == ServiceStatus.Running) + { + success = TryStart(service); + } + else if (item.Status == ServiceStatus.Stopped) + { + success = TryStop(service); + } + + if (success) + { + logger.Debug($"Successfully set service {item}."); + } + else + { + logger.Warn($"Could not set service {item}! Current status: {service.Status}."); + } + } + } + else + { + logger.Warn($"Cannot set service {item} as it does not exist!"); + } + } + catch (Exception e) + { + logger.Error($"Failed to set service {item}!", e); + } + + return success; + } + + private bool TryStart(ServiceController service) + { + var success = true; + + if (service.Status == ServiceControllerStatus.PausePending) + { + service.WaitForStatus(ServiceControllerStatus.Paused, FIVE_SECONDS); + service.Refresh(); + } + + if (service.Status == ServiceControllerStatus.Paused) + { + service.Continue(); + service.Refresh(); + } + + if (service.Status == ServiceControllerStatus.Stopped) + { + service.Start(); + service.Refresh(); + } + + if (service.Status == ServiceControllerStatus.StartPending || service.Status == ServiceControllerStatus.ContinuePending) + { + service.WaitForStatus(ServiceControllerStatus.Running, FIVE_SECONDS); + service.Refresh(); + } + + if (service.Status == ServiceControllerStatus.Running) + { + success = true; + } + + return success; + } + + private bool TryStop(ServiceController service) + { + var success = false; + + if (service.Status == ServiceControllerStatus.Paused) + { + service.Continue(); + service.Refresh(); + } + + if (service.Status == ServiceControllerStatus.StartPending || service.Status == ServiceControllerStatus.ContinuePending) + { + service.WaitForStatus(ServiceControllerStatus.Running, FIVE_SECONDS); + service.Refresh(); + } + + if (service.Status == ServiceControllerStatus.Running) + { + service.Stop(); + service.Refresh(); + } + + if (service.Status == ServiceControllerStatus.StopPending) + { + service.WaitForStatus(ServiceControllerStatus.Stopped, FIVE_SECONDS); + service.Refresh(); + } + + if (service.Status == ServiceControllerStatus.Stopped) + { + success = true; + } + + return success; + } + + private bool IsAvailable(ServiceDataItem item) + { + return ServiceController.GetServices().Any(s => s.ServiceName == item.Name); + } + + private ServiceStatus ToStatus(ServiceControllerStatus status) + { + switch (status) + { + case ServiceControllerStatus.ContinuePending: + case ServiceControllerStatus.Running: + case ServiceControllerStatus.StartPending: + return ServiceStatus.Running; + default: + return ServiceStatus.Stopped; + } + } + } +} diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceConfigurationItem.cs b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceConfigurationItem.cs new file mode 100644 index 00000000..1acd1747 --- /dev/null +++ b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceConfigurationItem.cs @@ -0,0 +1,24 @@ +/* + * 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/. + */ + +namespace SafeExamBrowser.Lockdown.FeatureConfigurations.ServiceConfigurations +{ + internal class ServiceConfigurationItem + { + internal ServiceStatus Disabled { get; } + internal ServiceStatus Enabled { get; } + internal string Name { get; } + + internal ServiceConfigurationItem(string name, ServiceStatus disabled, ServiceStatus enabled) + { + Disabled = disabled; + Enabled = enabled; + Name = name; + } + } +} diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceDataItem.cs b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceDataItem.cs new file mode 100644 index 00000000..9c24dfd7 --- /dev/null +++ b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceDataItem.cs @@ -0,0 +1,24 @@ +/* + * 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; + +namespace SafeExamBrowser.Lockdown.FeatureConfigurations.ServiceConfigurations +{ + [Serializable] + internal class ServiceDataItem + { + internal string Name { get; set; } + internal ServiceStatus Status { get; set; } + + public override string ToString() + { + return $@"'{Name}' => '{(Status == ServiceStatus.Running ? ServiceStatus.Running.ToString().ToLower() : ServiceStatus.Stopped.ToString().ToLower())}'"; + } + } +} diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceStatus.cs b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceStatus.cs new file mode 100644 index 00000000..4d495e58 --- /dev/null +++ b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/ServiceStatus.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/. + */ + +namespace SafeExamBrowser.Lockdown.FeatureConfigurations.ServiceConfigurations +{ + internal enum ServiceStatus + { + NotAvailable, + Running, + Stopped + } +} diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/WindowsUpdateConfiguration.cs b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/WindowsUpdateConfiguration.cs new file mode 100644 index 00000000..b3eb2b5f --- /dev/null +++ b/SafeExamBrowser.Lockdown/FeatureConfigurations/ServiceConfigurations/WindowsUpdateConfiguration.cs @@ -0,0 +1,27 @@ +/* + * 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; +using System.Collections.Generic; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Lockdown.FeatureConfigurations.ServiceConfigurations +{ + [Serializable] + internal class WindowsUpdateConfiguration : ServiceConfiguration + { + protected override IEnumerable Items => new [] + { + new ServiceConfigurationItem("wuauserv", ServiceStatus.Stopped, ServiceStatus.Running) + }; + + public WindowsUpdateConfiguration(Guid groupId, ILogger logger) : base(groupId, logger) + { + } + } +} diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurations/WindowsUpdateConfiguration.cs b/SafeExamBrowser.Lockdown/FeatureConfigurations/WindowsUpdateConfiguration.cs deleted file mode 100644 index 56bbda17..00000000 --- a/SafeExamBrowser.Lockdown/FeatureConfigurations/WindowsUpdateConfiguration.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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; -using SafeExamBrowser.Contracts.Lockdown; -using SafeExamBrowser.Contracts.Logging; - -namespace SafeExamBrowser.Lockdown.FeatureConfigurations -{ - [Serializable] - internal class WindowsUpdateConfiguration : FeatureConfiguration - { - public WindowsUpdateConfiguration(Guid groupId, ILogger logger) : base(groupId, logger) - { - } - - public override bool DisableFeature() - { - return true; - } - - public override bool EnableFeature() - { - return true; - } - - public override FeatureConfigurationStatus GetStatus() - { - return FeatureConfigurationStatus.Undefined; - } - - public override void Initialize() - { - - } - - public override bool Restore() - { - return true; - } - } -} diff --git a/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj b/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj index 2aa3e54c..c9209690 100644 --- a/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj +++ b/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj @@ -51,6 +51,7 @@ + @@ -74,7 +75,11 @@ - + + + + +