SEBWIN-221: Implemented changes in configuration algorithm to accomodate new functionality scope of 3.0 Alpha.
This commit is contained in:
parent
9b639b0c53
commit
f9f2bb3257
24 changed files with 330 additions and 124 deletions
|
@ -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}'.");
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
28
SafeExamBrowser.Contracts/Configuration/ParseResult.cs
Normal file
28
SafeExamBrowser.Contracts/Configuration/ParseResult.cs
Normal 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; }
|
||||
}
|
||||
}
|
31
SafeExamBrowser.Contracts/Configuration/PasswordInfo.cs
Normal file
31
SafeExamBrowser.Contracts/Configuration/PasswordInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -24,6 +24,8 @@ namespace SafeExamBrowser.Contracts.I18n
|
|||
MessageBox_ConfigurationDownloadErrorTitle,
|
||||
MessageBox_InvalidConfigurationData,
|
||||
MessageBox_InvalidConfigurationDataTitle,
|
||||
MessageBox_InvalidPasswordError,
|
||||
MessageBox_InvalidPasswordErrorTitle,
|
||||
MessageBox_NotSupportedConfigurationResource,
|
||||
MessageBox_NotSupportedConfigurationResourceTitle,
|
||||
MessageBox_Quit,
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))));
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in a new issue