SEBWIN-314: Completed infrastructure for request filter.

This commit is contained in:
dbuechel 2019-09-10 11:01:49 +02:00
parent 367ebf1329
commit 1dd65e1bda
25 changed files with 669 additions and 167 deletions

View file

@ -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) });
}
}
}

View file

@ -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")]

View file

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props" Condition="Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>SafeExamBrowser.Browser.UnitTests</RootNamespace>
<AssemblyName>SafeExamBrowser.Browser.UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
<IsCodedUITest>False</IsCodedUITest>
<TestProjectType>UnitTest</TestProjectType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="Filters\RequestFilterTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SafeExamBrowser.Browser.Contracts\SafeExamBrowser.Browser.Contracts.csproj">
<Project>{5fb5273d-277c-41dd-8593-a25ce1aff2e9}</Project>
<Name>SafeExamBrowser.Browser.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Browser\SafeExamBrowser.Browser.csproj">
<Project>{04e653f1-98e6-4e34-9dd7-7f2bc1a8b767}</Project>
<Name>SafeExamBrowser.Browser</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj">
<Project>{30b2d907-5861-4f39-abad-c4abf1b3470e}</Project>
<Name>SafeExamBrowser.Settings</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props'))" />
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets'))" />
</Target>
<Import Project="..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets" Condition="Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets')" />
</Project>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="MSTest.TestAdapter" version="1.3.2" targetFramework="net472" />
<package id="MSTest.TestFramework" version="1.3.2" targetFramework="net472" />
</packages>

View file

@ -14,9 +14,9 @@ using SafeExamBrowser.Browser.Contracts.Events;
using SafeExamBrowser.Browser.Events; using SafeExamBrowser.Browser.Events;
using SafeExamBrowser.Browser.Handlers; using SafeExamBrowser.Browser.Handlers;
using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Settings.Browser;
using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.Browser;
using SafeExamBrowser.UserInterface.Contracts; using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Browser; using SafeExamBrowser.UserInterface.Contracts.Browser;
using SafeExamBrowser.UserInterface.Contracts.MessageBox; using SafeExamBrowser.UserInterface.Contracts.MessageBox;
@ -102,7 +102,7 @@ namespace SafeExamBrowser.Browser
var keyboardHandler = new KeyboardHandler(); var keyboardHandler = new KeyboardHandler();
var lifeSpanHandler = new LifeSpanHandler(); var lifeSpanHandler = new LifeSpanHandler();
var requestLogger = logger.CloneFor($"{nameof(RequestHandler)} {Id}"); 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.FaviconChanged += DisplayHandler_FaviconChanged;
displayHandler.ProgressChanged += DisplayHandler_ProgressChanged; displayHandler.ProgressChanged += DisplayHandler_ProgressChanged;
@ -117,6 +117,8 @@ namespace SafeExamBrowser.Browser
control.AddressChanged += Control_AddressChanged; control.AddressChanged += Control_AddressChanged;
control.LoadingStateChanged += Control_LoadingStateChanged; control.LoadingStateChanged += Control_LoadingStateChanged;
control.TitleChanged += Control_TitleChanged; control.TitleChanged += Control_TitleChanged;
requestHandler.Initiailize();
control.Initialize(); control.Initialize();
logger.Debug("Initialized browser control."); logger.Debug("Initialized browser control.");

View file

@ -0,0 +1,3 @@
<div style="background-color: lightgray; display: table; font-family: 'Segoe UI'; height: 100%; text-align: center; width: 100%">
<p style="display: table-cell; font-weight: bold; vertical-align: middle">%%MESSAGE%%</p>
</div>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html style="height: 100%; width: 100%">
<head>
<meta charset="utf-8" />
<title>%%TITLE%%</title>
</head>
<body style="background-color: lightgray; display: table; font-family: 'Segoe UI'; height: 98%; text-align: center; width: 99%">
<div style="display: table-cell; vertical-align: middle">
<p style="font-weight: bold">%%MESSAGE%%</p>
<button onclick="window.history.back()" style="cursor: pointer">&#x2B60; %%BACK_BUTTON%%</button>
</div>
</body>
</html>

