diff --git a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs index 49478984..c8f61f86 100644 --- a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs +++ b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs @@ -33,7 +33,7 @@ namespace SafeExamBrowser.Browser.Handlers this.filter = filter; this.logger = logger; this.settings = settings; - this.resourceHandler = new ResourceHandler(appConfig, settings.Filter, filter, logger, text); + this.resourceHandler = new ResourceHandler(appConfig, settings, filter, logger, text); } protected override bool GetAuthCredentials(IWebBrowser webBrowser, IBrowser browser, string originUrl, bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) diff --git a/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs b/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs index 17b2d5ca..9564157b 100644 --- a/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs +++ b/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs @@ -10,30 +10,34 @@ using System; using System.Collections.Specialized; using System.IO; using System.Reflection; +using System.Security.Cryptography; +using System.Text; using CefSharp; using SafeExamBrowser.Browser.Contracts.Filters; using SafeExamBrowser.Browser.Filters; using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; -using SafeExamBrowser.Settings.Browser; using SafeExamBrowser.Settings.Browser.Filter; +using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings; namespace SafeExamBrowser.Browser.Handlers { internal class ResourceHandler : CefSharp.Handler.ResourceRequestHandler { private AppConfig appConfig; - private FilterSettings settings; + private SHA256Managed algorithm; + private BrowserSettings settings; private ILogger logger; private IRequestFilter filter; private IResourceHandler contentHandler; private IResourceHandler pageHandler; private IText text; - internal ResourceHandler(AppConfig appConfig, FilterSettings settings, IRequestFilter filter, ILogger logger, IText text) + internal ResourceHandler(AppConfig appConfig, BrowserSettings settings, IRequestFilter filter, ILogger logger, IText text) { this.appConfig = appConfig; + this.algorithm = new SHA256Managed(); this.filter = filter; this.logger = logger; this.settings = settings; @@ -52,14 +56,19 @@ namespace SafeExamBrowser.Browser.Handlers protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) { + if (IsMailtoUrl(request.Url)) + { + return CefReturnValue.Cancel; + } + // TODO: CEF does not yet support intercepting requests from service workers, thus the user agent must be statically set at browser // startup for now. Once CEF has full support of service workers, the static user agent should be removed and the method below // reactivated. See https://bitbucket.org/chromiumembedded/cef/issues/2622 for the current status of development. // AppendCustomUserAgent(request); - if (IsMailtoUrl(request.Url)) + if (settings.SendCustomHeaders) { - return CefReturnValue.Cancel; + AppendCustomHeaders(request); } ReplaceSebScheme(request); @@ -76,9 +85,22 @@ namespace SafeExamBrowser.Browser.Handlers request.Headers = headers; } + private void AppendCustomHeaders(IRequest request) + { + var headers = new NameValueCollection(request.Headers); + var urlWithoutFragment = request.Url.Split('#')[0]; + var hash = algorithm.ComputeHash(Encoding.UTF8.GetBytes(urlWithoutFragment + settings.HashValue)); + var configurationKey = BitConverter.ToString(hash).Replace("-", string.Empty); + + // TODO: Implement Browser Exam Key calculation. + // headers["X-SafeExamBrowser-RequestHash"] = ...; + headers["X-SafeExamBrowser-ConfigKeyHash"] = configurationKey; + request.Headers = headers; + } + private bool Block(IRequest request) { - if (settings.ProcessContentRequests) + if (settings.Filter.ProcessContentRequests) { var result = filter.Process(new Request { Url = request.Url }); var block = result == FilterResult.Block; diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/BrowserDataMapper.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/BrowserDataMapper.cs index c16f3536..a5592eeb 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/BrowserDataMapper.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/BrowserDataMapper.cs @@ -96,6 +96,9 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping case Keys.Browser.ShowToolbar: MapShowToolbar(settings, value); break; + case Keys.Browser.SendCustomHeaders: + MapSendCustomHeaders(settings, value); + break; case Keys.Browser.StartUrl: MapStartUrl(settings, value); break; @@ -275,6 +278,14 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping } } + private void MapSendCustomHeaders(AppSettings settings, object value) + { + if (value is bool send) + { + settings.Browser.SendCustomHeaders = send; + } + } + private void MapStartUrl(AppSettings settings, object value) { if (value is string url) diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs new file mode 100644 index 00000000..b76c3b2d --- /dev/null +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2020 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.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using SafeExamBrowser.Settings; + +namespace SafeExamBrowser.Configuration.ConfigurationData +{ + internal class DataProcessor + { + internal void Process(IDictionary rawData, AppSettings settings) + { + CalculateHashValue(rawData, settings); + } + + private void CalculateHashValue(IDictionary rawData, AppSettings settings) + { + using (var algorithm = new SHA256Managed()) + using (var stream = new MemoryStream()) + using (var writer = new StreamWriter(stream, Encoding.UTF8)) + { + Serialize(rawData, writer); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + + var hash = algorithm.ComputeHash(stream); + var hashString = BitConverter.ToString(hash).Replace("-", string.Empty); + + settings.Browser.HashValue = hashString; + } + } + + private void Serialize(IDictionary dictionary, StreamWriter stream) + { + var orderedByKey = dictionary.OrderBy(d => d.Key, StringComparer.OrdinalIgnoreCase).ToList(); + + stream.Write('{'); + + foreach (var kvp in orderedByKey) + { + var process = true; + + process &= !kvp.Key.Equals(Keys.General.OriginatorVersion, StringComparison.OrdinalIgnoreCase); + process &= !(kvp.Value is IDictionary d) || d.Any(); + + if (process) + { + stream.Write('"'); + stream.Write(kvp.Key); + stream.Write('"'); + stream.Write(':'); + Serialize(kvp.Value, stream); + + if (kvp.Key != orderedByKey.Last().Key) + { + stream.Write(','); + } + } + } + + stream.Write('}'); + } + + private void Serialize(IList list, StreamWriter stream) + { + stream.Write('['); + + foreach (var item in list) + { + Serialize(item, stream); + + if (item != list.Last()) + { + stream.Write(','); + } + } + + stream.Write(']'); + } + + private void Serialize(object value, StreamWriter stream) + { + switch (value) + { + case IDictionary dictionary: + Serialize(dictionary, stream); + break; + case IList list: + Serialize(list, stream); + break; + case byte[] data: + stream.Write('"'); + stream.Write(Convert.ToBase64String(data)); + stream.Write('"'); + break; + case DateTime date: + stream.Write(date.ToString("o")); + break; + case bool boolean: + stream.Write(boolean.ToString().ToLower()); + break; + case int integer: + stream.Write(integer.ToString(NumberFormatInfo.InvariantInfo)); + break; + case double number: + stream.Write(number.ToString(NumberFormatInfo.InvariantInfo)); + break; + case string text: + stream.Write('"'); + stream.Write(text); + stream.Write('"'); + break; + case null: + stream.Write('"'); + stream.Write('"'); + break; + } + } + } +} diff --git a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs index 24f307e7..4dc72a51 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs @@ -52,6 +52,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData internal const string QuitUrl = "quitURL"; internal const string QuitUrlConfirmation = "quitURLConfirm"; internal const string ShowToolbar = "enableBrowserWindowToolbar"; + internal const string SendCustomHeaders = "sendBrowserExamKey"; internal const string StartUrl = "startURL"; internal const string UserAgentModeDesktop = "browserUserAgentWinDesktopMode"; internal const string UserAgentModeMobile = "browserUserAgentWinTouchMode"; @@ -152,6 +153,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData { internal const string AllowApplicationLog = "allowApplicationLog"; internal const string LogLevel = "logLevel"; + internal const string OriginatorVersion = "originatorVersion"; } internal static class Keyboard diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs index 4aac448c..7a6e31a6 100644 --- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs +++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs @@ -15,8 +15,8 @@ using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts.Cryptography; using SafeExamBrowser.Configuration.Contracts.DataFormats; using SafeExamBrowser.Configuration.Contracts.DataResources; -using SafeExamBrowser.Settings; using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Settings; namespace SafeExamBrowser.Configuration { @@ -26,6 +26,7 @@ namespace SafeExamBrowser.Configuration private IList dataParsers; private IList dataSerializers; private DataMapper dataMapper; + private DataProcessor dataProcessor; private DataValues dataValues; private IHashAlgorithm hashAlgorithm; private ILogger logger; @@ -49,6 +50,7 @@ namespace SafeExamBrowser.Configuration dataParsers = new List(); dataSerializers = new List(); dataMapper = new DataMapper(); + dataProcessor = new DataProcessor(); dataValues = new DataValues(executablePath, programBuild, programCopyright, programTitle, programVersion); resourceLoaders = new List(); resourceSavers = new List(); @@ -144,6 +146,7 @@ namespace SafeExamBrowser.Configuration if (status == LoadStatus.Success) { dataMapper.MapRawDataToSettings(data, settings); + dataProcessor.Process(data, settings); } } diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index e687322d..6dca51e4 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -65,6 +65,7 @@ + diff --git a/SafeExamBrowser.Settings/Browser/BrowserSettings.cs b/SafeExamBrowser.Settings/Browser/BrowserSettings.cs index 5ab30e52..3ccc8ef5 100644 --- a/SafeExamBrowser.Settings/Browser/BrowserSettings.cs +++ b/SafeExamBrowser.Settings/Browser/BrowserSettings.cs @@ -51,6 +51,11 @@ namespace SafeExamBrowser.Settings.Browser /// public FilterSettings Filter { get; set; } + /// + /// The hash value of the raw settings data, used for integrity checks with server applications (see also ). + /// + public string HashValue { get; set; } + /// /// The settings to be used for the main browser window. /// @@ -71,6 +76,11 @@ namespace SafeExamBrowser.Settings.Browser /// public string QuitUrl { get; set; } + /// + /// Determines whether custom request headers (e.g. for integrity checks) are sent with every HTTP request. + /// + public bool SendCustomHeaders { get; set; } + /// /// The URL with which the main browser window will be loaded. ///