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 ?? "/*"));
}
}
}
}