using System; using System.Text.RegularExpressions; using System.Diagnostics; using System.Globalization; using DotNetOpenId.Yadis; using DotNetOpenId.Provider; using System.Collections.Generic; using System.Xml; namespace DotNetOpenId { /// /// A trust root to validate requests and match return URLs against. /// /// /// This fills the OpenID Authentication 2.0 specification for realms. /// See http://openid.net/specs/openid-authentication-2_0.html#realms /// public class Realm { /// /// Implicitly converts the string-form of a URI to a object. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads")] public static implicit operator Realm(string uri) { return uri != null ? new Realm(uri) : null; } /// /// Implicitly converts a to a object. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")] public static implicit operator Realm(Uri uri) { return uri != null ? new Realm(uri.AbsoluteUri) : null; } /// /// Implicitly converts a object to its form. /// public static implicit operator string(Realm realm) { return realm != null ? realm.ToString() : null; } /// /// Instantiates a from its string representation. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] public Realm(string realmUrl) { if (realmUrl == null) throw new ArgumentNullException("realmUrl"); DomainWildcard = Regex.IsMatch(realmUrl, wildcardDetectionPattern); uri = new Uri(Regex.Replace(realmUrl, wildcardDetectionPattern, m => m.Groups[1].Value)); if (!uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) && !uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) throw new UriFormatException(string.Format(CultureInfo.CurrentCulture, Strings.InvalidScheme, uri.Scheme)); } /// /// Instantiates a from its representation. /// public Realm(Uri realmUrl) { if (realmUrl == null) throw new ArgumentNullException("realmUrl"); uri = realmUrl; if (!uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) && !uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) throw new UriFormatException(string.Format(CultureInfo.CurrentCulture, Strings.InvalidScheme, uri.Scheme)); } /// /// Instantiates a from its representation. /// /// /// This is useful because UriBuilder can construct a host with a wildcard /// in the Host property, but once there it can't be converted to a Uri. /// internal Realm(UriBuilder realmUriBuilder) : this(safeUriBuilderToString(realmUriBuilder)) { } static string safeUriBuilderToString(UriBuilder realmUriBuilder) { if (realmUriBuilder == null) throw new ArgumentNullException("realmUriBuilder"); // Note: we MUST use ToString. Uri property throws if wildcard is present. return realmUriBuilder.ToString(); } Uri uri; const string wildcardDetectionPattern = @"^(\w+://)\*\."; /// /// Whether a '*.' prefix to the hostname is used in the realm to allow /// subdomains or hosts to be added to the URL. /// public bool DomainWildcard { get; private set; } /// /// Gets the host component of this instance. /// public string Host { get { return uri.Host; } } /// /// Gets the scheme name for this URI. /// public string Scheme { get { return uri.Scheme; } } /// /// Gets the port number of this URI. /// public int Port { get { return uri.Port; } } /// /// Gets the absolute path of the URI. /// public string AbsolutePath { get { return uri.AbsolutePath; } } /// /// Gets the System.Uri.AbsolutePath and System.Uri.Query properties separated /// by a question mark (?). /// public string PathAndQuery { get { return uri.PathAndQuery; } } /// /// Gets the realm URL. If the realm includes a wildcard, it is not included here. /// internal Uri NoWildcardUri { get { return uri; } } /// /// Produces the Realm URL. If the realm URL had a wildcard in it, /// the wildcard is replaced with a "www." prefix. /// /// /// See OpenID 2.0 spec section 9.2.1 for the explanation on the addition of /// the "www" prefix. /// internal Uri UriWithWildcardChangedToWww { get { if (DomainWildcard) { UriBuilder builder = new UriBuilder(NoWildcardUri); builder.Host = "www." + builder.Host; return builder.Uri; } else { return NoWildcardUri; } } } 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"}; /// /// This method checks the to see if a trust root represents a reasonable (sane) set of URLs. /// /// /// 'http://*.com/', for example is not a reasonable pattern, as it cannot meaningfully /// specify the site claiming it. This function attempts to find many related examples, /// but it can only work via heuristics. Negative responses from this method should be /// treated as advisory, used only to alert the user to examine the trust root carefully. /// internal bool IsSane { get { if (Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) return true; string[] host_parts = Host.Split('.'); string tld = host_parts[host_parts.Length - 1]; if (Array.IndexOf(_top_level_domains, tld) < 0) 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; } } /// /// Validates a URL against this trust root. /// /// A string specifying URL to check. /// Whether the given URL is within this trust root. internal bool Contains(string url) { return Contains(new Uri(url)); } /// /// Validates a URL against this trust root. /// /// The URL to check. /// Whether the given URL is within this trust root. internal bool Contains(Uri url) { if (url.Scheme != Scheme) return false; if (url.Port != Port) return false; if (!DomainWildcard) { if (url.Host != Host) { return false; } } else { Debug.Assert(!string.IsNullOrEmpty(Host), "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 containing 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; // Compare last part first and move forward. // Maybe could be done by using EndsWith, but piecewies helps ensure that // *.my.com doesn't match ohmeohmy.com but can still match my.com. for (int i = 0; i < host_parts.Length; i++) { string hostPart = host_parts[host_parts.Length - 1 - i]; string urlPart = url_parts[url_parts.Length - 1 - i]; if (!string.Equals(hostPart, urlPart, StringComparison.OrdinalIgnoreCase)) { return false; } } } // If path matches or is specified to root ... // (deliberately case sensitive to protect security on case sensitive systems) if (PathAndQuery.Equals(url.PathAndQuery, StringComparison.Ordinal) || PathAndQuery.Equals("/", StringComparison.Ordinal)) return true; // If trust root has a longer path, the return URL must be invalid. if (PathAndQuery.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 = PathAndQuery.Length; string url_prefix = url.PathAndQuery.Substring(0, path_len); if (PathAndQuery != url_prefix) return false; // If trust root includes a query string ... if (PathAndQuery.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 PathAndQuery.EndsWith("/", StringComparison.Ordinal) || url.PathAndQuery[path_len] == '?' || url.PathAndQuery[path_len] == '/'; } /// /// Searches for an XRDS document at the realm URL, and if found, searches /// for a description of a relying party endpoints (OpenId login pages). /// /// /// Whether redirects may be followed when discovering the Realm. /// This may be true when creating an unsolicited assertion, but must be /// false when performing return URL verification per 2.0 spec section 9.2.1. /// /// The details of the endpoints if found, otherwise null. internal IEnumerable Discover(bool allowRedirects) { // Attempt YADIS discovery DiscoveryResult yadisResult = Yadis.Yadis.Discover(UriWithWildcardChangedToWww, false); if (yadisResult != null) { if (!allowRedirects && yadisResult.NormalizedUri != yadisResult.RequestUri) { // Redirect occurred when it was not allowed. throw new OpenIdException(string.Format(CultureInfo.CurrentCulture, Strings.RealmCausedRedirectUponDiscovery, yadisResult.RequestUri)); } if (yadisResult.IsXrds) { try { XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText); return xrds.FindRelyingPartyReceivingEndpoints(); } catch (XmlException ex) { throw new OpenIdException(Strings.InvalidXRDSDocument, ex); } } } return new RelyingPartyReceivingEndpoint[0]; } /// /// Checks whether one is equal to another. /// public override bool Equals(object obj) { Realm other = obj as Realm; if (other == null) return false; return uri.Equals(other.uri) && DomainWildcard == other.DomainWildcard; } /// /// Returns the hash code used for storing this object in a hash table. /// /// public override int GetHashCode() { return uri.GetHashCode() + (DomainWildcard ? 1 : 0); } /// /// Returns the string form of this . /// public override string ToString() { if (DomainWildcard) { UriBuilder builder = new UriBuilder(uri); builder.Host = "*." + builder.Host; return UriUtil.UriBuilderToStringWithImpliedPorts(builder); } else { return uri.AbsoluteUri; } } } }