View file

@ -7,20 +7,74 @@
*/ */
using System; using System;
using System.Collections.Generic;
using SafeExamBrowser.Browser.Filters.Rules;
using SafeExamBrowser.Settings.Browser; using SafeExamBrowser.Settings.Browser;
namespace SafeExamBrowser.Browser.Filters namespace SafeExamBrowser.Browser.Filters
{ {
internal class RequestFilter internal class RequestFilter
{ {
internal void Load(FilterRule rule) private IList<Rule> allowRules;
{ private IList<Rule> blockRules;
internal FilterResult Default { get; set; }
internal RequestFilter()
{
allowRules = new List<Rule>();
blockRules = new List<Rule>();
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;
} }
} }
} }

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -7,46 +7,30 @@
*/ */
using CefSharp; using CefSharp;
using SafeExamBrowser.Browser.Filters;
using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Logging.Contracts;
using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings; using SafeExamBrowser.Settings.Browser;
namespace SafeExamBrowser.Browser.Handlers namespace SafeExamBrowser.Browser.Handlers
{ {
internal class RequestHandler : CefSharp.Handler.RequestHandler internal class RequestHandler : CefSharp.Handler.RequestHandler
{ {
private AppConfig appConfig; private ResourceHandler resourceHandler;
private BrowserSettings settings;
private RequestFilter filter;
private ILogger logger;
private ResourceRequestHandler resourceRequestHandler;
internal RequestHandler(AppConfig appConfig, BrowserSettings settings, ILogger logger) internal RequestHandler(AppConfig appConfig, BrowserFilterSettings settings, ILogger logger, IText text)
{ {
this.appConfig = appConfig; this.resourceHandler = new ResourceHandler(appConfig, settings, logger, text);
this.settings = settings;
this.filter = new RequestFilter();
this.logger = logger;
this.resourceRequestHandler = new ResourceRequestHandler(appConfig, settings, logger);
} }
internal void Initiailize() internal void Initiailize()
{ {
if (settings.FilterMainRequests || settings.FilterContentRequests) resourceHandler.Initialize();
{
foreach (var rule in settings.FilterRules)
{
filter.Load(rule);
}
logger.Debug("Initialized request filter.");
}
} }
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;
} }
} }
} }

View file

@ -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;
}
}
}
}
}

View file

@ -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("<html><body>Blocked!</body></html>");
}
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;
}
}
}
}
}

View file

@ -1,4 +1,5 @@
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following // 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 // to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type. // COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)] [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 // The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("04e653f1-98e6-4e34-9dd7-7f2bc1a8b767")] [assembly: Guid("04e653f1-98e6-4e34-9dd7-7f2bc1a8b767")]

View file

@ -59,6 +59,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
@ -72,6 +73,9 @@
<Compile Include="Events\PopupRequestedEventArgs.cs" /> <Compile Include="Events\PopupRequestedEventArgs.cs" />
<Compile Include="Events\PopupRequestedEventHandler.cs" /> <Compile Include="Events\PopupRequestedEventHandler.cs" />
<Compile Include="Filters\RequestFilter.cs" /> <Compile Include="Filters\RequestFilter.cs" />
<Compile Include="Filters\Rules\Rule.cs" />
<Compile Include="Filters\Rules\RegexRule.cs" />
<Compile Include="Filters\Rules\SimpleRule.cs" />
<Compile Include="Handlers\ContextMenuHandler.cs" /> <Compile Include="Handlers\ContextMenuHandler.cs" />
<Compile Include="BrowserControl.cs"> <Compile Include="BrowserControl.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
@ -82,7 +86,7 @@
<Compile Include="Handlers\KeyboardHandler.cs" /> <Compile Include="Handlers\KeyboardHandler.cs" />
<Compile Include="Handlers\LifeSpanHandler.cs" /> <Compile Include="Handlers\LifeSpanHandler.cs" />
<Compile Include="Handlers\RequestHandler.cs" /> <Compile Include="Handlers\RequestHandler.cs" />
<Compile Include="Handlers\ResourceRequestHandler.cs" /> <Compile Include="Handlers\ResourceHandler.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -124,6 +128,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Filters\BlockedContent.html" />
<EmbeddedResource Include="Filters\BlockedPage.html" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup> <PropertyGroup>
<PostBuildEvent> <PostBuildEvent>

