diff --git a/SafeExamBrowser.Browser.UnitTests/Filters/RequestFilterTests.cs b/SafeExamBrowser.Browser.UnitTests/Filters/RequestFilterTests.cs new file mode 100644 index 00000000..039e158d --- /dev/null +++ b/SafeExamBrowser.Browser.UnitTests/Filters/RequestFilterTests.cs @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SafeExamBrowser.Browser.Filters; +using SafeExamBrowser.Settings.Browser; + +namespace SafeExamBrowser.Browser.UnitTests.Filters +{ + [TestClass] + public class RequestFilterTests + { + private RequestFilter sut; + + [TestInitialize] + public void Initialize() + { + sut = new RequestFilter(); + } + + [TestMethod] + public void MustProcessBlockRulesFirst() + { + var allow = new FilterRuleSettings { Expression = "*", Type = FilterType.Simplified, Result = FilterResult.Allow }; + var block = new FilterRuleSettings { Expression = "*", Type = FilterType.Simplified, Result = FilterResult.Block }; + + sut.Load(allow); + sut.Load(block); + + var result = sut.Process("safeexambrowser.org"); + + Assert.AreEqual(FilterResult.Block, result); + } + + [TestMethod] + public void MustProcessAllowRulesSecond() + { + var allow = new FilterRuleSettings { Expression = "*", Type = FilterType.Simplified, Result = FilterResult.Allow }; + var block = new FilterRuleSettings { Expression = "xyz", Type = FilterType.Simplified, Result = FilterResult.Block }; + + sut.Load(allow); + sut.Load(block); + + var result = sut.Process("safeexambrowser.org"); + + Assert.AreEqual(FilterResult.Allow, result); + } + + [TestMethod] + public void MustReturnDefault() + { + var allow = new FilterRuleSettings { Expression = "xyz", Type = FilterType.Simplified, Result = FilterResult.Allow }; + var block = new FilterRuleSettings { Expression = "xyz", Type = FilterType.Simplified, Result = FilterResult.Block }; + + sut.Default = (FilterResult) (-1); + sut.Load(allow); + sut.Load(block); + + var result = sut.Process("safeexambrowser.org"); + + Assert.AreEqual((FilterResult) (-1), result); + } + + [TestMethod] + public void MustReturnDefaultWithoutRules() + { + sut.Default = FilterResult.Allow; + var result = sut.Process("safeexambrowser.org"); + Assert.AreEqual(FilterResult.Allow, result); + + sut.Default = FilterResult.Block; + result = sut.Process("safeexambrowser.org"); + Assert.AreEqual(FilterResult.Block, result); + } + + [TestMethod] + [ExpectedException(typeof(NotImplementedException))] + public void MustNotAllowUnsupportedResult() + { + sut.Load(new FilterRuleSettings { Result = (FilterResult) (-1) }); + } + + [TestMethod] + [ExpectedException(typeof(NotImplementedException))] + public void MustNotAllowUnsupportedFilterType() + { + sut.Load(new FilterRuleSettings { Type = (FilterType) (-1) }); + } + } +} diff --git a/SafeExamBrowser.Browser.UnitTests/Properties/AssemblyInfo.cs b/SafeExamBrowser.Browser.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..7c1bb291 --- /dev/null +++ b/SafeExamBrowser.Browser.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("SafeExamBrowser.Browser.UnitTests")] +[assembly: AssemblyDescription("Safe Exam Browser")] +[assembly: AssemblyCompany("ETH Zürich")] +[assembly: AssemblyProduct("SafeExamBrowser.Browser.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2019 ETH Zürich, Educational Development and Technology (LET)")] + +[assembly: ComVisible(false)] + +[assembly: Guid("f54c4c0e-4c72-4f88-a389-7f6de3ccb745")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] diff --git a/SafeExamBrowser.Browser.UnitTests/SafeExamBrowser.Browser.UnitTests.csproj b/SafeExamBrowser.Browser.UnitTests/SafeExamBrowser.Browser.UnitTests.csproj new file mode 100644 index 00000000..ff793437 --- /dev/null +++ b/SafeExamBrowser.Browser.UnitTests/SafeExamBrowser.Browser.UnitTests.csproj @@ -0,0 +1,100 @@ + + + + + + Debug + AnyCPU + {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745} + Library + Properties + SafeExamBrowser.Browser.UnitTests + SafeExamBrowser.Browser.UnitTests + v4.7.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + {5fb5273d-277c-41dd-8593-a25ce1aff2e9} + SafeExamBrowser.Browser.Contracts + + + {04e653f1-98e6-4e34-9dd7-7f2bc1a8b767} + SafeExamBrowser.Browser + + + {30b2d907-5861-4f39-abad-c4abf1b3470e} + SafeExamBrowser.Settings + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/SafeExamBrowser.Browser.UnitTests/packages.config b/SafeExamBrowser.Browser.UnitTests/packages.config new file mode 100644 index 00000000..2f7c5a18 --- /dev/null +++ b/SafeExamBrowser.Browser.UnitTests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/SafeExamBrowser.Browser/BrowserApplicationInstance.cs b/SafeExamBrowser.Browser/BrowserApplicationInstance.cs index 61e81cf1..259f2dda 100644 --- a/SafeExamBrowser.Browser/BrowserApplicationInstance.cs +++ b/SafeExamBrowser.Browser/BrowserApplicationInstance.cs @@ -14,9 +14,9 @@ using SafeExamBrowser.Browser.Contracts.Events; using SafeExamBrowser.Browser.Events; using SafeExamBrowser.Browser.Handlers; using SafeExamBrowser.Configuration.Contracts; -using SafeExamBrowser.Settings.Browser; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Settings.Browser; using SafeExamBrowser.UserInterface.Contracts; using SafeExamBrowser.UserInterface.Contracts.Browser; using SafeExamBrowser.UserInterface.Contracts.MessageBox; @@ -102,7 +102,7 @@ namespace SafeExamBrowser.Browser var keyboardHandler = new KeyboardHandler(); var lifeSpanHandler = new LifeSpanHandler(); var requestLogger = logger.CloneFor($"{nameof(RequestHandler)} {Id}"); - var requestHandler = new RequestHandler(appConfig, settings, logger); + var requestHandler = new RequestHandler(appConfig, settings.Filter, logger, text); displayHandler.FaviconChanged += DisplayHandler_FaviconChanged; displayHandler.ProgressChanged += DisplayHandler_ProgressChanged; @@ -117,6 +117,8 @@ namespace SafeExamBrowser.Browser control.AddressChanged += Control_AddressChanged; control.LoadingStateChanged += Control_LoadingStateChanged; control.TitleChanged += Control_TitleChanged; + + requestHandler.Initiailize(); control.Initialize(); logger.Debug("Initialized browser control."); diff --git a/SafeExamBrowser.Browser/Filters/BlockedContent.html b/SafeExamBrowser.Browser/Filters/BlockedContent.html new file mode 100644 index 00000000..f73bea4e --- /dev/null +++ b/SafeExamBrowser.Browser/Filters/BlockedContent.html @@ -0,0 +1,3 @@ +
+

