Thursday, December 31, 2009

Getting list of applications in a site from IIS metabase

We will start with creating a class for storing the application details.

public class IisWebApplication
{
   private string _Name = "";
   public string Name
   {
      get { return _Name; }
      set { _Name = value; }
   }

   private string _AspNetFrameworkVersion = "";
   public string AspNetFrameworkVersion
   {
      get { return _AspNetFrameworkVersion; }
      set { _AspNetFrameworkVersion = value; }
   }

   private string _Description = "";
   public string Description
   {
      get { return _Description; }
      set { _Description = value; }
   }

   private string _FolderPath;
   public string FolderPath
   {
      get { return _FolderPath; }
      set { _FolderPath = value; }
   }
}

As specified in my earlier post, each web site is assigned a unique integer value. The path for accessing a site will look like IIS://localhost/W3SVC/{site number}. The below method can help in getting the application details associated with a website passed as siteId. You can also specify whether to get details of sub applications.

public List<IisWebApplication> GetWebApplications(int siteId, bool showSubApplications)
{
   _ApplicationPools = GetApplicationPools();

   List<IisWebApplication> webApps = new List<IisWebApplication>();
   string MetabaseRootPath = @"IIS://localhost/W3SVC/" + siteId.ToString() + "/ROOT";
   DirectoryEntry appEntry = null;
   try
   {
      appEntry = new DirectoryEntry(MetabaseRootPath);
      foreach (DirectoryEntry s in appEntry.Children)
      {
         CheckAndAddWebApplication(s, webApps, showSubApplications);
      }
   }
   catch { }
   finally
   {
      if (appEntry != null)
         appEntry.Close();
   }
   return webApps;
}

private void CheckAndAddWebApplication(DirectoryEntry entry, List<IisWebApplication> webApps, bool addSubApplications)
{
   bool reserved = true;
   if (IsWebApplicationEntry(entry))
   {
      reserved = IsReservedWebApplication(entry.Name);
      if (!reserved)
         webApps.Add(GetWebApplication(entry));
   }
   if (entry.Children != null && addSubApplications && !reserved)
   {
      foreach (DirectoryEntry s in entry.Children)
      {
         CheckAndAddWebApplication(s, webApps, addSubApplications);
      }
   }
}

private IisWebApplication GetWebApplication(DirectoryEntry entry)
{
   IisWebApplication webApp = new IisWebApplication();
   webApp.Name = GetApplicationName(entry);
   webApp.AspNetFrameworkVersion = GetAspNetVersion(entry);
   webApp.FolderPath = GetApplicationFolderPath(entry);
   if (_ApplicationPools != null)
   {
      string appPoolName = entry.Properties["AppPoolId"].Value.ToString();
      webApp.ApplicationPool = GetApplicationPoolFromList(appPoolName);
   }
   return webApp;
}

private bool IsWebApplicationEntry(DirectoryEntry entry)
{
   string keyType = entry.Properties["KeyType"].Value.ToString();
   if (keyType == "IIsWebDirectory" || (keyType == "IIsWebVirtualDir" && entry.Properties["Path"].Value != null))
   {
      string[] appNames = GetApplicationName(entry).Split("/".ToCharArray());
      if (appNames[appNames.Length - 1] == entry.Name)
         return true;
   }
   return false;
}

private string GetApplicationName(DirectoryEntry entry)
{
   string appRoot = entry.Properties["AppRoot"].Value.ToString();
   string matchString = "/ROOT/";
   int index = appRoot.ToUpper().IndexOf(matchString);
   if (index > -1)
      return appRoot.Substring(index + matchString.Length);

   return entry.Name;
}

private bool IsReservedWebApplication(string appName)
{
   if (appName.StartsWith("_vti_") || appName == "_private" || appName == "bin" || appName == "Printers"
      || appName == "aspnet_client")
      return true;
   return false;
}

private string GetApplicationFolderPath(DirectoryEntry entry)
{
   string keyType = entry.Properties["KeyType"].Value.ToString();
   if (keyType == "IIsWebDirectory")
   {
      string path = entry.Path;
      string matchString = "/ROOT/";
      int index = path.IndexOf(matchString);
      return "/" + path.Substring(index + matchString.Length);
   }
   else if (keyType == "IIsWebVirtualDir")
   {
      return entry.Properties["Path"].Value.ToString();
   }
   return "";
}

