554 lines
14 KiB
C#
554 lines
14 KiB
C#
/*
|
||
* Copyright (c) 2025 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();
|
||
}
|
||
}
|
||
}
|
||
}
|