Thursday, 13 May 2010

Basic Example of On-The-Fly Config Section Encryption

I recently answered a question about how you could securely modify a value within an encrypted configuration website without having to create a duplicate config file.  Well, the answer’s pretty simple and straightforward – just use the API!

A quick example page could be:

   1: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
   2:  
   3: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   4: <html xmlns="http://www.w3.org/1999/xhtml">
   5: <head runat="server">
   6:     <title></title>
   7: </head>
   8: <body>
   9:     <form id="form1" runat="server">
  10:     <div>
  11:         <asp:Button ID="EncryptButton" runat="server" Text="Encrypt" OnClick="EncryptButton_Click" />
  12:         <asp:Button ID="DecryptButton" runat="server" Text="Decrypt" OnClick="DecryptButton_Click" />
  13:         <asp:Button ID="IncrementButton" runat="server" Text="Increment" OnClick="IncrementButton_Click" />
  14:         <asp:Label ID="CountLabel" runat="server" Text="0"></asp:Label>
  15:         <br />
  16:         <asp:Label ID="StatusLabel" runat="server" Text="" EnableViewState="false"></asp:Label>
  17:     </div>
  18:     </form>
  19: </body>
  20: </html>
   1: using System;
   2: using System.Configuration;
   3: using System.Linq;
   4: using System.Web.Configuration;
   5:  
   6: public partial class _Default : System.Web.UI.Page
   7: {
   8:  
   9:     /// <summary>
  10:     /// Handles the PreRender event of the Page control.
  11:     /// </summary>
  12:     /// <param name="sender">The source of the event.</param>
  13:     /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
  14:     protected void Page_PreRender(object sender, EventArgs e)
  15:     {
  16:         
  17:         
  18:         Configuration confg = WebConfigurationManager.OpenWebConfiguration(Request.ApplicationPath);
  19:         ConfigurationSection confStrSect = confg.GetSection(sectionKey);
  20:         StatusLabel.Text = "No Config Section";
  21:         if (confStrSect != null)
  22:         {
  23:             StatusLabel.Text = "Decrypted";
  24:             if (confStrSect.SectionInformation.IsProtected)
  25:             {
  26:                 StatusLabel.Text = "Encrypted";
  27:             }
  28:             int count = 0;
  29:             int.TryParse(confg.AppSettings.Settings[countKey].Value, out count);
  30:             CountLabel.Text = count.ToString();
  31:         }
  32:         
  33:  
  34:     }
  35:     
  36:     string providerKey = "RSAProtectedConfigurationProvider";
  37:     string sectionKey = "appSettings";
  38:     string countKey = "Count";
  39:  
  40:     /// <summary>
  41:     /// Handles the Click event of the EncryptButton control.
  42:     /// </summary>
  43:     /// <param name="sender">The source of the event.</param>
  44:     /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
  45:     protected void EncryptButton_Click(object sender, EventArgs e)
  46:     {
  47:         Configuration confg = WebConfigurationManager.OpenWebConfiguration(Request.ApplicationPath);
  48:         ConfigurationSection confStrSect = confg.GetSection(sectionKey);
  49:         if (confStrSect != null)
  50:         {
  51:             confStrSect.SectionInformation.ProtectSection(providerKey);
  52:             confg.Save();
  53:         }
  54:     }
  55:     /// <summary>
  56:     /// Handles the Click event of the DecryptButton control.
  57:     /// </summary>
  58:     /// <param name="sender">The source of the event.</param>
  59:     /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
  60:     protected void DecryptButton_Click(object sender, EventArgs e)
  61:     {
  62:         Configuration confg = WebConfigurationManager.OpenWebConfiguration(Request.ApplicationPath);
  63:         ConfigurationSection confStrSect = confg.GetSection(sectionKey);
  64:         if (confStrSect != null && confStrSect.SectionInformation.IsProtected)
  65:         {
  66:             confStrSect.SectionInformation.UnprotectSection();
  67:             confg.Save();
  68:         }
  69:     }
  70:     /// <summary>
  71:     /// Handles the Click event of the IncrementButton control.
  72:     /// </summary>
  73:     /// <param name="sender">The source of the event.</param>
  74:     /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
  75:     protected void IncrementButton_Click(object sender, EventArgs e)
  76:     {
  77:         int count = 0;
  78:         int.TryParse(WebConfigurationManager.AppSettings[countKey], out count);
  79:         count++;
  80:         
  81:         Configuration confg = WebConfigurationManager.OpenWebConfiguration(Request.ApplicationPath);
  82:         ConfigurationSection confStrSect = confg.GetSection(sectionKey);
  83:         {
  84:             if (confg.AppSettings.Settings.AllKeys.Contains(countKey))
  85:             {
  86:                 confg.AppSettings.Settings[countKey].Value = count.ToString();
  87:             }
  88:             else
  89:             {
  90:                 confg.AppSettings.Settings.Add(countKey, count.ToString());
  91:             }
  92:             confg.Save(ConfigurationSaveMode.Modified);
  93:         }
  94:     }
  95: }