Getting ASP.Net version for this application depends on the version of IIS you are using. In version 7, the runtime version is set for the App Pool. In earlier versions, runtime version is set for individual applications.

If you are using IIS 7, use the following method.
private string GetAspNetVersion(DirectoryEntry entry)
{
   string appPool = s.Properties["AppPoolId"].Value.ToString();
   string metabaseAppPoolPath = "IIS://localhost/W3SVC/AppPools/" + appPool;
   DirectoryEntry appPoolEntry = new DirectoryEntry(metabaseAppPoolPath);
   return appPoolEntry.Properties["ManagedRuntimeVersion"].Value.ToString();
}

The below method can be used for earlier versions.
private string GetAspNetVersion(DirectoryEntry entry)
{
   PropertyValueCollection vals = entry.Properties["ScriptMaps"];
   if (vals == null)
      return "Could not read the frame work version";
   foreach (string val in vals)
   {
      if (val.StartsWith(".aspx"))
      {
         int startIndex = val.ToLower().IndexOf("framework") + 10;
         int endIndex = val.IndexOf("\\", startIndex);
         string version = val.Substring(startIndex, endIndex - startIndex);
         return version;
      }
   }
   return "No version for .aspx files.";
}

Wednesday, December 30, 2009

Getting list of web sites from IIS metabase

Here we are going to see how we can get the list of web sites available using IIS metabase.
We will start with defining the types we are going to use. Class WebSite is used to hold information about a web site and enum ServerState specifies its state. The states are self explanatory.

public class WebSite
{
   private int _Id;
   public int Id
   {
      get { return _Id; }
      set { _Id = value; }
   }

   private string _Name;
   public string Name
   {
      get { return _Name; }
      set { _Name = value; }
   }

   private string _Description;
   public string Description
   {
      get { return _Description; }
      set { _Description = value; }
   }

   private string _ApplicationPool;
   public string ApplicationPool
   {
      get { return _ApplicationPool; }
      set { _ApplicationPool = value; }
   }

   private string _FolderPath;
   public string FolderPath
   {
      get { return _FolderPath; }
      set { _FolderPath = value; }
   }

   private ServerState _ServerState;
   public ServerState ServerState
   {
      get { return _ServerState; }
      set { _ServerState = value; }
   }
}

public enum ServerState
{
   Unknown = 0,
   Starting = 1,
   Started = 2,
   Stopping = 3,
   Stopped = 4,
   Pausing = 5,
   Paused = 6,
   Continuing = 7
}

We can use System.DirectoryServices.DirectoryEntry for accessing IIS metabase. For getting the registered web sites, we need to iterate through the child nodes of "IIS://localhost/W3SVC". A childer is identified as a web site if the SchemaClassName property is IIsWebServer. IIS assigns unique integer number to each websites which typically starts from 1. The Name property provides this value.

public List<WebSite> GetWebSites()
{
   string metabasePath = "IIS://localhost/W3SVC";
   DirectoryEntry root = null;
   List<WebSite> webSites = new List<WebSite>();
   try
   {
      root = new DirectoryEntry(metabasePath);
      bool hasAppPools = HasApplicationPools();
      foreach (DirectoryEntry s in root.Children)
      {
         int siteId;
         if (s.SchemaClassName == "IIsWebServer" && Int32.TryParse(s.Name, out siteId))
         {
            WebSite webSite = new WebSite();
            webSite.Id = siteId;
            webSite.Name = s.Properties["ServerComment"].Value.ToString();
            webSite.Description = s.Properties["ServerComment"].Value.ToString();
            webSite.FolderPath = GetFolderPath(s);
            webSite.ServerState = GetServerState(s.Properties["ServerState"].Value.ToString());
            if (hasAppPools)
            {
               webSite.ApplicationPool = s.Properties["AppPoolId"].Value.ToString();
            }
            webSites.Add(webSite);
         }
      }
   }
   catch { }
   finally
   {
      if (root != null)
         root.Close();
   }
   return webSites;
}

public bool IsValidMetabasePath(DirectoryEntry entry)
{
   try
   {
      if (entry != null && !string.IsNullOrEmpty(entry.SchemaClassName))
         return true;
   }
   catch { }
   return false;
}

public bool HasApplicationPools()
{
   string metabaseAppPoolsPath = "IIS://localhost/W3SVC/AppPools";
   DirectoryEntry appPoolsEntry = new DirectoryEntry(metabaseAppPoolsPath);
   return IsValidMetabasePath(appPoolsEntry);
}

