Restore SEBPatch

This commit is contained in:
2025-06-01 11:44:20 +02:00
commit 8c656e3137
1297 changed files with 142172 additions and 0 deletions

View File

@@ -0,0 +1,553 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.TrimEnd(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 or fragment is interpreted as part of the host address
if (this.host.Contains("?") || 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.TrimEnd(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();
}
}
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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 Moq;
using SafeExamBrowser.Browser.Contracts.Filters;
using SafeExamBrowser.Browser.Filters;
using SafeExamBrowser.Settings.Browser.Filter;
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 Mock<IRule>();
var block = new Mock<IRule>();
allow.SetupGet(r => r.Result).Returns(FilterResult.Allow);
block.SetupGet(r => r.Result).Returns(FilterResult.Block);
block.Setup(r => r.IsMatch(It.IsAny<Request>())).Returns(true);
sut.Load(allow.Object);
sut.Load(block.Object);
var result = sut.Process(new Request());
allow.Verify(r => r.IsMatch(It.IsAny<Request>()), Times.Never);
block.Verify(r => r.IsMatch(It.IsAny<Request>()), Times.Once);
Assert.AreEqual(FilterResult.Block, result);
}
[TestMethod]
public void MustProcessAllowRulesSecond()
{
var allow = new Mock<IRule>();
var block = new Mock<IRule>();
allow.SetupGet(r => r.Result).Returns(FilterResult.Allow);
allow.Setup(r => r.IsMatch(It.IsAny<Request>())).Returns(true);
block.SetupGet(r => r.Result).Returns(FilterResult.Block);
sut.Load(allow.Object);
sut.Load(block.Object);
var result = sut.Process(new Request());
allow.Verify(r => r.IsMatch(It.IsAny<Request>()), Times.Once);
block.Verify(r => r.IsMatch(It.IsAny<Request>()), Times.Once);
Assert.AreEqual(FilterResult.Allow, result);
}
[TestMethod]
public void MustReturnDefaultWithoutMatch()
{
var allow = new Mock<IRule>();
var block = new Mock<IRule>();
allow.SetupGet(r => r.Result).Returns(FilterResult.Allow);
block.SetupGet(r => r.Result).Returns(FilterResult.Block);
sut.Default = (FilterResult) (-1);
sut.Load(allow.Object);
sut.Load(block.Object);
var result = sut.Process(new Request());
allow.Verify(r => r.IsMatch(It.IsAny<Request>()), Times.Once);
block.Verify(r => r.IsMatch(It.IsAny<Request>()), Times.Once);
Assert.AreEqual((FilterResult) (-1), result);
}
[TestMethod]
public void MustReturnDefaultWithoutRules()
{
sut.Default = FilterResult.Allow;
var result = sut.Process(new Request());
Assert.AreEqual(FilterResult.Allow, result);
sut.Default = FilterResult.Block;
result = sut.Process(new Request());
Assert.AreEqual(FilterResult.Block, result);
}
[TestMethod]
[ExpectedException(typeof(NotImplementedException))]
public void MustNotAllowUnsupportedResult()
{
var rule = new Mock<IRule>();
rule.SetupGet(r => r.Result).Returns((FilterResult) (-1));
sut.Load(rule.Object);
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.Browser.Filters.Rules;
using SafeExamBrowser.Settings.Browser.Filter;
namespace SafeExamBrowser.Browser.UnitTests.Filters
{
[TestClass]
public class RuleFactoryTests
{
private RuleFactory sut;
[TestInitialize]
public void Initialize()
{
sut = new RuleFactory();
}
[TestMethod]
public void MustCreateCorrectRules()
{
Assert.IsInstanceOfType(sut.CreateRule(FilterRuleType.Regex), typeof(RegexRule));
Assert.IsInstanceOfType(sut.CreateRule(FilterRuleType.Simplified), typeof(SimplifiedRule));
}
[TestMethod]
[ExpectedException(typeof(NotImplementedException))]
public void MustNotAllowUnsupportedFilterType()
{
sut.CreateRule((FilterRuleType) (-1));
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SafeExamBrowser.Browser.Contracts.Filters;
using SafeExamBrowser.Browser.Filters.Rules;
using SafeExamBrowser.Settings.Browser.Filter;
namespace SafeExamBrowser.Browser.UnitTests.Filters.Rules
{
[TestClass]
public class RegexRuleTests
{
private RegexRule sut;
[TestInitialize]
public void Initialize()
{
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 = "ç+\"}%&*/(+)=?{=*+¦]@#°§]`?´^¨'°[¬|¢" });
}
}
}

View File

@@ -0,0 +1,962 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SafeExamBrowser.Browser.Contracts.Filters;
using SafeExamBrowser.Browser.Filters.Rules;
using SafeExamBrowser.Settings.Browser.Filter;
namespace SafeExamBrowser.Browser.UnitTests.Filters.Rules
{
[TestClass]
public class SimplifiedRuleTests
{
private SimplifiedRule sut;
[TestInitialize]
public void Initialize()
{
sut = new SimplifiedRule();
}
[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" }));
}
[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 }));
}
}
[TestMethod]
public void TestAlphanumericExpression()
{
var expression = "hostname123";
var positive = new[]
{
$"scheme://{expression}"
};
var negative = new[]
{
$"scheme://hostname",
$"scheme://hostname1",
$"scheme://hostname12",
$"scheme://hostname1234",
$"scheme://{expression}.org",
$"scheme://www.{expression}.org",
$"scheme://subdomain.{expression}.com",
$"scheme://www.realhost.{expression}",
$"scheme://subdomain-1.subdomain-2.{expression}.org",
$"scheme://user:password@www.{expression}.org/path/file.txt?param=123#fragment",
$"scheme://{expression}4",
$"scheme://hostname.org",
$"scheme://hostname12.org",
$"scheme://{expression}4.org",
$"scheme://{expression}.realhost.org",
$"scheme://subdomain.{expression}.realhost.org",
$"{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}"
};
Test(expression, positive, negative, false);
}
[TestMethod]
public void TestAlphanumericExpressionWithWildcard()
{
var expression = "hostname*";
var positive = new[]
{
"scheme://hostname.org",
"scheme://hostnameabc.org",
"scheme://hostname-12.org",
"scheme://hostname-abc-def-123-456.org",
"scheme://www.hostname-abc.org",
"scheme://www.realhost.hostname",
"scheme://subdomain.hostname-xyz.com",
"scheme://hostname.realhost.org",
"scheme://subdomain.hostname.realhost.org",
"scheme://subdomain-1.subdomain-2.hostname-abc-123.org",
"scheme://user:password@www.hostname-abc.org/path/file.txt?param=123#fragment"
};
var negative = new[]
{
"scheme://hostnam.org",
"hostname://www.host.org",
"scheme://www.host.org/hostname/path",
"scheme://www.host.org/path?param=hostname",
"scheme://hostname:password@www.host.org",
"scheme://user:hostname@www.host.org",
"scheme://user:password@www.host.org/path?param=123#hostname"
};
Test(expression, positive, negative, false);
}
[TestMethod]
public void TestHostWithDomain()
{
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}"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestHostWithWildcard()
{
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"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestHostWithWildcardAsSuffix()
{
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"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestHostWithWildcardAsPrefix()
{
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"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestHostWithExactSubdomain()
{
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"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestHostWithTrailingSlash()
{
var expression = "host.org/";
var positive = new[]
{
"scheme://host.org",
"scheme://host.org/",
"scheme://host.org/url",
"scheme://host.org/url/",
"scheme://host.org/url/path",
"scheme://host.org/url/path/",
"scheme://user:password@www.host.org/url/path?param=123#fragment",
"scheme://user:password@www.host.org/url/path/?param=123#fragment"
};
Test(expression, positive, Array.Empty<string>());
}
[TestMethod]
public void TestHostWithoutTrailingSlash()
{
var expression = "host.org";
var positive = new[]
{
"scheme://host.org",
"scheme://host.org/",
"scheme://host.org/url",
"scheme://host.org/url/",
"scheme://host.org/url/path",
"scheme://host.org/url/path/",
"scheme://user:password@www.host.org/url/path?param=123#fragment",
"scheme://user:password@www.host.org/url/path/?param=123#fragment"
};
Test(expression, positive, Array.Empty<string>());
}
[TestMethod]
public void TestPortNumber()
{
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"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestPortWildcard()
{
var expression = "host.org:*";
var positive = new[]
{
"scheme://host.org",
"scheme://host.org:0",
"scheme://host.org:1",
"scheme://host.org:2020",
"scheme://host.org:65535",
"scheme://www.host.org",
"scheme://www.host.org:2",
"scheme://www.host.org:20",
"scheme://www.host.org:202",
"scheme://www.host.org:2020",
"scheme://www.host.org:20202",
"scheme://user:password@www.host.org:2020/path/file.txt?param=123#fragment"
};
Test(expression, positive, Array.Empty<string>());
}
[TestMethod]
public void TestPortNumberWithHostWildcard()
{
var expression = "*:2020";
var positive = new[]
{
"scheme://host.org:2020",
"scheme://domain.com:2020",
"scheme://user:password@www.server.net:2020/path/file.txt?param=123#fragment"
};
var negative = new List<string>
{
"scheme://host.org"
};
for (var port = 0; port < 65536; port++)
{
if (port != 2020)
{
negative.Add($"{negative[0]}:{port}");
}
}
Test(expression, positive, negative.ToArray());
}
[TestMethod]
public void TestPath()
{
var expression = "host.org/url/path";
var positive = new[]
{
"scheme://host.org/url/path",
"scheme://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org",
"scheme://host.org//",
"scheme://host.org///",
"scheme://host.org/url",
"scheme://host.org/path",
"scheme://host.org/url/path.txt",
"scheme://host.org/another/url/path"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestPathWithFile()
{
var expression = "host.org/url/path/to/file.txt";
var positive = new[]
{
"scheme://host.org/url/path/to/file.txt",
"scheme://subdomain.host.org/url/path/to/file.txt",
"scheme://user:password@www.host.org/url/path/to/file.txt?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org",
"scheme://host.org/url",
"scheme://host.org/path",
"scheme://host.org/file.txt",
"scheme://host.org/url/path.txt",
"scheme://host.org/url/path/to.txt",
"scheme://host.org/url/path/to/file",
"scheme://host.org/url/path/to/file.",
"scheme://host.org/url/path/to/file.t",
"scheme://host.org/url/path/to/file.tx",
"scheme://host.org/url/path/to/file.txt/segment",
"scheme://host.org/url/path/to/file.txtt",
"scheme://host.org/another/url/path/to/file.txt"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestPathWithWildcard()
{
var expression = "host.org/*/path";
var positive = new[]
{
"scheme://host.org//path",
"scheme://host.org/url/path",
"scheme://host.org/another/url/path",
"scheme://user:password@www.host.org/yet/another/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org",
"scheme://host.org/url",
"scheme://host.org/path",
"scheme://host.org/url/path.txt",
"scheme://host.org/url/path/2"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestPathWithHostWildcard()
{
var expression = "*/url/path";
var positive = new[]
{
"scheme://local/url/path",
"scheme://host.org/url/path",
"scheme://www.host.org/url/path",
"scheme://another.server.org/url/path",
"scheme://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org",
"scheme://host.org/url",
"scheme://host.org/path",
"scheme://host.org/url/path.txt",
"scheme://host.org/url/path/2"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestPathWithTrailingSlash()
{
var expression = "host.org/url/path/";
var positive = new[]
{
"scheme://host.org/url/path",
"scheme://host.org/url/path/",
"scheme://user:password@www.host.org/url/path?param=123#fragment",
"scheme://user:password@www.host.org/url/path/?param=123#fragment"
};
Test(expression, positive, Array.Empty<string>());
}
[TestMethod]
public void TestPathWithoutTrailingSlash()
{
var expression = "host.org/url/path";
var positive = new[]
{
"scheme://host.org/url/path",
"scheme://host.org/url/path/",
"scheme://user:password@www.host.org/url/path?param=123#fragment",
"scheme://user:password@www.host.org/url/path/?param=123#fragment"
};
Test(expression, positive, Array.Empty<string>());
}
[TestMethod]
public void TestScheme()
{
var expression = "scheme://host.org";
var positive = new[]
{
"scheme://host.org",
"scheme://www.host.org",
"scheme://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"//host.org",
"https://host.org",
"ftp://host.org",
"ftps://host.org",
"schemes://host.org"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestSchemeWithWildcard()
{
var expression = "*tp://host.org";
var positive = new[]
{
"tp://host.org",
"ftp://www.host.org",
"http://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"//host.org",
"p://host.org",
"https://host.org",
"ftps://host.org"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestSchemeWithHostWildcard()
{
var expression = "scheme://*";
var positive = new[]
{
"scheme://host",
"scheme://www.server.org",
"scheme://subdomain.domain.org",
"scheme://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"//host.org",
"http://host.org",
"https://host.org",
"ftp://host.org",
"ftps://host.org",
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestUserInfoWithName()
{
var expression = "user@host.org";
var positive = new[]
{
"scheme://user@host.org",
"scheme://user@www.host.org",
"scheme://user:password@host.org",
"scheme://user:password-123@host.org",
"scheme://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://u@host.org",
"scheme://us@host.org",
"scheme://use@host.org",
"scheme://usera@host.org",
"scheme://user@server.net",
"scheme://usertwo@www.host.org"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestUserInfoWithNameWildcard()
{
var expression = "user*@host.org";
var positive = new[]
{
"scheme://user@host.org",
"scheme://userabc@host.org",
"scheme://user:abc@host.org",
"scheme://user-123@www.host.org",
"scheme://user-123:password@host.org",
"scheme://user-123:password-123@host.org",
"scheme://user-abc-123:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://u@host.org",
"scheme://us@host.org",
"scheme://use@host.org",
"scheme://user@server.net",
"scheme://usertwo@server.org"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestUserInfoWithPassword()
{
var expression = "user:password@host.org";
var positive = new[]
{
"scheme://user:password@host.org",
"scheme://user:password@www.host.org",
"scheme://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://u@host.org",
"scheme://us@host.org",
"scheme://use@host.org",
"scheme://user@server.net",
"scheme://usertwo@server.org",
"scheme://user@host.org",
"scheme://userabc@host.org",
"scheme://user:abc@host.org",
"scheme://user-123@www.host.org",
"scheme://user-123:password@host.org",
"scheme://user-123:password@www.host.org/url/path?param=123#fragment",
"scheme://user:password-123@host.org",
"scheme://user:password-123@www.host.org/url/path?param=123#fragment"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestUserInfoWithWildcard()
{
var expression = "*@host.org";
var positive = new[]
{
"scheme://host.org",
"scheme://user@host.org",
"scheme://user:password@host.org",
"scheme://www.host.org/url/path?param=123#fragment",
"scheme://user@www.host.org/url/path?param=123#fragment",
"scheme://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://server.org",
"scheme://user@server.org",
"scheme://www.server.org/url/path?param=123#fragment",
"scheme://user:password@www.server.org/url/path?param=123#fragment"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestUserInfoWithHostWildcard()
{
var expression = "user:password@*";
var positive = new[]
{
"scheme://user:password@host.org",
"scheme://user:password@server.net",
"scheme://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org",
"scheme://server.org",
"scheme://user@host.org",
"scheme://user@server.org",
"scheme://password@host.org",
"scheme://www.host.org/url/path?param=123#fragment",
"scheme://www.server.org/url/path?param=123#fragment",
"scheme://user@www.host.org/url/path?param=123#fragment",
"scheme://user@www.server.org/url/path?param=123#fragment",
"scheme://password@www.server.org/url/path?param=123#fragment"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestQuery()
{
var expression = "host.org?param=123";
var positive = new[]
{
"scheme://host.org?param=123",
"scheme://www.host.org/?param=123",
"scheme://www.host.org/path/?param=123",
"scheme://www.host.org/some/other/random/path?param=123",
"scheme://user:password@www.host.org/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org?",
"scheme://host.org?=",
"scheme://host.org?=123",
"scheme://host.org?param=",
"scheme://host.org?param=1",
"scheme://host.org?param=12",
"scheme://host.org?param=1234"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestQueryWithWildcardAsPrefix()
{
var expression = "host.org?*param=123";
var positive = new[]
{
"scheme://host.org?param=123",
"scheme://www.host.org?param=123",
"scheme://www.host.org/path/?param=123",
"scheme://www.host.org/some/other/random/path?param=123",
"scheme://user:password@www.host.org/url/path?param=123#fragment",
"scheme://host.org?other_param=456&param=123",
"scheme://host.org?param=123&another_param=123",
"scheme://www.host.org?other_param=456&param=123",
"scheme://www.host.org/path/?other_param=456&param=123",
"scheme://www.host.org/some/other/random/path?other_param=456&param=123",
"scheme://user:password@www.host.org/url/path?other_param=456&param=123#fragment",
"scheme://host.org?some_param=123469yvuiopwo&another_param=some%20whitespaces%26special%20characters%2B%22%2A%25%C3%A7%2F%28&param=123"
};
var negative = new[]
{
"scheme://host.org?",
"scheme://host.org?=",
"scheme://host.org?=123",
"scheme://host.org?aram=123",
"scheme://host.org?param=",
"scheme://host.org?param=1",
"scheme://host.org?param=12",
"scheme://host.org?param=1234",
"scheme://host.org?param=123&another_param=456"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestQueryWithWildcardAsSuffix()
{
var expression = "host.org?param=123*";
var positive = new[]
{
"scheme://host.org?param=123",
"scheme://host.org?param=1234",
"scheme://www.host.org?param=123",
"scheme://www.host.org/path/?param=123",
"scheme://host.org?param=123&another_param=456",
"scheme://www.host.org/some/other/random/path?param=123",
"scheme://user:password@www.host.org/url/path?param=123#fragment",
"scheme://host.org?param=123&other_param=456",
"scheme://www.host.org/path/?param=123&other_param=456",
"scheme://www.host.org/some/other/random/path?param=123&other_param=456",
"scheme://user:password@www.host.org/url/path?param=123&other_param=456#fragment",
"scheme://host.org?param=123&some_param=123469yvuiopwo&another_param=some%20whitespaces%26special%20characters%2B%22%2A%25%C3%A7%2F%28"
};
var negative = new[]
{
"scheme://host.org?",
"scheme://host.org?=",
"scheme://host.org?=123",
"scheme://host.org?aram=123",
"scheme://host.org?param=",
"scheme://host.org?param=1",
"scheme://host.org?param=12",
"scheme://host.org?aparam=123",
"scheme://www.host.org?param=456&param=123",
"scheme://host.org?some_param=123469yvuiopwo&another_param=some%20whitespaces%26special%20characters%2B%22%2A%25%C3%A7%2F%28&param=123"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestQueryNotAllowed()
{
var expression = "host.org?.";
var positive = new[]
{
"scheme://host.org",
"scheme://host.org?",
"scheme://user:password@www.host.org/url/path#fragment",
"scheme://user:password@www.host.org/url/path?#fragment"
};
var negative = new[]
{
"scheme://host.org?a",
"scheme://host.org?%20",
"scheme://host.org?=",
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestQueryWithHostWildcard()
{
var expression = "*?param=123";
var positive = new[]
{
"scheme://host.org?param=123",
"scheme://server.net?param=123",
"scheme://user:password@www.host.org/url/path?param=123#fragment",
"scheme://user:password@www.server.net/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org?param=1234",
"scheme://host.org?param=12",
"scheme://host.org?",
"scheme://host.org?param",
"scheme://host.org?123",
"scheme://host.org?="
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestFragment()
{
var expression = "host.org#fragment";
var positive = new[]
{
"scheme://host.org#fragment",
"scheme://www.host.org#fragment",
"scheme://user:password@www.host.org/url/path/file.txt?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org",
"scheme://host.org#",
"scheme://host.org#fragmen",
"scheme://host.org#fragment123",
"scheme://host.org#otherfragment"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestFragmentWithWildcardAsPrefix()
{
var expression = "host.org#*fragment";
var positive = new[]
{
"scheme://host.org#fragment",
"scheme://host.org#somefragment",
"scheme://www.host.org#another_fragment",
"scheme://user:password@www.host.org/url/path/file.txt?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org",
"scheme://host.org#",
"scheme://host.org#fragmen",
"scheme://host.org#fragment123"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestFragmentWithWildcardAsSuffix()
{
var expression = "host.org#fragment*";
var positive = new[]
{
"scheme://host.org#fragment",
"scheme://host.org#fragment-123",
"scheme://user:password@www.host.org/url/path/file.txt?param=123#fragment_abc"
};
var negative = new[]
{
"scheme://host.org",
"scheme://host.org#",
"scheme://host.org#fragmen",
"scheme://www.host.org#another_fragment"
};
Test(expression, positive, negative);
}
[TestMethod]
public void TestFragmentWithHostWildcard()
{
var expression = "*#fragment";
var positive = new[]
{
"scheme://host.org#fragment",
"scheme://server.net#fragment",
"scheme://user:password@www.host.org/url/path?param=123#fragment",
"scheme://user:password@www.server.net/url/path?param=123#fragment"
};
var negative = new[]
{
"scheme://host.org",
"scheme://host.org#",
"scheme://host.org#fragmen",
"scheme://host.org#fragment123"
};
Test(expression, positive, negative);
}
private void Test(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);
}
}
}
}
}