Update (18-Jan-2011): I uploaded the source for both PathUnwriter.cs and OutgoingStreamFilter.cs in case any one wants it.

As mentioned previously, I wrote an HttpModule to “undo” the path rewriting that GoDaddy seems to do on behalf of a hosted domain.  For reference here is the code that does it (PathUnwriter.cs):

using System;
using System.Configuration;
using System.Collections.Generic;
using System.Web;
using System.IO;
using System.Text;

namespace Spudnoggin.HostingUtils
{
    /// <summary>
    /// Clean outgoing path for ISPs that insist on clobbering the incoming path.
    /// </summary>
    /// <remarks>
    /// This module may need to be added *last* so that it's one of the outer-most
    /// filters... the filters are created inside-out...
    /// </remarks>
    public sealed class PathUnwriter : IHttpModule
    {
        void IHttpModule.Dispose()
        {
            //clean-up code here.
        }

        void IHttpModule.Init(HttpApplication context)
        {
            // Check to see if any unwriting has actually been requested...
            string sMapSpec = ConfigurationManager.AppSettings["Spudnoggin.HostingUtils.PathUnwriter"] ?? string.Empty;

            m_mapper.LoadMap(sMapSpec);

            if (m_mapper.HasMaps)
            {
                context.PreRequestHandlerExecute += new EventHandler(OnPreRequestHandlerExecute);
                context.PreSendRequestHeaders += new EventHandler(OnPreSendRequestHeaders);
            }
        }

        private Mapper m_mapper = new Mapper();

        void OnPreRequestHandlerExecute(Object sender, EventArgs e)
        {
            HttpApplication app = (HttpApplication)sender;
            HttpContext context = app.Context;

            // Only add the filter on appropriate content types...
            IHttpHandler handler = context.CurrentHandler;

            if (!(handler is System.Web.DefaultHttpHandler) &&
                !(handler is System.Web.Handlers.AssemblyResourceLoader))
            {
                HttpResponse response = app.Response;

                // Add a filter that will screen all of the output....
                response.Filter = new OutgoingStreamFilter(response.Filter, UnwritePaths);
            }
        }

        void OnPreSendRequestHeaders(Object sender, EventArgs e)
        {
            HttpApplication app = (HttpApplication)sender;
            HttpResponse response = app.Response;

            // Check for any redirects... (do we care about the handler?)
            // For some reason, response.IsRequestBeingRedirected isn't set
            // even though response.RedirectLocation has a value!
            //if (response.IsRequestBeingRedirected)
            if (!string.IsNullOrEmpty(response.RedirectLocation))
            {
                string sLocation;
                if (m_mapper.ReplaceAllMappedValues(response.RedirectLocation, out sLocation, false))
                {
                    response.RedirectLocation = sLocation;
                }
            }

            // We'd also like to catch other headers, but we don't always seem to get the chance!
        }

        private bool UnwritePaths(string sDataIn, out string sDataOut)
        {
            sDataOut = sDataIn;
            bool fUnwrite = false;
            bool fXmlContext = false;
            string sType = HttpContext.Current.Response.ContentType;

            switch (sType)
            {
                case "text":
                case "text/html":
                    fUnwrite = true;
                    break;

                case "text/xml":
                //case "application/xml":
                case "application/rss+xml":
                case "application/rdf+xml":
                case "application/atom+xml":
                case "application/apml+xml":
                case "application/opensearchdescription+xml":
                    fXmlContext = true;
                    fUnwrite = true;
                    break;
            }

            if (fUnwrite)
            {
                fUnwrite = m_mapper.ReplaceAllMappedValues(sDataIn, out sDataOut, fXmlContext);
            }

            return fUnwrite;
        }
    }