For getting the website root path, we need to read the Path property of its child named ROOT.

private string GetFolderPath(DirectoryEntry server)
{
   foreach (DirectoryEntry s in server.Children)
      if (s.Name.ToUpper() == "ROOT")
         return s.Properties["Path"].Value.ToString();
   return "";
}

private ServerState GetServerState(string serverStatePropertyValue)
{
   switch (serverStatePropertyValue)
   {
      case "1": return ServerState.Starting;
      case "2": return ServerState.Started;
      case "3": return ServerState.Stopping;
      case "4": return ServerState.Stopped;
      case "5": return ServerState.Pausing;
      case "6": return ServerState.Paused;
      case "7": return ServerState.Continuing;
   }
   return ServerState.Unknown;
}

Tuesday, December 29, 2009

Loading assemblies from a shared location

Recently we had a restriction on deploying assemblies to GAC. Our application structure is quite complex where in my team owns the root website and the framework and lots of applications are configured as sub applications (or even multi level sub applications). It is impossible to release the assemblies to individual teams and coordinate. So for solving this issue we have decided to deploy the shared assemblies to a common location and load it dynamically from there.

The approach we have taken is as follows.
Create an http module called SharedAssemblyLauncher. This assembly will be released to all teams and they have to define this module in their web.config.
The root web.config will define a key "SharedAssemblyBaseDirectory" to specify the base shared assembly location from where the probing starts. Each application web.config can override this too. For supporting multiple versions, you can create folders with the version number and then copy the assemblies to that folder. The probling rule is as follows
1. Look under the folder with version number.
2. Look under folder Common.
3. Base directory

We start with creating a new handler for AssemblyResolve for current domain.

public class SharedAssemblyLauncher : IHttpModule
{
   private static string _BaseAssemblyFolder = "";

   static SharedAssemblyLauncher()
   {
      if (HttpContext.Current != null)
      {
         string appRoot = HttpContext.Current.Server.MapPath("~/");
         string webRoot = HttpContext.Current.Server.MapPath("/");
         if (appRoot != webRoot)
            _BaseAssemblyFolder = GetSharedAssemblyBaseDirectoryFromConfigFile(GetSlashEndedFolder(appRoot) + "web.config");
         if (_BaseAssemblyFolder.Trim() == "")
            _BaseAssemblyFolder = GetSharedAssemblyBaseDirectoryFromConfigFile(GetSlashEndedFolder(webRoot) + "web.config");
      }
      AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
   }

   public void Dispose()
   {
   }

   public void Init(HttpApplication context)
   {
   }
}

In assembly resolve handler, the first thing what we will do is loop through the loaded assemblies to look for a match. We've noticed that, in case for a request for embedded resource the version number was not passed, only the assembly name is passed. So if we probe, we might not find any matching assembly. So it is very important to loop through and find a match before the actual probing starts. If no match found on loaded assemblies, probe based on the sequence defined earlier. If no match found while probing, return null so that the framework does its own probing.
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
   Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
   foreach (Assembly assembly in loadedAssemblies)
   {
      if (assembly.FullName == args.Name || assembly.FullName.Contains(args.Name))
         return assembly;
   }

   if (_BaseAssemblyFolder.Trim() == "")
      return null;

   string assemblyFileName = GetAssemblyFileName(args.Name);
   string assemblyVersion = GetAssemblyVersion(args.Name);
   try
   {
      string fileName = string.Format("{0}{1}\\{2}.dll", _BaseAssemblyFolder, assemblyVersion, assemblyFileName);
      if (File.Exists(fileName))
         return GetAssemblyFromPath(fileName);
      fileName = string.Format("{0}Common\\{1}.dll", _BaseAssemblyFolder, assemblyFileName);
      if (File.Exists(fileName))
         return GetAssemblyFromPath(fileName);
      fileName = string.Format("{0}{1}.dll", _BaseAssemblyFolder, assemblyFileName);
      if (File.Exists(fileName))
         return GetAssemblyFromPath(fileName);
   }
   catch { }
   return null;
}

private static string GetSlashEndedFolder(string baseFolder)
{
   return (baseFolder.Trim().EndsWith("\\") ? baseFolder.Trim() : baseFolder.Trim() + "\\");
}