Simples.

Monday, 10 May 2010

N2CMS Meet VWT2OC: Project History

Many moons ago (~3 years) I did a bit of family rate development to create a simple website for the Volkswagen Type 2 Owners Club – www.vwt2oc.com.  The brief was fairly sparse on detail, they needed a website to manage their memberships and take orders online, a few content manages pages and a basic forum to take over from their previous mainly static site that was looking a little long in the tooth. 

The key point at the time was that the system had to be fairly basic simple to use as the club was run by volunteers who wouldn’t have time for learning how more developed solutions worked.  Which suited me as, to be blunt, I wanted to get my teeth into a nice green field project where I didn’t have to work with someone else’s poorly provisioned API (at the time I worked for a very small time agency with a very buggy platform).

So a few busy weekends, I released my first line-in-the-sand website that members could use to renew, had some basic pages, a basic design and a very simple forum.  This was a version of the site with maybe a three month lifespan which could be used as a discussion point to drive more specific requirements.

Tragically (for my wallet) the site has worked well enough for them so no further changes have been needed…….until now!

A Change of Focus

The next version VWT2OC is going to improve the website and community driven features of the site, there needs to be greater control of the layout of the site, member profiles, image galleries and the forum needs to be more comprehensive and, most importantly, the club has developed the skills to support the extra features.  In short, the mainly administration focused website needs to become the hub for the clubs online activity.

So now the challenge is to migrate my previous three year old codebase into a new application and keep the membership administration features and functionality that the club still wants.  It’s my second favourite type of project – brown field…. building something new whilst keeping the best of the old.

The Winner of the One Horse Race – N2CMS

Essentially, to save myself  the extra work of adapting/extending the basic CMS functionality the site already has I needed a (free) cms system that would do the widget/drag-drop/web-part heavy lifting for me so I could focus on integrating the existing code base in around it. 

Unfortunately, whilst there are lots of CMS products out there only one didn’t seem to want to inflict a particular way of working on to me – n2cms.  Or more to the point, I already work (for preference) in a very similar manner as to the guys doing this so adapting to their code shouldn’t be a problem.

As an added bonus is that there is an established Forum Add in available (based on Yet Another Forum) which should take care of the new forum requirements.

Wednesday, 5 May 2010

Using MbUnit StaticTestFactory to Validate Sitemap.xml Links

I’ve been investigating a replacement for our current link checker (SEO Optimization Toolkit) to be run automatically as part of our build so that we can get a quick heads up if one of our dynamic pages breaks.  The problem is that as most of our sites are built with Ektron there’s a lot of potential for a combination of content and code to break individual pages that share a template with working pages. 

As these pages are data driven hard coding tests is very timely and very fragile.  Fortunately, most of our Ektron sites have automatically generated sitemap.xml which gives us a neat list of urls to test which is where MbUnit’s StaticTestFactory comes in useful as it allows us to dynamically create distinct tests for each url.

