SEBWIN-221: Implemented changes in configuration algorithm to accomodate new functionality scope of 3.0 Alpha.

This commit is contained in:
dbuechel 2018-12-11 16:06:10 +01:00
parent 9b639b0c53
commit f9f2bb3257
24 changed files with 330 additions and 124 deletions

View file

@ -42,7 +42,7 @@ namespace SafeExamBrowser.Browser.Handlers
{
var uri = new Uri(downloadItem.Url);
var extension = Path.GetExtension(uri.AbsolutePath);
var isConfigFile = String.Equals(extension, appConfig.ConfigurationFileExtension, StringComparison.InvariantCultureIgnoreCase);
var isConfigFile = String.Equals(extension, appConfig.ConfigurationFileExtension, StringComparison.OrdinalIgnoreCase);
logger.Debug($"Handling download request for '{uri}'.");

View file

@ -24,9 +24,10 @@ namespace SafeExamBrowser.Configuration.UnitTests
public void Initialize()
{
var executablePath = Assembly.GetExecutingAssembly().Location;
var hashAlgorithm = new Mock<IHashAlgorithm>();
var logger = new Mock<ILogger>();
sut = new ConfigurationRepository(logger.Object, executablePath, string.Empty, string.Empty, string.Empty);
sut = new ConfigurationRepository(hashAlgorithm.Object, logger.Object, executablePath, string.Empty, string.Empty, string.Empty);
}
[TestMethod]

View file

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SafeExamBrowser.Configuration.DataFormats;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.Logging;
@ -26,15 +27,23 @@ namespace SafeExamBrowser.Configuration
private readonly string programVersion;
private AppConfig appConfig;
private IHashAlgorithm hashAlgorithm;
private IList<IDataFormat> dataFormats;
private ILogger logger;
private IList<IResourceLoader> resourceLoaders;
private ILogger logger;
public ConfigurationRepository(ILogger logger, string executablePath, string programCopyright, string programTitle, string programVersion)
public ConfigurationRepository(
IHashAlgorithm hashAlgorithm,
ILogger logger,
string executablePath,
string programCopyright,
string programTitle,
string programVersion)
{
dataFormats = new List<IDataFormat>();
resourceLoaders = new List<IResourceLoader>();
this.hashAlgorithm = hashAlgorithm;
this.logger = logger;
this.executablePath = executablePath ?? string.Empty;
this.programCopyright = programCopyright ?? string.Empty;
@ -124,7 +133,7 @@ namespace SafeExamBrowser.Configuration
resourceLoaders.Add(resourceLoader);
}
public LoadStatus TryLoadSettings(Uri resource, out Settings settings, string password = null, bool passwordIsHash = false)
public LoadStatus TryLoadSettings(Uri resource, PasswordInfo passwordInfo, out Settings settings)
{
logger.Info($"Attempting to load '{resource}'...");
@ -136,15 +145,17 @@ namespace SafeExamBrowser.Configuration
using (data)
{
switch (status)
if (status == LoadStatus.LoadWithBrowser)
{
case LoadStatus.LoadWithBrowser:
return HandleBrowserResource(resource, settings);
case LoadStatus.Success:
return TryParseData(data, settings, password, passwordIsHash);
return HandleBrowserResource(resource, settings);
}
return status;
if (status != LoadStatus.Success)
{
return status;
}
return TryParseData(data, passwordInfo, resource, settings);
}
}
catch (Exception e)
@ -155,6 +166,35 @@ namespace SafeExamBrowser.Configuration
}
}
private void ExtractAndImportCertificates(IDictionary<string, object> data)
{
// TODO
}
private LoadStatus HandleBrowserResource(Uri resource, Settings settings)
{
settings.Browser.StartUrl = resource.AbsoluteUri;
logger.Info($"The resource needs authentication or is HTML data, loaded default settings with '{resource}' as startup URL.");
return LoadStatus.Success;
}
private void HandleParseSuccess(ParseResult result, Settings settings, PasswordInfo passwordInfo, Uri resource)
{
var appDataFile = new Uri(Path.Combine(appConfig.AppDataFolder, appConfig.DefaultSettingsFileName));
var programDataFile = new Uri(Path.Combine(appConfig.ProgramDataFolder, appConfig.DefaultSettingsFileName));
var isAppDataFile = resource.AbsolutePath.Equals(appDataFile.AbsolutePath, StringComparison.OrdinalIgnoreCase);
var isProgramDataFile = resource.AbsolutePath.Equals(programDataFile.AbsolutePath, StringComparison.OrdinalIgnoreCase);
logger.Info("Mapping settings data...");
result.RawData.MapTo(settings);
if (settings.ConfigurationMode == ConfigurationMode.ConfigureClient && !isAppDataFile && !isProgramDataFile)
{
result.Status = TryConfigureClient(result.RawData, settings, passwordInfo);
}
}
private LoadStatus TryLoadData(Uri resource, out Stream data)
{
var status = LoadStatus.NotSupported;
@ -175,15 +215,23 @@ namespace SafeExamBrowser.Configuration
return status;
}
private LoadStatus TryParseData(Stream data, Settings settings, string password = null, bool passwordIsHash = false)
private LoadStatus TryParseData(Stream data, PasswordInfo passwordInfo, Uri resource, Settings settings)
{
var status = LoadStatus.NotSupported;
var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data));
var status = LoadStatus.NotSupported;
if (dataFormat != null)
{
status = dataFormat.TryParse(data, settings, password, passwordIsHash);
logger.Info($"Tried to parse data from '{data}' using {dataFormat.GetType().Name} -> Result: {status}.");
var result = dataFormat.TryParse(data, passwordInfo);
logger.Info($"Tried to parse data from '{data}' using {dataFormat.GetType().Name} -> Result: {result.Status}.");
if (result.Status == LoadStatus.Success || result.Status == LoadStatus.SuccessConfigureClient)
{
HandleParseSuccess(result, settings, passwordInfo, resource);
}
status = result.Status;
}
else
{
@ -193,12 +241,42 @@ namespace SafeExamBrowser.Configuration
return status;
}
private LoadStatus HandleBrowserResource(Uri resource, Settings settings)
private LoadStatus TryConfigureClient(IDictionary<string, object> data, Settings settings, PasswordInfo passwordInfo)
{
settings.Browser.StartUrl = resource.AbsoluteUri;
logger.Info($"The resource needs authentication or is HTML data, loaded default settings with '{resource}' as startup URL.");
logger.Info("Attempting to configure local client settings...");
return LoadStatus.Success;
if (passwordInfo.AdminPasswordHash != null)
{
var adminPasswordHash = passwordInfo.AdminPassword != null ? hashAlgorithm.GenerateHashFor(passwordInfo.AdminPassword) : null;
var settingsPasswordHash = passwordInfo.SettingsPassword != null ? hashAlgorithm.GenerateHashFor(passwordInfo.SettingsPassword) : null;
var enteredCorrectPassword = passwordInfo.AdminPasswordHash.Equals(adminPasswordHash, StringComparison.OrdinalIgnoreCase);
var sameAdminPassword = passwordInfo.AdminPasswordHash.Equals(settings.AdminPasswordHash, StringComparison.OrdinalIgnoreCase);
var knowsAdminPassword = passwordInfo.AdminPasswordHash.Equals(settingsPasswordHash, StringComparison.OrdinalIgnoreCase);
if (sameAdminPassword || knowsAdminPassword || enteredCorrectPassword)
{
logger.Info("Authentication was successful.");
}
else
{
logger.Info("Authentication has failed!");
return LoadStatus.AdminPasswordNeeded;
}
}
else
{
logger.Info("Authentication is not required.");
}
// -> Certificates need to be imported and REMOVED from the settings before the data is saved!
ExtractAndImportCertificates(data);
// Save configuration data as local client config under %APPDATA%!
// -> Default settings password for local client configuration appears to be string.Empty
// -> Local client configuration needs to again be encrypted in the same way as the original file was!!
return LoadStatus.SuccessConfigureClient;
}
private void UpdateAppConfig()

