From c0ac3af2e62b056442f6f3f5fff9b81ec69e4c1e Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Fri, 23 Jan 2009 20:32:50 -0800 Subject: [PATCH] Added the XrdsPublisher ASP.NET control. --- src/DotNetOpenAuth/DotNetOpenAuth.csproj | 1 + src/DotNetOpenAuth/UriUtil.cs | 30 +++++ src/DotNetOpenAuth/XrdsPublisher.cs | 204 +++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 src/DotNetOpenAuth/XrdsPublisher.cs diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 23066c4..4e1b5c9 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -303,6 +303,7 @@ Strings.resx + True True diff --git a/src/DotNetOpenAuth/UriUtil.cs b/src/DotNetOpenAuth/UriUtil.cs index a5d0df9..3ef8d50 100644 --- a/src/DotNetOpenAuth/UriUtil.cs +++ b/src/DotNetOpenAuth/UriUtil.cs @@ -7,9 +7,11 @@ namespace DotNetOpenAuth { using System; using System.Collections.Specialized; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; using System.Web; + using System.Web.UI; using DotNetOpenAuth.Messaging; /// @@ -70,5 +72,33 @@ namespace DotNetOpenAuth { return builder.ToString(); } } + + /// + /// Validates that a URL will be resolvable at runtime. + /// + /// The page hosting the control that receives this URL as a property. + /// If set to true the page is in design-time mode rather than runtime mode. + /// The URI to check. + /// Thrown if the given URL is not a valid, resolvable URI. + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Just to throw an exception on invalid input.")] + internal static void ValidateResolvableUrl(Page page, bool designMode, string value) { + if (string.IsNullOrEmpty(value)) { + return; + } + + if (page != null && !designMode) { + // Validate new value by trying to construct a Realm object based on it. + new Uri(page.Request.Url, page.ResolveUrl(value)); // throws an exception on failure. + } else { + // We can't fully test it, but it should start with either ~/ or a protocol. + if (Regex.IsMatch(value, @"^https?://")) { + new Uri(value); // make sure it's fully-qualified, but ignore wildcards + } else if (value.StartsWith("~/", StringComparison.Ordinal)) { + // this is valid too + } else { + throw new UriFormatException(); + } + } + } } } diff --git a/src/DotNetOpenAuth/XrdsPublisher.cs b/src/DotNetOpenAuth/XrdsPublisher.cs new file mode 100644 index 0000000..142e5b4 --- /dev/null +++ b/src/DotNetOpenAuth/XrdsPublisher.cs @@ -0,0 +1,204 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Andrew Arnott. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Text; + using System.Web; + using System.Web.UI; + using System.Web.UI.WebControls; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Yadis; + + /// + /// The locations the YADIS protocol describes can contain a reference + /// to an XRDS document. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Correct spelling")] + [Flags] + public enum XrdsUrlLocations { + /// + /// Indicates XRDS document referencing from an HTTP protocol header (outside the HTML). + /// + HttpHeader = 0x1, + + /// + /// Indicates XRDS document referencing from within an HTML page's <HEAD> tag. + /// + HtmlMeta = 0x2, + + /// + /// Indicates XRDS document referencing in both HTTP headers and HTML HEAD tags. + /// + Both = 0x3, + } + + /// + /// An ASP.NET control that advertises an XRDS document and even responds to specially + /// crafted requests to retrieve it. + /// + [DefaultProperty("XrdsLocation")] + [ToolboxData("<{0}:XrdsPublisher runat=server>")] + public class XrdsPublisher : Control { + /// + /// The view state key to ues for storing the value of the property. + /// + private const string XrdsUrlViewStateKey = "XrdsUrl"; + + /// + /// The default value for the property. + /// + private const XrdsUrlLocations XrdsAdvertisementDefault = XrdsUrlLocations.HttpHeader; + + /// + /// The view state key to ues for storing the value of the property. + /// + private const string XrdsAdvertisementViewStateKey = "XrdsAdvertisement"; + + /// + /// The default value for the property. + /// + private const bool XrdsAutoAnswerDefault = true; + + /// + /// The view state key to ues for storing the value of the property. + /// + private const string XrdsAutoAnswerViewStateKey = "XrdsAutoAnswer"; + + /// + /// The default value for the property. + /// + private const bool EnabledDefault = true; + + /// + /// The view state key to ues for storing the value of the property. + /// + private const string EnabledViewStateKey = "Enabled"; + + #region Properties + + /// + /// Gets or sets the location of the XRDS document. + /// + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property grid")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Correct spelling")] + [Category("Behavior"), Bindable(true)] + public string XrdsUrl { + get { + return (string)ViewState[XrdsUrlViewStateKey]; + } + + set { + UriUtil.ValidateResolvableUrl(Page, DesignMode, value); + ViewState[XrdsUrlViewStateKey] = value; + } + } + + /// + /// Gets or sets where the XRDS document URL is advertised in the web response. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Correct spelling")] + [Category("Behavior"), DefaultValue(XrdsAdvertisementDefault), Bindable(true)] + [Description("Where the XRDS document URL is advertised in the web response.")] + public XrdsUrlLocations XrdsAdvertisement { + get { + return ViewState[XrdsAdvertisementViewStateKey] == null ? + XrdsAdvertisementDefault : (XrdsUrlLocations)ViewState[XrdsAdvertisementViewStateKey]; + } + + set { + ViewState[XrdsAdvertisementViewStateKey] = value; + } + } + + /// + /// Gets or sets a value indicating whether a specially crafted YADIS + /// search for an XRDS document is immediately answered by this control. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Correct spelling")] + [Category("Behavior"), DefaultValue(XrdsAutoAnswerDefault), Bindable(true)] + [Description("Whether XRDS requests should be immediately answered with the XRDS document if it is served by this web application.")] + public bool XrdsAutoAnswer { + get { + return ViewState[XrdsAutoAnswerViewStateKey] == null ? + XrdsAutoAnswerDefault : (bool)ViewState[XrdsAutoAnswerViewStateKey]; + } + + set { + ViewState[XrdsAutoAnswerViewStateKey] = value; + } + } + + /// + /// Gets or sets a value indicating whether the XRDS document is advertised. + /// + [Category("Behavior"), DefaultValue(EnabledDefault)] + public bool Enabled { + get { + return ViewState[EnabledViewStateKey] == null ? + EnabledDefault : (bool)ViewState[EnabledViewStateKey]; + } + + set { + ViewState[EnabledViewStateKey] = value; + } + } + + #endregion + + /// + /// Detects YADIS requests for the XRDS document and responds immediately + /// if is true. + /// + /// The object that contains the event data. + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + + if (!this.Enabled) { + return; + } + + if (!this.Page.IsPostBack) { + if (this.XrdsAutoAnswer && !string.IsNullOrEmpty(this.XrdsUrl) && + this.XrdsUrl.StartsWith("~/", StringComparison.Ordinal)) { + // Check for the presence of an accept types header that is looking + // for the XRDS document specifically. + if (this.Page.Request.AcceptTypes != null && Array.IndexOf(this.Page.Request.AcceptTypes, ContentTypes.Xrds) >= 0) { + // Respond to the caller immediately with an XRDS document + // and avoid sending the whole web page's contents to the + // client since it isn't interested anyway. + // We do NOT simply send a 301 redirect here because that would + // alter the Claimed Identifier. + this.Page.Server.Transfer(this.XrdsUrl); + } + } + } + } + + /// + /// Renders the HTTP Header and/or HTML HEAD tags. + /// + /// The object that receives the server control content. + protected override void Render(HtmlTextWriter writer) { + if (this.Enabled && this.Visible && !string.IsNullOrEmpty(this.XrdsUrl)) { + Uri xrdsAddress = new Uri(MessagingUtilities.GetRequestUrlFromContext(), Page.Response.ApplyAppPathModifier(this.XrdsUrl)); + if ((this.XrdsAdvertisement & XrdsUrlLocations.HttpHeader) != 0) { + Page.Response.AddHeader(Yadis.Yadis.HeaderName, xrdsAddress.AbsoluteUri); + } + if ((this.XrdsAdvertisement & XrdsUrlLocations.HtmlMeta) != 0) { + writer.WriteBeginTag("meta"); + writer.WriteAttribute("http-equiv", Yadis.Yadis.HeaderName); + writer.WriteAttribute("content", xrdsAddress.AbsoluteUri); + writer.Write("/>"); + writer.WriteLine(); + } + } + } + } +} -- 2.11.4.GIT