diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs index 8689cb90..4d615f65 100644 --- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs +++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs @@ -11,7 +11,6 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; -using SafeExamBrowser.Configuration.DataFormats; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; @@ -132,29 +131,17 @@ namespace SafeExamBrowser.Configuration try { - var resourceLoader = resourceLoaders.FirstOrDefault(l => l.CanLoad(resource)); + var status = TryLoadData(resource, out Stream data); - if (resourceLoader != null) + switch (status) { - var data = resourceLoader.Load(resource); - var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data)); - - logger.Info($"Successfully loaded {data.Length / 1000.0} KB data from '{resource}' using {resourceLoader.GetType().Name}."); - - if (dataFormat is HtmlFormat) - { - return HandleHtml(resource, out settings); - } - - if (dataFormat != null) - { - return dataFormat.TryParse(data, out settings, adminPassword, settingsPassword); - } + case LoadStatus.LoadWithBrowser: + return HandleBrowserResource(resource, out settings); + case LoadStatus.Success: + return TryParseData(data, out settings, adminPassword, settingsPassword); } - logger.Warn($"No {(resourceLoader == null ? "resource loader" : "data format")} found for '{resource}'!"); - - return LoadStatus.NotSupported; + return status; } catch (Exception e) { @@ -164,13 +151,53 @@ namespace SafeExamBrowser.Configuration } } - private LoadStatus HandleHtml(Uri resource, out Settings settings) + private LoadStatus TryLoadData(Uri resource, out Stream data) { - logger.Info($"Loaded data appears to be HTML, loading default settings and using '{resource}' as startup URL."); + var status = LoadStatus.NotSupported; + var resourceLoader = resourceLoaders.FirstOrDefault(l => l.CanLoad(resource)); + data = default(Stream); + + if (resourceLoader != null) + { + status = resourceLoader.TryLoad(resource, out data); + logger.Info($"Tried to load data from '{resource}' using {resourceLoader.GetType().Name} -> Result: {status}."); + } + else + { + logger.Warn($"No resource loader found for '{resource}'!"); + } + + return status; + } + + private LoadStatus TryParseData(Stream data, out Settings settings, string adminPassword, string settingsPassword) + { + var status = LoadStatus.NotSupported; + var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data)); + + settings = default(Settings); + + if (dataFormat != null) + { + status = dataFormat.TryParse(data, out settings, adminPassword, settingsPassword); + logger.Info($"Tried to parse data from '{data}' using {dataFormat.GetType().Name} -> Result: {status}."); + } + else + { + logger.Warn($"No data format found for '{data}'!"); + } + + return status; + } + + private LoadStatus HandleBrowserResource(Uri resource, out Settings settings) + { settings = LoadDefaultSettings(); 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; } diff --git a/SafeExamBrowser.Configuration/DataFormats/DefaultFormat.cs b/SafeExamBrowser.Configuration/DataFormats/DefaultFormat.cs index 1393811f..44826da7 100644 --- a/SafeExamBrowser.Configuration/DataFormats/DefaultFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/DefaultFormat.cs @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System.IO; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; @@ -21,17 +22,14 @@ namespace SafeExamBrowser.Configuration.DataFormats this.logger = logger; } - public bool CanParse(byte[] data) + public bool CanParse(Stream data) { - return true; + return false; } - public LoadStatus TryParse(byte[] data, out Settings settings, string adminPassword = null, string settingsPassword = null) + public LoadStatus TryParse(Stream data, out Settings settings, string adminPassword = null, string settingsPassword = null) { - settings = new Settings(); - settings.ServicePolicy = ServicePolicy.Optional; - - return LoadStatus.Success; + throw new System.NotImplementedException(); } } } diff --git a/SafeExamBrowser.Configuration/DataFormats/HtmlFormat.cs b/SafeExamBrowser.Configuration/DataFormats/HtmlFormat.cs deleted file mode 100644 index 8b92a371..00000000 --- a/SafeExamBrowser.Configuration/DataFormats/HtmlFormat.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.Configuration; -using SafeExamBrowser.Contracts.Configuration.Settings; -using SafeExamBrowser.Contracts.Logging; - -namespace SafeExamBrowser.Configuration.DataFormats -{ - public class HtmlFormat : IDataFormat - { - private ILogger logger; - - public HtmlFormat(ILogger logger) - { - this.logger = logger; - } - - public bool CanParse(byte[] data) - { - return false; - } - - public LoadStatus TryParse(byte[] data, out Settings settings, string adminPassword = null, string settingsPassword = null) - { - throw new System.NotImplementedException(); - } - } -} diff --git a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs index f1355cb8..5b882fc7 100644 --- a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs +++ b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System.IO; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; @@ -21,12 +22,12 @@ namespace SafeExamBrowser.Configuration.DataFormats this.logger = logger; } - public bool CanParse(byte[] data) + public bool CanParse(Stream data) { return false; } - public LoadStatus TryParse(byte[] data, out Settings settings, string adminPassword = null, string settingsPassword = null) + public LoadStatus TryParse(Stream data, out Settings settings, string adminPassword = null, string settingsPassword = null) { throw new System.NotImplementedException(); } diff --git a/SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs b/SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs index d023ac40..b731a554 100644 --- a/SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs +++ b/SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs @@ -24,23 +24,27 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders public bool CanLoad(Uri resource) { - if (resource.IsFile && File.Exists(resource.AbsolutePath)) + var exists = resource.IsFile && File.Exists(resource.AbsolutePath); + + if (exists) { logger.Debug($"Can load '{resource}' as it references an existing file."); - - return true; + } + else + { + logger.Debug($"Can't load '{resource}' since it isn't a file URI or no file exists at the specified path."); } - logger.Debug($"Can't load '{resource}' since it isn't a file URI or no file exists at the specified path."); - - return false; + return exists; } - public byte[] Load(Uri resource) + public LoadStatus TryLoad(Uri resource, out Stream data) { logger.Debug($"Loading data from '{resource}'..."); + data = new FileStream(resource.AbsolutePath, FileMode.Open, FileAccess.Read); + logger.Debug($"Created {data} for {data.Length / 1000.0} KB data in '{resource}'."); - return File.ReadAllBytes(resource.AbsolutePath); + return LoadStatus.Success; } } } diff --git a/SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs b/SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs index 0292e686..e5e3351c 100644 --- a/SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs +++ b/SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs @@ -7,8 +7,11 @@ */ using System; +using System.IO; using System.Linq; +using System.Net; using System.Net.Http; +using System.Net.Mime; using System.Threading.Tasks; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Logging; @@ -20,6 +23,12 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders private AppConfig appConfig; private ILogger logger; + private string[] SupportedContentTypes => new[] + { + MediaTypeNames.Application.Octet, + MediaTypeNames.Text.Xml + }; + private string[] SupportedSchemes => new[] { appConfig.SebUriScheme, @@ -36,54 +45,41 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders public bool CanLoad(Uri resource) { - if (SupportedSchemes.Contains(resource.Scheme) && IsAvailable(resource)) + var available = SupportedSchemes.Contains(resource.Scheme) && IsAvailable(resource); + + if (available) { logger.Debug($"Can load '{resource}' as it references an existing network resource."); - - return true; + } + else + { + logger.Debug($"Can't load '{resource}' since its URI scheme is not supported or the resource is unavailable."); } - logger.Debug($"Can't load '{resource}' since its URI scheme is not supported or the resource is unavailable."); - - return false; + return available; } - public byte[] Load(Uri resource) + public LoadStatus TryLoad(Uri resource, out Stream data) { var uri = BuildUriFor(resource); - logger.Debug($"Downloading data from '{uri}'..."); + logger.Debug($"Sending GET request for '{uri}'..."); var request = new HttpRequestMessage(HttpMethod.Get, uri); var response = Execute(request); - logger.Debug($"Sent GET request for '{uri}', received response '{(int) response.StatusCode} - {response.ReasonPhrase}'."); + logger.Debug($"Received response '{ToString(response)}'."); - var data = Extract(response.Content); - - logger.Debug($"Extracted {data.Length / 1000.0} KB data from response."); - - return data; - } - - private bool IsAvailable(Uri resource) - { - try + if (IsUnauthorized(response) || HasHtmlContent(response)) { - var uri = BuildUriFor(resource); - var request = new HttpRequestMessage(HttpMethod.Head, uri); - var response = Execute(request); - - logger.Debug($"Sent HEAD request for '{uri}', received response '{(int) response.StatusCode} - {response.ReasonPhrase}'."); - - return response.IsSuccessStatusCode; + return HandleBrowserResource(response, out data); } - catch (Exception e) - { - logger.Error($"Failed to check availability of '{resource}'!", e); - return false; - } + logger.Debug($"Trying to extract response data..."); + data = Extract(response.Content); + logger.Debug($"Created {data} for {data.Length / 1000.0} KB data of response body."); + + return LoadStatus.Success; } private Uri BuildUriFor(Uri resource) @@ -94,6 +90,29 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders return builder.Uri; } + private HttpResponseMessage Execute(HttpRequestMessage request) + { + var task = Task.Run(async () => + { + using (var client = new HttpClient()) + { + return await client.SendAsync(request); + } + }); + + return task.GetAwaiter().GetResult(); + } + + private Stream Extract(HttpContent content) + { + var task = Task.Run(async () => + { + return await content.ReadAsStreamAsync(); + }); + + return task.GetAwaiter().GetResult(); + } + private string GetSchemeFor(Uri resource) { if (resource.Scheme == appConfig.SebUriScheme) @@ -109,27 +128,52 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders return resource.Scheme; } - private HttpResponseMessage Execute(HttpRequestMessage request) + private LoadStatus HandleBrowserResource(HttpResponseMessage response, out Stream data) { - var task = Task.Run(async () => - { - using (var client = new HttpClient()) - { - return await client.SendAsync(request); - } - }); + data = default(Stream); - return task.GetAwaiter().GetResult(); + logger.Debug($"The {(IsUnauthorized(response) ? "resource needs authentication" : " response data is HTML")}."); + + return LoadStatus.LoadWithBrowser; } - private byte[] Extract(HttpContent content) + private bool HasHtmlContent(HttpResponseMessage response) { - var task = Task.Run(async () => - { - return await content.ReadAsByteArrayAsync(); - }); + return response.Content.Headers.ContentType.MediaType == MediaTypeNames.Text.Html; + } - return task.GetAwaiter().GetResult(); + private bool IsAvailable(Uri resource) + { + try + { + var uri = BuildUriFor(resource); + + logger.Debug($"Sending HEAD request for '{uri}'..."); + + var request = new HttpRequestMessage(HttpMethod.Head, uri); + var response = Execute(request); + var isAvailable = response.IsSuccessStatusCode || IsUnauthorized(response); + + logger.Debug($"Received response '{ToString(response)}'."); + + return isAvailable; + } + catch (Exception e) + { + logger.Error($"Failed to check availability of '{resource}'!", e); + + return false; + } + } + + private bool IsUnauthorized(HttpResponseMessage response) + { + return response.StatusCode == HttpStatusCode.Unauthorized; + } + + private string ToString(HttpResponseMessage response) + { + return $"{(int)response.StatusCode} - {response.ReasonPhrase}"; } } } diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index d7c8327d..17427d29 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -54,7 +54,6 @@ - diff --git a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs index 78eb4bb1..f2362488 100644 --- a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs +++ b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs @@ -31,12 +31,12 @@ namespace SafeExamBrowser.Contracts.Configuration Settings.Settings LoadDefaultSettings(); /// - /// Registers the specified as option to parse configuration data. + /// Registers the specified as option to parse data from a configuration resource. /// void Register(IDataFormat dataFormat); /// - /// Registers the specified as option to load configuration data. + /// Registers the specified as option to load data from a configuration resource. /// void Register(IResourceLoader resourceLoader); diff --git a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs index 659e8e2e..668f6289 100644 --- a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs +++ b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs @@ -6,21 +6,24 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System.IO; + namespace SafeExamBrowser.Contracts.Configuration { /// - /// Defines the data format for a configuration file. + /// Provides functionality to parse configuration data with a particular format. /// public interface IDataFormat { /// /// Indicates whether the given data complies with the required format. /// - bool CanParse(byte[] data); + bool CanParse(Stream data); /// - /// Attempts to parse the given binary data. + /// Tries to parse the given data, using the optional passwords. As long as the result is not , + /// the referenced settings may be null or in an undefinable state! /// - LoadStatus TryParse(byte[] data, out Settings.Settings settings, string adminPassword = null, string settingsPassword = null); + LoadStatus TryParse(Stream data, out Settings.Settings settings, string adminPassword = null, string settingsPassword = null); } } diff --git a/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs b/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs index 634ec9e7..c63051e2 100644 --- a/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs +++ b/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs @@ -7,11 +7,12 @@ */ using System; +using System.IO; namespace SafeExamBrowser.Contracts.Configuration { /// - /// Loads binary data from a particular resource. + /// Provides functionality to load configuration data from a particular resource. /// public interface IResourceLoader { @@ -21,8 +22,8 @@ namespace SafeExamBrowser.Contracts.Configuration bool CanLoad(Uri resource); /// - /// Loads the binary data from the specified resource. + /// Tries to load the configuration data from the specified resource. /// - byte[] Load(Uri resource); + LoadStatus TryLoad(Uri resource, out Stream data); } } diff --git a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs index f8580889..698d08f2 100644 --- a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs +++ b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs @@ -9,7 +9,7 @@ namespace SafeExamBrowser.Contracts.Configuration { /// - /// Defines all possible results of an attempt to load a configuration file. + /// Defines all possible results of an attempt to load a configuration resource. /// public enum LoadStatus { @@ -19,10 +19,15 @@ namespace SafeExamBrowser.Contracts.Configuration AdminPasswordNeeded = 1, /// - /// Indicates that a resource does not comply with the required data format. + /// Indicates that a resource does not comply with the declared data format. /// InvalidData, + /// + /// Indicates that a resource needs to be loaded with the browser. + /// + LoadWithBrowser, + /// /// Indicates that a resource is not supported. /// diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 54b9b78a..1f9639be 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -118,7 +118,6 @@ namespace SafeExamBrowser.Runtime appConfig = configuration.InitializeAppConfig(); configuration.Register(new DefaultFormat(new ModuleLogger(logger, nameof(DefaultFormat)))); - configuration.Register(new HtmlFormat(new ModuleLogger(logger, nameof(HtmlFormat)))); configuration.Register(new XmlFormat(new ModuleLogger(logger, nameof(XmlFormat)))); configuration.Register(new FileResourceLoader(new ModuleLogger(logger, nameof(FileResourceLoader)))); configuration.Register(new NetworkResourceLoader(appConfig, new ModuleLogger(logger, nameof(NetworkResourceLoader)))); diff --git a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs index fb8f291d..208fd308 100644 --- a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs @@ -48,17 +48,15 @@ namespace SafeExamBrowser.Runtime.Operations if (isValidUri) { - logger.Info($"Attempting to load settings from '{uri}'..."); - var result = LoadSettings(uri); - HandleClientConfiguration(ref result); + HandleClientConfiguration(ref result, uri); LogOperationResult(result); return result; } - logger.Info("No valid settings resource specified nor found in PROGRAMDATA or APPDATA - loading default settings..."); + logger.Info("No valid configuration resource specified nor found in PROGRAMDATA or APPDATA - loading default settings..."); Context.Next.Settings = configuration.LoadDefaultSettings(); return OperationResult.Success; @@ -73,8 +71,6 @@ namespace SafeExamBrowser.Runtime.Operations if (isValidUri) { - logger.Info($"Attempting to load settings from '{uri}'..."); - var result = LoadSettings(uri); LogOperationResult(result); @@ -82,7 +78,7 @@ namespace SafeExamBrowser.Runtime.Operations return result; } - logger.Warn($"The resource specified for reconfiguration does not exist or is not a file!"); + logger.Warn($"The resource specified for reconfiguration does not exist or is not valid!"); return OperationResult.Failed; } @@ -126,10 +122,17 @@ namespace SafeExamBrowser.Runtime.Operations if (status == LoadStatus.Success) { Context.Next.Settings = settings; - - return OperationResult.Success; + } + else + { + ShowFailureMessage(status, uri); } + return status == LoadStatus.Success ? OperationResult.Success : OperationResult.Failed; + } + + private void ShowFailureMessage(LoadStatus status, Uri uri) + { switch (status) { case LoadStatus.InvalidData: @@ -142,8 +145,6 @@ namespace SafeExamBrowser.Runtime.Operations ActionRequired?.Invoke(new UnexpectedErrorMessageArgs(uri.ToString())); break; } - - return OperationResult.Failed; } private PasswordRequiredEventArgs TryGetPassword(LoadStatus status) @@ -169,21 +170,21 @@ namespace SafeExamBrowser.Runtime.Operations { path = commandLineArgs[1]; isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri); - logger.Info($"Found command-line argument for settings file: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); + logger.Info($"Found command-line argument for configuration resource: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); } if (!isValidUri && File.Exists(programDataSettings)) { path = programDataSettings; isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri); - logger.Info($"Found settings file in PROGRAMDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); + logger.Info($"Found configuration file in PROGRAMDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); } if (!isValidUri && File.Exists(appDataSettings)) { path = appDataSettings; isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri); - logger.Info($"Found settings file in APPDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); + logger.Info($"Found configuration file in APPDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); } return isValidUri; @@ -199,9 +200,13 @@ namespace SafeExamBrowser.Runtime.Operations return isValidUri; } - private void HandleClientConfiguration(ref OperationResult result) + private void HandleClientConfiguration(ref OperationResult result, Uri uri) { - if (result == OperationResult.Success && Context.Next.Settings.ConfigurationMode == ConfigurationMode.ConfigureClient) + var configureMode = Context.Next.Settings?.ConfigurationMode == ConfigurationMode.ConfigureClient; + var loadWithBrowser = Context.Next.Settings?.Browser.StartUrl == uri.AbsoluteUri; + var successful = result == OperationResult.Success; + + if (successful && configureMode && !loadWithBrowser) { var args = new ConfigurationCompletedEventArgs();