SEBWIN-221: Implemented basic gzip decompression & binary format detection. The browser is now automatically started after client initialization.

This commit is contained in:
dbuechel 2018-11-15 08:45:17 +01:00
parent 1d58a84db8
commit b29fd8c2d7
16 changed files with 346 additions and 92 deletions

View file

@ -69,6 +69,11 @@ namespace SafeExamBrowser.Browser
this.button.Clicked += Button_OnClick;
}
public void Start()
{
CreateNewInstance();
}
public void Terminate()
{
foreach (var instance in instances)

View file

@ -105,6 +105,7 @@ namespace SafeExamBrowser.Client
if (success)
{
RegisterEvents();
StartBrowser();
var communication = runtime.InformClientReady();
@ -182,6 +183,12 @@ namespace SafeExamBrowser.Client
windowMonitor.WindowChanged -= WindowMonitor_WindowChanged;
}
private void StartBrowser()
{
logger.Info("Starting browser application...");
Browser.Start();
}
private void DisplayMonitor_DisplaySettingsChanged()
{
logger.Info("Reinitializing working area...");

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.IO;
using System.IO.Compression;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.Compression
{
/// <summary>
/// Data compression using the GNU-Zip format (see https://en.wikipedia.org/wiki/Gzip).
/// </summary>
public class GZipCompressor : IDataCompressor
{
private const int ID1 = 0x1F;
private const int ID2 = 0x8B;
private const int CM = 8;
private const int FOOTER_LENGTH = 8;
private const int HEADER_LENGTH = 10;
private ILogger logger;
public GZipCompressor(ILogger logger)
{
this.logger = logger;
}
public Stream Compress(Stream data)
{
throw new NotImplementedException();
}
public Stream Decompress(Stream data)
{
var decompressed = new MemoryStream();
logger.Debug($"Starting decompression of '{data}' with {data.Length / 1000.0} KB data...");
data.Seek(0, SeekOrigin.Begin);
using (var stream = new GZipStream(data, CompressionMode.Decompress))
{
var buffer = new byte[4096];
var bytesRead = 0;
do
{
bytesRead = stream.Read(buffer, 0, buffer.Length);
decompressed.Write(buffer, 0, bytesRead);
} while (bytesRead > 0);
}
logger.Debug($"Successfully decompressed {decompressed.Length / 1000.0} KB data into '{decompressed}'.");
return decompressed;
}
/// <remarks>
/// All gzip-compressed data has a 10-byte header and 8-byte footer. The header starts with two magic numbers (ID1 and ID2) and
/// the used compression method (CM), which normally denotes the DEFLATE algorithm. See https://tools.ietf.org/html/rfc1952 for
/// the original data format specification.
/// </remarks>
public bool IsCompressed(Stream data)
{
try
{
var longEnough = data.Length > HEADER_LENGTH + FOOTER_LENGTH;
data.Seek(0, SeekOrigin.Begin);
if (longEnough)
{
var id1 = data.ReadByte();
var id2 = data.ReadByte();
var cm = data.ReadByte();
var compressed = id1 == ID1 && id2 == ID2 && cm == CM;
logger.Debug($"'{data}' is {(compressed ? string.Empty : "not ")}a gzip-compressed stream.");
return compressed;
}
logger.Debug($"'{data}' is not long enough ({data.Length} bytes) to be a gzip-compressed stream.");
}
catch (Exception e)
{
logger.Error($"Failed to check whether '{data}' with {data.Length / 1000.0} KB data is a gzip-compressed stream!", e);
}
return false;
}
public byte[] Peek(Stream data, int count)
{
logger.Debug($"Peeking {count} bytes from '{data}'...");
data.Seek(0, SeekOrigin.Begin);
using (var stream = new GZipStream(data, CompressionMode.Decompress))
using (var decompressed = new MemoryStream())
{
var buffer = new byte[count];
var bytesRead = stream.Read(buffer, 0, buffer.Length);
decompressed.Write(buffer, 0, bytesRead);
return decompressed.ToArray();
}
}
}
}

View file

@ -9,7 +9,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
@ -133,12 +132,15 @@ namespace SafeExamBrowser.Configuration
{
var status = TryLoadData(resource, out Stream data);
switch (status)
using (data)
{
case LoadStatus.LoadWithBrowser:
return HandleBrowserResource(resource, out settings);
case LoadStatus.Success:
return TryParseData(data, out settings, adminPassword, settingsPassword);
switch (status)
{
case LoadStatus.LoadWithBrowser:
return HandleBrowserResource(resource, out settings);
case LoadStatus.Success:
return TryParseData(data, out settings, adminPassword, settingsPassword);
}
}
return status;
@ -201,32 +203,6 @@ namespace SafeExamBrowser.Configuration
return LoadStatus.Success;
}
private byte[] Decompress(byte[] bytes)
{
try
{
var buffer = new byte[4096];
using (var stream = new GZipStream(new MemoryStream(bytes), CompressionMode.Decompress))
using (var decompressed = new MemoryStream())
{
var bytesRead = 0;
do
{
bytesRead = stream.Read(buffer, 0, buffer.Length);
decompressed.Write(buffer, 0, bytesRead);
} while (bytesRead > 0);
return decompressed.ToArray();
}
}
catch (InvalidDataException)
{
return bytes;
}
}
private void UpdateAppConfig()
{
appConfig.ClientId = Guid.NewGuid();

View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.IO;
using System.Text;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.DataFormats
{
public class BinaryFormat : IDataFormat
{
private const int PREFIX_LENGTH = 4;
private IDataCompressor compressor;
private ILogger logger;
public BinaryFormat(IDataCompressor compressor, ILogger logger)
{
this.compressor = compressor;
this.logger = logger;
}
public bool CanParse(Stream data)
{
try
{
var longEnough = data.Length > PREFIX_LENGTH;
if (longEnough)
{
var prefix = ParsePrefix(data);
var success = TryDetermineFormat(prefix, out DataFormat format);
logger.Debug($"'{data}' starting with '{prefix}' does {(success ? string.Empty : "not ")}match the binary format.");
return success;
}
logger.Debug($"'{data}' is not long enough ({data.Length} bytes) to match the binary format.");
}
catch (Exception e)
{
logger.Error($"Failed to determine whether '{data}' with {data.Length / 1000.0} KB data matches the binary format!", e);
}
return false;
}
public LoadStatus TryParse(Stream data, out Settings settings, string adminPassword = null, string settingsPassword = null)
{
settings = new Settings();
settings.Browser.AllowAddressBar = true;
settings.Browser.StartUrl = "www.duckduckgo.com";
settings.Browser.AllowConfigurationDownloads = true;
return LoadStatus.Success;
}
private string ParsePrefix(Stream data)
{
var prefixData = new byte[PREFIX_LENGTH];
if (compressor.IsCompressed(data))
{
prefixData = compressor.Peek(data, PREFIX_LENGTH);
}
else
{
data.Seek(0, SeekOrigin.Begin);
data.Read(prefixData, 0, PREFIX_LENGTH);
}
return Encoding.UTF8.GetString(prefixData);
}
private bool TryDetermineFormat(string prefix, out DataFormat format)
{
format = default(DataFormat);
switch (prefix)
{
case "pswd":
format = DataFormat.Password;
return true;
case "pwcc":
format = DataFormat.PasswordForConfigureClient;
return true;
case "plnd":
format = DataFormat.PlainData;
return true;
case "pkhs":
format = DataFormat.PublicKeyHash;
return true;
case "phsk":
format = DataFormat.PublicKeyHashWithSymmetricKey;
return true;
}
return false;
}
private enum DataFormat
{
Password = 1,
PasswordForConfigureClient,
PlainData,
PublicKeyHash,
PublicKeyHashWithSymmetricKey
}
}
}

View file

@ -1,35 +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 System.IO;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.DataFormats
{
public class DefaultFormat : IDataFormat
{
private ILogger logger;
public DefaultFormat(ILogger logger)
{
this.logger = logger;
}
public bool CanParse(Stream data)
{
return false;
}
public LoadStatus TryParse(Stream data, out Settings settings, string adminPassword = null, string settingsPassword = null)
{
throw new System.NotImplementedException();
}
}
}

View file

@ -24,7 +24,7 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders
public bool CanLoad(Uri resource)
{
var exists = resource.IsFile && File.Exists(resource.AbsolutePath);
var exists = resource.IsFile && File.Exists(resource.LocalPath);
if (exists)
{
@ -41,8 +41,8 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders
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}'.");
data = new FileStream(resource.LocalPath, FileMode.Open, FileAccess.Read);
logger.Debug($"Created '{data}' for {data.Length / 1000.0} KB data in '{resource}'.");
return LoadStatus.Success;
}

View file

@ -23,6 +23,9 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders
private AppConfig appConfig;
private ILogger logger;
/// <remarks>
/// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types.
/// </remarks>
private string[] SupportedContentTypes => new[]
{
MediaTypeNames.Application.Octet,
@ -77,7 +80,7 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders
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.");
logger.Debug($"Created '{data}' for {data.Length / 1000.0} KB data of response body.");
return LoadStatus.Success;
}
@ -137,6 +140,9 @@ namespace SafeExamBrowser.Configuration.ResourceLoaders
return LoadStatus.LoadWithBrowser;
}
/// <remarks>
/// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type.
/// </remarks>
private bool HasHtmlContent(HttpResponseMessage response)
{
return response.Content.Headers.ContentType.MediaType == MediaTypeNames.Text.Html;

View file

@ -53,7 +53,8 @@
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
<Compile Include="DataFormats\DefaultFormat.cs" />
<Compile Include="Compression\GZipCompressor.cs" />
<Compile Include="DataFormats\BinaryFormat.cs" />
<Compile Include="DataFormats\XmlFormat.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ConfigurationRepository.cs" />

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.IO;
namespace SafeExamBrowser.Contracts.Configuration
{
/// <summary>
/// Defines the functionality for data compression and decompression.
/// </summary>
public interface IDataCompressor
{
/// <summary>
/// Compresses the data from the given stream.
/// </summary>
Stream Compress(Stream data);
/// <summary>
/// Decompresses the data from the given stream.
/// </summary>
Stream Decompress(Stream data);
/// <summary>
/// Indicates whether the given stream holds compressed data.
/// </summary>
bool IsCompressed(Stream data);
/// <summary>
/// Decompresses the specified number of bytes from the beginning of the given stream.
/// </summary>
byte[] Peek(Stream data, int count);
}
}

View file

@ -57,6 +57,10 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings
Keyboard = new KeyboardSettings();
Mouse = new MouseSettings();
Taskbar = new TaskbarSettings();
// TODO: For version 3.0 alpha only, remove for final release!
ServicePolicy = ServicePolicy.Optional;
Taskbar.AllowApplicationLog = true;
}
}
}

