using System; using System.Text.RegularExpressions; using System.Diagnostics; namespace Janrain.OpenId.Server { /// /// A trust root to validate requests and match return URLs against. /// /// /// public class TrustRoot { #region Private Members private static Regex _tr_regex = new Regex("^(?https?)://((?\\*)|(?\\*\\.)?(?[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*)\\.?)(:(?[0-9]+))?(?(/.*|$))"); private static string[] _top_level_domains = {"com", "edu", "gov", "int", "mil", "net", "org", "biz", "info", "name", "museum", "coop", "aero", "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "" + "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "" + "cu", "cv", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "fi", "fj", "fk", "fm", "fo", "" + "fr", "ga", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "" + "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "" + "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "mg", "mh", "mk", "ml", "mm", "" + "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "" + "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "ru", "rw", "sa", "" + "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", "sz", "tc", "td", "tf", "tg", "th", "" + "tj", "tk", "tm", "tn", "to", "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "um", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "" + "vn", "vu", "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw"}; private string _unparsed; private string _scheme; private bool _wildcard; private string _host; private int _port; private string _path; #endregion #region Constructor(s) public TrustRoot(string unparsed) { Match mo = _tr_regex.Match(unparsed); if (mo.Success) { _unparsed = unparsed; _scheme = mo.Groups["scheme"].Value; _wildcard = mo.Groups["wildcard"].Value != String.Empty; _host = mo.Groups["host"].Value.ToLower(); Group port_group = mo.Groups["port"]; if (port_group.Success) _port = Convert.ToInt32(port_group.Value); else if (_scheme == "https") _port = 443; else _port = 80; _path = mo.Groups["path"].Value; if (_path == String.Empty) _path = "/"; } else { throw new MalformedTrustRoot(null, unparsed + " does not appear to be a valid TrustRoot"); } } #endregion #region Properties public bool IsSane { get { if (_host == "localhost") return true; string[] host_parts = _host.Split('.'); string tld = host_parts[host_parts.Length - 1]; if (!Util.InArray(_top_level_domains, tld)) return false; if (tld.Length == 2) { if (host_parts.Length == 1) return false; if (host_parts[host_parts.Length - 2].Length <= 3) return host_parts.Length > 2; } else { return host_parts.Length > 1; } return false; } } #endregion #region Methods /// /// Validates a URL against this trust root. /// /// A string specifying URL to check. /// Whether the given URL is within this trust root. public bool ValidateUrl(string url) { Uri uri = new Uri(url); return ValidateUrl(uri); } /// /// Validates a URL against this trust root. /// /// The URL to check. /// Whether the given URL is within this trust root. public bool ValidateUrl(Uri url) { if (url.Scheme != _scheme) return false; if (url.Port != _port) return false; if (!_wildcard) { if (url.Host != _host) { return false; } } else { Debug.Assert(_host != string.Empty, "The host part of the Regex should evaluate to at least one char for successful parsed trust roots."); string[] host_parts = _host.Split('.'); string[] url_parts = url.Host.Split('.'); // If the domain contain the wildcard has more parts than the URL to match against, // it naturally can't be valid. // Unless *.example.com actually matches example.com too. if (host_parts.Length > url_parts.Length) return false; int offset = url_parts.Length - host_parts.Length; // Compare last part first and move forward. // Could be done by using EndsWith, but this solution seems more elegant. for (int i = host_parts.Length - 1; i >= 0; i--) { /* if (host_parts[i].Equals("*", StringComparison.Ordinal)) { break; } */ if (!host_parts[i].Equals(url_parts[i + 1], StringComparison.OrdinalIgnoreCase)) { return false; } } } // If path matches or is specified to root ... if (_path.Equals(url.PathAndQuery, StringComparison.Ordinal) || _path.Equals("/", StringComparison.Ordinal)) return true; // If trust root has a longer path, the return URL must be invalid. if (_path.Length > url.PathAndQuery.Length) return false; // The following code assures that http://example.com/directory isn't below http://example.com/dir, // but makes sure http://example.com/dir/ectory is below http://example.com/dir int path_len = _path.Length; string url_prefix = url.PathAndQuery.Substring(0, path_len); if (_path != url_prefix) return false; // If trust root includes a query string ... if (_path.Contains("?")) { // ... make sure return URL begins with a new argument return url.PathAndQuery[path_len] == '&'; } // Or make sure a query string is introduced or a path below trust root return _path.EndsWith("/", StringComparison.Ordinal) || url.PathAndQuery[path_len] == '?' || url.PathAndQuery[path_len] == '/'; } #endregion } }