View file

@ -110,7 +110,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
{ {
if (value is bool filter) 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) 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; const int ALLOW = 1;
if (value is IEnumerable<IDictionary<string, object>> ruleDataList) if (value is IList<object> 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<string, object> ruleData)
{ {
var rule = new FilterRule(); if (ruleData.TryGetValue(Keys.Browser.Filter.RuleIsActive, out var v) && v is bool active && active)
if (ruleData.TryGetValue(Keys.Browser.Filter.RuleExpression, out v) && v is string expression)
{ {
rule.Expression = expression; var rule = new FilterRuleSettings();
}
if (ruleData.TryGetValue(Keys.Browser.Filter.RuleAction, out v) && v is int action) if (ruleData.TryGetValue(Keys.Browser.Filter.RuleExpression, out v) && v is string expression)
{ {
rule.Result = action == ALLOW ? FilterResult.Allow : FilterResult.Block; rule.Expression = expression;
} }
if (ruleData.TryGetValue(Keys.Browser.Filter.RuleExpressionIsRegex, out v) && v is bool regex) if (ruleData.TryGetValue(Keys.Browser.Filter.RuleAction, out v) && v is int action)
{ {
rule.Type = regex ? FilterType.Regex : FilterType.Simplified; 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);
}
} }
} }
} }

View file

