using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Web; using System.Xml; using System.Xml.Linq; using Composite.Core.Logging; using Composite.C1Console.Security; namespace Composite.Demo { /// /// Uses file '/App_Data/RequestUrlRemappings.xml' to re-map incoming requests. /// public class RequestUrlRemapperHttpModule : IHttpModule { private const string _remappingsRelativePath = "/App_Data/RequestUrlRemappings.xml"; private string _remappingsFilePath; private List _pathRemappings = new List(); private List _pathStartsWithRemappings = new List(); private readonly List _validRemappingAttributeNames = new List { "requestPathStartsWith", "requestPath", "requestHostEndsWith", "requestHost", "rewritePath", "rewriteHost" }; private FileSystemWatcher _remappingsFileWatcher; public void Init(HttpApplication context) { _remappingsFilePath = context.Context.Server.MapPath(_remappingsRelativePath); LoadRemappings(); SetRemappingsFileWatcher(); context.BeginRequest += new EventHandler(ExecuteRemappingsOnRequest); } void ExecuteRemappingsOnRequest(object sender, EventArgs e) { HttpApplication application = (HttpApplication)sender; HttpContext context = application.Context; string host = context.Request.Url.Host.ToLower(); string path = context.Request.Url.AbsolutePath.ToLower(); Remapping remapping = _pathRemappings.Where( f => f.RequestPath == path && (f.RequestHost == null || f.RequestHost == host || f.RequestHostEndsWith == true && host.EndsWith(f.RequestHost))).FirstOrDefault(); if (remapping == null) { remapping = _pathStartsWithRemappings.Where( f => path.StartsWith(f.RequestPath) && (f.RequestHost == null || f.RequestHost == host || f.RequestHostEndsWith == true && host.EndsWith(f.RequestHost))).FirstOrDefault(); } if (remapping != null) { bool isLoggedIn = false; try { isLoggedIn = UserValidationFacade.IsLoggedIn(); } catch (Exception ex) { isLoggedIn = false; } if (isLoggedIn || context.Request.Url.PathAndQuery.Contains("dataScope=administrated")) return; string newHost = remapping.RewriteHost ?? context.Request.Url.Host; string newPathAndQuery = remapping.RewritePath ?? context.Request.Url.PathAndQuery; if (newHost.ToLower() != context.Request.Url.Host.ToLower() || newPathAndQuery.ToLower() != context.Request.Url.PathAndQuery.ToLower()) { string newUrl = string.Format("http://{0}{1}", newHost, newPathAndQuery); context.Response.RedirectLocation = newUrl; context.Response.StatusCode = 301; context.ApplicationInstance.CompleteRequest(); } } } public void Dispose() { _remappingsFileWatcher.Dispose(); } private void LoadRemappings() { XDocument remappingsDocument = LoadRemappingsDocument(); List pathRemappings = new List(); List pathStartsWithRemappings = new List(); foreach (XElement remappingElement in remappingsDocument.Descendants("Remapping")) { ValidateAttributes(remappingElement); Remapping remapping = new Remapping { RequestPath = LowerValue(remappingElement, "requestPath"), RequestHost = LowerValueOrNull(remappingElement.Attribute("requestHost")), RequestHostEndsWith = AsBool(remappingElement, "requestHostEndsWith", false), RewritePath = ValueOrNull(remappingElement.Attribute("rewritePath")), RewriteHost = LowerValueOrNull(remappingElement.Attribute("rewriteHost")) }; bool requestPathStartsWith = AsBool(remappingElement, "requestPathStartsWith", false); if (requestPathStartsWith == true) { pathStartsWithRemappings.Add(remapping); } else { pathRemappings.Add(remapping); } } // order so "most matching paths", "most matching hosts" are first in lists. Func orderByFunc = f => f.RequestPath.Length * 1000 + (f.RequestHost == null ? 0 : f.RequestHost.Length); _pathRemappings = new List(pathRemappings.OrderByDescending(orderByFunc)); _pathStartsWithRemappings = new List(pathStartsWithRemappings.OrderByDescending(orderByFunc)); } private void ValidateAttributes(XElement remappingElement) { IEnumerable unexpectedAttributes = remappingElement.Attributes().Where(f => _validRemappingAttributeNames.Contains(f.Name) == false); if (unexpectedAttributes.Any()) { string unexpectedAttributeList = ""; foreach (XAttribute unexpectedAttribute in unexpectedAttributes) { if (string.IsNullOrEmpty(unexpectedAttributeList) == false) { unexpectedAttributeList += ", "; } unexpectedAttributeList += unexpectedAttribute.Name.LocalName; } string expectedAttributeList = ""; foreach (XName expectedAttributeName in _validRemappingAttributeNames) { if (string.IsNullOrEmpty(expectedAttributeList) == false) { expectedAttributeList += ", "; } expectedAttributeList += expectedAttributeName.LocalName; } throw new InvalidOperationException(string.Format("Found unexpected attributes '{0}' on element '{1}'. Expected attributes are '{2}'.", unexpectedAttributeList, remappingElement.Name, expectedAttributeList)); } } private XDocument LoadRemappingsDocument() { if (File.Exists(_remappingsFilePath) == false) throw new InvalidOperationException(string.Format("The remappings XML File required by this module could not be found at '{0}'. Either create this file or remove this HTTP Module from web.config.", _remappingsRelativePath)); try { return XDocument.Load(_remappingsFilePath); } catch (XmlException ex) { throw new InvalidOperationException(string.Format("{0} Either remove this HTTP Handler from web.config or correct this error in file '{1}'.", ex.Message, _remappingsRelativePath)); } } private string LowerValue(XElement remappingElement, string attributeName) { XAttribute attribute = remappingElement.Attribute(attributeName); if (attribute == null) { throw new InvalidOperationException(string.Format("Required attribute '{0}' not found on element '{1}'. Either remove this HTTP Handler from web.config or add this attribute to the element in file '{2}'.", attributeName, remappingElement.Name.LocalName, _remappingsRelativePath)); } return attribute.Value.ToLower(); } private string LowerValueOrNull(XAttribute attribute) { if (attribute == null) return null; if (string.IsNullOrEmpty(attribute.Value)) return null; return attribute.Value.ToLower(); } private string ValueOrNull(XAttribute attribute) { if (attribute == null) return null; if (string.IsNullOrEmpty(attribute.Value)) return null; return attribute.Value; } private bool AsBool(XElement remappingElement, string attributeName, bool fallbackValue) { XAttribute attribute = remappingElement.Attribute(attributeName); if (attribute == null) { return fallbackValue; } try { return bool.Parse(attribute.Value); } catch { throw new InvalidOperationException(string.Format("Attribute '{0}' on element '{1}' could not be parsed as a boolean. Either remove this HTTP Handler from web.config or specify 'true' or 'false' for this attribute in file '{2}'.", attributeName, remappingElement.Name.LocalName, _remappingsRelativePath)); } } private void SetRemappingsFileWatcher() { _remappingsFileWatcher = new FileSystemWatcher(Path.GetDirectoryName(_remappingsFilePath), Path.GetFileName(_remappingsFilePath)); _remappingsFileWatcher.Changed += new FileSystemEventHandler(RemappingsFileChanged); _remappingsFileWatcher.Renamed += new RenamedEventHandler(RemappingsFileChanged); _remappingsFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.LastAccess | NotifyFilters.FileName; _remappingsFileWatcher.EnableRaisingEvents = true; } object _lock = new object(); void RemappingsFileChanged(object sender, FileSystemEventArgs e) { lock (_lock) { try { Thread.Sleep(50); LoadRemappings(); LoggingService.LogVerbose("RequestUrlRemapper", "Remappings reloaded"); } catch (Exception ex) { LoggingService.LogError("RequestUrlRemapper", ex); } } } private class Remapping { public string RequestPath { get; set; } public string RequestHost { get; set; } public bool RequestHostEndsWith { get; set; } public string RewritePath { get; set; } public string RewriteHost { get; set; } public override string ToString() { return string.Format("http://{0}{1} --> http://{2}{3}", (RequestHost ?? "*"), RequestPath, (RewriteHost ?? "*"), (RewritePath ?? "/*")); } } } }