Want to protect your cyber security and still get fast solutions? Ask a secure question today.Go Premium

x
  • Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 857
  • Last Modified:

Can I edit the IIS 6.0 metabase.xml file with XSLT?

Hey everybody,

What I'm trying to do may be a little above and beyond what XSLT should be used for, but I'm trying it regardless.

Since IIS 6.0 uses an XML file for its configuration, I decided to come up with an XSLT to modify it, since I may need to set up application pools for as many as 20 applications in my app servers, and continually doing it at every re-install gets old fast.

I figured I could use XSLT since:
1) It's an XML file, and
2) I don't usually program in .NET or Java these days, and I'd be able to come up with the XSLT quicker.

However, the metabase.xml has a lot of extra characters in their values (line feeds, carriage returns, etc.),
and it looks like IIS won't function without them. I did some basic XSLT that just made a copy of the original metabase.xml file, and it looks like IIS won't accept the new file, because the special characters didn't get copied over:

<xsl:template match="@*|node()">
      <xsl:copy>
            <xsl:apply-templates select="@*|node()" />
        </xsl:copy>    
</xsl:template>

Does anyone have some insight on how I could get use XSLT for this, even with all of these special characters?
All suggestions/comments are much appreciated, this would be a major time-saver if I could get something like this functioning.

A sample metabase file can be found here:
http://www.winnetmag.com/Files/11/22281/22281.zip

Thanks!
0
Inward_Spiral
Asked:
Inward_Spiral
  • 12
  • 10
1 Solution
 
rdcproCommented:
You can add whitespace between tags in XSLT, but I don't think there is any way of inserting whitespace like that *within* a tag, but truthfully it's hard to see how IIS would care about whitespace within an XML tag...that is this:

      <Custom
            Name="MD_0"
            ID="0"
            Value="SMTP Server"
            Type="STRING"
            UserType="UNKNOWN_UserType"
            Attributes="NO_ATTRIBUTES"
      />

Should be functionally equivalent to:

<Custom Name="MD_0" ID="0" Value="SMTP Server" Type="STRING" UserType="UNKNOWN_UserType" Attributes="NO_ATTRIBUTES"/>

The only time I can think of where whitespace like this is important is if the XML needs to be canonicalized for digital signing.  But the canonical form is quite different than metabase.xml...


I wonder if it was the namespace that caused the problem, if it's not getting correctly sent to the output.

If you have a copy of XML Spy, you can try opening the metabase.xml in it, then select "prettyprint" to reformat the XML.  Or just try manually removing some of the spaces from an existing copy, and restart IIS to see if it chokes.  

I know you can't change some nodes in metabase.xml, because these are encrypted.  But you're wanting to change stuff like custom error messages and other setup info?

Oh, I would also be sure the BOM (byte order mark) is preserved, though I don't think IIS will have a problem if it's not there.  

I'll try it out on one of my VPCs...

Regards,
Mike Sharp
0
 
rdcproCommented:
No, it's not the whitespace...I used this Identity transform:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  xmlns:out="urn:microsoft-catalog:XML_Metabase_V54_0">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>

<xsl:template match="/ | node()">
      <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="node()"/>
      </xsl:copy>
</xsl:template>

<xsl:template match="@*">
      <xsl:copy>
      </xsl:copy>
</xsl:template>

<!-- Templates to handle changes go here -->

<xsl:template match="@HttpCustomHeaders">
      <xsl:attribute name="HttpCustomHeaders">X-Powered-By: MikeSharp</xsl:attribute>
</xsl:template>


</xsl:stylesheet>


and it set the HttpCustomHeaders correctly...no problem.

If you modify tags other than attributes, be cautious of the namespace.

Also, make sure you stop IIS before you try to save the result of the transform.  I notice that after a little while, IIS refreshes the metabase.xml, and pretty prints the whole thing, too.  But my header change has persisted.

I think you have a great idea, actually.  You could store your configuration changes in a separate XML file that you retrieve via the XSLT document() function.  And, you could have a variety of XSLT for different purposes, and import/include their templates into your main identity transform.  

In fact, it's hard to believe it hasn't been done before.  I'll have to ask our build engineer how he does this.