View file

@ -25,6 +25,11 @@ namespace SafeExamBrowser.Contracts.Core
/// </summary>
void RegisterApplicationButton(IApplicationButton button);
/// <summary>
/// Starts the execution of the application.
/// </summary>
void Start();
/// <summary>
/// Performs any termination work, e.g. releasing of used resources.
/// </summary>

View file

@ -53,6 +53,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Communication\Events\ClientConfigurationEventArgs.cs" />
<Compile Include="Configuration\IDataCompressor.cs" />
<Compile Include="Configuration\IDataFormat.cs" />
<Compile Include="Core\Events\InstanceTerminatedEventHandler.cs" />
<Compile Include="Core\Events\NameChangedEventHandler.cs" />

View file

@ -381,7 +381,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
var location = Path.GetDirectoryName(GetType().Assembly.Location);
var resource = new Uri(Path.Combine(location, nameof(Operations), "SettingsDummy.txt"));
sessionContext.ReconfigurationFilePath = resource.AbsolutePath;
sessionContext.ReconfigurationFilePath = resource.LocalPath;
repository.Setup(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null)).Returns(LoadStatus.Success);
sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext);
@ -408,7 +408,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null), Times.Never);
Assert.AreEqual(OperationResult.Failed, result);
sessionContext.ReconfigurationFilePath = resource.AbsolutePath;
sessionContext.ReconfigurationFilePath = resource.LocalPath;
result = sut.Repeat();
repository.Verify(r => r.TryLoadSettings(It.Is<Uri>(u => u.Equals(resource)), out settings, null, null), Times.Never);