@ -14,6 +14,10 @@ namespace SafeExamBrowser.I18n.Contracts
/// </summary> /// </summary>
public enum TextKey public enum TextKey
{ {
Browser_BlockedContentMessage,
Browser_BlockedPageButton,
Browser_BlockedPageMessage,
Browser_BlockedPageTitle,
BrowserWindow_DeveloperConsoleMenuItem, BrowserWindow_DeveloperConsoleMenuItem,
BrowserWindow_ZoomMenuItem, BrowserWindow_ZoomMenuItem,
Build, Build,

View file

@ -1,5 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<Text> <Text>
<Entry key="Browser_BlockedContentMessage">
Content blocked
</Entry>
<Entry key="Browser_BlockedPageButton">
Back to previous page
</Entry>
<Entry key="Browser_BlockedPageMessage">
Access to this page is not allowed according to the application configuration.
</Entry>
<Entry key="Browser_BlockedPageTitle">
Page Blocked
</Entry>
<Entry key="BrowserWindow_DeveloperConsoleMenuItem"> <Entry key="BrowserWindow_DeveloperConsoleMenuItem">
Developer Console Developer Console
</Entry> </Entry>

View file

@ -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
{
/// <summary>
/// Defines all configuration options for the request filter of the browser.
/// </summary>
[Serializable]
public class BrowserFilterSettings
{
/// <summary>
/// Defines whether all content requests for a web page should be filtered according to the defined <see cref="Rules"/>.
/// </summary>
public bool FilterContentRequests { get; set; }
/// <summary>
/// Defines whether the main request for a web page should be filtered according to the defined <see cref="Rules"/>.
/// </summary>
public bool FilterMainRequests { get; set; }
/// <summary>
/// Defines all rules to be used to filter web requests.
/// </summary>
public IList<FilterRuleSettings> Rules { get; set; }
public BrowserFilterSettings()
{
Rules = new List<FilterRuleSettings>();
}
}
}

View file

@ -7,7 +7,6 @@
*/ */
using System; using System;
using System.Collections.Generic;
namespace SafeExamBrowser.Settings.Browser namespace SafeExamBrowser.Settings.Browser
{ {
@ -48,19 +47,9 @@ namespace SafeExamBrowser.Settings.Browser
public string CustomUserAgent { get; set; } public string CustomUserAgent { get; set; }
/// <summary> /// <summary>
/// Defines whether all content requests for a web page should be filtered according to the defined <see cref="FilterRules"/>. /// The settings to be used for the browser request filter.
/// </summary> /// </summary>
public bool FilterContentRequests { get; set; } public BrowserFilterSettings Filter { get; set; }
/// <summary>
/// Defines whether the main request for a web page should be filtered according to the defined <see cref="FilterRules"/>.
/// </summary>
public bool FilterMainRequests { get; set; }
/// <summary>
/// Defines all rules to be used to filter web requests.
/// </summary>
public IList<FilterRule> FilterRules { get; set; }
/// <summary> /// <summary>
/// The configuration to be used for the main browser window. /// The configuration to be used for the main browser window.
@ -80,7 +69,7 @@ namespace SafeExamBrowser.Settings.Browser
public BrowserSettings() public BrowserSettings()
{ {
AdditionalWindowSettings = new BrowserWindowSettings(); AdditionalWindowSettings = new BrowserWindowSettings();
FilterRules = new List<FilterRule>(); Filter = new BrowserFilterSettings();
MainWindowSettings = new BrowserWindowSettings(); MainWindowSettings = new BrowserWindowSettings();
} }
} }

View file

@ -11,10 +11,10 @@ using System;
namespace SafeExamBrowser.Settings.Browser namespace SafeExamBrowser.Settings.Browser
{ {
/// <summary> /// <summary>
/// Defines a request filter rule. /// Defines the settings for a request filter rule.
/// </summary> /// </summary>
[Serializable] [Serializable]
public class FilterRule public class FilterRuleSettings
{ {
/// <summary> /// <summary>
/// The expression according to which requests should be filtered. /// The expression according to which requests should be filtered.

View file

@ -53,10 +53,11 @@
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Browser\BrowserFilterSettings.cs" />
<Compile Include="Browser\BrowserSettings.cs" /> <Compile Include="Browser\BrowserSettings.cs" />
<Compile Include="Browser\BrowserWindowSettings.cs" /> <Compile Include="Browser\BrowserWindowSettings.cs" />
<Compile Include="Browser\FilterResult.cs" /> <Compile Include="Browser\FilterResult.cs" />
<Compile Include="Browser\FilterRule.cs" /> <Compile Include="Browser\FilterRuleSettings.cs" />
<Compile Include="Browser\FilterType.cs" /> <Compile Include="Browser\FilterType.cs" />
<Compile Include="ConfigurationMode.cs" /> <Compile Include="ConfigurationMode.cs" />
<Compile Include="KioskMode.cs" /> <Compile Include="KioskMode.cs" />

View file

@ -104,6 +104,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.WindowsApi.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Settings", "SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj", "{30B2D907-5861-4F39-ABAD-C4ABF1B3470E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Settings", "SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj", "{30B2D907-5861-4F39-ABAD-C4ABF1B3470E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Browser.UnitTests", "SafeExamBrowser.Browser.UnitTests\SafeExamBrowser.Browser.UnitTests.csproj", "{F54C4C0E-4C72-4F88-A389-7F6DE3CCB745}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|Any CPU.Build.0 = Release|Any CPU
{30B2D907-5861-4F39-ABAD-C4ABF1B3470E}.Release|x86.ActiveCfg = Release|x86 {30B2D907-5861-4F39-ABAD-C4ABF1B3470E}.Release|x86.ActiveCfg = Release|x86
{30B2D907-5861-4F39-ABAD-C4ABF1B3470E}.Release|x86.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -7,7 +7,8 @@ before_build:
build_script: build_script:
- msbuild /verbosity:minimal "SafeExamBrowser.sln" - msbuild /verbosity:minimal "SafeExamBrowser.sln"
test_script: 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.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.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" - .\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"