    public class Mapper
    {
        /// <summary>
        /// Loads the map from the specification string.
        /// </summary>
        /// <param name="sMapSpec"></param>
        /// <remarks>
        /// <para>
        /// The mapping specification is a comma-separated list of pairs of from/to strings.
        /// By default, the 'from' string is looked for both as-is, and as a URL-encoded
        /// string.
        /// </para>
        /// <para>
        /// The first characters of the 'from' string can indicate some additional special
        /// handling:
        ///    ^   - the string is expected at the "beginning" of a path, so we look for
        ///          it to be immediately following a " or ' character (in an attribute
        ///          value), or at the beginning of the string (for redirect re-writes).
        ///    ~   - do not perform any additional URL-encoding
        ///    \   - the following character is meant literally, not as a 'special handling'
        ///          flag ("\^..." would look for a literal '^').  Use "\\..." to indicate
        ///          a literal '\'.
        /// When the first '\' or non-special character is found, no more special characters
        /// are interpreted.  You can indicate "no encoding, at beginning of path" with
        /// "^~...", "~^...", or even "^^~^~^^^~~...".
        /// </para>
        /// </remarks>
        public void LoadMap(string sMapSpec)
        {
            m_listMaps.Clear();

            if (string.IsNullOrEmpty(sMapSpec))
            {
                return;
            }

            string[] rgsMapSpec = sMapSpec.Split(',');

            if (rgsMapSpec.Length % 2 != 0)
            {
                // TODO: flag error?
                return;
            }

            for (int iMap = 0; iMap < rgsMapSpec.Length; iMap += 2)
            {
                string sFrom = rgsMapSpec[iMap];
                string sTo = rgsMapSpec[iMap + 1];

                // parse out any special characters
                Map.MapFlags flags = Map.MapFlags.Normal;
                int ichStart = 0;
                bool fPeek = true;
                for (int ich = 0; fPeek && ich < sFrom.Length; ++ich)
                {
                    switch (sFrom[ich])
                    {
                        case '^':
                            flags |= Map.MapFlags.BeginningOfPath;
                            break;
                        case '~':
                            flags |= Map.MapFlags.NoEncoding;
                            break;
                        case '\\':
                            ichStart = ich + 1;
                            fPeek = false;
                            break;
                        default:
                            ichStart = ich;
                            fPeek = false;
                            break;
                    }
                }

                if (ichStart > 0)
                {
                    sFrom = sFrom.Substring(ichStart);
                }

                // Can't map an empty 'from' string!
                if (string.IsNullOrEmpty(sFrom))
                {
                    continue;
                }

                string sFromEnc = HttpUtility.UrlEncode(sFrom);
                string sToEnc = HttpUtility.UrlEncode(sTo);

                bool fDoEncoding = (((flags & Map.MapFlags.NoEncoding) != Map.MapFlags.NoEncoding) &&
                                    !string.Equals(sFromEnc, sFrom));
                bool fBeginningOfPath = ((flags & Map.MapFlags.BeginningOfPath) == Map.MapFlags.BeginningOfPath);

                m_listMaps.Add(new Map(sFrom, sTo, flags));

                if (fDoEncoding)
                {
                    m_listMaps.Add(new Map(sFromEnc, sToEnc, flags));
                }

                if (fBeginningOfPath)
                {
                    Map.MapFlags flagsNew = flags & ~Map.MapFlags.BeginningOfPath;
                    // Create "- and '-prefixed strings for the search and *remove* the "beginning of path"
                    // flag, because the " and ' provide that for us.
                    m_listMaps.Add(new Map(string.Format("\"{0}", sFrom), string.Format("\"{0}", sTo), flagsNew));
                    m_listMaps.Add(new Map(string.Format("\'{0}", sFrom), string.Format("\'{0}", sTo), flagsNew));

                    // Add some XML-only checks...
                    m_listMaps.Add(new Map(string.Format(">{0}", sFrom), string.Format(">{0}", sTo), flagsNew | Map.MapFlags.ForXmlOnly));

                    if (fDoEncoding)
                    {
                        m_listMaps.Add(new Map(string.Format("\"{0}", sFromEnc), string.Format("\"{0}", sToEnc), flagsNew));
                        m_listMaps.Add(new Map(string.Format("\'{0}", sFromEnc), string.Format("\'{0}", sToEnc), flagsNew));
                        // also add encoded =-prefixed, because I've seen that some, but
                        // only for encoded strings...
                        m_listMaps.Add(new Map(string.Format("={0}", sFromEnc), string.Format("={0}", sToEnc), flagsNew));

                        // but... we have to undo the BlogEngine.NET-sepcific path to js.axd, because the
                        // remapping can break things... Note that we're mapping from TO to FROM!
                        m_listMaps.Add(new Map(string.Format("js.axd?path={0}", sToEnc), string.Format("js.axd?path={0}", sFromEnc), flagsNew));

                        // Add some XML-only checks...
                        m_listMaps.Add(new Map(string.Format(">{0}", sFromEnc), string.Format(">{0}", sToEnc), flagsNew | Map.MapFlags.ForXmlOnly));

                    }
                }
            }
        }

        private List<Map> m_listMaps = new List<Map>();

        public bool HasMaps { get { return (m_listMaps.Count > 0); } }

        public bool ReplaceAllMappedValues(string sOriginal, out string sOut, bool fIsXmlContext)
        {
            sOut = sOriginal;
            if (!HasMaps)
            {
                return false;
            }

            StringBuilder sb = new StringBuilder(sOriginal);
            bool fReplaced = false;

            foreach (Map map in m_listMaps)
            {
                if (!fIsXmlContext && ((map.Flags & Map.MapFlags.ForXmlOnly) == Map.MapFlags.ForXmlOnly))
                {
                    continue;
                }

                int ichExists = sOriginal.IndexOf(map.From);

                if (ichExists < 0)
                {
                    continue;
                }

                if ((map.Flags & Map.MapFlags.BeginningOfPath) == Map.MapFlags.BeginningOfPath)
                {
                    if (sb.Length >= map.From.Length)
                    {
                        sb.Replace(map.From, map.To, 0, map.From.Length);
                        fReplaced = true;
                    }
                }
                else
                {
                    // not restricted to beginning of path!
                    sb.Replace(map.From, map.To);
                    fReplaced = true;
                }
            }

            if (fReplaced)
            {
                sOut = sb.ToString();
            }
            // Only return the altered string if we have reason to believe we made a change!
            return fReplaced;
        }

        private class Map
        {
            public Map(string sFrom, string sTo, MapFlags flags)
            {
                From = sFrom;
                To = sTo;
                Flags = flags;
            }

            public string From { get; private set; }
            public string To { get; private set; }

            public MapFlags Flags { get; private set; }

            [Flags]
            public enum MapFlags
            {
                Normal = 0,
                BeginningOfPath = 0x01,
                NoEncoding = 0x02,
                ForXmlOnly = 0x04,
            }
        }    
    }

}

With the PathUnwriter in place (and the module loaded in web.config), turning on the functionality is as easy as:

<add key="Spudnoggin.HostingUtils.PathUnwriter" value="^/jaredreisinger-blogengine.web/,/"/>

Not too bad!

You may note that I opted not to use regular expressions.  Call me old-school, but it felt like they would be overkill for something as simple as this.  Also, I wanted it to be absolutely clear in what situations a match would occur. 

There’s an OutgoingStreamFilter referred to on line 54 — this is a pass-through write-only stream implementation that uses the callback delegate to perform any filtering of the stream as it’s written.  I’m also using this pass-through to implement my SyntaxHighlighter injector (which is helping to syntax color the code above).

PathUnwriter.cs (13.57 kb)

OutgoingStreamFilter.cs (5.00 kb)

Comments are closed