Thursday, April 26, 2007

How to walk the IIS metabase using WMI and System.Management

Here's a little addition to my last post where I described how to set a property (HttpRedirect) in the IIS 6.0 metabase. I got curious about how one could output the entire metabase using WMI by walking the hierarchy. The trick is the ManagementObject.GetRelated() method which gets all the objects related to the current object. You have to be carefull because that includes the parent and my first attempt at recursively walking the hierarchy got stuck in a loop. I just use a hashtable to keep track of objects that I've already found. Some properties can be arrays and sometimes they are arrays of ManagementObjects that themselves have properties so WritePropertiesAsArray calls back to WriteProperties. The whole thing is written to a file by the Output static class. You'll need to set your own file path to the outputPath constant.
using System;
using System.Collections;
using System.Management;

namespace WMITest.MetabaseWalker
{
 /// 
 /// Summary description for Program.
 /// 
 public class Program
 {
        const string serverName = "192.168.0.166";
        const string userName = "Administrator";
        const string password = "mike";
        const string wmiPathToDefaultWebsite = "IIsComputer.Name=\"LM\"";

        Hashtable outputObjects = new Hashtable();

        [STAThread]
        public static void Main()
        {
            Program program = new Program();
            program.WalkMetabase();
        }

        private void WalkMetabase()
        {
            ConnectionOptions options = new ConnectionOptions();
            options.Username = userName;
            options.Password = password;
            options.Authentication = AuthenticationLevel.PacketPrivacy;

            ManagementPath path = new ManagementPath();
            path.Server = serverName;
            path.NamespacePath = "root/MicrosoftIISv2";

            ManagementScope scope = new ManagementScope(path, options);

            using(ManagementObject obj = new ManagementObject(
                      scope,
                      new ManagementPath(wmiPathToDefaultWebsite), null))
            {
                OutputObject(obj);
            }
        }

        private void OutputObject(ManagementObject obj)
        {
            outputObjects.Add(obj.Path.RelativePath, new object());

            Output.WriteLine();
            Output.WriteLine("{0}", true, obj.Path.RelativePath);
            Output.WriteLine();

            WriteProperties(obj.Properties);
            WriteProperties(obj.SystemProperties);

            foreach(ManagementObject relatedObject in obj.GetRelated())
            {
                if(!outputObjects.ContainsKey(relatedObject.Path.RelativePath))
                {
                    Output.TabIn();
                    OutputObject(relatedObject);
                    Output.TabOut();
                }
            }
        }

        private void WriteProperties(PropertyDataCollection properties)
        {
            Output.TabIn();
            foreach(PropertyData property in properties)
            {
                Output.WriteLine("{0}:\t{1}, \t{2}",
                    property.Name,
                    (property.Value == null) ? "null" : property.Value.ToString(),
                    property.Type.ToString());

                WritePropertyAsArray(property);
            }
            Output.TabOut();
        }

        private void WritePropertyAsArray(PropertyData property)
        {
            if(property.IsArray && property.Value != null)
            {
                ICollection propertyArray = property.Value as ICollection;
                if(propertyArray == null)
                    throw new ApplicationException("can't cast property.Value as ICollection");
                Output.TabIn();
                if(propertyArray.Count == 0)
                {
                    Output.WriteLine("No Items");
                }
                int counter = 0;
                foreach(object item in propertyArray)
                {
                    ManagementBaseObject managementObject = item as ManagementBaseObject;
                    if(managementObject != null)
                    {
                        Output.WriteLine("{0}[{1}]", property.Name, counter.ToString());
                        WriteProperties(managementObject.Properties);
                        counter++;
                    }
                    else
                    {
                        Output.WriteLine("{0}", item.ToString());
                    }
                }
                Output.TabOut();
            }
        }
 }
}
And here's the output class
using System;
using System.IO;

namespace WMITest.MetabaseWalker
{
    /// 
    /// Summary description for Output.
    /// 
    public class Output
    {
        const string outputPath = @"C:\VisualStudio\WMITest\WMITest.MetabaseWalker\Output.txt";
        static int tabs = 0;

        static Output()
        {
            using(File.Create(outputPath)){}
        }

        public static void WriteLine()
        {
            WriteLine("");
        }

        public static void WriteLine(string format, params object[] args)
        {
            WriteLine(format, false, args);
        }

        public static void WriteLine(string format, bool writeToConsole, params object[] args)
        {
            if(writeToConsole)
            {
                Console.WriteLine(new string('\t', tabs) + format, args);
            }
            using(StreamWriter writer = File.AppendText(outputPath))
            {
                writer.WriteLine(new string('\t', tabs) + format, args);
            }
        }

        public static void TabIn()
        {
            tabs++;
        }

        public static void TabOut()
        {
            tabs--;
        }
    }
}

Wednesday, April 25, 2007

IIS Redirects by setting the HttpRedirect metabase property with WMI

