diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/HostMetaDiscoveryService.cs')
-rw-r--r-- | src/DotNetOpenAuth.OpenId/OpenId/HostMetaDiscoveryService.cs | 516 |
1 files changed, 516 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/HostMetaDiscoveryService.cs b/src/DotNetOpenAuth.OpenId/OpenId/HostMetaDiscoveryService.cs new file mode 100644 index 0000000..215ea24 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/HostMetaDiscoveryService.cs @@ -0,0 +1,516 @@ +//----------------------------------------------------------------------- +// <copyright file="HostMetaDiscoveryService.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Security; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using System.Security.Permissions; + using System.Text; + using System.Text.RegularExpressions; + using System.Xml; + using System.Xml.XPath; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + using DotNetOpenAuth.Xrds; + using DotNetOpenAuth.Yadis; + + /// <summary> + /// The discovery service to support host-meta based discovery, such as Google Apps for Domains. + /// </summary> + /// <remarks> + /// The spec for this discovery mechanism can be found at: + /// http://groups.google.com/group/google-federated-login-api/web/openid-discovery-for-hosted-domains + /// and the XMLDSig spec referenced in that spec can be found at: + /// http://wiki.oasis-open.org/xri/XrdOne/XmlDsigProfile + /// </remarks> + public class HostMetaDiscoveryService : IIdentifierDiscoveryService { + /// <summary> + /// The URI template for discovery host-meta on domains hosted by + /// Google Apps for Domains. + /// </summary> + private static readonly HostMetaProxy GoogleHostedHostMeta = new HostMetaProxy("https://www.google.com/accounts/o8/.well-known/host-meta?hd={0}", "hosted-id.google.com"); + + /// <summary> + /// Path to the well-known location of the host-meta document at a domain. + /// </summary> + private const string LocalHostMetaPath = "/.well-known/host-meta"; + + /// <summary> + /// The pattern within a host-meta file to look for to obtain the URI to the XRDS document. + /// </summary> + private static readonly Regex HostMetaLink = new Regex(@"^Link: <(?<location>.+?)>; rel=""describedby http://reltype.google.com/openid/xrd-op""; type=""application/xrds\+xml""$"); + + /// <summary> + /// Initializes a new instance of the <see cref="HostMetaDiscoveryService"/> class. + /// </summary> + public HostMetaDiscoveryService() { + this.TrustedHostMetaProxies = new List<HostMetaProxy>(); + } + + /// <summary> + /// Gets the set of URI templates to use to contact host-meta hosting proxies + /// for domain discovery. + /// </summary> + public IList<HostMetaProxy> TrustedHostMetaProxies { get; private set; } + + /// <summary> + /// Gets or sets a value indicating whether to trust Google to host domains' host-meta documents. + /// </summary> + /// <remarks> + /// This property is just a convenient mechanism for checking or changing the set of + /// trusted host-meta proxies in the <see cref="TrustedHostMetaProxies"/> property. + /// </remarks> + public bool UseGoogleHostedHostMeta { + get { + return this.TrustedHostMetaProxies.Contains(GoogleHostedHostMeta); + } + + set { + if (value != this.UseGoogleHostedHostMeta) { + if (value) { + this.TrustedHostMetaProxies.Add(GoogleHostedHostMeta); + } else { + this.TrustedHostMetaProxies.Remove(GoogleHostedHostMeta); + } + } + } + } + + #region IIdentifierDiscoveryService Members + + /// <summary> + /// Performs discovery on the specified identifier. + /// </summary> + /// <param name="identifier">The identifier to perform discovery on.</param> + /// <param name="requestHandler">The means to place outgoing HTTP requests.</param> + /// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param> + /// <returns> + /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty. + /// </returns> + public IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain) { + abortDiscoveryChain = false; + + // Google Apps are always URIs -- not XRIs. + var uriIdentifier = identifier as UriIdentifier; + if (uriIdentifier == null) { + return Enumerable.Empty<IdentifierDiscoveryResult>(); + } + + var results = new List<IdentifierDiscoveryResult>(); + string signingHost; + using (var response = GetXrdsResponse(uriIdentifier, requestHandler, out signingHost)) { + if (response != null) { + try { + var document = new XrdsDocument(XmlReader.Create(response.ResponseStream)); + ValidateXmlDSig(document, uriIdentifier, response, signingHost); + var xrds = GetXrdElements(document, uriIdentifier.Uri.Host); + + // Look for claimed identifier template URIs for an additional XRDS document. + results.AddRange(GetExternalServices(xrds, uriIdentifier, requestHandler)); + + // If we couldn't find any claimed identifiers, look for OP identifiers. + // Normally this would be the opposite (OP Identifiers take precedence over + // claimed identifiers, but for Google Apps, XRDS' always have OP Identifiers + // mixed in, which the OpenID spec mandate should eclipse Claimed Identifiers, + // which would break positive assertion checks). + if (results.Count == 0) { + results.AddRange(xrds.CreateServiceEndpoints(uriIdentifier, uriIdentifier)); + } + + abortDiscoveryChain = true; + } catch (XmlException ex) { + Logger.Yadis.ErrorFormat("Error while parsing XRDS document at {0} pointed to by host-meta: {1}", response.FinalUri, ex); + } + } + } + + return results; + } + + #endregion + + /// <summary> + /// Gets the XRD elements that have a given CanonicalID. + /// </summary> + /// <param name="document">The XRDS document.</param> + /// <param name="canonicalId">The CanonicalID to match on.</param> + /// <returns>A sequence of XRD elements.</returns> + private static IEnumerable<XrdElement> GetXrdElements(XrdsDocument document, string canonicalId) { + // filter to include only those XRD elements describing the host whose host-meta pointed us to this document. + return document.XrdElements.Where(xrd => string.Equals(xrd.CanonicalID, canonicalId, StringComparison.Ordinal)); + } + + /// <summary> + /// Gets the described-by services in XRD elements. + /// </summary> + /// <param name="xrds">The XRDs to search.</param> + /// <returns>A sequence of services.</returns> + private static IEnumerable<ServiceElement> GetDescribedByServices(IEnumerable<XrdElement> xrds) { + Contract.Requires<ArgumentNullException>(xrds != null); + Contract.Ensures(Contract.Result<IEnumerable<ServiceElement>>() != null); + + var describedBy = from xrd in xrds + from service in xrd.SearchForServiceTypeUris(p => "http://www.iana.org/assignments/relation/describedby") + select service; + return describedBy; + } + + /// <summary> + /// Gets the services for an identifier that are described by an external XRDS document. + /// </summary> + /// <param name="xrds">The XRD elements to search for described-by services.</param> + /// <param name="identifier">The identifier under discovery.</param> + /// <param name="requestHandler">The request handler.</param> + /// <returns>The discovered services.</returns> + private static IEnumerable<IdentifierDiscoveryResult> GetExternalServices(IEnumerable<XrdElement> xrds, UriIdentifier identifier, IDirectWebRequestHandler requestHandler) { + Contract.Requires<ArgumentNullException>(xrds != null); + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Requires<ArgumentNullException>(requestHandler != null); + Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null); + + var results = new List<IdentifierDiscoveryResult>(); + foreach (var serviceElement in GetDescribedByServices(xrds)) { + var templateNode = serviceElement.Node.SelectSingleNode("google:URITemplate", serviceElement.XmlNamespaceResolver); + var nextAuthorityNode = serviceElement.Node.SelectSingleNode("google:NextAuthority", serviceElement.XmlNamespaceResolver); + if (templateNode != null) { + Uri externalLocation = new Uri(templateNode.Value.Trim().Replace("{%uri}", Uri.EscapeDataString(identifier.Uri.AbsoluteUri))); + string nextAuthority = nextAuthorityNode != null ? nextAuthorityNode.Value.Trim() : identifier.Uri.Host; + try { + using (var externalXrdsResponse = GetXrdsResponse(identifier, requestHandler, externalLocation)) { + XrdsDocument externalXrds = new XrdsDocument(XmlReader.Create(externalXrdsResponse.ResponseStream)); + ValidateXmlDSig(externalXrds, identifier, externalXrdsResponse, nextAuthority); + results.AddRange(GetXrdElements(externalXrds, identifier).CreateServiceEndpoints(identifier, identifier)); + } + } catch (ProtocolException ex) { + Logger.Yadis.WarnFormat("HTTP GET error while retrieving described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex); + } catch (XmlException ex) { + Logger.Yadis.ErrorFormat("Error while parsing described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex); + } + } + } + + return results; + } + + /// <summary> + /// Validates the XML digital signature on an XRDS document. + /// </summary> + /// <param name="document">The XRDS document whose signature should be validated.</param> + /// <param name="identifier">The identifier under discovery.</param> + /// <param name="response">The response.</param> + /// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param> + /// <exception cref="ProtocolException">Thrown if the XRDS document has an invalid or a missing signature.</exception> + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "XmlDSig", Justification = "xml")] + private static void ValidateXmlDSig(XrdsDocument document, UriIdentifier identifier, IncomingWebResponse response, string signingHost) { + Contract.Requires<ArgumentNullException>(document != null); + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Requires<ArgumentNullException>(response != null); + + var signatureNode = document.Node.SelectSingleNode("/xrds:XRDS/ds:Signature", document.XmlNamespaceResolver); + ErrorUtilities.VerifyProtocol(signatureNode != null, OpenIdStrings.MissingElement, "Signature"); + var signedInfoNode = signatureNode.SelectSingleNode("ds:SignedInfo", document.XmlNamespaceResolver); + ErrorUtilities.VerifyProtocol(signedInfoNode != null, OpenIdStrings.MissingElement, "SignedInfo"); + ErrorUtilities.VerifyProtocol( + signedInfoNode.SelectSingleNode("ds:CanonicalizationMethod[@Algorithm='http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets']", document.XmlNamespaceResolver) != null, + OpenIdStrings.UnsupportedCanonicalizationMethod); + ErrorUtilities.VerifyProtocol( + signedInfoNode.SelectSingleNode("ds:SignatureMethod[@Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1']", document.XmlNamespaceResolver) != null, + OpenIdStrings.UnsupportedSignatureMethod); + var certNodes = signatureNode.Select("ds:KeyInfo/ds:X509Data/ds:X509Certificate", document.XmlNamespaceResolver); + ErrorUtilities.VerifyProtocol(certNodes.Count > 0, OpenIdStrings.MissingElement, "X509Certificate"); + var certs = certNodes.Cast<XPathNavigator>().Select(n => new X509Certificate2(Convert.FromBase64String(n.Value.Trim()))).ToList(); + + // Verify that we trust the signer of the certificates. + // Start by trying to validate just the certificate used to sign the XRDS document, + // since we can do that with partial trust. + Logger.OpenId.Debug("Verifying that we trust the certificate used to sign the discovery document."); + if (!certs[0].Verify()) { + // We couldn't verify just the signing certificate, so try to verify the whole certificate chain. + try { + Logger.OpenId.Debug("Verifying the whole certificate chain."); + VerifyCertChain(certs); + Logger.OpenId.Debug("Certificate chain verified."); + } catch (SecurityException) { + Logger.Yadis.Warn("Signing certificate verification failed and we have insufficient code access security permissions to perform certificate chain validation."); + ErrorUtilities.ThrowProtocol(OpenIdStrings.X509CertificateNotTrusted); + } + } + + // Verify that the certificate is issued to the host on whom we are performing discovery. + string hostName = certs[0].GetNameInfo(X509NameType.DnsName, false); + ErrorUtilities.VerifyProtocol(string.Equals(hostName, signingHost, StringComparison.OrdinalIgnoreCase), OpenIdStrings.MisdirectedSigningCertificate, hostName, signingHost); + + // Verify the signature itself + byte[] signature = Convert.FromBase64String(response.Headers["Signature"]); + var provider = (RSACryptoServiceProvider)certs.First().PublicKey.Key; + byte[] data = new byte[response.ResponseStream.Length]; + response.ResponseStream.Seek(0, SeekOrigin.Begin); + response.ResponseStream.Read(data, 0, data.Length); + ErrorUtilities.VerifyProtocol(provider.VerifyData(data, "SHA1", signature), OpenIdStrings.InvalidDSig); + } + + /// <summary> + /// Verifies the cert chain. + /// </summary> + /// <param name="certs">The certs.</param> + /// <remarks> + /// This must be in a method of its own because there is a LinkDemand on the <see cref="X509Chain.Build"/> + /// method. By being in a method of its own, the caller of this method may catch a + /// <see cref="SecurityException"/> that is thrown if we're not running with full trust and execute + /// an alternative plan. + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the certificate chain is invalid or unverifiable.</exception> + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "DotNetOpenAuth.Messaging.ErrorUtilities.ThrowProtocol(System.String,System.Object[])", Justification = "The localized portion is a string resource already."), SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "By design")] + private static void VerifyCertChain(List<X509Certificate2> certs) { + var chain = new X509Chain(); + foreach (var cert in certs) { + chain.Build(cert); + } + + if (chain.ChainStatus.Length > 0) { + ErrorUtilities.ThrowProtocol( + string.Format( + CultureInfo.CurrentCulture, + OpenIdStrings.X509CertificateNotTrusted + " {0}", + string.Join(", ", chain.ChainStatus.Select(status => status.StatusInformation).ToArray()))); + } + } + + /// <summary> + /// Gets the XRDS HTTP response for a given identifier. + /// </summary> + /// <param name="identifier">The identifier.</param> + /// <param name="requestHandler">The request handler.</param> + /// <param name="xrdsLocation">The location of the XRDS document to retrieve.</param> + /// <returns> + /// A HTTP response carrying an XRDS document. + /// </returns> + /// <exception cref="ProtocolException">Thrown if the XRDS document could not be obtained.</exception> + private static IncomingWebResponse GetXrdsResponse(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, Uri xrdsLocation) { + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Requires<ArgumentNullException>(requestHandler != null); + Contract.Requires<ArgumentNullException>(xrdsLocation != null); + Contract.Ensures(Contract.Result<IncomingWebResponse>() != null); + + var request = (HttpWebRequest)WebRequest.Create(xrdsLocation); + request.CachePolicy = Yadis.IdentifierDiscoveryCachePolicy; + request.Accept = ContentTypes.Xrds; + var options = identifier.IsDiscoverySecureEndToEnd ? DirectWebRequestOptions.RequireSsl : DirectWebRequestOptions.None; + var response = requestHandler.GetResponse(request, options).GetSnapshot(Yadis.MaximumResultToScan); + if (!string.Equals(response.ContentType.MediaType, ContentTypes.Xrds, StringComparison.Ordinal)) { + Logger.Yadis.WarnFormat("Host-meta pointed to XRDS at {0}, but Content-Type at that URL was unexpected value '{1}'.", xrdsLocation, response.ContentType); + } + + return response; + } + + /// <summary> + /// Gets the XRDS HTTP response for a given identifier. + /// </summary> + /// <param name="identifier">The identifier.</param> + /// <param name="requestHandler">The request handler.</param> + /// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param> + /// <returns>A HTTP response carrying an XRDS document, or <c>null</c> if one could not be obtained.</returns> + /// <exception cref="ProtocolException">Thrown if the XRDS document could not be obtained.</exception> + private IncomingWebResponse GetXrdsResponse(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, out string signingHost) { + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Requires<ArgumentNullException>(requestHandler != null); + Uri xrdsLocation = this.GetXrdsLocation(identifier, requestHandler, out signingHost); + if (xrdsLocation == null) { + return null; + } + + var response = GetXrdsResponse(identifier, requestHandler, xrdsLocation); + + return response; + } + + /// <summary> + /// Gets the location of the XRDS document that describes a given identifier. + /// </summary> + /// <param name="identifier">The identifier under discovery.</param> + /// <param name="requestHandler">The request handler.</param> + /// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param> + /// <returns>An absolute URI, or <c>null</c> if one could not be determined.</returns> + private Uri GetXrdsLocation(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, out string signingHost) { + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Requires<ArgumentNullException>(requestHandler != null); + using (var hostMetaResponse = this.GetHostMeta(identifier, requestHandler, out signingHost)) { + if (hostMetaResponse == null) { + return null; + } + + using (var sr = hostMetaResponse.GetResponseReader()) { + string line = sr.ReadLine(); + Match m = HostMetaLink.Match(line); + if (m.Success) { + Uri location = new Uri(m.Groups["location"].Value); + Logger.Yadis.InfoFormat("Found link to XRDS at {0} in host-meta document {1}.", location, hostMetaResponse.FinalUri); + return location; + } + } + + Logger.Yadis.WarnFormat("Could not find link to XRDS in host-meta document: {0}", hostMetaResponse.FinalUri); + return null; + } + } + + /// <summary> + /// Gets the host-meta for a given identifier. + /// </summary> + /// <param name="identifier">The identifier.</param> + /// <param name="requestHandler">The request handler.</param> + /// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param> + /// <returns> + /// The host-meta response, or <c>null</c> if no host-meta document could be obtained. + /// </returns> + private IncomingWebResponse GetHostMeta(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, out string signingHost) { + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Requires<ArgumentNullException>(requestHandler != null); + foreach (var hostMetaProxy in this.GetHostMetaLocations(identifier)) { + var hostMetaLocation = hostMetaProxy.GetProxy(identifier); + var request = (HttpWebRequest)WebRequest.Create(hostMetaLocation); + request.CachePolicy = Yadis.IdentifierDiscoveryCachePolicy; + var options = DirectWebRequestOptions.AcceptAllHttpResponses; + if (identifier.IsDiscoverySecureEndToEnd) { + options |= DirectWebRequestOptions.RequireSsl; + } + var response = requestHandler.GetResponse(request, options).GetSnapshot(Yadis.MaximumResultToScan); + try { + if (response.Status == HttpStatusCode.OK) { + Logger.Yadis.InfoFormat("Found host-meta for {0} at: {1}", identifier.Uri.Host, hostMetaLocation); + signingHost = hostMetaProxy.GetSigningHost(identifier); + return response; + } else { + Logger.Yadis.InfoFormat("Could not obtain host-meta for {0} from {1}", identifier.Uri.Host, hostMetaLocation); + response.Dispose(); + } + } catch { + response.Dispose(); + throw; + } + } + + signingHost = null; + return null; + } + + /// <summary> + /// Gets the URIs authorized to host host-meta documents on behalf of a given domain. + /// </summary> + /// <param name="identifier">The identifier.</param> + /// <returns>A sequence of URIs that MAY provide the host-meta for a given identifier.</returns> + private IEnumerable<HostMetaProxy> GetHostMetaLocations(UriIdentifier identifier) { + Contract.Requires<ArgumentNullException>(identifier != null); + + // First try the proxies, as they are considered more "secure" than the local + // host-meta for a domain since the domain may be defaced. + IEnumerable<HostMetaProxy> result = this.TrustedHostMetaProxies; + + // Finally, look for the local host-meta. + UriBuilder localHostMetaBuilder = new UriBuilder(); + localHostMetaBuilder.Scheme = identifier.IsDiscoverySecureEndToEnd || identifier.Uri.IsTransportSecure() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + localHostMetaBuilder.Host = identifier.Uri.Host; + localHostMetaBuilder.Path = LocalHostMetaPath; + result = result.Concat(new[] { new HostMetaProxy(localHostMetaBuilder.Uri.AbsoluteUri, identifier.Uri.Host) }); + + return result; + } + + /// <summary> + /// A description of a web server that hosts host-meta documents. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "By design")] + public class HostMetaProxy { + /// <summary> + /// Initializes a new instance of the <see cref="HostMetaProxy"/> class. + /// </summary> + /// <param name="proxyFormat">The proxy formatting string.</param> + /// <param name="signingHostFormat">The signing host formatting string.</param> + public HostMetaProxy(string proxyFormat, string signingHostFormat) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(proxyFormat)); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(signingHostFormat)); + this.ProxyFormat = proxyFormat; + this.SigningHostFormat = signingHostFormat; + } + + /// <summary> + /// Gets the URL of the host-meta proxy. + /// </summary> + /// <value>The absolute proxy URL, which may include {0} to be replaced with the host of the identifier to be discovered.</value> + public string ProxyFormat { get; private set; } + + /// <summary> + /// Gets the formatting string to determine the expected host name on the certificate + /// that is expected to be used to sign the XRDS document. + /// </summary> + /// <value> + /// Either a string literal, or a formatting string where these placeholders may exist: + /// {0} the host on the identifier discovery was originally performed on; + /// {1} the host on this proxy. + /// </value> + public string SigningHostFormat { get; private set; } + + /// <summary> + /// Gets the absolute proxy URI. + /// </summary> + /// <param name="identifier">The identifier being discovered.</param> + /// <returns>The an absolute URI.</returns> + public virtual Uri GetProxy(UriIdentifier identifier) { + Contract.Requires<ArgumentNullException>(identifier != null); + return new Uri(string.Format(CultureInfo.InvariantCulture, this.ProxyFormat, Uri.EscapeDataString(identifier.Uri.Host))); + } + + /// <summary> + /// Gets the signing host URI. + /// </summary> + /// <param name="identifier">The identifier being discovered.</param> + /// <returns>A host name.</returns> + public virtual string GetSigningHost(UriIdentifier identifier) { + Contract.Requires<ArgumentNullException>(identifier != null); + return string.Format(CultureInfo.InvariantCulture, this.SigningHostFormat, identifier.Uri.Host, this.GetProxy(identifier).Host); + } + + /// <summary> + /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param> + /// <returns> + /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false. + /// </returns> + /// <exception cref="T:System.NullReferenceException"> + /// The <paramref name="obj"/> parameter is null. + /// </exception> + public override bool Equals(object obj) { + var other = obj as HostMetaProxy; + if (other == null) { + return false; + } + + return this.ProxyFormat == other.ProxyFormat && this.SigningHostFormat == other.SigningHostFormat; + } + + /// <summary> + /// Serves as a hash function for a particular type. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + return this.ProxyFormat.GetHashCode(); + } + } + } +} |