private static string GetSharedAssemblyBaseDirectoryFromConfigFile(string configPath)
{
   XmlDocument configXmlDoc = new XmlDocument();
   if (!File.Exists(configPath))
      return "";
   configXmlDoc.Load(configPath);
   XmlNode baseDirectoryNode = configXmlDoc.SelectSingleNode("/configuration/appSettings/add[@key=\"SharedAssemblyBaseDirectory\"]");
   if (baseDirectoryNode == null)
      return "";
   return baseDirectoryNode.Attributes["value"].Value;
}

private static string GetAssemblyFileName(string assemblyName)
{
   return assemblyName.Split(",".ToCharArray())[0];
}

private static string GetAssemblyVersion(string assemblyName)
{
   string[] parts = assemblyName.Split(",".ToCharArray());
   for (int i = 0; i < parts.Length; i++)
   {
      if (parts[i].Trim().StartsWith("Version="))
      {
         string[] versionParts = parts[i].Split("=".ToCharArray());
         return versionParts[1].Trim();
      }
   }
   return "";
}

private static Assembly GetAssemblyFromPath(string assemblyPath)
{
   return Assembly.LoadFile(assemblyPath);
}

One important thing to remeber here is that never try to read configuration values using ConfigurationManager in this module.

Happy sharing!!!

Monday, December 28, 2009

Use POST instead of GET

Why do we use POST instead of GET?
GET shows lots of ugly names and values in url. Most browsers have some restrictions on the size of url string. The above 2 points make POST a better choice over GET.

We will quickly see, how can we convert our existing GETs to POSTs.

When a new page is to be called we will create a form dynamically, create hidden fields for all the values to be passed and submit it. The following script can help you in achieving this.
Assume that valuesToPost is a javascript array of objects which has fields Name and Value.

function PostIt(valuesToPost, targetUrl)
{
   var form = document.createElement("form");
   form.setAttribute("method", "post");
   form.setAttribute("action", targetUrl);
   for (var i=0; i<valuesToPost.length; i++)
   {
      var hiddenField = document.createElement("input");
      hiddenField.setAttribute("type", "hidden");
      hiddenField.setAttribute("name", valuesToPost[i].Name);
      hiddenField.setAttribute("value", valuesToPost[i].Value);
      form.appendChild(hiddenField);
   }
   document.body.appendChild(form);
   form.submit();
}

We can read these passed values from the page as follows. These dynamically posted values will be available only when IsPostback is false.
So on page_load, the following code can read these posted values.

if(!IsPostback)
{
   string id = GetPostedValue("Id");
   Dictionary<string, string> postedValues = GetPostedValues();
}

public string GetPostedValue(string key)
{
   return Request.Form[key];
}

public Dictionary<string, string> GetPostedValues()
{
   Dictionary<string, string> postedVals = new Dictionary<string, string>();
   foreach (string key in Request.Form.AllKeys)
      postedVals.Add(key, Request.Form[key]);
   return postedVals;
}

Now bye bye to ugly URLs.
Happy POSTing!!!

Secure values stored in hidden fields

It is a common practice to store values in hidden fields when items are edited. Value like id are a common candidate for these operations. Most times modifying these values from client side by script can be dangerous. Here we will look for a solution where in the complete set of values which we have to store and avail on postbacks be saved in a secure way.

The suggested solution is as follows.

First we will define a dictionary which can hold string key value pairs. We will also provide methods for storing and retrieving these values.

private string _KeyStoreName = "__KeyValueStore__";
private Dictionary<string, string> _KeyValues = new Dictionary<string, string>();
public void SetKeyValue(string name, string value)
{
   if (_KeyValues.ContainsKey(name))
    _KeyValues[name] = value;
   else
    _KeyValues.Add(name, value);
}
public string GetValue(string name)
{
   if (_KeyValues.ContainsKey(name))
    return _KeyValues[name];
   return "";
}

public Dictionary<string, string> GetKeyValues()
{
   Dictionary<string, string> keyVals = new Dictionary<string, string>();
   foreach (string key in _KeyValues.Keys)
    keyVals.Add(key, _KeyValues[key]);
   return keyVals;
}

On pre-render, we will serialize the dictionary and encrypt it. This value will be stored in a hidden field which will be generated dynamically.

protected override void OnPreRender(EventArgs e)
{
   StringWriter sw = new StringWriter();
   los.Serialize(sw, _KeyValues);
   string valueToStore = sw.GetStringBuilder().ToString();
   // Encrypt valueToStore to make it secured.
   HtmlInputHidden hid = new HtmlInputHidden();
   hid.ID = _KeyStoreName;
   hid.Name = _KeyStoreName;
   hid.Value = valueToStore;
   Page.Form.Controls.Add(hid);
   base.OnPreRender(e);
}

