1 //-----------------------------------------------------------------------
2 // <copyright file="Realm.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth
.OpenId
{
9 using System
.Collections
.Generic
;
10 using System
.Diagnostics
;
11 using System
.Diagnostics
.CodeAnalysis
;
12 using System
.Globalization
;
13 using System
.Text
.RegularExpressions
;
15 using DotNetOpenAuth
.Messaging
;
16 using DotNetOpenAuth
.OpenId
.Provider
;
19 /// A trust root to validate requests and match return URLs against.
22 /// This fills the OpenID Authentication 2.0 specification for realms.
23 /// See http://openid.net/specs/openid-authentication-2_0.html#realms
27 /// A regex used to detect a wildcard that is being used in the realm.
29 private const string WildcardDetectionPattern
= @"^(\w+://)\*\.";
32 /// A (more or less) comprehensive list of top-level (i.e. ".com") domains,
33 /// for use by <see cref="IsSane"/> in order to disallow overly-broad realms
34 /// that allow all web sites ending with '.com', for example.
36 private static readonly string[] topLevelDomains
= { "com", "edu", "gov", "int", "mil", "net", "org", "biz", "info", "name", "museum", "coop", "aero", "ac", "ad", "ae",
37 "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj",
38 "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr",
39 "cu", "cv", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "fi", "fj", "fk", "fm", "fo",
40 "fr", "ga", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr",
41 "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp",
42 "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "mg", "mh", "mk", "ml", "mm",
43 "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr",
44 "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "ru", "rw", "sa",
45 "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", "sz", "tc", "td", "tf", "tg", "th",
46 "tj", "tk", "tm", "tn", "to", "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "um", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi",
47 "vn", "vu", "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw" };
50 /// The Uri of the realm, with the wildcard (if any) removed.
55 /// Initializes a new instance of the <see cref="Realm"/> class.
57 /// <param name="realmUrl">The realm URL to use in the new instance.</param>
58 [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification
= "TODO")]
59 public Realm(string realmUrl
) {
60 ErrorUtilities
.VerifyArgumentNotNull(realmUrl
, "realmUrl");
61 this.DomainWildcard
= Regex
.IsMatch(realmUrl
, WildcardDetectionPattern
);
62 this.uri
= new Uri(Regex
.Replace(realmUrl
, WildcardDetectionPattern
, m
=> m
.Groups
[1].Value
));
63 if (!this.uri
.Scheme
.Equals("http", StringComparison
.OrdinalIgnoreCase
) &&
64 !this.uri
.Scheme
.Equals("https", StringComparison
.OrdinalIgnoreCase
)) {
65 throw new UriFormatException(
66 string.Format(CultureInfo
.CurrentCulture
, OpenIdStrings
.InvalidScheme
, this.uri
.Scheme
));
71 /// Initializes a new instance of the <see cref="Realm"/> class.
73 /// <param name="realmUrl">The realm URL of the Relying Party.</param>
74 public Realm(Uri realmUrl
) {
75 ErrorUtilities
.VerifyArgumentNotNull(realmUrl
, "realmUrl");
77 if (!this.uri
.Scheme
.Equals("http", StringComparison
.OrdinalIgnoreCase
) &&
78 !this.uri
.Scheme
.Equals("https", StringComparison
.OrdinalIgnoreCase
)) {
79 throw new UriFormatException(
80 string.Format(CultureInfo
.CurrentCulture
, OpenIdStrings
.InvalidScheme
, this.uri
.Scheme
));
85 /// Initializes a new instance of the <see cref="Realm"/> class.
87 /// <param name="realmUriBuilder">The realm URI builder.</param>
89 /// This is useful because UriBuilder can construct a host with a wildcard
90 /// in the Host property, but once there it can't be converted to a Uri.
92 internal Realm(UriBuilder realmUriBuilder
)
93 : this(SafeUriBuilderToString(realmUriBuilder
)) { }
96 /// Gets a value indicating whether a '*.' prefix to the hostname is
97 /// used in the realm to allow subdomains or hosts to be added to the URL.
99 public bool DomainWildcard { get; private set; }
102 /// Gets the host component of this instance.
104 public string Host { get { return this.uri.Host; }
}
107 /// Gets the scheme name for this URI.
109 public string Scheme { get { return this.uri.Scheme; }
}
112 /// Gets the port number of this URI.
114 public int Port { get { return this.uri.Port; }
}
117 /// Gets the absolute path of the URI.
119 public string AbsolutePath { get { return this.uri.AbsolutePath; }
}
122 /// Gets the System.Uri.AbsolutePath and System.Uri.Query properties separated
123 /// by a question mark (?).
125 public string PathAndQuery { get { return this.uri.PathAndQuery; }
}
128 /// Gets the realm URL. If the realm includes a wildcard, it is not included here.
130 internal Uri NoWildcardUri { get { return this.uri; }
}
133 /// Gets the Realm discovery URL, where the wildcard (if present) is replaced with "www.".
136 /// See OpenID 2.0 spec section 9.2.1 for the explanation on the addition of
137 /// the "www" prefix.
139 internal Uri UriWithWildcardChangedToWww
{
141 if (this.DomainWildcard
) {
142 UriBuilder builder
= new UriBuilder(this.NoWildcardUri
);
143 builder
.Host
= "www." + builder
.Host
;
146 return this.NoWildcardUri
;
152 /// Gets a value indicating whether this realm represents a reasonable (sane) set of URLs.
155 /// 'http://*.com/', for example is not a reasonable pattern, as it cannot meaningfully
156 /// specify the site claiming it. This function attempts to find many related examples,
157 /// but it can only work via heuristics. Negative responses from this method should be
158 /// treated as advisory, used only to alert the user to examine the trust root carefully.
160 internal bool IsSane
{
162 if (this.Host
.Equals("localhost", StringComparison
.OrdinalIgnoreCase
)) {
166 string[] host_parts
= this.Host
.Split('.');
168 string tld
= host_parts
[host_parts
.Length
- 1];
170 if (Array
.IndexOf(topLevelDomains
, tld
) < 0) {
174 if (tld
.Length
== 2) {
175 if (host_parts
.Length
== 1) {
179 if (host_parts
[host_parts
.Length
- 2].Length
<= 3) {
180 return host_parts
.Length
> 2;
183 return host_parts
.Length
> 1;
191 /// Implicitly converts the string-form of a URI to a <see cref="Realm"/> object.
193 /// <param name="uri">The URI that the new Realm instance will represent.</param>
194 /// <returns>The result of the conversion.</returns>
195 [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification
= "TODO")]
196 [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification
= "TODO")]
197 public static implicit operator Realm(string uri
) {
198 return uri
!= null ? new Realm(uri
) : null;
202 /// Implicitly converts a <see cref="Uri"/> to a <see cref="Realm"/> object.
204 /// <param name="uri">The URI to convert to a realm.</param>
205 /// <returns>The result of the conversion.</returns>
206 [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification
= "TODO")]
207 public static implicit operator Realm(Uri uri
) {
208 return uri
!= null ? new Realm(uri
.AbsoluteUri
) : null;
212 /// Implicitly converts a <see cref="Realm"/> object to its <see cref="String"/> form.
214 /// <param name="realm">The realm to convert to a string value.</param>
215 /// <returns>The result of the conversion.</returns>
216 public static implicit operator string(Realm realm
) {
217 return realm
!= null ? realm
.ToString() : null;
221 /// Checks whether one <see cref="Realm"/> is equal to another.
223 /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
225 /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
227 /// <exception cref="T:System.NullReferenceException">
228 /// The <paramref name="obj"/> parameter is null.
230 public override bool Equals(object obj
) {
231 Realm other
= obj
as Realm
;
235 return this.uri
.Equals(other
.uri
) && this.DomainWildcard
== other
.DomainWildcard
;
239 /// Returns the hash code used for storing this object in a hash table.
242 /// A hash code for the current <see cref="T:System.Object"/>.
244 public override int GetHashCode() {
245 return this.uri
.GetHashCode() + (this.DomainWildcard
? 1 : 0);
249 /// Returns the string form of this <see cref="Realm"/>.
252 /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
254 public override string ToString() {
255 if (this.DomainWildcard
) {
256 UriBuilder builder
= new UriBuilder(this.uri
);
257 builder
.Host
= "*." + builder
.Host
;
258 return builder
.ToStringWithImpliedPorts();
260 return this.uri
.AbsoluteUri
;
265 /// Validates a URL against this trust root.
267 /// <param name="url">A string specifying URL to check.</param>
268 /// <returns>Whether the given URL is within this trust root.</returns>
269 internal bool Contains(string url
) {
270 return this.Contains(new Uri(url
));
274 /// Validates a URL against this trust root.
276 /// <param name="url">The URL to check.</param>
277 /// <returns>Whether the given URL is within this trust root.</returns>
278 internal bool Contains(Uri url
) {
279 if (url
.Scheme
!= this.Scheme
) {
283 if (url
.Port
!= this.Port
) {
287 if (!this.DomainWildcard
) {
288 if (url
.Host
!= this.Host
) {
292 Debug
.Assert(!string.IsNullOrEmpty(this.Host
), "The host part of the Regex should evaluate to at least one char for successful parsed trust roots.");
293 string[] host_parts
= this.Host
.Split('.');
294 string[] url_parts
= url
.Host
.Split('.');
296 // If the domain containing the wildcard has more parts than the URL to match against,
297 // it naturally can't be valid.
298 // Unless *.example.com actually matches example.com too.
299 if (host_parts
.Length
> url_parts
.Length
) {
303 // Compare last part first and move forward.
304 // Maybe could be done by using EndsWith, but piecewies helps ensure that
305 // *.my.com doesn't match ohmeohmy.com but can still match my.com.
306 for (int i
= 0; i
< host_parts
.Length
; i
++) {
307 string hostPart
= host_parts
[host_parts
.Length
- 1 - i
];
308 string urlPart
= url_parts
[url_parts
.Length
- 1 - i
];
309 if (!string.Equals(hostPart
, urlPart
, StringComparison
.OrdinalIgnoreCase
)) {
315 // If path matches or is specified to root ...
316 // (deliberately case sensitive to protect security on case sensitive systems)
317 if (this.PathAndQuery
.Equals(url
.PathAndQuery
, StringComparison
.Ordinal
)
318 || this.PathAndQuery
.Equals("/", StringComparison
.Ordinal
)) {
322 // If trust root has a longer path, the return URL must be invalid.
323 if (this.PathAndQuery
.Length
> url
.PathAndQuery
.Length
) {
327 // The following code assures that http://example.com/directory isn't below http://example.com/dir,
328 // but makes sure http://example.com/dir/ectory is below http://example.com/dir
329 int path_len
= this.PathAndQuery
.Length
;
330 string url_prefix
= url
.PathAndQuery
.Substring(0, path_len
);
332 if (this.PathAndQuery
!= url_prefix
) {
336 // If trust root includes a query string ...
337 if (this.PathAndQuery
.Contains("?")) {
338 // ... make sure return URL begins with a new argument
339 return url
.PathAndQuery
[path_len
] == '&';
342 // Or make sure a query string is introduced or a path below trust root
343 return this.PathAndQuery
.EndsWith("/", StringComparison
.Ordinal
)
344 || url
.PathAndQuery
[path_len
] == '?'
345 || url
.PathAndQuery
[path_len
] == '/';
348 #if DISCOVERY // TODO: Add discovery and then re-enable this code block
350 /////// Searches for an XRDS document at the realm URL, and if found, searches
351 /////// for a description of a relying party endpoints (OpenId login pages).
353 /////// <param name="allowRedirects">
354 /////// Whether redirects may be followed when discovering the Realm.
355 /////// This may be true when creating an unsolicited assertion, but must be
356 /////// false when performing return URL verification per 2.0 spec section 9.2.1.
358 /////// <returns>The details of the endpoints if found, otherwise null.</returns>
359 ////internal IEnumerable<DotNetOpenId.Provider.RelyingPartyReceivingEndpoint> Discover(bool allowRedirects) {
360 //// // Attempt YADIS discovery
361 //// DiscoveryResult yadisResult = Yadis.Yadis.Discover(UriWithWildcardChangedToWww, false);
362 //// if (yadisResult != null) {
363 //// if (!allowRedirects && yadisResult.NormalizedUri != yadisResult.RequestUri) {
364 //// // Redirect occurred when it was not allowed.
365 //// throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
366 //// Strings.RealmCausedRedirectUponDiscovery, yadisResult.RequestUri));
368 //// if (yadisResult.IsXrds) {
370 //// XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
371 //// return xrds.FindRelyingPartyReceivingEndpoints();
372 //// } catch (XmlException ex) {
373 //// throw new OpenIdException(Strings.InvalidXRDSDocument, ex);
377 //// return new RelyingPartyReceivingEndpoint[0];
382 /// Calls <see cref="UriBuilder.ToString"/> if the argument is non-null.
383 /// Otherwise throws <see cref="ArgumentNullException"/>.
385 /// <param name="realmUriBuilder">The realm URI builder.</param>
386 /// <returns>The result of UriBuilder.ToString()</returns>
388 /// This simple method is worthwhile because it checks for null
389 /// before dereferencing the UriBuilder. Since this is called from
390 /// within a constructor's base(...) call, this avoids a <see cref="NullReferenceException"/>
391 /// when we should be throwing an <see cref="ArgumentNullException"/>.
393 private static string SafeUriBuilderToString(UriBuilder realmUriBuilder
) {
394 ErrorUtilities
.VerifyArgumentNotNull(realmUriBuilder
, "realmUriBuilder");
396 // Note: we MUST use ToString. Uri property throws if wildcard is present.
397 // TODO: I now know that Uri.ToString and Uri.AbsoluteUri are very different
398 // for some strings. Do we have to worry about that here?
399 return realmUriBuilder
.ToString();