Enjoy.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Globalization;
   4: using System.Linq;
   5: using System.Net;
   6: using System.Xml.Linq;
   7: using MbUnit.Framework;
   8:  
   9: namespace MartinOnDotNet.VerificationTests
  10: {
  11:     /// <summary>
  12:     /// Includes methods to verify the validity of a sitemap.xml
  13:     /// </summary>
  14:     public sealed class ValidateSiteMap
  15:     {
  16:  
  17:         /// <summary>
  18:         /// Generates a static test for each url referenced within the sitemap
  19:         /// </summary>
  20:         [StaticTestFactory, Parallelizable(TestScope.Descendants)]
  21:         public static IEnumerable<Test> GenerateSiteMapLinkTests()
  22:         {
  23:             Uri sitemapUri = new Uri(Properties.Settings.Default.SiteMapXmlUri); // Uri for Xml Sitemap to test : http://localhost/sitemap.xml
  24:             int requestTimeout = Properties.Settings.Default.SiteMapRequestTimeout; //timeout for each request in ms : 300ms
  25:  
  26:             IEnumerable<string> locations = GetSitemapLocations(sitemapUri);
  27:             //is sitemap populated
  28:             yield return CreateSitemapHasNodesTest(sitemapUri, locations);
  29:             
  30:             //are all reference urls valid
  31:             foreach (string location in locations)
  32:             {
  33:                 yield return CreateLocationTest(requestTimeout, location, HttpStatusCode.OK);
  34:             }
  35:             
  36:             // check that robots.txt is present
  37:             Uri robotstxtUri = new Uri(sitemapUri, "/robots.txt");
  38:             yield return CreateLocationTest(requestTimeout, robotstxtUri.ToString(), HttpStatusCode.OK);
  39:             //finally, let's check that a deliberately incorrect url
  40:             Uri nonExistantUri = new Uri(sitemapUri, "/nonexistantfileonserver/");
  41:             yield return CreateLocationTest(requestTimeout, nonExistantUri.ToString(), HttpStatusCode.NotFound);
  42:             
  43:         }
  44:  
  45:         /// <summary>
  47:         /// </summary>
  48:         /// <param name="sitemapUri">The sitemap URI.</param>
  49:         /// <param name="locations">The locations.</param>
  50:         /// <returns>A test that checks the sitemap has nodes</returns>
  51:         private static TestCase CreateSitemapHasNodesTest(Uri sitemapUri, IEnumerable<string> locations)
  52:         {
  53:             return new TestCase(string.Format(CultureInfo.InvariantCulture, "{0} - Sitemap Has Entries", sitemapUri), () =>
  54:             {
  55:                 Assert.IsTrue(locations.Any());
  56:             });
  57:         }
  58:  
  59:         /// <summary>
  60:         /// Creates the location test.
  61:         /// </summary>
  62:         /// <param name="requestTimeout">The request timeout.</param>
  63:         /// <param name="location">The location.</param>
  64:         /// <returns>A unique test for a sitemap location</returns>
  65:         private static TestCase CreateLocationTest(int requestTimeout, string location, HttpStatusCode expectedResult)
  66:         {
  67:             return new TestCase(location, () =>
  68:             {
  69:                 HttpWebRequest wrq = HttpWebRequest.Create(location) as HttpWebRequest;
  70:                 wrq.UserAgent = "Googlebot/2.1 (+http://www.google.com/bot.html)"; // appear to be google to escape any custom error handling
  71:                 wrq.Timeout = requestTimeout;
  72:                 HttpWebResponse wrp = null;
  73:                 try
  74:                 {
  75:                     wrp = GetResponse(wrq);
  76:                     Assert.AreEqual<System.Net.HttpStatusCode>(expectedResult, wrp.StatusCode);
  77:                 }
  78:                 finally
  79:                 {
  80:                     if (wrp != null) wrp.Close();
  81:                 }
  82:             });
  83:         }
  84:  
  85:         #region Helper Methods
  86:  
  87:         /// <summary>
  88:         /// Gets the sitemap locations.
  89:         /// </summary>
  90:         /// <param name="sitemapUri">The sitemap URI.</param>
  91:         /// <returns>A list of locations referenced within the sitemap</returns>
  92:         private static IEnumerable<string> GetSitemapLocations(Uri sitemapUri)
  93:         {
  94:             XNamespace xn = XNamespace.Get(@"http://www.sitemaps.org/schemas/sitemap/0.9");
  95:             XDocument xdoc = XDocument.Load(sitemapUri.ToString(), LoadOptions.PreserveWhitespace);
  96:             return from loc in xdoc.Descendants(xn + "loc")
  97:                             select loc.Value;
  98:         }
  99:    
 100:         /// <summary>
 101:         /// Gets the response object and handles any protocol exceptions
 102:         /// </summary>
 103:         /// <param name="request">The request.</param>
 104:         /// <returns>The response object if available</returns>
 105:         private static HttpWebResponse GetResponse(HttpWebRequest request)
 106:         {
 107:             try
 108:             {
 109:                 return request.GetResponse() as HttpWebResponse;
 110:             }
 111:             catch (WebException wex)
 112:             {
 113:                 if (wex.Status == WebExceptionStatus.ProtocolError)
 114:                 {
 115:                     return wex.Response as HttpWebResponse;
 116:                 }
 117:                 else
 118:                 {
 119:                     throw;
 120:                 }
 121:             }
 122:         }
 123:  
 124:         #endregion
 125:  
 126:     }
 127: }