On pre-init, we will retrieve the hidden field value, decrypt it and deserialize it.
protected override void OnPreInit(EventArgs e)
{
   string storedValue = Request.Form[_KeyStoreName];
   if (!string.IsNullOrEmpty(storedValue))
   {
    // Decrypt storedValue, if you have encrypted it.
    LosFormatter los = new LosFormatter();
    _KeyValues = los.Deserialize(storedValue) as Dictionary<string, string>;
   }
   base.OnPreInit(e);
}

Happy coding!!!

Secure your cookies

Recently our IT Risk team suggested us to hide our cookies as part of making our site more secure. One of their main concerns were that all cookie names suggest exactly what data is stored. So we came up with the following solution.

First abstract the cookies to a new class called WebCookie.
Based on our requirement we only need to make sure that the cookies expire after a specified number of seconds.

[Serializable]
public class WebCookie
{
   private string _Name = "";
   public string Name
   {
    get { return _Name; }
    set { _Name = value; }
   }
   private string _Value = "";
   public string Value
   {
    get { return _Value; }
    set { _Value = value; }
   }
   private int _ExpiresInSeconds = 1200;
   public int ExpiresInSeconds
   {
    get { return _ExpiresInSeconds; }
    set { _ExpiresInSeconds = value; }
   }
   public WebCookie() { }
   public WebCookie(string name, string value)
   {
    _Name = name;
    _Value = value;
   }
   public WebCookie(string name, string value, int expiresInSeconds)
   {
    _Name = name;
    _Value = value;
    _ExpiresInSeconds = expiresInSeconds;
   }
}

Use the base web page to store these cookies as a dictionary. Also expose some methods to add/read cookies.

private Dictionary<string, WebCookie> _WebCookies = new Dictionary<string, WebCookie>();

public void SetCookie(string name, string value)
{
   string keyName = name.ToUpper();
   if (_WebCookies.ContainsKey(keyName))
    _WebCookies[keyName].Value = value;
   else
    _WebCookies.Add(keyName, new WebCookie(name, value));
}

public WebCookie GetCookie(string name)
{
   string keyName = name.ToUpper();
   if (_WebCookies.ContainsKey(keyName))
    return _WebCookies[keyName];
   return null;
}

On pre render, serialize the dictionary, then encrypt and send it as cookie. Also make sure to add the Cookie generated time to the dictionary before serializing. We can use this time stamp later to findout which cookie items have timed out.

private string _CookieName = "__CookieStore__";
private string _LastPostTimeCookieKeyName = "LastPostTime";
protected override void OnPreRender(EventArgs e)
{
   _WebCookies.Add(_LastPostTimeCookieKeyName, new WebCookie(_LastPostTimeCookieKeyName, DateTime.Now.Ticks.ToString()));
   LosFormatter los = new LosFormatter();
   StringWriter sw = new StringWriter();
   los.Serialize(sw, _WebCookies);
   string cookieValueToStore = sw.GetStringBuilder().ToString();
   // Encrypt cookieValueToStore to make it more secure.
   HttpCookie cookie = new HttpCookie(_CookieName, cookieValueToStore);
   Response.Cookies.Add(cookie);
   base.OnPreRender(e);
}

On pre init, read the cookie and populate the dictionary. Also check the last post time to findout which items have really timed out.

protected override void OnPreInit(EventArgs e)
{
   if (Request.Cookies[_CookieName] != null && !string.IsNullOrEmpty(Request.Cookies[_CookieName].Value))
   {
    string cookieValue = Request.Cookies[_CookieName].Value;
    // Decrypt it, if you've encrypted it.
    LosFormatter los = new LosFormatter();
    _WebCookies = los.Deserialize(cookieValue) as Dictionary<string, WebCookie>;
    if (_WebCookies.ContainsKey(_LastPostTimeCookieKeyName))
    {
       _LastPostTime = new DateTime(long.Parse(_WebCookies[_LastPostTimeCookieKeyName].Value));
       _WebCookies.Remove(_LastPostTimeCookieKeyName);
    }
    // Remove all items which are expired.
   }
   base.OnPreInit(e);
}

Now you have secured your cookies.
Enjoy!!!