StyleCop clean.
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / Realm.cs
blob4b0266e177256f3fffa540c93f423727b048cf23
1 //-----------------------------------------------------------------------
2 // <copyright file="Realm.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
4 // </copyright>
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth.OpenId {
8 using System;
9 using System.Collections.Generic;
10 using System.Diagnostics;
11 using System.Diagnostics.CodeAnalysis;
12 using System.Globalization;
13 using System.Text.RegularExpressions;
14 using System.Xml;
15 using DotNetOpenAuth.Messaging;
16 using DotNetOpenAuth.OpenId.Provider;
18 /// <summary>
19 /// A trust root to validate requests and match return URLs against.
20 /// </summary>
21 /// <remarks>
22 /// This fills the OpenID Authentication 2.0 specification for realms.
23 /// See http://openid.net/specs/openid-authentication-2_0.html#realms
24 /// </remarks>
25 public class Realm {
26 /// <summary>
27 /// A regex used to detect a wildcard that is being used in the realm.
28 /// </summary>
29 private const string WildcardDetectionPattern = @"^(\w+://)\*\.";
31 /// <summary>
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.
35 /// </summary>
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" };
49 /// <summary>
50 /// The Uri of the realm, with the wildcard (if any) removed.
51 /// </summary>
52 private Uri uri;
54 /// <summary>
55 /// Initializes a new instance of the <see cref="Realm"/> class.
56 /// </summary>
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));
70 /// <summary>
71 /// Initializes a new instance of the <see cref="Realm"/> class.
72 /// </summary>
73 /// <param name="realmUrl">The realm URL of the Relying Party.</param>
74 public Realm(Uri realmUrl) {
75 ErrorUtilities.VerifyArgumentNotNull(realmUrl, "realmUrl");
76 this.uri = 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));
84 /// <summary>
85 /// Initializes a new instance of the <see cref="Realm"/> class.
86 /// </summary>
87 /// <param name="realmUriBuilder">The realm URI builder.</param>
88 /// <remarks>
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.
91 /// </remarks>
92 internal Realm(UriBuilder realmUriBuilder)
93 : this(SafeUriBuilderToString(realmUriBuilder)) { }
95 /// <summary>
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.
98 /// </summary>
99 public bool DomainWildcard { get; private set; }
101 /// <summary>
102 /// Gets the host component of this instance.
103 /// </summary>
104 public string Host { get { return this.uri.Host; } }
106 /// <summary>
107 /// Gets the scheme name for this URI.
108 /// </summary>
109 public string Scheme { get { return this.uri.Scheme; } }
111 /// <summary>
112 /// Gets the port number of this URI.
113 /// </summary>
114 public int Port { get { return this.uri.Port; } }
116 /// <summary>
117 /// Gets the absolute path of the URI.
118 /// </summary>
119 public string AbsolutePath { get { return this.uri.AbsolutePath; } }
121 /// <summary>
122 /// Gets the System.Uri.AbsolutePath and System.Uri.Query properties separated
123 /// by a question mark (?).
124 /// </summary>
125 public string PathAndQuery { get { return this.uri.PathAndQuery; } }
127 /// <summary>
128 /// Gets the realm URL. If the realm includes a wildcard, it is not included here.
129 /// </summary>
130 internal Uri NoWildcardUri { get { return this.uri; } }
132 /// <summary>
133 /// Gets the Realm discovery URL, where the wildcard (if present) is replaced with "www.".
134 /// </summary>
135 /// <remarks>
136 /// See OpenID 2.0 spec section 9.2.1 for the explanation on the addition of
137 /// the "www" prefix.
138 /// </remarks>
139 internal Uri UriWithWildcardChangedToWww {
140 get {
141 if (this.DomainWildcard) {
142 UriBuilder builder = new UriBuilder(this.NoWildcardUri);
143 builder.Host = "www." + builder.Host;
144 return builder.Uri;
145 } else {
146 return this.NoWildcardUri;
151 /// <summary>
152 /// Gets a value indicating whether this realm represents a reasonable (sane) set of URLs.
153 /// </summary>
154 /// <remarks>
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.
159 /// </remarks>
160 internal bool IsSane {
161 get {
162 if (this.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) {
163 return true;
166 string[] host_parts = this.Host.Split('.');
168 string tld = host_parts[host_parts.Length - 1];
170 if (Array.IndexOf(topLevelDomains, tld) < 0) {
171 return false;
174 if (tld.Length == 2) {
175 if (host_parts.Length == 1) {
176 return false;
179 if (host_parts[host_parts.Length - 2].Length <= 3) {
180 return host_parts.Length > 2;
182 } else {
183 return host_parts.Length > 1;
186 return false;
190 /// <summary>
191 /// Implicitly converts the string-form of a URI to a <see cref="Realm"/> object.
192 /// </summary>
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;
201 /// <summary>
202 /// Implicitly converts a <see cref="Uri"/> to a <see cref="Realm"/> object.
203 /// </summary>
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;
211 /// <summary>
212 /// Implicitly converts a <see cref="Realm"/> object to its <see cref="String"/> form.
213 /// </summary>
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;
220 /// <summary>
221 /// Checks whether one <see cref="Realm"/> is equal to another.
222 /// </summary>
223 /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
224 /// <returns>
225 /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
226 /// </returns>
227 /// <exception cref="T:System.NullReferenceException">
228 /// The <paramref name="obj"/> parameter is null.
229 /// </exception>
230 public override bool Equals(object obj) {
231 Realm other = obj as Realm;
232 if (other == null) {
233 return false;
235 return this.uri.Equals(other.uri) && this.DomainWildcard == other.DomainWildcard;
238 /// <summary>
239 /// Returns the hash code used for storing this object in a hash table.
240 /// </summary>
241 /// <returns>
242 /// A hash code for the current <see cref="T:System.Object"/>.
243 /// </returns>
244 public override int GetHashCode() {
245 return this.uri.GetHashCode() + (this.DomainWildcard ? 1 : 0);
248 /// <summary>
249 /// Returns the string form of this <see cref="Realm"/>.
250 /// </summary>
251 /// <returns>
252 /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
253 /// </returns>
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();
259 } else {
260 return this.uri.AbsoluteUri;
264 /// <summary>
265 /// Validates a URL against this trust root.
266 /// </summary>
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));
273 /// <summary>
274 /// Validates a URL against this trust root.
275 /// </summary>
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) {
280 return false;
283 if (url.Port != this.Port) {
284 return false;
287 if (!this.DomainWildcard) {
288 if (url.Host != this.Host) {
289 return false;
291 } else {
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) {
300 return false;
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)) {
310 return false;
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)) {
319 return true;
322 // If trust root has a longer path, the return URL must be invalid.
323 if (this.PathAndQuery.Length > url.PathAndQuery.Length) {
324 return false;
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) {
333 return false;
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
349 /////// <summary>
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).
352 /////// </summary>
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.
357 /////// </param>
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));
367 //// }
368 //// if (yadisResult.IsXrds) {
369 //// try {
370 //// XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
371 //// return xrds.FindRelyingPartyReceivingEndpoints();
372 //// } catch (XmlException ex) {
373 //// throw new OpenIdException(Strings.InvalidXRDSDocument, ex);
374 //// }
375 //// }
376 //// }
377 //// return new RelyingPartyReceivingEndpoint[0];
378 ////}
379 #endif
381 /// <summary>
382 /// Calls <see cref="UriBuilder.ToString"/> if the argument is non-null.
383 /// Otherwise throws <see cref="ArgumentNullException"/>.
384 /// </summary>
385 /// <param name="realmUriBuilder">The realm URI builder.</param>
386 /// <returns>The result of UriBuilder.ToString()</returns>
387 /// <remarks>
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"/>.
392 /// </remarks>
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();