Attachment: Visual Studio Project

Thursday, 22 April 2010

Fixing the Precompilation Marker File Ektron Issue on IIS 7

After recently updating a 7.65 based website to 8.0 we recently found that parts of the workarea were unusable.  The upgrade went well and all the pre-deployment checks went well.  However, when we performed the UAT deployment the workarea was broken.  Where the treeview menu on the left should be, there was a helpful message:

This is a marker file generated by the precompilation tool, and should not be deleted!

Well, yes the UAT deployment is the first production quality deployment so precompilation is part of the build and this is part of the message that’s within all of the precompiled UI files (aspx, ascx, ashx, etc).  But on a precompiled site, the pages are actually stored in dll’s in the site’s bin folder – the actual files are a nicety to ensure IIS routes the request the ASP.Net.

Clearly this mechanism was working for aspx files as the frontend website worked as well as bits of the workearea.  After a bit of investigation, it appears that html and htm files are also being precompiled using the standard PageBuildProvider:

   1: <compilation debug="false">
   2:     <buildProviders>
   3:         <add extension=".htm"
   4:                  type="System.Web.Compilation.PageBuildProvider" />
   5:         <add extension=".html"
   6:                  type="System.Web.Compilation.PageBuildProvider" />
   7:     </buildProviders>
   8: </compilation>

The Fix

In the web.config ensure the following handler mappings  are added to configuration/system.webServer/handlers:

   1: <!-- 
   2: Add Under the aspx mapping for Integrated Mode 
   3: <add name="PageHandlerFactory-Integrated" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode"/>
   4:  -->
   5: <add name="Html-Integrated" path="*.html" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode"/>
   6: <add name="Htm-Integrated" path="*.htm" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode"/>
   7: <!-- Add under the aspx mapping for Classic Mode
   8: <add name="PageHandlerFactory-ISAPI-2.0" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0"/>
   9: -->
  10: <add name="Html-ISAPI-2.0" path="*.html" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0"/>
  11: <add name="Htm-ISAPI-2.0" path="*.htm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0"/>
  12:  

The lines above are for IIS 7, if the same issue occurs on an earlier version then it should be trivial to add appropriate entries to configuration/system.web/httpHandlers.

It’s important that these handlers are registered prior to any wildcard handlers (path=”*.*”) and is mapped to the standard ASP.Net handler.  If it’s mapped to the Ektron EkDavHttpHandlerFactory then the html and htm files will be served as text and not processed as a serverside page.