Regards,
Mike Sharp
0
 
Inward_SpiralAuthor Commented:
Glad you like the idea...and I can see how HttpCustomHeaders would work, but what about attributes like HttpErrors?

The HttpErrors attribute has values separated by spacing and a new line:
            HttpErrors="400,*,FILE,C:\WINDOWS\help\iisHelp\common\400.htm
                  401,1,FILE,C:\WINDOWS\help\iisHelp\common\401-1.htm
                  401,2,FILE,C:\WINDOWS\help\iisHelp\common\401-2.htm"

If I transform it, the new line is translated to hex(&#xA;) in the output: HttpErrors="400,*,FILE,C:\WINDOWS\help\iisHelp\common\400.htm&#xA;

But this works for you? IIS accepts these values anyway?
Well, if it does, great. That means I've just slipped up somewhere else in saving the changes back out to metabase.xml
0
VIDEO: THE CONCERTO CLOUD FOR HEALTHCARE

Modern healthcare requires a modern cloud. View this brief video to understand how the Concerto Cloud for Healthcare can help your organization.

 
Inward_SpiralAuthor Commented:
Okay, I just got it to work by getting rid of the "&#xA;" characters, recursively replacing them all with"&#11;".

<xsl:template name="replaceChar">
<xsl:param name="string" />
      <xsl:if test="contains($string, '&#xA;')">
        <xsl:value-of select="substring-before($string, '&#xA;')" /><xsl:text>&#11;</xsl:text>
        <xsl:call-template name="replaceChar">
              <xsl:with-param name="string"><xsl:value-of select="substring-after($string, '&#xA;')" /></xsl:with-param>
                 </xsl:call-template>
      </xsl:if>
      <xsl:if test="not(contains($string, '&#xA;'))"><xsl:value-of select="$string" /></xsl:if>
</xsl:template>

Seems like a lot of work to get the attribute values to work, but there you go.

0
 
rdcproCommented:
I don't get the &#xA; characters at all.  You're seeing an encoding problem, I think, related to how you're performing the transformation.  

What does your xsl:output tag say, and precisely how are you doing the transform?

Regards,
Mike Sharp
0
 
Inward_SpiralAuthor Commented:

My output tag is this:
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>

Nothing majorly different from what you posted earlier.

I think it might be what I'm using to process the XSLT with. When I use .NET for the XSL transformation, I get the &#xA; characters. When I use Xalan or Saxon with my XSLT debugger, I don't get the characters.

Any other theories on that one?

0
 
Inward_SpiralAuthor Commented:
What are you using to process the XSLT with? Maybe that's the question I should have asked.
0
 
rdcproCommented:
I was using XML Spy in my case.  The 0xA is illegal anyway--it should be x0A or more likely, x0D x0A, since it's really two characters; carriage return and linefeed. I think your problem is definitely in the encoding or somewhere in your transform, but you can probably cheat by removing the indent="yes" in your xsl:output.  This should prevent the processor from indenting anything at all--it will come out on a single line.  Then ASP.NET will "refresh" it when it feels like it, and it will be pretty printed again.  I don't see what schedule ASP.NET uses to refresh the thing, either, but it does.

Also make sure your .NET code isn't trying to do any indentation...

Regards,
Mike Sharp

0
 
rdcproCommented:
Actually, that may not help either.  I wonder if there's some whitespace normalization that's going on here...does your code explicitly set PreserveWhiteSpace?  

XmlDocument.PreserveWhitespace Property
    Gets or sets a value indicating whether to preserve white space.

    true to preserve white space; otherwise false. The default is false.

This property determines how white space is handled during the load and save process.

If PreserveWhitespace is true before Load or LoadXml is called, white space nodes are preserved; otherwise, if this property is false, significant white space is preserved, white space is not.

If PreserveWhitespace is true before Save is called, white space in the document is preserved in the output; otherwise, if this property is false, XmlDocument auto-indents the output.

This method is a Microsoft extension to the Document Object Model (DOM).
0
 
Inward_SpiralAuthor Commented:
This is getting interesting...maddening, but interesting. That "fix" I mentioned earlier only works if I'm using the XSLT Magic Profiler that I downloaded from Microsoft. Go figure.

When I actually run it through our .NET code to transform it, I get code like this within the attribute:
403,7,Forbidden,Client certificate required,0&#xD;&#xA;                  403,7,Forbidden,Client certificate required,0&#xD;&#xA;

The "&#xD;&#xA;" characters show up instead of a carriage return and a line feed.

This only occurs if I'm using the xsl:attribute function. If I render that value as shown below, I get those extra characters.
<xsl:attribute name="{name()}"><xsl:value-of select="$theAttributeValue"/></xsl:attribute>

However, If I map that value to a new node like below, then the line feeds and the carriage returns are mapped over correctly.
<textNode>403,7,Forbidden,Client certificate required,0                  
          403,7,Forbidden,Client certificate required,0</textNode>

I tried the PreserveWhitespace property, but that didn't change the output. How can I get the special characters like line feeds to be mapped over correctly without showing up as "&#xA;"?

You asked earlier why no one has tried this before...I think this is why!
0
 
rdcproCommented:
I think if they were &#x0D;&#x0A it would probably be fine.  &#xD;&#xA are not valid UTF-8 character entities. It looks like the result of the transform is somehow coming out as an 8 bit character encoding...

What happens if you try to open the XML output file in Internet Explorer?  Do you get an error (ie: "switch from current encoding is not allowed here" or some such)?

I'll try setting up a .NET transform myself, and see what happens.

Regards,
Mike Sharp
0
 
Inward_SpiralAuthor Commented:
Well, I think this might just work...even though there are still some invalid characters show up in the output, it can be loaded in IE, and I just added 2 application pools to IIS with the output file.

I did have to switch encoding though, I was sending output at UTF-16 by default.

0
 
rdcproCommented:
I hate to say this, but I can't get it to fail.  I take back what I said about the character entities...these aren't the hex codes themselves, but a character reference.  So &#xD;&#xA; is functionally equivalent to &#13;&#11; as far as a parser is concerned.  Once the character references are expanded, *then* they become 0D0A.

My Metabase.xml, once it's transformed, has the &#xD;&#xA; just like yours.  But my IIS doesn't seem to be bothered by it.  If I reboot, or if I load the IIS Services manager MMC, or if I simply wait a while, the metabase.xml gets rewritten (pretty printed), and since the character references are gone, I'm assuming the base class that handles this is using an xml text writer, which might not excape these characters. In an XSLT, within an attribute, it seems like it *always* wants to escape them.  Even if I add this to my transform:

<xsl:template match="@*">
      <xsl:attribute name="{local-name()}"><xsl:value-of select="." disable-output-escaping="yes"/></xsl:attribute>
</xsl:template>

I seem to remember somewhere that disable-output-escaping is overridden if you try to do it in an attribute.

Anyway, it works fine for me no matter what I do.  Here's my transform code (a console app):

using System;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;

namespace BasicTransform
{
      /// <summary>
      /// Summary description for Class1.
      /// </summary>
      class MyTransform
      {
            /// <summary>
            /// The main entry point for the application.
            /// </summary>
            [STAThread]
            static void Main(string[] args)
            {
                  XmlDocument xml = new XmlDocument();
                  xml.PreserveWhitespace = true;
                  xml.Load(@"V:\WINDOWS\system32\inetsrv\Metabase.xml");

                  XPathNavigator nav = xml.CreateNavigator();
                  
                  Encoding enc = Encoding.UTF8;
                  XmlTextWriter writer = new XmlTextWriter(@"V:\WINDOWS\system32\inetsrv\Metabase.xml", enc);

                  // Create a resolver with default credentials.
                  XmlUrlResolver resolver = new XmlUrlResolver();
                  resolver.Credentials = System.Net.CredentialCache.DefaultCredentials;

                  XslTransform xsl = new XslTransform();
                  xsl.Load("identity.xslt");
                  xsl.Transform(nav,null,writer,resolver);

            }
      }
}

Regards,
Mike Sharp
0
 
rdcproCommented:
Ah that's good to hear.  I couldn't make it fail at all!

Mike
0
 
Inward_SpiralAuthor Commented:
Still one more hurdle in what I'm trying, though. I'm trying to automatically move my applications into different application pools.
I think my XPath is off, it shouldn't be as hard as I'm making it out to be.

Each application is set as an IIsConfigObject by default, and every value is set in several <Custom> nodes.
<IIsConfigObject Location="/../../myWebAppLocation">
...
     <Custom
          Name="AppPoolID"
          ID="0"
          Value="AppPool1"
          Type="STRING"
          UserType="UNKNOWN_UserType"
          Attributes="NO_ATTRIBUTES"
     />
...
</IIsConfigObject>

I added a case statement to your root template below, for a test to catch the IIsConfigObjects, but I must not be using the right XPath to select the Custom nodes. I stepped through it and can make it to the "name() = 'IIsConfigObject'" case, but it doesn't get to the Custom node.

What's a simple way to access the Custom nodes?
I'm betting I'm just making it harder than it has to be...which has usually been the case in this post. :)