View file

@ -11,7 +11,6 @@ using System.IO;
using System.Text;
using SafeExamBrowser.Configuration.DataFormats.Cryptography;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.DataFormats
@ -57,7 +56,7 @@ namespace SafeExamBrowser.Configuration.DataFormats
return false;
}
public LoadStatus TryParse(Stream data, Settings settings, string password = null, bool passwordIsHash = false)
public ParseResult TryParse(Stream data, PasswordInfo passwordInfo)
{
var prefix = ParsePrefix(data);
var success = TryDetermineFormat(prefix, out FormatType format);
@ -76,22 +75,22 @@ namespace SafeExamBrowser.Configuration.DataFormats
{
case FormatType.Password:
case FormatType.PasswordConfigureClient:
return ParsePasswordBlock(data, format, settings, password, passwordIsHash);
return ParsePasswordBlock(data, passwordInfo, format);
case FormatType.PlainData:
return ParsePlainDataBlock(data, settings);
return ParsePlainDataBlock(data, passwordInfo);
case FormatType.PublicKeyHash:
return ParsePublicKeyHashBlock(data, settings, password, passwordIsHash);
return ParsePublicKeyHashBlock(data, passwordInfo);
case FormatType.PublicKeyHashWithSymmetricKey:
return ParsePublicKeyHashWithSymmetricKeyBlock(data, settings, password, passwordIsHash);
return ParsePublicKeyHashWithSymmetricKeyBlock(data, passwordInfo);
}
}
logger.Error($"'{data}' starting with '{prefix}' does not match the binary format!");
return LoadStatus.InvalidData;
return new ParseResult { Status = LoadStatus.InvalidData };
}
private LoadStatus ParsePasswordBlock(Stream data, FormatType format, Settings settings, string password, bool passwordIsHash)
private ParseResult ParsePasswordBlock(Stream data, PasswordInfo passwordInfo, FormatType format)
{
var decrypted = default(Stream);
var encryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption)));
@ -99,28 +98,32 @@ namespace SafeExamBrowser.Configuration.DataFormats
if (format == FormatType.PasswordConfigureClient)
{
password = password == null ? string.Empty : (passwordIsHash ? password : hashAlgorithm.GenerateHashFor(password));
status = encryption.Decrypt(data, out decrypted, password);
status = encryption.Decrypt(data, string.Empty, out decrypted);
if (status == LoadStatus.PasswordNeeded && passwordIsHash && password != string.Empty)
if (status == LoadStatus.SettingsPasswordNeeded && passwordInfo.AdminPasswordHash != null)
{
status = encryption.Decrypt(data, out decrypted, string.Empty);
status = encryption.Decrypt(data, passwordInfo.AdminPasswordHash, out decrypted);
}
if (status == LoadStatus.SettingsPasswordNeeded && passwordInfo.SettingsPassword != null)
{
status = encryption.Decrypt(data, hashAlgorithm.GenerateHashFor(passwordInfo.SettingsPassword), out decrypted);
}
}
else
{
status = encryption.Decrypt(data, out decrypted, password);
status = encryption.Decrypt(data, passwordInfo.SettingsPassword, out decrypted);
}
if (status == LoadStatus.Success)
{
return ParsePlainDataBlock(decrypted, settings);
return ParsePlainDataBlock(decrypted, passwordInfo);
}
return status;
return new ParseResult { Status = status };
}
private LoadStatus ParsePlainDataBlock(Stream data, Settings settings)
private ParseResult ParsePlainDataBlock(Stream data, PasswordInfo passwordInfo)
{
var xmlFormat = new XmlFormat(logger.CloneFor(nameof(XmlFormat)));
@ -129,23 +132,23 @@ namespace SafeExamBrowser.Configuration.DataFormats
data = compressor.Decompress(data);
}
return xmlFormat.TryParse(data, settings);
return xmlFormat.TryParse(data, passwordInfo);
}
private LoadStatus ParsePublicKeyHashBlock(Stream data, Settings settings, string password, bool passwordIsHash)
private ParseResult ParsePublicKeyHashBlock(Stream data, PasswordInfo passwordInfo)
{
var encryption = new PublicKeyHashEncryption(logger.CloneFor(nameof(PublicKeyHashEncryption)));
var status = encryption.Decrypt(data, out Stream decrypted);
if (status == LoadStatus.Success)
{
return TryParse(decrypted, settings, password, passwordIsHash);
return TryParse(decrypted, passwordInfo);
}
return status;
return new ParseResult { Status = status };
}
private LoadStatus ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, Settings settings, string password, bool passwordIsHash)
private ParseResult ParsePublicKeyHashWithSymmetricKeyBlock(Stream data, PasswordInfo passwordInfo)
{
var logger = this.logger.CloneFor(nameof(PublicKeyHashWithSymmetricKeyEncryption));
var passwordEncryption = new PasswordEncryption(logger.CloneFor(nameof(PasswordEncryption)));
@ -154,10 +157,10 @@ namespace SafeExamBrowser.Configuration.DataFormats
if (status == LoadStatus.Success)
{
return TryParse(decrypted, settings, password, passwordIsHash);
return TryParse(decrypted, passwordInfo);
}
return status;
return new ParseResult { Status = status };
}
private string ParsePrefix(Stream data)