In my last post I talked about how to use ISAPI_Rewrite to do redirects in IIS. I said "It looks like the only way of doing this is to use an ISAPI extension." How wrong I was! That'll teach you, dear reader, to believe everything you read in Code Rant. In fact I guy I once worked with (hello Chris) used to say, "Everything you say is wrong Mike":) What I've discovered since then is that you can do the same thing directly in IIS by changing a metabase property 'HttpRedirect' using WMI. This took a bit of digging since I'm not particularly familiar with the IIS metabase or WMI, but discovering the IIS Metabase Explorer made all the difference. It's part of the IIS 6.0 Resource Kit and you can use it to dig around the metabase and find out the name and type of the objects you need to access to get the property you're after. The main reference for the metabase is also quite comprehensive. I wanted to programatically set up redirects from C# so the first thing to dig into was the System.Management namespace which is the managed API for WMI. The documentation's not bad, but as soon as you start doing anything WMI related you're in the world of the sys admin , a world you don't want to go to where VBScript rules. You'll find plenty of examples for what you want to do, but not using System.Management. A bit of translation is required. Here's a little console app which sets up a couple of redirects on the default web site. The first redirects any requests for the 'google' directory to Google, so if you type the following into your browser (you'll have to change the server name) :
http://192.168.0.166/google/
You'll get redirected to the Google homepage. The other redirect redirects /bbc/ to the BBC News homepage.
using System;
using System.Management;

namespace WMITest
{
    public class Program
    {
        const string serverName = "192.168.0.166";
        const string userName = "Administrator"; 
        const string password = "mike";
        const string wmiPathToDefaultWebsite = "IIsWebVirtualDirSetting='W3SVC/1/ROOT'";
        const string redirectValue = 
            "*; /google/; http://www.google.com/" +
            "; /bbc/; http://news.bbc.co.uk/" +
            ", EXACT_DESTINATION";

        public static void Main()
        {
            ConnectionOptions options = new ConnectionOptions();
            options.Username = userName;
            options.Password = password;
            options.Authentication = AuthenticationLevel.PacketPrivacy;

            ManagementPath path = new ManagementPath();
            path.Server = serverName;
            path.NamespacePath = "root/MicrosoftIISv2";

            ManagementScope scope = new ManagementScope(path, options);

            using(ManagementObject obj = new ManagementObject(
                      scope, 
                      new ManagementPath(wmiPathToDefaultWebsite), null))
            {
                Console.WriteLine("{0}", obj.Path.RelativePath);
                if(obj.Properties.Count == 0)
                    Output.WriteLine("No properties found");

                obj.SetPropertyValue("HttpRedirect", redirectValue);
                obj.Put();

                string httpRedirectValue = obj.GetPropertyValue("HttpRedirect").ToString();
                Console.WriteLine("HttpRedirect='{0}'", httpRedirectValue);
            }
            Console.ReadLine();
        }
    }
}
The tricky stuff is knowing what value to put into the wmiPathToDefaultWebsite variable. The syntax seems to be ='', but it took me a while to find the correct type to get to the HttpRedirect property. The Metabase Explorer displayed 'IIsWebVirtualDir' as the type for the 'W3SVC/1/ROOT' node, but that property is actually only returned when you set the type to 'IIsWebVirtualDirSetting'. The actual path to the default web site (W3SVC/1/ROOT) is pretty easy to work out from looking at the Metabase Explorer. The WMI provider for the IIS metabase is 'root/MicrosoftIISv2' and also note that you have to call Put() on the ManagementObject after you set a property value for it to be persisted. You'll probabaly want to experiment with a virtual PC IIS, because it's easy to totally muck up your web site by playing with metabase values. The amount of information that's exposed by WMI is enormous. If you find yourself needing to do anything sys-admin-like programmatically it's often the only way to go. A lot of the main Microsoft applications now expose WMI APIs, SQL Server being one of them, so it can often be a choice for that kind of thing too. It has to remain a choice of last resort though, because it's a real pain trying to work out how to manipulate the given WMI object model. It's loosely typed, runtime discoverable stuff, so any managed API always has to be preferable to WMI.

Tuesday, April 24, 2007

URL Rewriting

I’ve been digging into URL rewriting recently. I want to take a request for a directory e.g: Http://www.mikehadlow.com/special/ And redirect it to a resource on a sub domain e.g: Http://features.mikehadlow.com/special_features.htm I first thought of using an HttpModule, but the problem is that it requires the request to be routed to the ASP.NET pipeline, but the ASP.NET pipeline is only invoked if the requested resource has an ASP.NET related file extension (.aspx, .asmx etc), a request for a directory will never invoke the ASP.NET pipeline, even if we ask HTTP.SYS to process all requests by doing a wildcard mapping to the ASP.NET handler (which is a bad idea anyway, since it creates a huge overhead for static content). It looks like the only way of doing this is to use an ISAPI extension like ISAPI_Rewrite (http://www.isapirewrite.com/). Reading up on it, this could be quite an elegant solution. It has built in redirection, so a simple configuration entry is all I would need to satisfy my requirements. The configuration file can be updated on the fly, so a little management interface would only have to do a simple file manipulation task to add, remove or edit redirects. It picks up the changes without requiring IIS to restart. I’ve downloaded the trial version, it seems to do the trick. I’ve experimented redirecting 'http://locahost/google' to 'http://www.google.com/', the only possible issue is that, because the browser is asked to redirect the url shown in the address bar changes to 'http://www.google.com/', but I’d get that with whatever solution I chose. The configuration file looks like this:
[ISAPI_Rewrite]
RewriteRule /google http://www.google.com [I,R]
There’s a free version that does per server URL rewriting, or a ‘pro’ version that costs $99 and allows per site configuration. Jeff Altwood talks about here. Update In my next post I realise that I can do the same thing by setting the HttpRedirect property in the IIS metabase. Also Travis Hawkins has a nice article here on using ISAPI_Rewrite. He also mentions another technique I hadn't considered before, of using the 404 error page to redirect unknown page requests to a generic handler. Travis also mentions RemapUrl, a tool that comes with the IIS 6.0 resource kit, but as he says, it's pretty limited.

Friday, April 13, 2007

Resolving mutipart WSDL documents with DiscoveryClientProtocol

Following on from yesterday's post on the WSDL that's generated by WCF, I ran up Relector and started digging into WSDL.exe. It doesn't do exactly the same thing that I want to do, its job is to generate proxy classes for web services, but still has to download and resolve mutiple linked WSDL and XSD files when given the URL of the root WSDL file. It turns out that is uses a class in the .NET framework, DiscoveryClientProtocol that does all the work of resolving and downloading the WSDL and XSD classes. It can even save them all to disk. I got quite excited when I also found out about ServiceDescriptionImporter, the name suggested that it could create a single ServiceDescription (the class that represents a WSDL file in memory) instance from a number of files, but alas it's the class that actually generates proxy class code. Matt Ward has an interesting article about how he used these classes to write the web reference functionality in SharpDevelop. Here's a little NUnit test showing the basic functionality of DiscoveryClientProtocol, it loads all the parts of the WSDL into its Documents collection, I then write the Keys of the documents (actually the URLs) to the console and finally save the documents to disk in 'My Documents\Wsdl":
using System;
using System.IO;
using System.Collections;
using System.Net;
using System.Web;
using System.Web.Services;
using System.Web.Services.Discovery;
using NUnit.Framework;

namespace MH.WsdlWorks.Tests
{
    [TestFixture]
    public class DiscoveryClientProtocolSpike
    {
        const string wsdlUrl = "http://localhost:1105/EchoService.svc?wsdl";

        [Test]
        public void DiscoveryClientProtocolTest()
        {
            DiscoveryClientProtocol client = new DiscoveryClientProtocol();
            client.Credentials = CredentialCache.DefaultCredentials;
            client.DiscoverAny(wsdlUrl);
            client.ResolveAll();

            foreach (DictionaryEntry dictionaryEntry in client.Documents)
            {
                Console.WriteLine("Key: {0}", dictionaryEntry.Key.ToString());
            }

            string myDocuments = Path.Combine(
                System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Wsdl");
            client.WriteAll(myDocuments, "wsdlDocumentsMap.xml");
        }
    }
}

Thursday, April 12, 2007

WCF and WSDL

I've recently started using WCF to develop Web Services. It's a very compelling model that sits much better with a service oriented view of web services than ASMX. The flexibility it gives you is wonderful, you can have any executable host a web service, you can serialize your messages any way you want and you can use any transport. However, if you're just doing standard SOAP, XML over HTTP then it's almost as easy as ASMX.

Part of the change to a more standards oriented web service infratructure in WCF is the way it produces WSDL documents. ASMX by default will always spew out a WSDL document when you appended '?WSDL' to your web services' URL and that WSDL document is always a single file. WCF doesn't create a WSDL document by default, you have to configure that behaviour by adding a serviceMetadata behaviour to your config file:

<serviceBehaviors>
  <behavior name="myServiceBehaviour">
    <serviceMetadata httpGetEnabled="true" />
  </behavior>
</serviceBehaviors>

Also the default pattern for the WSDL file itself has changed, it follows best practices and devides the WSDL document up by namespaces, so if your portTypes namespace is different from your service namespace, you'll get two WSDL files and the service WSDL will reference the portTypes WSDL with a wsdl:import element. The types schemas are always produced as seperate files, again, one for each namespace.

The problem with factoring a WSDL document into seperate files is that many tools don't understand wsdl:import (or xml:include and xml:import) and will simply choke if you try to feed them it. You can override this default behaviour as Tomas Restrepo outlines in this blog post 'Inline XSD in WSDL with WCF', but of course it requires you to have access to the web service.

This behaviour has been giving me a headache because I'm currently trying to create a generic web service test tool. I'm using System.Web.Services.Description.ServiceDescription to load and parse a WSDL file that's given by the user as a URL, but when I tried to run it against a WCF service is choked because the URL only points to the root WSDL document which is in fact exported as several linked files. It looks like I'm going to have load the WSDL more intelligently. I tried running the wsdl.exe tool against the same URL and it works fine, so there might be solution there, I'll have to open up Reflector and have a look:)

Update: I resoloved how to do this in my next post.

More update: Christian Weyer has an interesting post on WCF and multipart WSDLs. He shows how to get WCF to output a single unfactored WSDL document.