1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-06-25 10:26:02 +02:00
bitwarden-mobile/src/Core/Utilities/DomainName.cs

341 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
namespace Bit.Core.Utilities
{
// ref: https://github.com/danesparza/domainname-parser
public class DomainName
{
private readonly string _subDomain = string.Empty;
private readonly string _domain = string.Empty;
private readonly string _tld = string.Empty;
private readonly TLDRule _tldRule = null;
public string SubDomain => _subDomain;
public string Domain => _domain;
public string Sld => _domain;
public string Tld => _tld;
public TLDRule Rule => _tldRule;
public string BaseDomain => $"{_domain}.{_tld}";
public DomainName(string tld, string sld, string subDdomain, TLDRule tldRule)
{
_tld = tld;
_domain = sld;
_subDomain = subDdomain;
_tldRule = tldRule;
}
public static bool TryParse(string domainString, out DomainName result)
{
bool retval = false;
// Our temporary domain parts:
var tld = string.Empty;
var sld = string.Empty;
var subdomain = string.Empty;
TLDRule _tldrule = null;
result = null;
try
{
// Try parsing the domain name ... this might throw formatting exceptions
ParseDomainName(domainString, out tld, out sld, out subdomain, out _tldrule);
// Construct a new DomainName object and return it
result = new DomainName(tld, sld, subdomain, _tldrule);
// Return 'true'
retval = true;
}
catch
{
// Looks like something bad happened -- return 'false'
retval = false;
}
return retval;
}
public static bool TryParseBaseDomain(string domainString, out string result)
{
var retVal = TryParse(domainString, out DomainName domain);
result = domain?.BaseDomain;
return retVal;
}
private static void ParseDomainName(string domainString, out string TLD, out string SLD,
out string SubDomain, out TLDRule MatchingRule)
{
// Make sure domain is all lowercase
domainString = domainString.ToLower();
TLD = string.Empty;
SLD = string.Empty;
SubDomain = string.Empty;
MatchingRule = null;
// If the fqdn is empty, we have a problem already
if (domainString.Trim() == string.Empty)
{
throw new ArgumentException("The domain cannot be blank");
}
// Next, find the matching rule:
MatchingRule = FindMatchingTLDRule(domainString);
// At this point, no rules match, we have a problem
if (MatchingRule == null)
{
throw new FormatException("The domain does not have a recognized TLD");
}
// Based on the tld rule found, get the domain (and possibly the subdomain)
var tempSudomainAndDomain = string.Empty;
var tldIndex = 0;
// First, determine what type of rule we have, and set the TLD accordingly
switch (MatchingRule.Type)
{
case TLDRule.RuleType.Normal:
tldIndex = domainString.LastIndexOf("." + MatchingRule.Name);
tempSudomainAndDomain = domainString.Substring(0, tldIndex);
TLD = domainString.Substring(tldIndex + 1);
break;
case TLDRule.RuleType.Wildcard:
// This finds the last portion of the TLD...
tldIndex = domainString.LastIndexOf("." + MatchingRule.Name);
tempSudomainAndDomain = domainString.Substring(0, tldIndex);
// But we need to find the wildcard portion of it:
tldIndex = tempSudomainAndDomain.LastIndexOf(".");
tempSudomainAndDomain = domainString.Substring(0, tldIndex);
TLD = domainString.Substring(tldIndex + 1);
break;
case TLDRule.RuleType.Exception:
tldIndex = domainString.LastIndexOf(".");
tempSudomainAndDomain = domainString.Substring(0, tldIndex);
TLD = domainString.Substring(tldIndex + 1);
break;
}
// See if we have a subdomain:
List<string> lstRemainingParts = new List<string>(tempSudomainAndDomain.Split('.'));
// If we have 0 parts left, there is just a tld and no domain or subdomain
// If we have 1 part, it's the domain, and there is no subdomain
// If we have 2+ parts, the last part is the domain, the other parts (combined) are the subdomain
if (lstRemainingParts.Count > 0)
{
// Set the domain:
SLD = lstRemainingParts[lstRemainingParts.Count - 1];
// Set the subdomain, if there is one to set:
if (lstRemainingParts.Count > 1)
{
// We strip off the trailing period, too
SubDomain = tempSudomainAndDomain.Substring(0, tempSudomainAndDomain.Length - SLD.Length - 1);
}
}
}
private static TLDRule FindMatchingTLDRule(string domainString)
{
// Split our domain into parts (based on the '.')
// ...Put these parts in a list
// ...Make sure these parts are in reverse order (we'll be checking rules from the
// right -most pat of the domain)
var lstDomainParts = domainString.Split('.').ToList();
lstDomainParts.Reverse();
// Begin building our partial domain to check rules with:
var checkAgainst = string.Empty;
// Our 'matches' collection:
var ruleMatches = new List<TLDRule>();
foreach (string domainPart in lstDomainParts)
{
// Add on our next domain part:
checkAgainst = string.Format("{0}.{1}", domainPart, checkAgainst);
// If we end in a period, strip it off:
if (checkAgainst.EndsWith("."))
{
checkAgainst = checkAgainst.Substring(0, checkAgainst.Length - 1);
}
var rules = Enum.GetValues(typeof(TLDRule.RuleType)).Cast<TLDRule.RuleType>();
foreach (var rule in rules)
{
// Try to match rule:
if (TLDRulesCache.Instance.TLDRuleLists[rule].TryGetValue(checkAgainst, out TLDRule result))
{
ruleMatches.Add(result);
}
//Debug.WriteLine(string.Format("Domain part {0} matched {1} {2} rules",
// checkAgainst, result == null ? 0 : 1, rule));
}
}
// Sort our matches list (longest rule wins, according to :
var results = from match in ruleMatches
orderby match.Name.Length descending
select match;
// Take the top result (our primary match):
TLDRule primaryMatch = results.Take(1).SingleOrDefault();
if (primaryMatch != null)
{
//Debug.WriteLine(string.Format("Looks like our match is: {0}, which is a(n) {1} rule.",
// primaryMatch.Name, primaryMatch.Type));
}
else
{
//Debug.WriteLine(string.Format("No rules matched domain: {0}", domainString));
}
return primaryMatch;
}
public class TLDRule : IComparable<TLDRule>
{
public string Name { get; private set; }
public RuleType Type { get; private set; }
public TLDRule(string ruleInfo)
{
// Parse the rule and set properties accordingly:
if (ruleInfo.StartsWith("*"))
{
Type = RuleType.Wildcard;
Name = ruleInfo.Substring(2);
}
else if (ruleInfo.StartsWith("!"))
{
Type = RuleType.Exception;
Name = ruleInfo.Substring(1);
}
else
{
Type = RuleType.Normal;
Name = ruleInfo;
}
}
public int CompareTo(TLDRule other)
{
if (other == null)
{
return -1;
}
return Name.CompareTo(other.Name);
}
public enum RuleType
{
Normal,
Wildcard,
Exception
}
}
private class TLDRulesCache
{
private static volatile TLDRulesCache _uniqueInstance;
private static object _syncObj = new object();
private static object _syncList = new object();
private IDictionary<TLDRule.RuleType, IDictionary<string, TLDRule>> _lstTLDRules;
private TLDRulesCache()
{
// Initialize our internal list:
_lstTLDRules = GetTLDRules();
}
public static TLDRulesCache Instance
{
get
{
if (_uniqueInstance == null)
{
lock (_syncObj)
{
if (_uniqueInstance == null)
{
_uniqueInstance = new TLDRulesCache();
}
}
}
return (_uniqueInstance);
}
}
public IDictionary<TLDRule.RuleType, IDictionary<string, TLDRule>> TLDRuleLists
{
get
{
return _lstTLDRules;
}
set
{
_lstTLDRules = value;
}
}
public static void Reset()
{
lock (_syncObj)
{
_uniqueInstance = null;
}
}
private IDictionary<TLDRule.RuleType, IDictionary<string, TLDRule>> GetTLDRules()
{
var results = new Dictionary<TLDRule.RuleType, IDictionary<string, TLDRule>>();
var rules = Enum.GetValues(typeof(TLDRule.RuleType)).Cast<TLDRule.RuleType>();
foreach (var rule in rules)
{
results[rule] = new Dictionary<string, TLDRule>(StringComparer.CurrentCultureIgnoreCase);
}
var ruleStrings = ReadRulesData();
// Strip out any lines that are:
// a.) A comment
// b.) Blank
var filteredRuleString = ruleStrings.Where(ruleString =>
!ruleString.StartsWith("//") && ruleString.Trim().Length != 0);
foreach (var ruleString in filteredRuleString)
{
var result = new TLDRule(ruleString);
results[result.Type][result.Name] = result;
}
// Return our results:
if (CoreHelpers.InDebugMode())
{
Debug.WriteLine(string.Format("Loaded {0} rules into cache.",
results.Values.Sum(r => r.Values.Count)));
}
return results;
}
private IEnumerable<string> ReadRulesData()
{
var assembly = typeof(TLDRulesCache).GetTypeInfo().Assembly;
var stream = assembly.GetManifestResourceStream("Bit.Core.Resources.public_suffix_list.dat");
string line;
using (var reader = new StreamReader(stream))
{
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}
}
}
}