View file

@ -31,13 +31,13 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
this.logger = logger;
}
internal LoadStatus Decrypt(Stream data, out Stream decrypted, string password)
internal LoadStatus Decrypt(Stream data, string password, out Stream decrypted)
{
decrypted = default(Stream);
if (password == null)
{
return LoadStatus.PasswordNeeded;
return LoadStatus.SettingsPasswordNeeded;
}
var (version, options) = ParseHeader(data);
@ -64,7 +64,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
if (version != VERSION || options != OPTIONS)
{
logger.Warn($"Invalid encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]");
logger.Debug($"Unknown encryption header! Expected: [{VERSION},{OPTIONS},...] - Actual: [{version},{options},...]");
}
return (version, options);
@ -110,9 +110,9 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
private LoadStatus FailForInvalidHmac()
{
logger.Warn($"The authentication failed due to an invalid password or corrupted data!");
logger.Debug($"The authentication failed due to an invalid password or corrupted data!");
return LoadStatus.PasswordNeeded;
return LoadStatus.SettingsPasswordNeeded;
}
private Stream Decrypt(Stream data, byte[] encryptionKey, int hmacLength)

View file

@ -39,7 +39,7 @@ namespace SafeExamBrowser.Configuration.DataFormats.Cryptography
var symmetricKey = ParseSymmetricKey(data, certificate);
var stream = new SubStream(data, data.Position, data.Length - data.Position);
var status = passwordEncryption.Decrypt(stream, out decrypted, symmetricKey);
var status = passwordEncryption.Decrypt(stream, symmetricKey, out decrypted);
return status;
}