<xsl:template match="/ | node()">
 <xsl:choose>
  <xsl:when test="name() = 'IIsConfigObject' and contains(@Local,'myWebAppLocation')">
      <xsl:variable name="AppLocation" select="@Location"/>
       <xsl:variable name="AppPoolIDValue">
           <xsl:for-each select="Custom">
             <xsl:if test="@Name='AppPoolID'"><xsl:value-of select="@Value"/></xsl:if>
           </xsl:for-each>
       </xsl:variable>
       <xsl:element name="VirtualWebDir">
          <xsl:attribute name="Location"><xsl:value-of select="$AppLocation"/></xsl:attribute>
          <xsl:attribute name="AppPoolID"><xsl:value-of select="$AppPoolIDValue"/></xsl:attribute>
       </xsl:element>
  </xsl:when>
  <xsl:otherwise>
     <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:apply-templates select="node()"/>
     </xsl:copy>
  </xsl:otherwise>
 </xsl:choose>
</xsl:template>
0
 
rdcproCommented:
For Identity transformations, I find it better to put the modifications in separate templates.  So rather than modify the general template for nodes, add a new template that explicitly matches your condition:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  xmlns:out="urn:microsoft-catalog:XML_Metabase_V54_0">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>

<xsl:template match="/ | node()">
      <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="node()"/>
      </xsl:copy>
