From 7f38c0b8c346e4257cec6f4b928dfbc37289372b Mon Sep 17 00:00:00 2001 From: dbuechel Date: Thu, 22 Nov 2018 14:36:20 +0100 Subject: [PATCH] SEBWIN-221: Implemented basic password decryption of binary configuration files and finally found correct solution for multi-threading issues related to WPF. --- .../BrowserApplicationController.cs | 2 +- .../SafeExamBrowser.Client.UnitTests.csproj | 13 +- SafeExamBrowser.Client.UnitTests/app.config | 2 +- .../packages.config | 2 +- .../Proxies/BaseProxy.cs | 2 +- .../Compression/GZipCompressor.cs | 3 +- .../ConfigurationRepository.cs | 10 +- .../DataFormats/BinaryFormat.Password.cs | 155 ++++++++++++++++++ .../DataFormats/BinaryFormat.PlainData.cs | 48 ++++++ .../DataFormats/BinaryFormat.PublicKeyHash.cs | 23 +++ ...aryFormat.PublicKeyHashWithSymmetricKey.cs | 23 +++ .../DataFormats/BinaryFormat.cs | 62 +++++-- .../DataFormats/XmlFormat.cs | 2 +- .../SafeExamBrowser.Configuration.csproj | 19 +++ SafeExamBrowser.Configuration/SubStream.cs | 141 ++++++++++++++++ SafeExamBrowser.Configuration/packages.config | 4 + .../Configuration/IConfigurationRepository.cs | 4 +- .../Configuration/IDataFormat.cs | 4 +- .../Configuration/LoadStatus.cs | 11 +- .../Operations/ConfigurationOperationTests.cs | 67 ++++---- .../SafeExamBrowser.Runtime.UnitTests.csproj | 5 +- SafeExamBrowser.Runtime.UnitTests/app.config | 2 +- .../packages.config | 2 +- SafeExamBrowser.Runtime/App.cs | 23 +-- .../Operations/ClientOperation.cs | 53 +++--- .../Operations/ConfigurationOperation.cs | 31 +--- .../PasswordDialog.xaml | 4 +- .../PasswordDialog.xaml.cs | 11 ++ .../RuntimeWindow.xaml.cs | 4 + .../UserInterfaceFactory.cs | 21 +-- 30 files changed, 589 insertions(+), 164 deletions(-) create mode 100644 SafeExamBrowser.Configuration/DataFormats/BinaryFormat.Password.cs create mode 100644 SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PlainData.cs create mode 100644 SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PublicKeyHash.cs create mode 100644 SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PublicKeyHashWithSymmetricKey.cs create mode 100644 SafeExamBrowser.Configuration/SubStream.cs create mode 100644 SafeExamBrowser.Configuration/packages.config diff --git a/SafeExamBrowser.Browser/BrowserApplicationController.cs b/SafeExamBrowser.Browser/BrowserApplicationController.cs index 063c9f59..81fc220f 100644 --- a/SafeExamBrowser.Browser/BrowserApplicationController.cs +++ b/SafeExamBrowser.Browser/BrowserApplicationController.cs @@ -127,7 +127,7 @@ namespace SafeExamBrowser.Browser private void Button_OnClick(InstanceIdentifier id = null) { - if (id is null) + if (id == null) { CreateNewInstance(); } diff --git a/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj b/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj index 08a78944..f555208c 100644 --- a/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj +++ b/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj @@ -71,9 +71,8 @@ ..\packages\System.Threading.Tasks.Extensions.4.4.0\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll - - ..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll - True + + ..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll @@ -96,8 +95,12 @@ - - + + Designer + + + Designer + diff --git a/SafeExamBrowser.Client.UnitTests/app.config b/SafeExamBrowser.Client.UnitTests/app.config index 7b7e0134..0f9345fa 100644 --- a/SafeExamBrowser.Client.UnitTests/app.config +++ b/SafeExamBrowser.Client.UnitTests/app.config @@ -8,7 +8,7 @@ - + diff --git a/SafeExamBrowser.Client.UnitTests/packages.config b/SafeExamBrowser.Client.UnitTests/packages.config index 3912bdfc..19774a67 100644 --- a/SafeExamBrowser.Client.UnitTests/packages.config +++ b/SafeExamBrowser.Client.UnitTests/packages.config @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/SafeExamBrowser.Communication/Proxies/BaseProxy.cs b/SafeExamBrowser.Communication/Proxies/BaseProxy.cs index bb31c3c6..75d9cb39 100644 --- a/SafeExamBrowser.Communication/Proxies/BaseProxy.cs +++ b/SafeExamBrowser.Communication/Proxies/BaseProxy.cs @@ -229,7 +229,7 @@ namespace SafeExamBrowser.Communication.Proxies private void FailIfNull(Message message) { - if (message is null) + if (message == null) { throw new ArgumentNullException(nameof(message)); } diff --git a/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs b/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs index 43d09b93..e10d542d 100644 --- a/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs +++ b/SafeExamBrowser.Configuration/Compression/GZipCompressor.cs @@ -98,10 +98,11 @@ namespace SafeExamBrowser.Configuration.Compression public byte[] Peek(Stream data, int count) { + var stream = new GZipStream(data, CompressionMode.Decompress); + logger.Debug($"Peeking {count} bytes from '{data}'..."); data.Seek(0, SeekOrigin.Begin); - using (var stream = new GZipStream(data, CompressionMode.Decompress)) using (var decompressed = new MemoryStream()) { var buffer = new byte[count]; diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs index 141be2c6..c566a060 100644 --- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs +++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs @@ -93,6 +93,8 @@ namespace SafeExamBrowser.Configuration { var settings = new Settings(); + // TODO: Specify default settings + settings.KioskMode = KioskMode.None; settings.ServicePolicy = ServicePolicy.Optional; @@ -122,7 +124,7 @@ namespace SafeExamBrowser.Configuration resourceLoaders.Add(resourceLoader); } - public LoadStatus TryLoadSettings(Uri resource, out Settings settings, string adminPassword = null, string settingsPassword = null) + public LoadStatus TryLoadSettings(Uri resource, out Settings settings, string password = null) { settings = default(Settings); @@ -139,7 +141,7 @@ namespace SafeExamBrowser.Configuration case LoadStatus.LoadWithBrowser: return HandleBrowserResource(resource, out settings); case LoadStatus.Success: - return TryParseData(data, out settings, adminPassword, settingsPassword); + return TryParseData(data, out settings, password); } } @@ -173,7 +175,7 @@ namespace SafeExamBrowser.Configuration return status; } - private LoadStatus TryParseData(Stream data, out Settings settings, string adminPassword, string settingsPassword) + private LoadStatus TryParseData(Stream data, out Settings settings, string password) { var status = LoadStatus.NotSupported; var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data)); @@ -182,7 +184,7 @@ namespace SafeExamBrowser.Configuration if (dataFormat != null) { - status = dataFormat.TryParse(data, out settings, adminPassword, settingsPassword); + status = dataFormat.TryParse(data, out settings, password); logger.Info($"Tried to parse data from '{data}' using {dataFormat.GetType().Name} -> Result: {status}."); } else diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.Password.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.Password.cs new file mode 100644 index 00000000..000e5871 --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.Password.cs @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2018 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.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; + +namespace SafeExamBrowser.Configuration.DataFormats +{ + public partial class BinaryFormat + { + private const int BLOCK_SIZE = 16; + private const int HEADER_SIZE = 2; + private const int ITERATIONS = 10000; + private const int KEY_SIZE = 32; + private const int OPTIONS = 0x1; + private const int SALT_SIZE = 8; + private const int VERSION = 0x2; + + private LoadStatus ParsePassword(Stream data, FormatType format, out Settings settings, string password = null) + { + settings = default(Settings); + + if (password == null) + { + return LoadStatus.PasswordNeeded; + } + + var (version, options) = ParseHeader(data); + + if (version != VERSION || options != OPTIONS) + { + return FailForInvalidPasswordHeader(version, options); + } + + if (format == FormatType.PasswordConfigureClient) + { + // TODO: Shouldn't this not only be done for admin password, and not settings password?!? + password = GeneratePasswordHash(password); + } + + var (authenticationKey, encryptionKey) = GenerateKeys(data, password); + var (originalHmac, computedHmac) = GenerateHmac(data, authenticationKey); + + if (!computedHmac.SequenceEqual(originalHmac)) + { + return FailForInvalidPasswordHmac(); + } + + using (var plainData = Decrypt(data, encryptionKey, originalHmac.Length)) + { + return ParsePlainData(plainData, out settings); + } + } + + private (int version, int options) ParseHeader(Stream data) + { + data.Seek(0, SeekOrigin.Begin); + + var version = data.ReadByte(); + var options = data.ReadByte(); + + return (version, options); + } + + private LoadStatus FailForInvalidPasswordHeader(int version, int options) + { + logger.Error($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]"); + + return LoadStatus.InvalidData; + } + + private string GeneratePasswordHash(string input) + { + using (var algorithm = new SHA256Managed()) + { + var bytes = Encoding.UTF8.GetBytes(input); + var hash = algorithm.ComputeHash(bytes); + var @string = String.Join(String.Empty, hash.Select(b => b.ToString("x2"))); + + return @string; + } + } + + private (byte[] authenticationKey, byte[] encryptionKey) GenerateKeys(Stream data, string password) + { + var authenticationSalt = new byte[SALT_SIZE]; + var encryptionSalt = new byte[SALT_SIZE]; + + data.Seek(HEADER_SIZE, SeekOrigin.Begin); + data.Read(encryptionSalt, 0, SALT_SIZE); + data.Read(authenticationSalt, 0, SALT_SIZE); + + using (var authenticationGenerator = new Rfc2898DeriveBytes(password, authenticationSalt, ITERATIONS)) + using (var encryptionGenerator = new Rfc2898DeriveBytes(password, encryptionSalt, ITERATIONS)) + { + var authenticationKey = authenticationGenerator.GetBytes(KEY_SIZE); + var encryptionKey = encryptionGenerator.GetBytes(KEY_SIZE); + + return (authenticationKey, encryptionKey); + } + } + + private (byte[] originalHmac, byte[] computedHmac) GenerateHmac(Stream data, byte[] authenticationKey) + { + using (var hmac = new HMACSHA256(authenticationKey)) + { + var originalHmac = new byte[hmac.HashSize / 8]; + var hashStream = new SubStream(data, 0, data.Length - originalHmac.Length); + var computedHmac = hmac.ComputeHash(hashStream); + + data.Seek(originalHmac.Length, SeekOrigin.End); + data.Read(originalHmac, 0, originalHmac.Length); + + return (originalHmac, computedHmac); + } + } + + private LoadStatus FailForInvalidPasswordHmac() + { + logger.Warn($"The authentication failed due to an invalid password or corrupted data!"); + + return LoadStatus.PasswordNeeded; + } + + private Stream Decrypt(Stream data, byte[] encryptionKey, int hmacLength) + { + var initializationVector = new byte[BLOCK_SIZE]; + + data.Seek(HEADER_SIZE + 2 * SALT_SIZE, SeekOrigin.Begin); + data.Read(initializationVector, 0, BLOCK_SIZE); + + var decryptedData = new MemoryStream(); + var encryptedData = new SubStream(data, data.Position, data.Length - data.Position - hmacLength); + + using (var aes = new AesManaged { KeySize = KEY_SIZE * 8, BlockSize = BLOCK_SIZE * 8, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 }) + using (var decryptor = aes.CreateDecryptor(encryptionKey, initializationVector)) + using (var cryptoStream = new CryptoStream(encryptedData, decryptor, CryptoStreamMode.Read)) + { + cryptoStream.CopyTo(decryptedData); + } + + return decryptedData; + } + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PlainData.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PlainData.cs new file mode 100644 index 00000000..f811a4bd --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PlainData.cs @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018 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.IO; +using System.Text; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; + +namespace SafeExamBrowser.Configuration.DataFormats +{ + public partial class BinaryFormat + { + private LoadStatus ParsePlainData(Stream data, out Settings settings) + { + if (compressor.IsCompressed(data)) + { + data = compressor.Decompress(data); + } + + var buffer = new byte[4096]; + var bytesRead = 0; + var xmlBuilder = new StringBuilder(); + + data.Seek(0, SeekOrigin.Begin); + + do + { + bytesRead = data.Read(buffer, 0, buffer.Length); + xmlBuilder.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + } while (bytesRead > 0); + + var xml = xmlBuilder.ToString(); + + // TODO: Parse XML data... + + settings = new Settings(); + settings.Browser.AllowAddressBar = true; + settings.Browser.AllowConfigurationDownloads = true; + + return LoadStatus.Success; + } + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PublicKeyHash.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PublicKeyHash.cs new file mode 100644 index 00000000..ed848ffa --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PublicKeyHash.cs @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018 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.IO; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; + +namespace SafeExamBrowser.Configuration.DataFormats +{ + public partial class BinaryFormat + { + private LoadStatus ParsePublicKeyHash(Stream data, out Settings settings, string password) + { + throw new NotImplementedException(); + } + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PublicKeyHashWithSymmetricKey.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PublicKeyHashWithSymmetricKey.cs new file mode 100644 index 00000000..439f9d8f --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.PublicKeyHashWithSymmetricKey.cs @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018 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.IO; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; + +namespace SafeExamBrowser.Configuration.DataFormats +{ + public partial class BinaryFormat + { + private LoadStatus ParsePublicKeyHashWithSymmetricKey(Stream data, out Settings settings, string password) + { + throw new NotImplementedException(); + } + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs index ecbb1e3a..0e96d1a6 100644 --- a/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/BinaryFormat.cs @@ -15,7 +15,7 @@ using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.DataFormats { - public class BinaryFormat : IDataFormat + public partial class BinaryFormat : IDataFormat { private const int PREFIX_LENGTH = 4; @@ -37,7 +37,7 @@ namespace SafeExamBrowser.Configuration.DataFormats if (longEnough) { var prefix = ParsePrefix(data); - var success = TryDetermineFormat(prefix, out DataFormat format); + var success = TryDetermineFormat(prefix, out FormatType format); logger.Debug($"'{data}' starting with '{prefix}' does {(success ? string.Empty : "not ")}match the binary format."); @@ -54,14 +54,40 @@ namespace SafeExamBrowser.Configuration.DataFormats return false; } - public LoadStatus TryParse(Stream data, out Settings settings, string adminPassword = null, string settingsPassword = null) + public LoadStatus TryParse(Stream data, out Settings settings, string password = null) { - settings = new Settings(); - settings.Browser.AllowAddressBar = true; - settings.Browser.StartUrl = "www.duckduckgo.com"; - settings.Browser.AllowConfigurationDownloads = true; + var prefix = ParsePrefix(data); + var success = TryDetermineFormat(prefix, out FormatType format); - return LoadStatus.Success; + settings = default(Settings); + + if (success) + { + if (compressor.IsCompressed(data)) + { + data = compressor.Decompress(data); + } + + data = new SubStream(data, PREFIX_LENGTH, data.Length - PREFIX_LENGTH); + + // TODO: Try to abstract (Parser -> Binary, Xml, ...; DataBlock -> Password, PlainData, ...) once fully implemented! + switch (format) + { + case FormatType.Password: + case FormatType.PasswordConfigureClient: + return ParsePassword(data, format, out settings, password); + case FormatType.PlainData: + return ParsePlainData(data, out settings); + case FormatType.PublicKeyHash: + return ParsePublicKeyHash(data, out settings, password); + case FormatType.PublicKeyHashSymmetricKey: + return ParsePublicKeyHashWithSymmetricKey(data, out settings, password); + } + } + + logger.Error($"'{data}' starting with '{prefix}' does not match the binary format!"); + + return LoadStatus.InvalidData; } private string ParsePrefix(Stream data) @@ -81,39 +107,39 @@ namespace SafeExamBrowser.Configuration.DataFormats return Encoding.UTF8.GetString(prefixData); } - private bool TryDetermineFormat(string prefix, out DataFormat format) + private bool TryDetermineFormat(string prefix, out FormatType format) { - format = default(DataFormat); + format = default(FormatType); switch (prefix) { case "pswd": - format = DataFormat.Password; + format = FormatType.Password; return true; case "pwcc": - format = DataFormat.PasswordForConfigureClient; + format = FormatType.PasswordConfigureClient; return true; case "plnd": - format = DataFormat.PlainData; + format = FormatType.PlainData; return true; case "pkhs": - format = DataFormat.PublicKeyHash; + format = FormatType.PublicKeyHash; return true; case "phsk": - format = DataFormat.PublicKeyHashWithSymmetricKey; + format = FormatType.PublicKeyHashSymmetricKey; return true; } return false; } - private enum DataFormat + private enum FormatType { Password = 1, - PasswordForConfigureClient, + PasswordConfigureClient, PlainData, PublicKeyHash, - PublicKeyHashWithSymmetricKey + PublicKeyHashSymmetricKey } } } diff --git a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs index 5b882fc7..a3fa0ae3 100644 --- a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs @@ -27,7 +27,7 @@ namespace SafeExamBrowser.Configuration.DataFormats return false; } - public LoadStatus TryParse(Stream data, out Settings settings, string adminPassword = null, string settingsPassword = null) + public LoadStatus TryParse(Stream data, out Settings settings, string password = null) { throw new System.NotImplementedException(); } diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index aa22a3c4..c3e497ad 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -49,18 +49,34 @@ + + ..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll + + + BinaryFormat.cs + + + BinaryFormat.cs + + + BinaryFormat.cs + + + BinaryFormat.cs + + @@ -69,5 +85,8 @@ SafeExamBrowser.Contracts + + + \ No newline at end of file diff --git a/SafeExamBrowser.Configuration/SubStream.cs b/SafeExamBrowser.Configuration/SubStream.cs new file mode 100644 index 00000000..7cdfaf41 --- /dev/null +++ b/SafeExamBrowser.Configuration/SubStream.cs @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2018 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.IO; + +namespace SafeExamBrowser.Configuration +{ + /// + /// A read-only wrapper for a subsection of another, larger stream. + /// TODO: Unit Test! + /// + internal class SubStream : Stream + { + private long length; + private long offset; + private Stream original; + + public override bool CanRead => original.CanRead; + public override bool CanSeek => original.CanSeek; + public override bool CanWrite => false; + public override long Length => length; + public override long Position { get; set; } + + /// + /// Creates a new wrapper for the specified subsection of the given stream. + /// + /// + /// + /// Below an example of a subsection within a stream: + /// + /// +==============+==============================================================+==============================+ + /// | ... |####################### subsection ###########################| ... | + /// +==============+==============================================================+==============================+ + /// ^ ^ ^ ^ + /// | | | | + /// | + offset + length | + /// | | + /// + start of original end of original + + /// + /// + /// In case the original stream does not support . + /// In case the original stream does not support . + /// In case the specified subsection is outside the bounds of the original stream. + public SubStream(Stream original, long offset, long length) + { + this.original = original; + this.offset = offset; + this.length = length; + + if (!original.CanRead) + { + throw new ArgumentException("The original stream must support reading!", nameof(original)); + } + + if (!original.CanSeek) + { + throw new ArgumentException("The original stream must support seeking!", nameof(original)); + } + + if (original.Length < offset + length || offset < 0 || length < 1) + { + throw new ArgumentOutOfRangeException($"Specified subsection is outside the bounds of the original stream!"); + } + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var originalPosition = original.Position; + + if (Position < 0 || Position >= Length) + { + return 0; + } + + if (Position + count >= Length) + { + count = Convert.ToInt32(Length - Position); + } + + original.Seek(this.offset + Position, SeekOrigin.Begin); + + var bytesRead = original.Read(buffer, offset, count); + + Position += bytesRead; + original.Seek(originalPosition, SeekOrigin.Begin); + + return bytesRead; + } + + public override int ReadByte() + { + if (Position < 0 || Position >= Length) + { + return -1; + } + + return base.ReadByte(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + Position = offset; + break; + case SeekOrigin.Current: + Position += offset; + break; + case SeekOrigin.End: + Position = length - offset; + break; + default: + throw new NotImplementedException($"Seeking from position '{origin}' is not implemented!"); + } + + return Position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/SafeExamBrowser.Configuration/packages.config b/SafeExamBrowser.Configuration/packages.config new file mode 100644 index 00000000..8653b8e4 --- /dev/null +++ b/SafeExamBrowser.Configuration/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs index f2362488..fd68ebda 100644 --- a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs +++ b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs @@ -41,9 +41,9 @@ namespace SafeExamBrowser.Contracts.Configuration void Register(IResourceLoader resourceLoader); /// - /// Attempts to load settings from the specified resource, using the optional passwords. As long as the result is not + /// Attempts to load settings from the specified resource, using the optional password. As long as the result is not /// , the referenced settings may be null or in an undefinable state! /// - LoadStatus TryLoadSettings(Uri resource, out Settings.Settings settings, string adminPassword = null, string settingsPassword = null); + LoadStatus TryLoadSettings(Uri resource, out Settings.Settings settings, string password = null); } } diff --git a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs index 668f6289..dd2c6c37 100644 --- a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs +++ b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs @@ -21,9 +21,9 @@ namespace SafeExamBrowser.Contracts.Configuration bool CanParse(Stream data); /// - /// Tries to parse the given data, using the optional passwords. As long as the result is not , + /// Tries to parse the given data, using the optional password. As long as the result is not , /// the referenced settings may be null or in an undefinable state! /// - LoadStatus TryParse(Stream data, out Settings.Settings settings, string adminPassword = null, string settingsPassword = null); + LoadStatus TryParse(Stream data, out Settings.Settings settings, string password = null); } } diff --git a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs index 698d08f2..295d661d 100644 --- a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs +++ b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs @@ -13,15 +13,10 @@ namespace SafeExamBrowser.Contracts.Configuration /// public enum LoadStatus { - /// - /// Indicates that an admin password is needed in order to load the settings. - /// - AdminPasswordNeeded = 1, - /// /// Indicates that a resource does not comply with the declared data format. /// - InvalidData, + InvalidData = 1, /// /// Indicates that a resource needs to be loaded with the browser. @@ -34,9 +29,9 @@ namespace SafeExamBrowser.Contracts.Configuration NotSupported, /// - /// Indicates that a settings password is needed in order to load the settings. + /// Indicates that a password is needed in order to load the settings. /// - SettingsPasswordNeeded, + PasswordNeeded, /// /// The settings were loaded successfully. diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs index 58d17b7f..786916fe 100644 --- a/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs +++ b/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs @@ -60,14 +60,14 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations appConfig.ProgramDataFolder = location; appConfig.AppDataFolder = location; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.Perform(); var resource = new Uri(url); - repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null, null), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null), Times.Once); } [TestMethod] @@ -78,14 +78,14 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations appConfig.ProgramDataFolder = location; appConfig.AppDataFolder = $@"{location}\WRONG"; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.Perform(); var resource = new Uri(Path.Combine(location, "SettingsDummy.txt")); - repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null, null), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null), Times.Once); } [TestMethod] @@ -95,14 +95,14 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations appConfig.AppDataFolder = location; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.Perform(); var resource = new Uri(Path.Combine(location, "SettingsDummy.txt")); - repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null, null), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null), Times.Once); } [TestMethod] @@ -127,7 +127,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations public void MustAbortIfWishedByUser() { appConfig.ProgramDataFolder = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations)); - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -146,7 +146,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations [TestMethod] public void MustNotAbortIfNotWishedByUser() { - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -166,7 +166,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations public void MustNotAllowToAbortIfNotInConfigureClientMode() { settings.ConfigurationMode = ConfigurationMode.Exam; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -218,7 +218,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations { var url = @"http://www.safeexambrowser.org/whatever.seb"; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.AdminPasswordNeeded); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.PasswordNeeded); sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -231,7 +231,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations sut.Perform(); - repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null, null), Times.Exactly(5)); + repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null), Times.Exactly(5)); } [TestMethod] @@ -239,7 +239,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations { var url = @"http://www.safeexambrowser.org/whatever.seb"; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.PasswordNeeded); sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -252,7 +252,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations sut.Perform(); - repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null, null), Times.Exactly(5)); + repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null), Times.Exactly(5)); } [TestMethod] @@ -261,8 +261,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var password = "test"; var url = @"http://www.safeexambrowser.org/whatever.seb"; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.AdminPasswordNeeded); - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, password, null)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.PasswordNeeded); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, password)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -276,8 +276,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations sut.Perform(); - repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null, null), Times.Once); - repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, password, null), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, password), Times.Once); } [TestMethod] @@ -286,8 +286,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var password = "test"; var url = @"http://www.safeexambrowser.org/whatever.seb"; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, password)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.PasswordNeeded); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, password)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -301,8 +301,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations sut.Perform(); - repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null, null), Times.Once); - repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null, password), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, password), Times.Once); } [TestMethod] @@ -310,7 +310,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations { var url = @"http://www.safeexambrowser.org/whatever.seb"; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.AdminPasswordNeeded); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.PasswordNeeded); sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -331,7 +331,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations { var url = @"http://www.safeexambrowser.org/whatever.seb"; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.PasswordNeeded); sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -354,9 +354,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var settingsPassword = "abc"; var url = @"http://www.safeexambrowser.org/whatever.seb"; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, settingsPassword)).Returns(LoadStatus.AdminPasswordNeeded); - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, adminPassword, settingsPassword)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.PasswordNeeded); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, settingsPassword)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => @@ -370,9 +369,9 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations sut.Perform(); - repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null, null), Times.Once); - repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null, settingsPassword), Times.Once); - repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, adminPassword, settingsPassword), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, null), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, settingsPassword), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, settingsPassword), Times.Once); } [TestMethod] @@ -382,13 +381,13 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var resource = new Uri(Path.Combine(location, nameof(Operations), "SettingsDummy.txt")); sessionContext.ReconfigurationFilePath = resource.LocalPath; - repository.Setup(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null, null)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); var result = sut.Repeat(); - repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null, null), Times.Once); + repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null), Times.Once); Assert.AreEqual(OperationResult.Success, result); } @@ -399,19 +398,19 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var resource = new Uri("file:///C:/does/not/exist.txt"); sessionContext.ReconfigurationFilePath = null; - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); + repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null)).Returns(LoadStatus.Success); sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); var result = sut.Repeat(); - repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null, null), Times.Never); + repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null), Times.Never); Assert.AreEqual(OperationResult.Failed, result); sessionContext.ReconfigurationFilePath = resource.LocalPath; result = sut.Repeat(); - repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null, null), Times.Never); + repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null), Times.Never); Assert.AreEqual(OperationResult.Failed, result); } } diff --git a/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj b/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj index db89bd65..bf2e1a3e 100644 --- a/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj +++ b/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj @@ -73,9 +73,8 @@ ..\packages\System.Threading.Tasks.Extensions.4.4.0\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll - - ..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll - True + + ..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll diff --git a/SafeExamBrowser.Runtime.UnitTests/app.config b/SafeExamBrowser.Runtime.UnitTests/app.config index 7b7e0134..0f9345fa 100644 --- a/SafeExamBrowser.Runtime.UnitTests/app.config +++ b/SafeExamBrowser.Runtime.UnitTests/app.config @@ -8,7 +8,7 @@ - + diff --git a/SafeExamBrowser.Runtime.UnitTests/packages.config b/SafeExamBrowser.Runtime.UnitTests/packages.config index 3912bdfc..19774a67 100644 --- a/SafeExamBrowser.Runtime.UnitTests/packages.config +++ b/SafeExamBrowser.Runtime.UnitTests/packages.config @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/SafeExamBrowser.Runtime/App.cs b/SafeExamBrowser.Runtime/App.cs index 8dc0f138..d79a2c3e 100644 --- a/SafeExamBrowser.Runtime/App.cs +++ b/SafeExamBrowser.Runtime/App.cs @@ -8,6 +8,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using System.Windows; namespace SafeExamBrowser.Runtime @@ -60,6 +61,11 @@ namespace SafeExamBrowser.Runtime instances.BuildObjectGraph(Shutdown); instances.LogStartupInformation(); + Task.Run(new Action(TryStart)); + } + + private void TryStart() + { var success = instances.RuntimeController.TryStart(); if (!success) @@ -70,18 +76,15 @@ namespace SafeExamBrowser.Runtime public new void Shutdown() { - void shutdown() - { - instances.RuntimeController.Terminate(); - instances.LogShutdownInformation(); + Task.Run(new Action(ShutdownInternal)); + } - // TODO: Which UI operation is being cancelled without the timeout? Is this only a debugger issue? Same problem with client? -> Debug! - Thread.Sleep(20); + private void ShutdownInternal() + { + instances.RuntimeController.Terminate(); + instances.LogShutdownInformation(); - base.Shutdown(); - } - - Dispatcher.BeginInvoke(new Action(shutdown)); + Dispatcher.Invoke(base.Shutdown); } } } diff --git a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs index e21a5fdd..81815abf 100644 --- a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs @@ -159,34 +159,37 @@ namespace SafeExamBrowser.Runtime.Operations var terminatedEvent = new AutoResetEvent(false); var terminatedEventHandler = new ProcessTerminatedEventHandler((_) => terminatedEvent.Set()); - runtimeHost.ClientDisconnected += disconnectedEventHandler; - ClientProcess.Terminated += terminatedEventHandler; - - logger.Info("Instructing client to initiate shutdown procedure."); - ClientProxy.InitiateShutdown(); - - logger.Info("Disconnecting from client communication host."); - ClientProxy.Disconnect(); - - logger.Info("Waiting for client to disconnect from runtime communication host..."); - disconnected = disconnectedEvent.WaitOne(timeout_ms); - - if (!disconnected) + if (ClientProxy != null) { - logger.Error($"Client failed to disconnect within {timeout_ms / 1000} seconds!"); + runtimeHost.ClientDisconnected += disconnectedEventHandler; + ClientProcess.Terminated += terminatedEventHandler; + + logger.Info("Instructing client to initiate shutdown procedure."); + ClientProxy.InitiateShutdown(); + + logger.Info("Disconnecting from client communication host."); + ClientProxy.Disconnect(); + + logger.Info("Waiting for client to disconnect from runtime communication host..."); + disconnected = disconnectedEvent.WaitOne(timeout_ms); + + if (!disconnected) + { + logger.Error($"Client failed to disconnect within {timeout_ms / 1000} seconds!"); + } + + logger.Info("Waiting for client process to terminate..."); + terminated = terminatedEvent.WaitOne(timeout_ms); + + if (!terminated) + { + logger.Error($"Client failed to terminate within {timeout_ms / 1000} seconds!"); + } + + runtimeHost.ClientDisconnected -= disconnectedEventHandler; + ClientProcess.Terminated -= terminatedEventHandler; } - logger.Info("Waiting for client process to terminate..."); - terminated = terminatedEvent.WaitOne(timeout_ms); - - if (!terminated) - { - logger.Error($"Client failed to terminate within {timeout_ms / 1000} seconds!"); - } - - runtimeHost.ClientDisconnected -= disconnectedEventHandler; - ClientProcess.Terminated -= terminatedEventHandler; - if (disconnected && terminated) { logger.Info("Client has been successfully terminated."); diff --git a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs index 858f1bf0..34f56dc7 100644 --- a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs @@ -99,33 +99,18 @@ namespace SafeExamBrowser.Runtime.Operations private OperationResult LoadSettings(Uri uri) { - var adminPassword = default(string); - var settingsPassword = default(string); - var settings = default(Settings); - var status = default(LoadStatus); + var status = configuration.TryLoadSettings(uri, out Settings settings); - for (int adminAttempts = 0, settingsAttempts = 0; adminAttempts < 5 && settingsAttempts < 5;) + for (var attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++) { - status = configuration.TryLoadSettings(uri, out settings, adminPassword, settingsPassword); + var result = TryGetPassword(status); - if (status == LoadStatus.AdminPasswordNeeded || status == LoadStatus.SettingsPasswordNeeded) + if (!result.Success) { - var result = TryGetPassword(status); - - if (!result.Success) - { - return OperationResult.Aborted; - } - - adminAttempts += status == LoadStatus.AdminPasswordNeeded ? 1 : 0; - adminPassword = status == LoadStatus.AdminPasswordNeeded ? result.Password : adminPassword; - settingsAttempts += status == LoadStatus.SettingsPasswordNeeded ? 1 : 0; - settingsPassword = status == LoadStatus.SettingsPasswordNeeded ? result.Password : settingsPassword; - } - else - { - break; + return OperationResult.Aborted; } + + status = configuration.TryLoadSettings(uri, out settings, result.Password); } if (status == LoadStatus.Success) @@ -158,7 +143,7 @@ namespace SafeExamBrowser.Runtime.Operations private PasswordRequiredEventArgs TryGetPassword(LoadStatus status) { - var purpose = status == LoadStatus.AdminPasswordNeeded ? PasswordRequestPurpose.Administrator : PasswordRequestPurpose.Settings; + var purpose = PasswordRequestPurpose.Settings; var args = new PasswordRequiredEventArgs { Purpose = purpose }; ActionRequired?.Invoke(args); diff --git a/SafeExamBrowser.UserInterface.Classic/PasswordDialog.xaml b/SafeExamBrowser.UserInterface.Classic/PasswordDialog.xaml index c541bbaf..a8a44544 100644 --- a/SafeExamBrowser.UserInterface.Classic/PasswordDialog.xaml +++ b/SafeExamBrowser.UserInterface.Classic/PasswordDialog.xaml @@ -13,10 +13,10 @@ - + - + diff --git a/SafeExamBrowser.UserInterface.Classic/PasswordDialog.xaml.cs b/SafeExamBrowser.UserInterface.Classic/PasswordDialog.xaml.cs index 60c88e00..e637b40e 100644 --- a/SafeExamBrowser.UserInterface.Classic/PasswordDialog.xaml.cs +++ b/SafeExamBrowser.UserInterface.Classic/PasswordDialog.xaml.cs @@ -7,6 +7,7 @@ */ using System.Windows; +using System.Windows.Input; using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.UserInterface.Taskbar.Events; using SafeExamBrowser.Contracts.UserInterface.Windows; @@ -72,6 +73,7 @@ namespace SafeExamBrowser.UserInterface.Classic ConfirmButton.Click += ConfirmButton_Click; Closing += (o, args) => closing?.Invoke(); + Password.KeyUp += Password_KeyUp; } private void CancelButton_Click(object sender, RoutedEventArgs e) @@ -86,6 +88,15 @@ namespace SafeExamBrowser.UserInterface.Classic Close(); } + private void Password_KeyUp(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + DialogResult = true; + Close(); + } + } + private class PasswordDialogResult : IPasswordDialogResult { public string Password { get; set; } diff --git a/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml.cs b/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml.cs index 5fc4d1ba..f731a7f5 100644 --- a/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml.cs @@ -141,6 +141,10 @@ namespace SafeExamBrowser.UserInterface.Classic StatusTextBlock.DataContext = model; Closing += (o, args) => args.Cancel = !allowClose; + +#if DEBUG + Topmost = false; +#endif } private void RuntimeWindow_Loaded(object sender, RoutedEventArgs e) diff --git a/SafeExamBrowser.UserInterface.Classic/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Classic/UserInterfaceFactory.cs index 5415680e..f2c513a6 100644 --- a/SafeExamBrowser.UserInterface.Classic/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Classic/UserInterfaceFactory.cs @@ -90,26 +90,7 @@ namespace SafeExamBrowser.UserInterface.Classic public IRuntimeWindow CreateRuntimeWindow(AppConfig appConfig) { - RuntimeWindow runtimeWindow = null; - var windowReadyEvent = new AutoResetEvent(false); - var runtimeWindowThread = new Thread(() => - { - runtimeWindow = new RuntimeWindow(appConfig, text); - runtimeWindow.Closed += (o, args) => runtimeWindow.Dispatcher.InvokeShutdown(); - - windowReadyEvent.Set(); - - System.Windows.Threading.Dispatcher.Run(); - }); - - runtimeWindowThread.SetApartmentState(ApartmentState.STA); - runtimeWindowThread.Name = nameof(RuntimeWindow); - runtimeWindowThread.IsBackground = true; - runtimeWindowThread.Start(); - - windowReadyEvent.WaitOne(); - - return runtimeWindow; + return Application.Current.Dispatcher.Invoke(() => new RuntimeWindow(appConfig, text)); } public ISplashScreen CreateSplashScreen(AppConfig appConfig = null)