View file

@ -14,7 +14,7 @@ namespace SafeExamBrowser.Configuration.DataFormats
{
internal static class DataMapper
{
internal static void MapTo(this Dictionary<string, object> rawData, Settings settings)
internal static void MapTo(this IDictionary<string, object> rawData, Settings settings)
{
foreach (var kvp in rawData)
{
@ -26,6 +26,9 @@ namespace SafeExamBrowser.Configuration.DataFormats
{
switch (key)
{
case "hashedAdminPassword":
settings.MapAdminPasswordHash(value);
break;
case "sebConfigPurpose":
settings.MapConfigurationMode(value);
break;
@ -35,6 +38,14 @@ namespace SafeExamBrowser.Configuration.DataFormats
}
}
private static void MapAdminPasswordHash(this Settings settings, object value)
{
if (value is string hash)
{
settings.AdminPasswordHash = hash;
}
}
private static void MapConfigurationMode(this Settings settings, object value)
{
if (value is Int32 mode)
@ -45,9 +56,9 @@ namespace SafeExamBrowser.Configuration.DataFormats
private static void MapStartUrl(this Settings settings, object value)
{
if (value is string startUrl)
if (value is string url)
{
settings.Browser.StartUrl = startUrl;
settings.Browser.StartUrl = url;
}
}
}

View file

@ -13,7 +13,6 @@ using System.Text;
using System.Xml;
using System.Xml.Linq;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.DataFormats
@ -59,9 +58,9 @@ namespace SafeExamBrowser.Configuration.DataFormats
return false;
}
public LoadStatus TryParse(Stream data, Settings settings, string password = null, bool passwordIsHash = false)
public ParseResult TryParse(Stream data, PasswordInfo passwordInfo)
{
var status = LoadStatus.InvalidData;
var result = new ParseResult { Status = LoadStatus.InvalidData };
var xmlSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore };
data.Seek(0, SeekOrigin.Begin);
@ -75,15 +74,11 @@ namespace SafeExamBrowser.Configuration.DataFormats
if (hasRoot && hasDictionary)
{
logger.Debug($"Found root node, starting to parse data...");
status = ParseDictionary(reader, rawData);
if (status == LoadStatus.Success)
{
logger.Debug("Mapping raw settings data...");
rawData.MapTo(settings);
}
result.Status = ParseDictionary(reader, rawData);
result.RawData = rawData;
logger.Debug($"Finished parsing -> Result: {status}.");
logger.Debug($"Finished parsing -> Result: {result.Status}.");
}
else
{
@ -91,7 +86,7 @@ namespace SafeExamBrowser.Configuration.DataFormats
}
}
return status;
return result;
}
private LoadStatus ParseArray(XmlReader reader, List<object> array)

View file

@ -13,8 +13,6 @@ namespace SafeExamBrowser.Contracts.Communication.Data
/// </summary>
public enum PasswordRequestPurpose
{
Undefined = 0,
/// <summary>
/// The password is to be used as administrator password for an application configuration.
/// </summary>

View file

@ -16,6 +16,11 @@ namespace SafeExamBrowser.Contracts.Communication.Hosts
/// </summary>
public interface IRuntimeHost : ICommunicationHost
{
/// <summary>
/// Determines whether another application component may establish a connection with the host.
/// </summary>
bool AllowConnection { get; set; }
/// <summary>
/// The startup token used for initial authentication.
/// </summary>

View file

@ -11,7 +11,7 @@ using System;
namespace SafeExamBrowser.Contracts.Configuration
{
/// <summary>
/// The repository which controls the loading and initializing of configuration data.
/// The repository which controls the loading and saving of configuration data.
/// </summary>
public interface IConfigurationRepository
{
@ -41,9 +41,9 @@ namespace SafeExamBrowser.Contracts.Configuration
void Register(IResourceLoader resourceLoader);
/// <summary>
/// Attempts to load settings from the specified resource, using the optional password. As long as the result is not
/// <see cref="LoadStatus.Success"/>, the referenced settings may be <c>null</c> or in an undefinable state!
/// Attempts to load settings from the specified resource. As long as the result is not <see cref="LoadStatus.Success"/>,
/// the referenced settings may be <c>null</c> or in an undefinable state!
/// </summary>
LoadStatus TryLoadSettings(Uri resource, out Settings.Settings settings, string password = null, bool passwordIsHash = false);
LoadStatus TryLoadSettings(Uri resource, PasswordInfo passwordInfo, out Settings.Settings settings);
}
}

View file

@ -21,9 +21,8 @@ namespace SafeExamBrowser.Contracts.Configuration
bool CanParse(Stream data);
/// <summary>
/// Tries to parse the given data, using the optional password. As long as the result is not <see cref="LoadStatus.Success"/>,
/// the referenced settings can be in an undefinable state!
/// Tries to parse the given data.
/// </summary>
LoadStatus TryParse(Stream data, Settings.Settings settings, string password = null, bool passwordIsHash = false);
ParseResult TryParse(Stream data, PasswordInfo passwordInfo);
}
}

View file

@ -14,9 +14,14 @@ namespace SafeExamBrowser.Contracts.Configuration
public enum LoadStatus
{
/// <summary>
/// Indicates that a resource does not comply with the declared data format.
/// Indicates that the current administrator password is needed to be allowed to configure the local client.
/// </summary>
InvalidData = 1,
AdminPasswordNeeded = 1,
/// <summary>
/// Indicates that a resource contains invalid data.
/// </summary>
InvalidData,
/// <summary>
/// Indicates that a resource needs to be loaded with the browser.
@ -29,15 +34,20 @@ namespace SafeExamBrowser.Contracts.Configuration
NotSupported,
/// <summary>
/// Indicates that a password is needed in order to load the settings.
/// Indicates that the settings password is needed in order to decrypt the settings.
/// </summary>
PasswordNeeded,
SettingsPasswordNeeded,
/// <summary>
/// The settings were loaded successfully.
/// </summary>
Success,
/// <summary>
/// The settings were loaded and the local client configuration was performed successfully.
/// </summary>
SuccessConfigureClient,
/// <summary>
/// An unexpected error occurred while trying to load the settings.
/// </summary>

View file

@ -0,0 +1,28 @@
/*
* 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.Collections.Generic;
namespace SafeExamBrowser.Contracts.Configuration
{
/// <summary>
/// Defines the result of a data parsing operation by an <see cref="IDataFormat"/>.
/// </summary>
public class ParseResult
{
/// <summary>
/// The parsed settings data. Might be <c>null</c> or in an undefinable state, depending on <see cref="Status"/>.
/// </summary>
public IDictionary<string, object> RawData { get; set; }
/// <summary>
/// The status result of a parsing operation.
/// </summary>
public LoadStatus Status { get; set; }
}
}

View file

@ -0,0 +1,31 @@
/*
* 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/.
*/
namespace SafeExamBrowser.Contracts.Configuration
{
/// <summary>
/// Holds all password data necessary to load an application configuration.
/// </summary>
public class PasswordInfo
{
/// <summary>
/// The current administrator password in plain text.
/// </summary>
public string AdminPassword { get; set; }
/// <summary>
/// The hash code of the current administrator password.
/// </summary>
public string AdminPasswordHash { get; set; }
/// <summary>
/// The settings password of the configuration in plain text.
/// </summary>
public string SettingsPassword { get; set; }
}
}

View file

@ -24,6 +24,8 @@ namespace SafeExamBrowser.Contracts.I18n
MessageBox_ConfigurationDownloadErrorTitle,
MessageBox_InvalidConfigurationData,
MessageBox_InvalidConfigurationDataTitle,
MessageBox_InvalidPasswordError,
MessageBox_InvalidPasswordErrorTitle,
MessageBox_NotSupportedConfigurationResource,
MessageBox_NotSupportedConfigurationResourceTitle,
MessageBox_Quit,

View file

@ -56,6 +56,8 @@
<Compile Include="Configuration\IDataCompressor.cs" />
<Compile Include="Configuration\IDataFormat.cs" />
<Compile Include="Configuration\IHashAlgorithm.cs" />
<Compile Include="Configuration\ParseResult.cs" />
<Compile Include="Configuration\PasswordInfo.cs" />
<Compile Include="Core\Events\InstanceTerminatedEventHandler.cs" />
<Compile Include="Core\Events\NameChangedEventHandler.cs" />
<Compile Include="Core\IApplicationController.cs" />

View file

@ -30,6 +30,12 @@
<Entry key="MessageBox_InvalidConfigurationDataTitle">
Configuration Error
</Entry>
<Entry key="MessageBox_InvalidPasswordError">
You failed to enter the correct password within 5 attempts. The application will now terminate...
</Entry>
<Entry key="MessageBox_InvalidPasswordErrorTitle">
Invalid Password
</Entry>
<Entry key="MessageBox_NotSupportedConfigurationResource">
The configuration resource '%%URI%%' is not supported!
</Entry>

View file

@ -17,8 +17,7 @@ namespace SafeExamBrowser.Runtime.Communication
{
internal class RuntimeHost : BaseHost, IRuntimeHost
{
private bool allowConnection = true;
public bool AllowConnection { get; set; }
public Guid StartupToken { private get; set; }
public event CommunicationEventHandler ClientDisconnected;
@ -35,11 +34,11 @@ namespace SafeExamBrowser.Runtime.Communication
protected override bool OnConnect(Guid? token = null)
{
var authenticated = StartupToken == token;
var accepted = allowConnection && authenticated;
var accepted = AllowConnection && authenticated;
if (accepted)
{
allowConnection = false;
AllowConnection = false;
}
return accepted;
@ -48,13 +47,6 @@ namespace SafeExamBrowser.Runtime.Communication
protected override void OnDisconnect()
{
ClientDisconnected?.Invoke();
// TODO: Handle client crash scenario!
// If a client crashes or hangs when terminating (which should not happen!), it could be that it never gets to disconnect from
// the RuntimeHost - in that case, allowConnection prohibits restarting a new session as long as it's only set here!
// -> Move AllowConnection to interface and reset it in RuntimeController?
// -> Only possible as long as just the client connects, with service and client a more elaborate solution will be needed!
// -> E.g. ClientId and ServiceId, and then AllowClientConnection and AllowServiceConnection?
allowConnection = true;
}
protected override Response OnReceive(Message message)

View file

@ -116,7 +116,7 @@ namespace SafeExamBrowser.Runtime
var compressor = new GZipCompressor(new ModuleLogger(logger, nameof(GZipCompressor)));
var repositoryLogger = new ModuleLogger(logger, nameof(ConfigurationRepository));
configuration = new ConfigurationRepository(repositoryLogger, executable.Location, programCopyright, programTitle, programVersion);
configuration = new ConfigurationRepository(new HashAlgorithm(), repositoryLogger, executable.Location, programCopyright, programTitle, programVersion);
appConfig = configuration.InitializeAppConfig();
configuration.Register(new BinaryFormat(compressor, new HashAlgorithm(), new ModuleLogger(logger, nameof(BinaryFormat))));

View file

@ -106,12 +106,14 @@ namespace SafeExamBrowser.Runtime.Operations
var token = Context.Next.StartupToken.ToString("D");
logger.Info("Starting new client process...");
runtimeHost.AllowConnection = true;
runtimeHost.ClientReady += clientReadyEventHandler;
ClientProcess = processFactory.StartNew(clientExecutable, clientLogFile, hostUri, token);
logger.Info("Waiting for client to complete initialization...");
clientReady = clientReadyEvent.WaitOne(timeout_ms);
runtimeHost.ClientReady -= clientReadyEventHandler;
runtimeHost.AllowConnection = false;
if (!clientReady)
{

View file

@ -35,8 +35,8 @@ namespace SafeExamBrowser.Runtime.Operations
SessionContext sessionContext) : base(sessionContext)
{
this.commandLineArgs = commandLineArgs;
this.logger = logger;
this.configuration = configuration;
this.logger = logger;
}
public override OperationResult Perform()
@ -50,7 +50,6 @@ namespace SafeExamBrowser.Runtime.Operations
if (isValidUri)
{
result = LoadSettings(uri);
HandleClientConfiguration(ref result, uri);
}
else
{
@ -73,7 +72,6 @@ namespace SafeExamBrowser.Runtime.Operations
if (isValidUri)
{
result = LoadSettings(uri);
HandleClientConfiguration(ref result, uri);
}
else
{
@ -100,36 +98,55 @@ namespace SafeExamBrowser.Runtime.Operations
private OperationResult LoadSettings(Uri uri)
{
var status = configuration.TryLoadSettings(uri, out Settings settings, Context.Current?.Settings?.AdminPasswordHash, true);
var settings = default(Settings);
var status = default(LoadStatus);
var passwordInfo = new PasswordInfo { AdminPasswordHash = Context.Current?.Settings?.AdminPasswordHash };
for (var attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++)
for (int adminAttempts = 0, settingsAttempts = 0; adminAttempts < 5 && settingsAttempts < 5;)
{
var result = TryGetPassword();
status = configuration.TryLoadSettings(uri, passwordInfo, out settings);
if (!result.Success)
if (status != LoadStatus.AdminPasswordNeeded && status != LoadStatus.SettingsPasswordNeeded)
{
break;
}
var success = TryGetPassword(status, passwordInfo);
if (!success)
{
return OperationResult.Aborted;
}
status = configuration.TryLoadSettings(uri, out settings, result.Password);
adminAttempts += status == LoadStatus.AdminPasswordNeeded ? 1 : 0;
settingsAttempts += status == LoadStatus.SettingsPasswordNeeded ? 1 : 0;
}
Context.Next.Settings = settings;
if (status == LoadStatus.Success)
{
Context.Next.Settings = settings;
}
else
{
ShowFailureMessage(status, uri);
return OperationResult.Success;
}
return status == LoadStatus.Success ? OperationResult.Success : OperationResult.Failed;
if (status == LoadStatus.SuccessConfigureClient)
{
return HandleClientConfiguration();
}
ShowFailureMessage(status, uri);
return OperationResult.Failed;
}
private void ShowFailureMessage(LoadStatus status, Uri uri)
{
switch (status)
{
case LoadStatus.AdminPasswordNeeded:
case LoadStatus.SettingsPasswordNeeded:
ActionRequired?.Invoke(new InvalidPasswordMessageArgs());
break;
case LoadStatus.InvalidData:
ActionRequired?.Invoke(new InvalidDataMessageArgs(uri.ToString()));
break;
@ -142,14 +159,23 @@ namespace SafeExamBrowser.Runtime.Operations
}
}
private PasswordRequiredEventArgs TryGetPassword()
private bool TryGetPassword(LoadStatus status, PasswordInfo passwordInfo)
{
var purpose = PasswordRequestPurpose.Settings;
var purpose = status == LoadStatus.AdminPasswordNeeded ? PasswordRequestPurpose.Administrator : PasswordRequestPurpose.Settings;
var args = new PasswordRequiredEventArgs { Purpose = purpose };
ActionRequired?.Invoke(args);
return args;
if (purpose == PasswordRequestPurpose.Administrator)
{
passwordInfo.AdminPassword = args.Password;
}
else
{
passwordInfo.SettingsPassword = args.Password;
}
return args.Success;
}
private bool TryInitializeSettingsUri(out Uri uri)
@ -195,37 +221,30 @@ namespace SafeExamBrowser.Runtime.Operations
return isValidUri;
}
private void HandleClientConfiguration(ref OperationResult result, Uri uri)
private OperationResult HandleClientConfiguration()
{
var configureMode = Context.Next.Settings?.ConfigurationMode == ConfigurationMode.ConfigureClient;
var loadWithBrowser = Context.Next.Settings?.Browser.StartUrl == uri.AbsoluteUri;
var successful = result == OperationResult.Success;
var firstSession = Context.Current == null;
if (successful && configureMode && !loadWithBrowser)
if (firstSession)
{
var args = new ConfigurationCompletedEventArgs();
var filePath = Path.Combine(Context.Next.AppConfig.AppDataFolder, Context.Next.AppConfig.DefaultSettingsFileName);
// TODO: Save / overwrite configuration file in APPDATA directory!
// -> Check whether current and new admin passwords are the same! If not, current needs to be verified before overwriting!
// -> Special case: If current admin password is same as new settings password, verification is not necessary!
// -> Default settings password appears to be string.Empty for local client configuration
// -> Any (new?) certificates need to be imported and REMOVED from the settings before the data is saved!
// -> DO NOT transform settings, just simply copy the given configuration file to %APPDATA%\SebClientSettings.seb !!!
//configuration.SaveSettings(Context.Next.Settings, filePath);
// TODO: If the client configuration happens while the application is already running, the new configuration should first
// be loaded and then the user should have the option to terminate!
// -> Introduce flag in Context, e.g. AskForTermination?
ActionRequired?.Invoke(args);
logger.Info($"The user chose to {(args.AbortStartup ? "abort" : "continue")} after successful client configuration.");
if (args.AbortStartup)
{
result = OperationResult.Aborted;
return OperationResult.Aborted;
}
}
else
{
// TODO: If the client configuration happens while the application is already running, the new configuration should first
// be loaded and then the user should have the option to terminate!
// -> Introduce flag in Context, e.g. AskForTermination?
}
return OperationResult.Success;
}
private void LogOperationResult(OperationResult result)

View file

@ -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 SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.UserInterface.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class InvalidPasswordMessageArgs : MessageEventArgs
{
internal InvalidPasswordMessageArgs()
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_InvalidPasswordError;
Title = TextKey.MessageBox_InvalidPasswordErrorTitle;
}
}
}

View file

@ -91,6 +91,7 @@
<Compile Include="Operations\ConfigurationOperation.cs" />
<Compile Include="Operations\Events\ConfigurationCompletedEventArgs.cs" />
<Compile Include="Operations\Events\InvalidDataMessageArgs.cs" />
<Compile Include="Operations\Events\InvalidPasswordMessageArgs.cs" />
<Compile Include="Operations\Events\MessageEventArgs.cs" />
<Compile Include="Operations\Events\NotSupportedMessageArgs.cs" />
<Compile Include="Operations\Events\PasswordRequiredEventArgs.cs" />