</xsl:template>

<xsl:template match="@*">
      <xsl:copy>
      </xsl:copy>
</xsl:template>

<!-- Templates to handle changes go here -->

<xsl:template match="@HttpCustomHeaders">
      <xsl:attribute name="HttpCustomHeaders">X-Powered-By: MikeSharp</xsl:attribute>
</xsl:template>


<xsl:template match="IIsConfigObject[contains(@Location, 'myWebApp')]">
      <!-- Creates a new VirtualWebDir element based on the current IISConfigObject -->
      <xsl:element name="VirtualWebDir">
            <xsl:attribute name="Location"><xsl:value-of select="@Location"/></xsl:attribute>
            <xsl:attribute name="AppPoolID"><xsl:value-of select="Custom[@Name='AppPoolID']/@Value"/></xsl:attribute>
      </xsl:element>
      <!-- copies the current IISConfigObject node and children -->
      <xsl:copy>
             <xsl:apply-templates select="@*"/>
             <xsl:apply-templates select="node()"/>
      </xsl:copy>
</xsl:template>

</xsl:template>


</xsl:stylesheet>



Again, watch out for namespaces!

Regards,
Mike Sharp
0
 
Inward_SpiralAuthor Commented:
You weren't kidding about those namespaces.

My output generates "xmlns" for the new nodes I'm creating.

Thankfully, MS strips those out of the file when IIS is restarted, and works anyway. It does leave this message in the log though:
"The property (xmlns) is not valid for the class it has been associated with.  This property will be ignored.  Incorrect XML:xmlns"

0
 
rdcproCommented:
You can get rid of these if you set up the namespaces in the XSLT correctly.  For example, if my XML root element looks like:

<configuration xmlns="urn:microsoft-catalog:null-placeholder">

And my XSLT root element looks like:

<xsl:stylesheet version="1.0"
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  
      xmlns:out="urn:microsoft-catalog:null-placeholder"  
      xmlns="urn:microsoft-catalog:null-placeholder"
      exclude-result-prefixes="out">