View file

@ -13,6 +13,7 @@ using System.Reflection;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Configuration;
using SafeExamBrowser.Configuration.Compression;
using SafeExamBrowser.Configuration.DataFormats;
using SafeExamBrowser.Configuration.ResourceLoaders;
using SafeExamBrowser.Contracts.Configuration;
@ -112,12 +113,13 @@ namespace SafeExamBrowser.Runtime
var programCopyright = executable.GetCustomAttribute<AssemblyCopyrightAttribute>().Copyright;
var programTitle = executable.GetCustomAttribute<AssemblyTitleAttribute>().Title;
var programVersion = executable.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
var moduleLogger = new ModuleLogger(logger, nameof(ConfigurationRepository));
var compressor = new GZipCompressor(new ModuleLogger(logger, nameof(GZipCompressor)));
var repositoryLogger = new ModuleLogger(logger, nameof(ConfigurationRepository));
configuration = new ConfigurationRepository(moduleLogger, executable.Location, programCopyright, programTitle, programVersion);
configuration = new ConfigurationRepository(repositoryLogger, executable.Location, programCopyright, programTitle, programVersion);
appConfig = configuration.InitializeAppConfig();
configuration.Register(new DefaultFormat(new ModuleLogger(logger, nameof(DefaultFormat))));
configuration.Register(new BinaryFormat(compressor, new ModuleLogger(logger, nameof(BinaryFormat))));
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))));

