SEBWIN-314: Started implementing filter rules with unit tests.
This commit is contained in:
parent
5209103c97
commit
6d1b282b33
7 changed files with 995 additions and 3 deletions
553
SafeExamBrowser.Browser.UnitTests/Filters/LegacyFilter.cs
Normal file
553
SafeExamBrowser.Browser.UnitTests/Filters/LegacyFilter.cs
Normal file
|
@ -0,0 +1,553 @@
|
|||
/*
|
||||
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla internal
|
||||
* 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.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SafeExamBrowser.Browser.UnitTests.Filters
|
||||
{
|
||||
internal class LegacyFilter
|
||||
{
|
||||
internal Regex scheme;
|
||||
internal Regex user;
|
||||
internal Regex password;
|
||||
internal Regex host;
|
||||
internal int? port;
|
||||
internal Regex path;
|
||||
internal Regex query;
|
||||
internal Regex fragment;
|
||||
|
||||
internal LegacyFilter(string filterExpressionString)
|
||||
{
|
||||
SEBURLFilterExpression URLFromString = new SEBURLFilterExpression(filterExpressionString);
|
||||
try
|
||||
{
|
||||
this.scheme = RegexForFilterString(URLFromString.scheme);
|
||||
this.user = RegexForFilterString(URLFromString.user);
|
||||
this.password = RegexForFilterString(URLFromString.password);
|
||||
this.host = RegexForHostFilterString(URLFromString.host);
|
||||
this.port = URLFromString.port;
|
||||
this.path = RegexForPathFilterString(URLFromString.path);
|
||||
this.query = RegexForQueryFilterString(URLFromString.query);
|
||||
this.fragment = RegexForFilterString(URLFromString.fragment);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Method comparing all components of a passed URL with the filter expression
|
||||
// and returning YES (= allow or block) if it matches
|
||||
internal bool IsMatch(Uri URLToFilter)
|
||||
{
|
||||
Regex filterComponent;
|
||||
|
||||
// If a scheme is indicated in the filter expression, it has to match
|
||||
filterComponent = scheme;
|
||||
UriBuilder urlToFilterParts = new UriBuilder(URLToFilter);
|
||||
|
||||
if (filterComponent != null &&
|
||||
!Regex.IsMatch(URLToFilter.Scheme, filterComponent.ToString(), RegexOptions.IgnoreCase))
|
||||
{
|
||||
// Scheme of the URL to filter doesn't match the one from the filter expression: Exit with matching = NO
|
||||
return false;
|
||||
}
|
||||
|
||||
string userInfo = URLToFilter.UserInfo;
|
||||
filterComponent = user;
|
||||
if (filterComponent != null &&
|
||||
!Regex.IsMatch(urlToFilterParts.UserName, filterComponent.ToString(), RegexOptions.IgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
filterComponent = password;
|
||||
if (filterComponent != null &&
|
||||
!Regex.IsMatch(urlToFilterParts.Password, filterComponent.ToString(), RegexOptions.IgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
filterComponent = host;
|
||||
if (filterComponent != null &&
|
||||
!Regex.IsMatch(URLToFilter.Host, filterComponent.ToString(), RegexOptions.IgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (port != null && URLToFilter.Port != port)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
filterComponent = path;
|
||||
if (filterComponent != null &&
|
||||
!Regex.IsMatch(URLToFilter.AbsolutePath.Trim(new char[] { '/' }), filterComponent.ToString(), RegexOptions.IgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string urlQuery = URLToFilter.GetComponents(UriComponents.Query, UriFormat.Unescaped);
|
||||
filterComponent = query;
|
||||
if (filterComponent != null)
|
||||
{
|
||||
// If there's a query filter component, then we need to even filter empty URL query strings
|
||||
// as the filter might either allow some specific queries or no query at all ("?." query filter)
|
||||
if (urlQuery == null)
|
||||
{
|
||||
urlQuery = "";
|
||||
}
|
||||
if (!Regex.IsMatch(urlQuery, filterComponent.ToString(), RegexOptions.IgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string urlFragment = URLToFilter.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
|
||||
filterComponent = fragment;
|
||||
if (filterComponent != null &&
|
||||
!Regex.IsMatch(urlFragment, filterComponent.ToString(), RegexOptions.IgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// URL matches the filter expression
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static Regex RegexForFilterString(string filterString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filterString))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
string regexString = Regex.Escape(filterString);
|
||||
regexString = regexString.Replace("\\*", ".*?");
|
||||
// Add regex command characters for matching at start and end of a line (part)
|
||||
regexString = string.Format("^{0}$", regexString);
|
||||
|
||||
try
|
||||
{
|
||||
Regex regex = new Regex(regexString, RegexOptions.IgnoreCase);
|
||||
return regex;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static Regex RegexForHostFilterString(string filterString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filterString))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if host string has a dot "." prefix to disable subdomain matching
|
||||
if (filterString.Length > 1 && filterString.StartsWith("."))
|
||||
{
|
||||
// Get host string without the "." prefix
|
||||
filterString = filterString.Substring(1);
|
||||
// Get regex for host <*://example.com> (without possible subdomains)
|
||||
return RegexForFilterString(filterString);
|
||||
}
|
||||
// Allow subdomain matching: Create combined regex for <example.com> and <*.example.com>
|
||||
string regexString = Regex.Escape(filterString);
|
||||
regexString = regexString.Replace("\\*", ".*?");
|
||||
// Add regex command characters for matching at start and end of a line (part)
|
||||
regexString = string.Format("^(({0})|(.*?\\.{0}))$", regexString);
|
||||
Regex regex = new Regex(regexString, RegexOptions.IgnoreCase);
|
||||
return regex;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static Regex RegexForPathFilterString(string filterString)
|
||||
{
|
||||
// Trim a possible trailing slash "/", we will instead add a rule to also match paths to directories without trailing slash
|
||||
filterString = filterString.TrimEnd(new char[] { '/' });
|
||||
;
|
||||
|
||||
if (string.IsNullOrEmpty(filterString))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if path string ends with a "/*" for matching contents of a directory
|
||||
if (filterString.EndsWith("/*"))
|
||||
{
|
||||
// As the path filter string matches for a directory, we need to add a string to match directories without trailing slash
|
||||
|
||||
// Get path string without the "/*" suffix
|
||||
string filterStringDirectory = filterString.Substring(0, filterString.Length - 2);
|
||||
|
||||
string regexString = Regex.Escape(filterString);
|
||||
regexString = regexString.Replace("\\*", ".*?");
|
||||
|
||||
string regexStringDir = Regex.Escape(filterString);
|
||||
regexStringDir = regexStringDir.Replace("\\*", ".*?");
|
||||
|
||||
// Add regex command characters for matching at start and end of a line (part)
|
||||
regexString = string.Format("^(({0})|({1}))$", regexString, regexStringDir);
|
||||
|
||||
Regex regex = new Regex(regexString, RegexOptions.IgnoreCase);
|
||||
return regex;
|
||||
}
|
||||
else
|
||||
{
|
||||
return RegexForFilterString(filterString);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static Regex RegexForQueryFilterString(string filterString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filterString))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (filterString.Equals("."))
|
||||
{
|
||||
// Add regex command characters for matching at start and end of a line (part)
|
||||
// and regex for no string allowed
|
||||
string regexString = @"^$";
|
||||
try
|
||||
{
|
||||
Regex regex = new Regex(regexString, RegexOptions.IgnoreCase);
|
||||
return regex;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return RegexForFilterString(filterString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder expressionString = new StringBuilder();
|
||||
string part;
|
||||
expressionString.Append("^");
|
||||
|
||||
/// Scheme
|
||||
if (this.scheme != null)
|
||||
{
|
||||
// If there is a regex filter for scheme
|
||||
// get stripped regex pattern
|
||||
part = StringForRegexFilter(this.scheme);
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise use the regex wildcard pattern for scheme
|
||||
part = @".*?";
|
||||
}
|
||||
|
||||
expressionString.AppendFormat("{0}:\\/\\/", part);
|
||||
|
||||
/// User/Password
|
||||
if (this.user != null)
|
||||
{
|
||||
part = StringForRegexFilter(this.user);
|
||||
|
||||
expressionString.Append(part);
|
||||
|
||||
if (this.password != null)
|
||||
{
|
||||
expressionString.AppendFormat(":{0}@", StringForRegexFilter(this.password));
|
||||
}
|
||||
else
|
||||
{
|
||||
expressionString.Append("@");
|
||||
}
|
||||
}
|
||||
|
||||
/// Host
|
||||
string hostPort = "";
|
||||
if (this.host != null)
|
||||
{
|
||||
hostPort = StringForRegexFilter(this.host);
|
||||
}
|
||||
else
|
||||
{
|
||||
hostPort = ".*?";
|
||||
}
|
||||
|
||||
/// Port
|
||||
if (this.port != null && this.port > 0 && this.port <= 65535)
|
||||
{
|
||||
hostPort = string.Format("{0}:{1}", hostPort, this.port);
|
||||
}
|
||||
|
||||
// When there is a host, but no path
|
||||
if (this.host != null && this.path == null)
|
||||
{
|
||||
hostPort = string.Format("(({0})|({0}\\/.*?))", hostPort);
|
||||
}
|
||||
|
||||
expressionString.Append(hostPort);
|
||||
|
||||
/// Path
|
||||
if (this.path != null)
|
||||
{
|
||||
string path = StringForRegexFilter(this.path);
|
||||
if (path.StartsWith("\\/"))
|
||||
{
|
||||
expressionString.Append(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
expressionString.AppendFormat("\\/{0}", path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Query
|
||||
if (this.query != null)
|
||||
{
|
||||
// Check for special case Query = "?." which means no query string is allowed
|
||||
if (StringForRegexFilter(this.query).Equals("."))
|
||||
{
|
||||
expressionString.AppendFormat("[^\\?]");
|
||||
}
|
||||
else
|
||||
{
|
||||
expressionString.AppendFormat("\\?{0}", StringForRegexFilter(this.query));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
expressionString.AppendFormat("(()|(\\?.*?))");
|
||||
}
|
||||
|
||||
/// Fragment
|
||||
if (this.fragment != null)
|
||||
{
|
||||
expressionString.AppendFormat("#{0}", StringForRegexFilter(this.fragment));
|
||||
}
|
||||
|
||||
expressionString.Append("$");
|
||||
|
||||
return expressionString.ToString();
|
||||
}
|
||||
|
||||
internal string StringForRegexFilter(Regex regexFilter)
|
||||
{
|
||||
// Get pattern string from regular expression
|
||||
string regexPattern = regexFilter.ToString();
|
||||
if (regexPattern.Length <= 2)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
// Remove the regex command characters for matching at start and end of a line
|
||||
regexPattern = regexPattern.Substring(1, regexPattern.Length - 2);
|
||||
return regexPattern;
|
||||
}
|
||||
|
||||
private class SEBURLFilterExpression
|
||||
{
|
||||
internal string scheme;
|
||||
internal string user;
|
||||
internal string password;
|
||||
internal string host;
|
||||
internal int? port;
|
||||
internal string path;
|
||||
internal string query;
|
||||
internal string fragment;
|
||||
|
||||
internal SEBURLFilterExpression(string filterExpressionString)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filterExpressionString))
|
||||
{
|
||||
/// Convert Uri to a SEBURLFilterExpression
|
||||
string splitURLRegexPattern = @"(?:([^\:]*)\:\/\/)?(?:([^\:\@]*)(?:\:([^\@]*))?\@)?(?:([^\/\:]*))?(?:\:([0-9\*]*))?([^\?#]*)?(?:\?([^#]*))?(?:#(.*))?";
|
||||
Regex splitURLRegex = new Regex(splitURLRegexPattern);
|
||||
Match regexMatch = splitURLRegex.Match(filterExpressionString);
|
||||
if (regexMatch.Success == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.scheme = regexMatch.Groups[1].Value;
|
||||
this.user = regexMatch.Groups[2].Value;
|
||||
this.password = regexMatch.Groups[3].Value;
|
||||
this.host = regexMatch.Groups[4].Value;
|
||||
|
||||
// Treat a special case when a query is interpreted as part of the host address
|
||||
if (this.host.Contains("?"))
|
||||
{
|
||||
string splitURLRegexPattern2 = @"([^\?#]*)?(?:\?([^#]*))?(?:#(.*))?";
|
||||
Regex splitURLRegex2 = new Regex(splitURLRegexPattern2);
|
||||
Match regexMatch2 = splitURLRegex2.Match(this.host);
|
||||
if (regexMatch.Success == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.host = regexMatch2.Groups[1].Value;
|
||||
this.port = null;
|
||||
this.path = "";
|
||||
this.query = regexMatch2.Groups[2].Value;
|
||||
this.fragment = regexMatch2.Groups[3].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
string portNumber = regexMatch.Groups[5].Value;
|
||||
|
||||
// We only want a port if the filter expression string explicitely defines one!
|
||||
if (portNumber.Length == 0 || portNumber == "*")
|
||||
{
|
||||
this.port = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.port = UInt16.Parse(portNumber);
|
||||
}
|
||||
|
||||
this.path = regexMatch.Groups[6].Value.Trim(new char[] { '/' });
|
||||
this.query = regexMatch.Groups[7].Value;
|
||||
this.fragment = regexMatch.Groups[8].Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static string User(string userInfo)
|
||||
{
|
||||
string user = "";
|
||||
if (!string.IsNullOrEmpty(userInfo))
|
||||
{
|
||||
int userPasswordSeparator = userInfo.IndexOf(":");
|
||||
if (userPasswordSeparator == -1)
|
||||
{
|
||||
user = userInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (userPasswordSeparator != 0)
|
||||
{
|
||||
user = userInfo.Substring(0, userPasswordSeparator);
|
||||
}
|
||||
}
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
internal static string Password(string userInfo)
|
||||
{
|
||||
string password = "";
|
||||
if (!string.IsNullOrEmpty(userInfo))
|
||||
{
|
||||
int userPasswordSeparator = userInfo.IndexOf(":");
|
||||
if (userPasswordSeparator != -1)
|
||||
{
|
||||
if (userPasswordSeparator < userInfo.Length - 1)
|
||||
{
|
||||
password = userInfo.Substring(userPasswordSeparator + 1, userInfo.Length - 1 - userPasswordSeparator);
|
||||
}
|
||||
}
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
internal SEBURLFilterExpression(string scheme, string user, string password, string host, int port, string path, string query, string fragment)
|
||||
{
|
||||
this.scheme = scheme;
|
||||
this.user = user;
|
||||
this.password = password;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.path = path;
|
||||
this.query = query;
|
||||
this.fragment = fragment;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder expressionString = new StringBuilder();
|
||||
if (!string.IsNullOrEmpty(this.scheme))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this.host))
|
||||
{
|
||||
expressionString.AppendFormat("{0}://", this.scheme);
|
||||
}
|
||||
else
|
||||
{
|
||||
expressionString.AppendFormat("{0}:", this.scheme);
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(this.user))
|
||||
{
|
||||
expressionString.Append(this.user);
|
||||
|
||||
if (!string.IsNullOrEmpty(this.password))
|
||||
{
|
||||
expressionString.AppendFormat(":{0}@", this.password);
|
||||
}
|
||||
else
|
||||
{
|
||||
expressionString.Append("@");
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(this.host))
|
||||
{
|
||||
expressionString.Append(this.host);
|
||||
}
|
||||
if (this.port != null && this.port > 0 && this.port <= 65535)
|
||||
{
|
||||
expressionString.AppendFormat(":{0}", this.port);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(this.path))
|
||||
{
|
||||
if (this.path.StartsWith("/"))
|
||||
{
|
||||
expressionString.Append(this.path);
|
||||
}
|
||||
else
|
||||
{
|
||||
expressionString.AppendFormat("/{0}", this.path);
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(this.query))
|
||||
{
|
||||
expressionString.AppendFormat("?{0}", this.query);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(this.fragment))
|
||||
{
|
||||
expressionString.AppendFormat("#{0}", this.fragment);
|
||||
}
|
||||
|
||||
return expressionString.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,8 +6,12 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SafeExamBrowser.Browser.Contracts.Filters;
|
||||
using SafeExamBrowser.Browser.Filters.Rules;
|
||||
using SafeExamBrowser.Settings.Browser;
|
||||
|
||||
namespace SafeExamBrowser.Browser.UnitTests.Filters.Rules
|
||||
{
|
||||
|
@ -21,5 +25,43 @@ namespace SafeExamBrowser.Browser.UnitTests.Filters.Rules
|
|||
{
|
||||
sut = new RegexRule();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MustIgnoreCase()
|
||||
{
|
||||
sut.Initialize(new FilterRuleSettings { Expression = Regex.Escape("http://www.test.org/path/file.txt?param=123") });
|
||||
|
||||
Assert.IsTrue(sut.IsMatch(new Request { Url = "hTtP://wWw.TeSt.OrG/pAtH/fIlE.tXt?PaRaM=123" }));
|
||||
Assert.IsTrue(sut.IsMatch(new Request { Url = "HtTp://WwW.tEst.oRg/PaTh/FiLe.TxT?pArAm=123" }));
|
||||
|
||||
sut.Initialize(new FilterRuleSettings { Expression = Regex.Escape("HTTP://WWW.TEST.ORG/PATH/FILE.TXT?PARAM=123") });
|
||||
|
||||
Assert.IsTrue(sut.IsMatch(new Request { Url = "hTtP://wWw.TeSt.OrG/pAtH/fIlE.tXt?PaRaM=123" }));
|
||||
Assert.IsTrue(sut.IsMatch(new Request { Url = "HtTp://WwW.tEst.oRg/PaTh/FiLe.TxT?pArAm=123" }));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MustInitializeResult()
|
||||
{
|
||||
foreach (var result in Enum.GetValues(typeof(FilterResult)))
|
||||
{
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "", Result = (FilterResult) result });
|
||||
Assert.AreEqual(result, sut.Result);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void MustNotAllowUndefinedExpression()
|
||||
{
|
||||
sut.Initialize(new FilterRuleSettings());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void MustValidateExpression()
|
||||
{
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "ç+\"}%&*/(+)=?{=*+¦]@#°§]`?´^¨'°[¬|¢" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SafeExamBrowser.Browser.Contracts.Filters;
|
||||
using SafeExamBrowser.Browser.Filters.Rules;
|
||||
using SafeExamBrowser.Settings.Browser;
|
||||
|
||||
namespace SafeExamBrowser.Browser.UnitTests.Filters.Rules
|
||||
{
|
||||
|
@ -21,5 +24,269 @@ namespace SafeExamBrowser.Browser.UnitTests.Filters.Rules
|
|||
{
|
||||
sut = new SimplifiedRule();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestAlphanumericExpressionAsHost()
|
||||
{
|
||||
var expression = "hostname-123";
|
||||
var positive = new[]
|
||||
{
|
||||
$"scheme://{expression}.org",
|
||||
$"scheme://www.{expression}.org",
|
||||
$"scheme://subdomain.{expression}.com",
|
||||
$"scheme://subdomain-1.subdomain-2.{expression}.org",
|
||||
$"scheme://user:password@www.{expression}.org/path/file.txt?param=123#fragment"
|
||||
};
|
||||
var negative = new[]
|
||||
{
|
||||
$"scheme://hostname.org",
|
||||
$"scheme://hostname-12.org",
|
||||
$"scheme://{expression}4.org",
|
||||
$"scheme://{expression}.realhost.org",
|
||||
$"scheme://subdomain.{expression}.realhost.org",
|
||||
$"scheme://www.realhost.{expression}",
|
||||
$"{expression}://www.host.org",
|
||||
$"scheme://www.host.org/{expression}/path",
|
||||
$"scheme://www.host.org/path?param={expression}",
|
||||
$"scheme://{expression}:password@www.host.org",
|
||||
$"scheme://user:{expression}@www.host.org",
|
||||
$"scheme://user:password@www.host.org/path?param=123#{expression}"
|
||||
};
|
||||
|
||||
Execute(expression, positive, negative, false);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestHostExpressionWithDomain()
|
||||
{
|
||||
var expression = "123-hostname.org";
|
||||
var positive = new[]
|
||||
{
|
||||
$"scheme://{expression}",
|
||||
$"scheme://www.{expression}",
|
||||
$"scheme://subdomain.{expression}",
|
||||
$"scheme://subdomain-1.subdomain-2.{expression}",
|
||||
$"scheme://user:password@www.{expression}/path/file.txt?param=123#fragment"
|
||||
};
|
||||
var negative = new[]
|
||||
{
|
||||
$"scheme://123.org",
|
||||
$"scheme://123-host.org",
|
||||
$"scheme://{expression}.com",
|
||||
$"scheme://{expression}s.org",
|
||||
$"scheme://{expression}.realhost.org",
|
||||
$"scheme://subdomain.{expression}.realhost.org",
|
||||
$"scheme{expression}://www.host.org",
|
||||
$"scheme://www.host.org/{expression}/path",
|
||||
$"scheme://www.host.org/path?param={expression}",
|
||||
$"scheme://{expression}:password@www.host.org",
|
||||
$"scheme://user:{expression}@www.host.org",
|
||||
$"scheme://user:password@www.host.org/path?param=123#{expression}"
|
||||
};
|
||||
|
||||
Execute(expression, positive, negative);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestHostExpressionWithWildcard()
|
||||
{
|
||||
var expression = "test.*.org";
|
||||
var positive = new[]
|
||||
{
|
||||
"scheme://test.host.org",
|
||||
"scheme://test.host.domain.org",
|
||||
"scheme://subdomain.test.host.org",
|
||||
"scheme://user:password@test.domain.org/path/file.txt?param=123#fragment"
|
||||
};
|
||||
var negative = new[]
|
||||
{
|
||||
"scheme://test.org",
|
||||
"scheme://host.com/test.host.org",
|
||||
"scheme://www.host.org/test.host.org/path",
|
||||
"scheme://www.host.org/path?param=test.host.org",
|
||||
"scheme://test.host.org:password@www.host.org",
|
||||
"scheme://user:test.host.org@www.host.org",
|
||||
"scheme://user:password@www.host.org/path?param=123#test.host.org"
|
||||
};
|
||||
|
||||
Execute(expression, positive, negative);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestHostExpressionWithWildcardAsSuffix()
|
||||
{
|
||||
var expression = "test.host.*";
|
||||
var positive = new[]
|
||||
{
|
||||
"scheme://test.host.org",
|
||||
"scheme://test.host.domain.org",
|
||||
"scheme://subdomain.test.host.org",
|
||||
"scheme://user:password@test.host.org/path/file.txt?param=123#fragment"
|
||||
};
|
||||
var negative = new[]
|
||||
{
|
||||
"scheme://host.com",
|
||||
"scheme://test.host",
|
||||
"scheme://host.com/test.host.txt"
|
||||
};
|
||||
|
||||
Execute(expression, positive, negative);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestHostExpressionWithWildcardAsPrefix()
|
||||
{
|
||||
var expression = "*.org";
|
||||
var positive = new[]
|
||||
{
|
||||
"scheme://domain.org",
|
||||
"scheme://test.host.org",
|
||||
"scheme://test.host.domain.org",
|
||||
"scheme://user:password@www.host.org/path/file.txt?param=123#fragment"
|
||||
};
|
||||
var negative = new[]
|
||||
{
|
||||
"scheme://org",
|
||||
"scheme://host.com",
|
||||
"scheme://test.net",
|
||||
"scheme://test.ch",
|
||||
"scheme://host.com/test.org"
|
||||
};
|
||||
|
||||
Execute(expression, positive, negative);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestHostExpressionWithExactSubdomain()
|
||||
{
|
||||
var expression = ".www.host.org";
|
||||
var positive = new[]
|
||||
{
|
||||
"scheme://www.host.org",
|
||||
"scheme://user:password@www.host.org/path/file.txt?param=123#fragment"
|
||||
};
|
||||
var negative = new[]
|
||||
{
|
||||
"scheme://host.org",
|
||||
"scheme://test.www.host.org",
|
||||
"scheme://www.host.org.com"
|
||||
};
|
||||
|
||||
Execute(expression, positive, negative);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestExpressionWithPortNumber()
|
||||
{
|
||||
var expression = "host.org:2020";
|
||||
var positive = new[]
|
||||
{
|
||||
"scheme://host.org:2020",
|
||||
"scheme://www.host.org:2020",
|
||||
"scheme://user:password@www.host.org:2020/path/file.txt?param=123#fragment"
|
||||
};
|
||||
var negative = new[]
|
||||
{
|
||||
"scheme://host.org",
|
||||
"scheme://www.host.org",
|
||||
"scheme://www.host.org:2",
|
||||
"scheme://www.host.org:20",
|
||||
"scheme://www.host.org:202",
|
||||
"scheme://www.host.org:20202"
|
||||
};
|
||||
|
||||
Execute(expression, positive, negative);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MustIgnoreCase()
|
||||
{
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "http://www.test.org/path/file.txt?param=123" });
|
||||
|
||||
Assert.IsTrue(sut.IsMatch(new Request { Url = "hTtP://wWw.TeSt.OrG/pAtH/fIlE.tXt?PaRaM=123" }));
|
||||
Assert.IsTrue(sut.IsMatch(new Request { Url = "HtTp://WwW.tEst.oRg/PaTh/FiLe.TxT?pArAm=123" }));
|
||||
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "HTTP://WWW.TEST.ORG/PATH/FILE.TXT?PARAM=123" });
|
||||
|
||||
Assert.IsTrue(sut.IsMatch(new Request { Url = "hTtP://wWw.TeSt.OrG/pAtH/fIlE.tXt?PaRaM=123" }));
|
||||
Assert.IsTrue(sut.IsMatch(new Request { Url = "HtTp://WwW.tEst.oRg/PaTh/FiLe.TxT?pArAm=123" }));
|
||||
}
|
||||
|
||||
// TODO
|
||||
//[TestMethod]
|
||||
//public void MustIgnoreTrailingSlash()
|
||||
//{
|
||||
// Assert.Fail();
|
||||
//}
|
||||
|
||||
//[TestMethod]
|
||||
//public void MustAllowWildcard()
|
||||
//{
|
||||
// Assert.Fail();
|
||||
//}
|
||||
|
||||
[TestMethod]
|
||||
public void MustInitializeResult()
|
||||
{
|
||||
foreach (var result in Enum.GetValues(typeof(FilterResult)))
|
||||
{
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "*", Result = (FilterResult) result });
|
||||
Assert.AreEqual(result, sut.Result);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void MustNotAllowUndefinedExpression()
|
||||
{
|
||||
sut.Initialize(new FilterRuleSettings());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MustValidateExpression()
|
||||
{
|
||||
var invalid = new[]
|
||||
{
|
||||
".", "+", "\"", "ç", "%", "&", "/", "(", ")", "=", "?", "^", "!", "[", "]", "{", "}", "¦", "@", "#", "°", "§", "¬", "|", "¢", "´", "'", "`", "~", "<", ">", "\\"
|
||||
};
|
||||
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "*" });
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "a" });
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "A" });
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "0" });
|
||||
sut.Initialize(new FilterRuleSettings { Expression = "abcdeFGHIJK-12345" });
|
||||
|
||||
foreach (var expression in invalid)
|
||||
{
|
||||
Assert.ThrowsException<ArgumentException>(() => sut.Initialize(new FilterRuleSettings { Expression = expression }));
|
||||
}
|
||||
}
|
||||
|
||||
private void Execute(string expression, string[] positive, string[] negative, bool testLegacy = true)
|
||||
{
|
||||
var legacy = new LegacyFilter(expression);
|
||||
|
||||
sut.Initialize(new FilterRuleSettings { Expression = expression });
|
||||
|
||||
foreach (var url in positive)
|
||||
{
|
||||
Assert.IsTrue(sut.IsMatch(new Request { Url = url }), url);
|
||||
|
||||
if (testLegacy)
|
||||
{
|
||||
Assert.IsTrue(legacy.IsMatch(new Uri(url)), url);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var url in negative)
|
||||
{
|
||||
Assert.IsFalse(sut.IsMatch(new Request { Url = url }), url);
|
||||
|
||||
if (testLegacy)
|
||||
{
|
||||
Assert.IsFalse(legacy.IsMatch(new Uri(url)), url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@
|
|||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Filters\LegacyFilter.cs" />
|
||||
<Compile Include="Filters\RequestFilterTests.cs" />
|
||||
<Compile Include="Filters\RuleFactoryTests.cs" />
|
||||
<Compile Include="Filters\Rules\RegexRuleTests.cs" />
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using SafeExamBrowser.Browser.Contracts.Filters;
|
||||
using SafeExamBrowser.Settings.Browser;
|
||||
|
@ -20,6 +21,8 @@ namespace SafeExamBrowser.Browser.Filters.Rules
|
|||
|
||||
public void Initialize(FilterRuleSettings settings)
|
||||
{
|
||||
ValidateExpression(settings.Expression);
|
||||
|
||||
expression = settings.Expression;
|
||||
Result = settings.Result;
|
||||
}
|
||||
|
@ -28,5 +31,22 @@ namespace SafeExamBrowser.Browser.Filters.Rules
|
|||
{
|
||||
return Regex.IsMatch(request.Url, expression, RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
private void ValidateExpression(string expression)
|
||||
{
|
||||
if (expression == default(string))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Regex.Match("", expression);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ArgumentException($"Invalid regular expression!", nameof(expression), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using SafeExamBrowser.Browser.Contracts.Filters;
|
||||
using SafeExamBrowser.Settings.Browser;
|
||||
|
@ -14,19 +15,123 @@ namespace SafeExamBrowser.Browser.Filters.Rules
|
|||
{
|
||||
internal class SimplifiedRule : IRule
|
||||
{
|
||||
private string expression;
|
||||
private const string URL_DELIMITER_PATTERN = @"(?:([^\:]*)\:\/\/)?(?:([^\:\@]*)(?:\:([^\@]*))?\@)?(?:([^\/\:]*))?(?:\:([0-9\*]*))?([^\?#]*)?(?:\?([^#]*))?(?:#(.*))?";
|
||||
|
||||
private Regex fragment;
|
||||
private Regex host;
|
||||
private Regex password;
|
||||
private Regex path;
|
||||
private int? port;
|
||||
private Regex query;
|
||||
private Regex scheme;
|
||||
private Regex user;
|
||||
|
||||
public FilterResult Result { get; private set; }
|
||||
|
||||
public void Initialize(FilterRuleSettings settings)
|
||||
{
|
||||
expression = settings.Expression.Replace("*", @".*");
|
||||
ValidateExpression(settings.Expression);
|
||||
ParseExpression(settings.Expression);
|
||||
|
||||
Result = settings.Result;
|
||||
}
|
||||
|
||||
public bool IsMatch(Request request)
|
||||
{
|
||||
return Regex.IsMatch(request.Url, expression, RegexOptions.IgnoreCase);
|
||||
var url = new Uri(request.Url, UriKind.Absolute);
|
||||
var isMatch = true;
|
||||
|
||||
//isMatch &= scheme == default(Regex) || ...;
|
||||
//isMatch &= user == default(Regex) || ...;
|
||||
//isMatch &= password == default(Regex) || ...;
|
||||
isMatch &= host.IsMatch(url.Host);
|
||||
isMatch &= !port.HasValue || port == url.Port;
|
||||
//isMatch &= path == default(Regex) || ...;
|
||||
//isMatch &= query == default(Regex) || ...;
|
||||
//isMatch &= fragment == default(Regex) || ...;
|
||||
|
||||
return isMatch;
|
||||
}
|
||||
|
||||
private void ParseExpression(string expression)
|
||||
{
|
||||
var match = Regex.Match(expression, URL_DELIMITER_PATTERN);
|
||||
|
||||
//ParseScheme(match.Groups[1].Value);
|
||||
//ParseUser(match.Groups[2].Value);
|
||||
//ParsePassword(match.Groups[3].Value);
|
||||
ParseHost(match.Groups[4].Value);
|
||||
ParsePort(match.Groups[5].Value);
|
||||
//ParsePath(match.Groups[6].Value);
|
||||
//ParseQuery(match.Groups[7].Value);
|
||||
//ParseFragment(match.Groups[8].Value);
|
||||
}
|
||||
|
||||
private void ParseHost(string expression)
|
||||
{
|
||||
var hasToplevelDomain = Regex.IsMatch(expression, @"\.+");
|
||||
var hasSubdomain = Regex.IsMatch(expression, @"\.{2,}");
|
||||
var allowOnlyExactSubdomain = expression.StartsWith(".");
|
||||
|
||||
if (allowOnlyExactSubdomain)
|
||||
{
|
||||
expression = expression.Substring(1);
|
||||
}
|
||||
|
||||
expression = Regex.Escape(expression);
|
||||
expression = ReplaceWildcard(expression);
|
||||
|
||||
if (!hasToplevelDomain)
|
||||
{
|
||||
expression = $@"{expression}(\.[a-z]+)";
|
||||
}
|
||||
|
||||
if (!hasSubdomain && !allowOnlyExactSubdomain)
|
||||
{
|
||||
expression = $@"(.+?\.)*{expression}";
|
||||
}
|
||||
|
||||
host = Build(expression);
|
||||
}
|
||||
|
||||
private void ParsePort(string expression)
|
||||
{
|
||||
if (int.TryParse(expression, out var port))
|
||||
{
|
||||
this.port = port;
|
||||
}
|
||||
}
|
||||
|
||||
private Regex Build(string expression)
|
||||
{
|
||||
return new Regex($"^{expression}$", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
private string ReplaceWildcard(string expression)
|
||||
{
|
||||
return expression.Replace(@"\*", ".*?");
|
||||
}
|
||||
|
||||
private void ValidateExpression(string expression)
|
||||
{
|
||||
if (expression == default(string))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression));
|
||||
}
|
||||
|
||||
if (!Regex.IsMatch(expression, @"[a-zA-Z0-9\*]+"))
|
||||
{
|
||||
throw new ArgumentException("Expression must consist of at least one alphanumeric character or asterisk!", nameof(expression));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Regex.Match(expression, URL_DELIMITER_PATTERN);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ArgumentException("Expression is not a valid simplified filter expression!", nameof(expression), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,10 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
|
|||
MapApplicationLogAccess(rawData, settings);
|
||||
MapKioskMode(rawData, settings);
|
||||
MapUserAgentMode(rawData, settings);
|
||||
|
||||
// TODO: Automatically create filter rule for start URL!
|
||||
// -> Only if filter active
|
||||
// -> Create mechanism for post-processing of settings?
|
||||
}
|
||||
|
||||
private void MapAudioSettings(string key, object value, ApplicationSettings settings)
|
||||
|
|
Loading…
Reference in a new issue