%%MESSAGE%%

+
\ No newline at end of file diff --git a/SafeExamBrowser.Browser/Filters/BlockedPage.html b/SafeExamBrowser.Browser/Filters/BlockedPage.html new file mode 100644 index 00000000..f9dad271 --- /dev/null +++ b/SafeExamBrowser.Browser/Filters/BlockedPage.html @@ -0,0 +1,13 @@ + + + + + %%TITLE%% + + +
+

%%MESSAGE%%

+ +
+ + \ No newline at end of file diff --git a/SafeExamBrowser.Browser/Filters/RequestFilter.cs b/SafeExamBrowser.Browser/Filters/RequestFilter.cs index cb84e344..6cec7142 100644 --- a/SafeExamBrowser.Browser/Filters/RequestFilter.cs +++ b/SafeExamBrowser.Browser/Filters/RequestFilter.cs @@ -7,20 +7,74 @@ */ using System; +using System.Collections.Generic; +using SafeExamBrowser.Browser.Filters.Rules; using SafeExamBrowser.Settings.Browser; namespace SafeExamBrowser.Browser.Filters { internal class RequestFilter { - internal void Load(FilterRule rule) - { + private IList allowRules; + private IList blockRules; + internal FilterResult Default { get; set; } + + internal RequestFilter() + { + allowRules = new List(); + blockRules = new List(); + Default = FilterResult.Block; } - internal FilterResult Process(Uri request) + internal void Load(FilterRuleSettings settings) { - return FilterResult.Allow; + var rule = default(Rule); + + switch (settings.Type) + { + case FilterType.Regex: + rule = new RegexRule(settings.Expression); + break; + case FilterType.Simplified: + rule = new SimpleRule(settings.Expression); + break; + default: + throw new NotImplementedException($"Filter rule of type '{settings.Type}' is not yet implemented!"); + } + + switch (settings.Result) + { + case FilterResult.Allow: + allowRules.Add(rule); + break; + case FilterResult.Block: + blockRules.Add(rule); + break; + default: + throw new NotImplementedException($"Filter result '{settings.Result}' is not yet implemented!"); + } + } + + internal FilterResult Process(string url) + { + foreach (var rule in blockRules) + { + if (rule.IsMatch(url)) + { + return FilterResult.Block; + } + } + + foreach (var rule in allowRules) + { + if (rule.IsMatch(url)) + { + return FilterResult.Allow; + } + } + + return Default; } } } diff --git a/SafeExamBrowser.Browser/Filters/Rules/RegexRule.cs b/SafeExamBrowser.Browser/Filters/Rules/RegexRule.cs new file mode 100644 index 00000000..d8bf6989 --- /dev/null +++ b/SafeExamBrowser.Browser/Filters/Rules/RegexRule.cs @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 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.Text.RegularExpressions; + +namespace SafeExamBrowser.Browser.Filters.Rules +{ + internal class RegexRule : Rule + { + private string expression; + + public RegexRule(string expression) : base(expression) + { + } + + protected override void Initialize(string expression) + { + this.expression = expression; + } + + internal override bool IsMatch(string url) + { + return Regex.IsMatch(url, expression, RegexOptions.IgnoreCase); + } + } +} diff --git a/SafeExamBrowser.Browser/Filters/Rules/Rule.cs b/SafeExamBrowser.Browser/Filters/Rules/Rule.cs new file mode 100644 index 00000000..7c0a85c3 --- /dev/null +++ b/SafeExamBrowser.Browser/Filters/Rules/Rule.cs @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2019 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.Browser.Filters.Rules +{ + internal abstract class Rule + { + internal Rule(string expression) + { + Initialize(expression); + } + + internal abstract bool IsMatch(string url); + protected abstract void Initialize(string expression); + } +} diff --git a/SafeExamBrowser.Browser/Filters/Rules/SimpleRule.cs b/SafeExamBrowser.Browser/Filters/Rules/SimpleRule.cs new file mode 100644 index 00000000..ccf793a1 --- /dev/null +++ b/SafeExamBrowser.Browser/Filters/Rules/SimpleRule.cs @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 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.Text.RegularExpressions; + +namespace SafeExamBrowser.Browser.Filters.Rules +{ + internal class SimpleRule : Rule + { + private string expression; + + public SimpleRule(string expression) : base(expression) + { + } + + protected override void Initialize(string expression) + { + this.expression = expression.Replace("*", @".*"); + } + + internal override bool IsMatch(string url) + { + return Regex.IsMatch(url, expression, RegexOptions.IgnoreCase); + } + } +} diff --git a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs index 78dc4ffe..60369c30 100644 --- a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs +++ b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs @@ -7,46 +7,30 @@ */ using CefSharp; -using SafeExamBrowser.Browser.Filters; using SafeExamBrowser.Configuration.Contracts; +using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; -using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings; +using SafeExamBrowser.Settings.Browser; namespace SafeExamBrowser.Browser.Handlers { internal class RequestHandler : CefSharp.Handler.RequestHandler { - private AppConfig appConfig; - private BrowserSettings settings; - private RequestFilter filter; - private ILogger logger; - private ResourceRequestHandler resourceRequestHandler; + private ResourceHandler resourceHandler; - internal RequestHandler(AppConfig appConfig, BrowserSettings settings, ILogger logger) + internal RequestHandler(AppConfig appConfig, BrowserFilterSettings settings, ILogger logger, IText text) { - this.appConfig = appConfig; - this.settings = settings; - this.filter = new RequestFilter(); - this.logger = logger; - this.resourceRequestHandler = new ResourceRequestHandler(appConfig, settings, logger); + this.resourceHandler = new ResourceHandler(appConfig, settings, logger, text); } internal void Initiailize() { - if (settings.FilterMainRequests || settings.FilterContentRequests) - { - foreach (var rule in settings.FilterRules) - { - filter.Load(rule); - } - - logger.Debug("Initialized request filter."); - } + resourceHandler.Initialize(); } - protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) + protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) { - return resourceRequestHandler; + return resourceHandler; } } } diff --git a/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs b/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs new file mode 100644 index 00000000..ac86d6bc --- /dev/null +++ b/SafeExamBrowser.Browser/Handlers/ResourceHandler.cs @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2019 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.Specialized; +using System.IO; +using System.Reflection; +using CefSharp; +using SafeExamBrowser.Browser.Filters; +using SafeExamBrowser.Configuration.Contracts; +using SafeExamBrowser.I18n.Contracts; +using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Settings.Browser; + +namespace SafeExamBrowser.Browser.Handlers +{ + internal class ResourceHandler : CefSharp.Handler.ResourceRequestHandler + { + private AppConfig appConfig; + private BrowserFilterSettings settings; + private ILogger logger; + private RequestFilter filter; + private IResourceHandler contentBlockedHandler; + private IResourceHandler pageBlockedHandler; + private IText text; + + internal ResourceHandler(AppConfig appConfig, BrowserFilterSettings settings, ILogger logger, IText text) + { + this.appConfig = appConfig; + this.filter = new RequestFilter(); + this.logger = logger; + this.settings = settings; + this.text = text; + } + + internal void Initialize() + { + if (settings.FilterMainRequests || settings.FilterContentRequests) + { + InitializeFilter(); + } + } + + protected override IResourceHandler GetResourceHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request) + { + if (BlockMainRequest(request)) + { + return pageBlockedHandler; + } + + if (BlockContentRequest(request)) + { + return contentBlockedHandler; + } + + return base.GetResourceHandler(chromiumWebBrowser, browser, frame, request); + } + + protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) + { + // 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)) + { + return CefReturnValue.Cancel; + } + + ReplaceCustomScheme(request); + + return base.OnBeforeResourceLoad(webBrowser, browser, frame, request, callback); + } + + private void AppendCustomUserAgent(IRequest request) + { + var headers = new NameValueCollection(request.Headers); + var userAgent = request.Headers["User-Agent"]; + + headers["User-Agent"] = $"{userAgent} SEB/{appConfig.ProgramInformationalVersion}"; + request.Headers = headers; + } + + private bool BlockContentRequest(IRequest request) + { + if (settings.FilterContentRequests && request.ResourceType != ResourceType.MainFrame) + { + var result = filter.Process(request.Url); + var block = result == FilterResult.Block; + + if (block) + { + logger.Info($"Blocked content request for '{request.Url}'."); + } + + return block; + } + + return false; + } + + private bool BlockMainRequest(IRequest request) + { + if (settings.FilterMainRequests && request.ResourceType == ResourceType.MainFrame) + { + var result = filter.Process(request.Url); + var block = result == FilterResult.Block; + + if (block) + { + logger.Info($"Blocked main request for '{request.Url}'."); + } + + return block; + } + + return false; + } + + private void InitializeFilter() + { + var assembly = Assembly.GetAssembly(typeof(RequestFilter)); + var contentMessage = text.Get(TextKey.Browser_BlockedContentMessage); + var contentStream = assembly.GetManifestResourceStream($"{typeof(RequestFilter).Namespace}.BlockedContent.html"); + var pageButton = text.Get(TextKey.Browser_BlockedPageButton); + var pageMessage = text.Get(TextKey.Browser_BlockedPageMessage); + var pageTitle = text.Get(TextKey.Browser_BlockedPageTitle); + var pageStream = assembly.GetManifestResourceStream($"{typeof(RequestFilter).Namespace}.BlockedPage.html"); + var contentHtml = new StreamReader(contentStream).ReadToEnd(); + var pageHtml = new StreamReader(pageStream).ReadToEnd(); + + contentHtml = contentHtml.Replace("%%MESSAGE%%", contentMessage); + pageHtml = pageHtml.Replace("%%MESSAGE%%", pageMessage).Replace("%%TITLE%%", pageTitle).Replace("%%BACK_BUTTON%%", pageButton); + + contentBlockedHandler = CefSharp.ResourceHandler.FromString(contentHtml); + pageBlockedHandler = CefSharp.ResourceHandler.FromString(pageHtml); + + foreach (var rule in settings.Rules) + { + filter.Load(rule); + } + + logger.Debug($"Initialized request filter with {settings.Rules.Count} rules."); + } + + private bool IsMailtoUrl(string url) + { + return url.StartsWith(Uri.UriSchemeMailto); + } + + private void ReplaceCustomScheme(IRequest request) + { + if (Uri.IsWellFormedUriString(request.Url, UriKind.RelativeOrAbsolute)) + { + var uri = new Uri(request.Url); + + if (uri.Scheme == appConfig.SebUriScheme) + { + request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri.AbsoluteUri; + } + else if (uri.Scheme == appConfig.SebUriSchemeSecure) + { + request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps }.Uri.AbsoluteUri; + } + } + } + } +} diff --git a/SafeExamBrowser.Browser/Handlers/ResourceRequestHandler.cs b/SafeExamBrowser.Browser/Handlers/ResourceRequestHandler.cs deleted file mode 100644 index 35897e24..00000000 --- a/SafeExamBrowser.Browser/Handlers/ResourceRequestHandler.cs +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2019 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.Specialized; -using CefSharp; -using SafeExamBrowser.Configuration.Contracts; -using SafeExamBrowser.Logging.Contracts; -using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings; - -namespace SafeExamBrowser.Browser.Handlers -{ - internal class ResourceRequestHandler : CefSharp.Handler.ResourceRequestHandler - { - private AppConfig appConfig; - private BrowserSettings settings; - private ILogger logger; - - internal ResourceRequestHandler(AppConfig appConfig, BrowserSettings settings, ILogger logger) - { - this.appConfig = appConfig; - this.settings = settings; - this.logger = logger; - } - - protected override IResourceHandler GetResourceHandler(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request) - { - if (FilterMainRequest(request) || FilterContentRequest(request)) - { - return ResourceHandler.FromString("Blocked!"); - } - - return base.GetResourceHandler(webBrowser, browser, frame, request); - } - - protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) - { - // 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)) - { - return CefReturnValue.Cancel; - } - - ReplaceCustomScheme(request); - - return base.OnBeforeResourceLoad(webBrowser, browser, frame, request, callback); - } - - private void AppendCustomUserAgent(IRequest request) - { - var headers = new NameValueCollection(request.Headers); - var userAgent = request.Headers["User-Agent"]; - - headers["User-Agent"] = $"{userAgent} SEB/{appConfig.ProgramInformationalVersion}"; - request.Headers = headers; - } - - private bool FilterContentRequest(IRequest request) - { - return settings.FilterContentRequests && request.ResourceType != ResourceType.MainFrame; - } - - private bool FilterMainRequest(IRequest request) - { - return settings.FilterMainRequests && request.ResourceType == ResourceType.MainFrame; - } - - private bool IsMailtoUrl(string url) - { - return url.StartsWith(Uri.UriSchemeMailto); - } - - private void ReplaceCustomScheme(IRequest request) - { - if (Uri.IsWellFormedUriString(request.Url, UriKind.RelativeOrAbsolute)) - { - var uri = new Uri(request.Url); - - if (uri.Scheme == appConfig.SebUriScheme) - { - request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri.AbsoluteUri; - } - else if (uri.Scheme == appConfig.SebUriSchemeSecure) - { - request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps }.Uri.AbsoluteUri; - } - } - } - } -} diff --git a/SafeExamBrowser.Browser/Properties/AssemblyInfo.cs b/SafeExamBrowser.Browser/Properties/AssemblyInfo.cs index e827a509..357b5ac6 100644 --- a/SafeExamBrowser.Browser/Properties/AssemblyInfo.cs +++ b/SafeExamBrowser.Browser/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -14,6 +15,7 @@ using System.Runtime.InteropServices; // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] +[assembly: InternalsVisibleTo("SafeExamBrowser.Browser.UnitTests")] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("04e653f1-98e6-4e34-9dd7-7f2bc1a8b767")] diff --git a/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj b/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj index 253bda8c..93c88d9a 100644 --- a/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj +++ b/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj @@ -59,6 +59,7 @@ + @@ -72,6 +73,9 @@ + + + Component @@ -82,7 +86,7 @@ - + @@ -124,6 +128,10 @@ Designer + + + + diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.Browser.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.Browser.cs index 9579c2ac..4cabd897 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.Browser.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.Browser.cs @@ -110,7 +110,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData { if (value is bool filter) { - settings.Browser.FilterContentRequests = filter; + settings.Browser.Filter.FilterContentRequests = filter; } } @@ -118,7 +118,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData { if (value is bool filter) { - settings.Browser.FilterMainRequests = filter; + settings.Browser.Filter.FilterMainRequests = filter; } } @@ -126,30 +126,33 @@ namespace SafeExamBrowser.Configuration.ConfigurationData { const int ALLOW = 1; - if (value is IEnumerable> ruleDataList) + if (value is IList ruleDataList) { - foreach (var ruleData in ruleDataList) + foreach (var item in ruleDataList) { - if (ruleData.TryGetValue(Keys.Browser.Filter.RuleIsActive, out var v) && v is bool active && active) + if (item is IDictionary ruleData) { - var rule = new FilterRule(); - - if (ruleData.TryGetValue(Keys.Browser.Filter.RuleExpression, out v) && v is string expression) + if (ruleData.TryGetValue(Keys.Browser.Filter.RuleIsActive, out var v) && v is bool active && active) { - rule.Expression = expression; - } + var rule = new FilterRuleSettings(); - if (ruleData.TryGetValue(Keys.Browser.Filter.RuleAction, out v) && v is int action) - { - rule.Result = action == ALLOW ? FilterResult.Allow : FilterResult.Block; - } + if (ruleData.TryGetValue(Keys.Browser.Filter.RuleExpression, out v) && v is string expression) + { + rule.Expression = expression; + } - if (ruleData.TryGetValue(Keys.Browser.Filter.RuleExpressionIsRegex, out v) && v is bool regex) - { - rule.Type = regex ? FilterType.Regex : FilterType.Simplified; - } + if (ruleData.TryGetValue(Keys.Browser.Filter.RuleAction, out v) && v is int action) + { + rule.Result = action == ALLOW ? FilterResult.Allow : FilterResult.Block; + } - settings.Browser.FilterRules.Add(rule); + if (ruleData.TryGetValue(Keys.Browser.Filter.RuleExpressionIsRegex, out v) && v is bool regex) + { + rule.Type = regex ? FilterType.Regex : FilterType.Simplified; + } + + settings.Browser.Filter.Rules.Add(rule); + } } } } diff --git a/SafeExamBrowser.I18n.Contracts/TextKey.cs b/SafeExamBrowser.I18n.Contracts/TextKey.cs index 58e6600f..51557657 100644 --- a/SafeExamBrowser.I18n.Contracts/TextKey.cs +++ b/SafeExamBrowser.I18n.Contracts/TextKey.cs @@ -14,6 +14,10 @@ namespace SafeExamBrowser.I18n.Contracts /// public enum TextKey { + Browser_BlockedContentMessage, + Browser_BlockedPageButton, + Browser_BlockedPageMessage, + Browser_BlockedPageTitle, BrowserWindow_DeveloperConsoleMenuItem, BrowserWindow_ZoomMenuItem, Build, diff --git a/SafeExamBrowser.I18n/Text.xml b/SafeExamBrowser.I18n/Text.xml index 78a959dc..17a3f8a4 100644 --- a/SafeExamBrowser.I18n/Text.xml +++ b/SafeExamBrowser.I18n/Text.xml @@ -1,5 +1,17 @@  + + Content blocked + + + Back to previous page + + + Access to this page is not allowed according to the application configuration. + + + Page Blocked + Developer Console diff --git a/SafeExamBrowser.Settings/Browser/BrowserFilterSettings.cs b/SafeExamBrowser.Settings/Browser/BrowserFilterSettings.cs new file mode 100644 index 00000000..b6545759 --- /dev/null +++ b/SafeExamBrowser.Settings/Browser/BrowserFilterSettings.cs @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 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; + +namespace SafeExamBrowser.Settings.Browser +{ + /// + /// Defines all configuration options for the request filter of the browser. + /// + [Serializable] + public class BrowserFilterSettings + { + /// + /// Defines whether all content requests for a web page should be filtered according to the defined . + /// + public bool FilterContentRequests { get; set; } + + /// + /// Defines whether the main request for a web page should be filtered according to the defined . + /// + public bool FilterMainRequests { get; set; } + + /// + /// Defines all rules to be used to filter web requests. + /// + public IList Rules { get; set; } + + public BrowserFilterSettings() + { + Rules = new List(); + } + } +} diff --git a/SafeExamBrowser.Settings/Browser/BrowserSettings.cs b/SafeExamBrowser.Settings/Browser/BrowserSettings.cs index e371d2c5..b6e2db36 100644 --- a/SafeExamBrowser.Settings/Browser/BrowserSettings.cs +++ b/SafeExamBrowser.Settings/Browser/BrowserSettings.cs @@ -7,7 +7,6 @@ */ using System; -using System.Collections.Generic; namespace SafeExamBrowser.Settings.Browser { @@ -48,19 +47,9 @@ namespace SafeExamBrowser.Settings.Browser public string CustomUserAgent { get; set; } /// - /// Defines whether all content requests for a web page should be filtered according to the defined . + /// The settings to be used for the browser request filter. /// - public bool FilterContentRequests { get; set; } - - /// - /// Defines whether the main request for a web page should be filtered according to the defined . - /// - public bool FilterMainRequests { get; set; } - - /// - /// Defines all rules to be used to filter web requests. - /// - public IList FilterRules { get; set; } + public BrowserFilterSettings Filter { get; set; } /// /// The configuration to be used for the main browser window. @@ -80,7 +69,7 @@ namespace SafeExamBrowser.Settings.Browser public BrowserSettings() { AdditionalWindowSettings = new BrowserWindowSettings(); - FilterRules = new List(); + Filter = new BrowserFilterSettings(); MainWindowSettings = new BrowserWindowSettings(); } } diff --git a/SafeExamBrowser.Settings/Browser/FilterRule.cs b/SafeExamBrowser.Settings/Browser/FilterRuleSettings.cs similarity index 90% rename from SafeExamBrowser.Settings/Browser/FilterRule.cs rename to SafeExamBrowser.Settings/Browser/FilterRuleSettings.cs index f8c72693..4e6489c3 100644 --- a/SafeExamBrowser.Settings/Browser/FilterRule.cs +++ b/SafeExamBrowser.Settings/Browser/FilterRuleSettings.cs @@ -11,10 +11,10 @@ using System; namespace SafeExamBrowser.Settings.Browser { /// - /// Defines a request filter rule. + /// Defines the settings for a request filter rule. /// [Serializable] - public class FilterRule + public class FilterRuleSettings { /// /// The expression according to which requests should be filtered. diff --git a/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj b/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj index ab3498c7..61a9266f 100644 --- a/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj +++ b/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj @@ -53,10 +53,11 @@ + - + diff --git a/SafeExamBrowser.sln b/SafeExamBrowser.sln index 1278951d..d6912168 100644 --- a/SafeExamBrowser.sln +++ b/SafeExamBrowser.sln @@ -104,6 +104,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.WindowsApi. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Settings", "SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj", "{30B2D907-5861-4F39-ABAD-C4ABF1B3470E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Browser.UnitTests", "SafeExamBrowser.Browser.UnitTests\SafeExamBrowser.Browser.UnitTests.csproj", "{F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -466,6 +468,14 @@ Global {30B2D907-5861-4F39-ABAD-C4ABF1B3470E}.Release|Any CPU.Build.0 = Release|Any CPU {30B2D907-5861-4F39-ABAD-C4ABF1B3470E}.Release|x86.ActiveCfg = Release|x86 {30B2D907-5861-4F39-ABAD-C4ABF1B3470E}.Release|x86.Build.0 = Release|x86 + {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}.Debug|x86.ActiveCfg = Debug|x86 + {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}.Debug|x86.Build.0 = Debug|x86 + {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}.Release|Any CPU.Build.0 = Release|Any CPU + {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}.Release|x86.ActiveCfg = Release|x86 + {F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/appveyor-test.yml b/appveyor-test.yml index 8766d0e2..d0e876d6 100644 --- a/appveyor-test.yml +++ b/appveyor-test.yml @@ -7,7 +7,8 @@ before_build: build_script: - msbuild /verbosity:minimal "SafeExamBrowser.sln" test_script: - - .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.Client.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.Client.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -output:"coverage.xml" + - .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.Browser.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.Browser.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -output:"coverage.xml" + - .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.Client.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.Client.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -mergeoutput -output:"coverage.xml" - .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.Communication.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.Communication.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -mergeoutput -output:"coverage.xml" - .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.Configuration.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.Configuration.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -mergeoutput -output:"coverage.xml" - .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.Core.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.Core.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -mergeoutput -output:"coverage.xml"