and my template uses the out: prefix in the element XPath expression:

<xsl:template match="out:IIsConfigObject[contains(@Location, 'tsweb')]">
      <!-- Creates a new VirtualWebDir element based on the current IISConfigObject -->
      <xsl:element name="VirtualWebDir">
            <xsl:attribute name="Location">
                <xsl:value-of select="@Location"/>
            </xsl:attribute>
            <xsl:attribute name="AppPoolID">
                <xsl:value-of select="out:Custom[@Name='AuthFlags']/@Value"/>
            </xsl:attribute>
      </xsl:element>
      <!-- copies the current IISConfigObject node and children -->
      <xsl:copy>
             <xsl:apply-templates select="@*"/>
             <xsl:apply-templates select="node()"/>
      </xsl:copy>
</xsl:template>

then I get the correct output nodes with no namespaces:

<VirtualWebDir Location="/LM/W3SVC/1/ROOT/tsweb" AppPoolID="AuthNTLM" />
<IIsConfigObject Location="/LM/W3SVC/1/ROOT/tsweb">
    <Custom
                    Name="AccessFlags"
                    ID="6016"
                    Value="AccessRead | AccessScript"
                    Type="DWORD"
                    UserType="IIS_MD_UT_FILE"
                    Attributes="INHERIT">
    </Custom>
    <Custom
                    Name="AuthFlags"
                    ID="6000"
                    Value="AuthNTLM"
                    Type="DWORD"
                    UserType="IIS_MD_UT_FILE"
                    Attributes="INHERIT">
    </Custom>
    <Custom
                    Name="DefaultDoc"
                    ID="6006"
                    Value="Default.htm,Default.asp"
                    Type="STRING"
                    UserType="IIS_MD_UT_FILE"
                    Attributes="NO_ATTRIBUTES">
    </Custom>
[...]

Please note, the data is bogus--I'm not precisely sure what your desired output was, but in this case I created a new VirtualWebDir element ahead of the IIsConfigObject that matches the template.  Then I created two attributes with some data from the IIsConfigObject element.  

Also note that I'm specifying BOTH a prefixed namespace and a default namespace in the XSLT root element.  One governs the namespace used for select statements (the one with the out:prefix), and the other is used for the namespace of the output.  In retrospect, I named that prefix a little misleadingly...I usually use something like user:  or temp: for these.

Regards,
Mike Sharp
0
 
rdcproCommented:
Oh, one other suggestion.  You can use xsl parameters to specify, before the transformation, what values you want to test for and what values you want to substitute.  

For example, instead of this:

<xsl:template match="out:IIsConfigObject[contains(@Location, 'tsweb')]">

you can use:

<xsl:param name="pConfigObj" >tsweb</xsl:param>

[...]

<xsl:template match="out:IIsConfigObject[contains(@Location, $pConfigObj)]">

These parameters can be set externally, before the transform.  You could even load a different XML file that has the build information in it, and either include it into the XSLT using the document() function, or parse it and set the parameters for the transform.  Lotsa possibilities.

Regards,
Mike Sharp
0
 
Inward_SpiralAuthor Commented:
Lotsa possibilities...there's the kicker.
I'll pick one and go with it, and see if I can make it without any "xmlns".

Other than that, it's looking pretty good.

One lesson learned: Do NOT use XSLT Majic to debug when working on the IIS metabase.
For some reason, the whitespace characters came out corrupted somehow, and I had to replace with &#11; to get it to work.

If I'm stepping through my .NET code, I don't have that issue.
0
 
Inward_SpiralAuthor Commented:
Mike, thanks for all your help with this.
I've still got some kinks to iron out, but that's just going to require more testing.

Sorry I didn't award you the points sooner!
0
 
rdcproCommented:
No worries, glad to help.

Regards,
Mike Sharp
0

Featured Post

Free Tool: Subnet Calculator

The subnet calculator helps you design networks by taking an IP address and network mask and returning information such as network, broadcast address, and host range.

One of a set of tools we're offering as a way of saying thank you for being a part of the community.

  • 12
  • 10
Tackle projects and never again get stuck behind a technical roadblock.
Join Now