View file

@ -44,22 +44,22 @@ namespace SafeExamBrowser.Runtime.Operations
logger.Info("Initializing application configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeConfiguration);
var result = OperationResult.Failed;
var isValidUri = TryInitializeSettingsUri(out Uri uri);
if (isValidUri)
{
var result = LoadSettings(uri);
result = LoadSettings(uri);
HandleClientConfiguration(ref result, uri);
LogOperationResult(result);
return result;
}
else
{
result = LoadDefaultSettings();
}
logger.Info("No valid configuration resource specified nor found in PROGRAMDATA or APPDATA - loading default settings...");
Context.Next.Settings = configuration.LoadDefaultSettings();
LogOperationResult(result);
return OperationResult.Success;
return result;
}
public override OperationResult Repeat()
@ -67,20 +67,21 @@ namespace SafeExamBrowser.Runtime.Operations
logger.Info("Initializing new application configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeConfiguration);
var result = OperationResult.Failed;
var isValidUri = TryValidateSettingsUri(Context.ReconfigurationFilePath, out Uri uri);
if (isValidUri)
{
var result = LoadSettings(uri);
LogOperationResult(result);
return result;
result = LoadSettings(uri);
}
else
{
logger.Warn($"The resource specified for reconfiguration does not exist or is not valid!");
}
logger.Warn($"The resource specified for reconfiguration does not exist or is not valid!");
LogOperationResult(result);
return OperationResult.Failed;
return result;
}
public override OperationResult Revert()
@ -88,6 +89,14 @@ namespace SafeExamBrowser.Runtime.Operations
return OperationResult.Success;
}
private OperationResult LoadDefaultSettings()
{
logger.Info("No valid configuration resource specified nor found in PROGRAMDATA or APPDATA - loading default settings...");
Context.Next.Settings = configuration.LoadDefaultSettings();
return OperationResult.Success;
}
private OperationResult LoadSettings(Uri uri)
{
var adminPassword = default(string);