Also See...

You should also check out my non-technology blog at:

http://www.bobsbasement.net/

Adding Custom FTP Providers with the IIS Configuration Editor - Part 2

In Part 1 of this blog series about adding custom FTP providers with the IIS Configuration Editor, I showed you how to add a custom FTP provider with a custom setting for the provider that is stored in your IIS configuration settings. For my examples, I showed how to do this by using both the AppCmd.exe application from a command line and by using the IIS Configuration Editor. In part 2 of this blog series, I will show you how to use the IIS Configuration Editor to add custom FTP providers to your FTP sites.

As a brief review from Part 1, the following XML excerpt illustrates what the provider's settings should resemble when added to your IIS settings:

<system.ftpServer>
  <providerDefinitions>
    <add name="FtpXmlAuthorization"
    
 type="FtpXmlAuthorization, FtpXmlAuthorization, version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73" />
    <activation>
      <providerData name="FtpXmlAuthorization">
        <add key="xmlFileName"
        
 value="C:\inetpub\FtpUsers\Users.xml" />
      </providerData>
    </activation>
  </providerDefinitions>
</system.ftpServer>

The above example shows the settings that are added globally to register an FTP provider. Note that this example only contains the settings for my custom provider; you would normally see the settings for the IisManagerAuth and AspNetAuth providers that ship with the FTP service in the providerDefinitions collection.

To actually use a provider for an FTP site, you would need to add the provider to the settings for the FTP site in your IIS settings. So for part 2 of this blog series, we will focus on how to add a custom provider to an FTP site by using the IIS Configuration Editor.

Having said all of that, the rest of this blog is broken down into the following sections:

Before continuing, I should reiterate that custom FTP providers fall into two categories: providers that are used for authentication and providers that are used for everything else. This distinction is important, because the settings are stored in different sections of your IIS settings. With that in mind, let's take a look at the settings for an example FTP site.

Step 1 - Looking at the configuration settings for custom FTP providers

The following example shows an FTP site with several custom FTP providers added:

<site name="ftp.contoso.com" id="2">
  <application path="/">
    <virtualDirectory path="/"
    
 physicalPath="c:\inetpub\www.contoso.com\wwwroot" />
  </application>
  <bindings>
    <binding protocol="ftp"
    
 bindingInformation="*:21:ftp.contoso.com" />
  </bindings>
  <ftpServer>
    <security>
      <ssl controlChannelPolicy="SslAllow"
      
 dataChannelPolicy="SslAllow" />
      <authentication>
        <customAuthentication>
          <providers>
            <add name="MyCustomFtpAuthenticationProvider" />
          </providers>
        </customAuthentication>
      </authentication>
    </security>
    <customFeatures>
      <providers>
        <add name="MyCustomFtpHomeDirectoryProvider" />
        <add name="MyCustomFtpLoggingProvider" />
      </providers>
    </customFeatures>
    <userIsolation mode="Custom" />
  </ftpServer>
</site>

If you look at the above example, you will notice the following providers have been added:

  • A custom FTP authentication provider named MyCustomFtpAuthenticationProvider has been added to the ftpServer/security/authentication/customAuthentication/providers collection; this provider will obviously be used by the FTP service to validate usernames and passwords.
  • A custom FTP home directory provider named MyCustomFtpHomeDirectoryProvider has been added to the ftpServer/customFeatures/providers collection; this will be used by the FTP service for custom user isolation. Note the mode for the userIsolation element is set to custom.
  • A custom FTP logging provider named MyCustomFtpLoggingProvider has been added to the ftpServer/customFeatures/providers collection; this will be used by the FTP service for creating custom log files.

As I mentioned earlier, you will notice that the settings for FTP custom providers are stored in different sections of the ftpServer collection depending on whether they are used for authentication or some other purpose.

Step 2 - Navigate to an FTP Site in the Configuration Editor

Open the IIS Manager and click on the Configuration Editor at feature the server level:

Click the Section drop-down menu, expand the the system.applicationHost collection, and then highlight the sites node:

If you click on the Collection row, an ellipsis [...] will appear:

When you click the ellipsis [...], IIS will display the Collection Editor dialog box for your sites; both HTTP and FTP sites will be displayed:

Expand the ftpServer node, which is where all of the site-level settings for an FTP site are kept.

Step 3 - Add custom FTP providers to an FTP site

As I mentioned earlier, custom FTP providers fall into two categories: providers that are used for authentication and everything else. Because of this distinction, the following steps show you how to add a provider to the correct section of your settings depending on the provider's purpose.

Add a custom FTP provider to an FTP site that is not used for authentication

Expand the customFeatures node, which is located under the ftpServer node for an FTP site; this collection defines the custom providers for an FTP site that are not used for authentication, for example: home directory providers, logging providers, etc. When you highlight the providers row, an ellipsis [...] will appear:

When you click the ellipsis [...], IIS will display the Collection Editor dialog box for your custom features (providers). When you click Add in the Actions pane, you need to enter the name of an FTP provider that you entered by following the instructions in Part 1 of this blog series:

Once you enter the name of your FTP provider in the Collection Editor dialog box for your custom features, you can close that dialog box. The Collection Editor for your sites will reflect the updated provider count for your FTP site:

Important Note: If you are adding a custom FTP Home Directory Provider, you have to configure the mode for FTP's User Isolation features. To do so, you need to expand the userIsolation node, which is located under the ftpServer node for an FTP site. Once you have done so, click the mode drop-down menu and choose Custom from the list of choices:

When you close the Collection Editor dialog box for your sites, you need to click Apply in the Actions pane to commit the changes to your IIS settings:

Add a custom FTP authentication provider to an FTP site

First and foremost - there is built-in support for adding custom authentication providers in IIS Manager; to see the steps to do so, see the FTP Custom Authentication <customAuthentication> article on the IIS.NET website. However, if you want to add a custom FTP authentication provider to an FTP site by using the IIS Configuration Editor, you can do so by using the following steps.

Expand the security node under the ftpServer node for an FTP site, then expand the authentication node, and then expand the customAuthentication node; this collection defines the custom authentication providers for an FTP site. When you highlight the providers row, an ellipsis [...] will appear:

When you click the ellipsis [...], IIS will display the Collection Editor dialog box for your custom authentication providers. When you click Add in the Actions pane, you need to enter the name of an FTP authentication provider that you entered by following the instructions in Part 1 of this blog series:

Once you enter the name of your FTP authentication provider in the Collection Editor dialog box for your custom authentication providers, you can close that dialog box. The Collection Editor for your sites will reflect the updated authentication provider count for your FTP site:

When you close the Collection Editor dialog box for your sites, you need to click Apply in the Actions pane to commit the changes to your IIS settings:

Summary and Parting Thoughts

As I mentioned in part 1 of this series, I admit that this might seem like a lot of steps to go through, but it's not that difficult once you understand how the configuration settings are organized and you get the hang of using the IIS Configuration Editor to add or modify these settings.

Disabling Custom User Isolation

In the Add a custom FTP provider to an FTP site that is not used for authentication section of this blog, I added a step to specify Custom as the User Isolation mode. Since this is something of an advanced feature, there is no user interface for enabling custom user isolation; this was a design decision to keep people from breaking their FTP sites. Here's why: if you enable custom user isolation and you don't install a custom Home Directory provider for FTP, all users will be denied access to your FTP site.

That being said, once you have enabled custom user isolation, the option to disable custom user isolation will "magically" appear in the FTP User Isolation feature in the IIS Manager. To see this for yourself, you would first need to follow the steps to custom user isolation in the Add a custom FTP provider to an FTP site that is not used for authentication section of this blog.

Once you have enabled custom user isolation, highlight your FTP site in the list of Sites pane of IIS Manager, then open the FTP User Isolation feature:

When you open the FTP User Isolation feature, you will see that an option for Custom now appears in the list of user isolation modes:

This option will appear as long as custom user isolation is enabled. If you change the user isolation mode to something other than Custom, this option will continue appear in the list of user isolation modes until you navigate somewhere else in IIS Manager. Once you have changed the user isolation mode to one of the built-in modes and you navigate somewhere else, the Custom option will not show up in the FTP User Isolation feature until you follow the steps to re-enable custom user isolation.

Additional Information

If you want additional information about configuring the settings for FTP providers, you can find detailed reference documentation at the following URLs:

Each of these articles contain "How-To" steps, detailed information about each of the configuration settings, and code samples for AppCmd.exe, C#/VB.NET, and JavaScript/VBScript.

As always, let me know if you have any questions. ;-]

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: May 02 2013, 11:31 by Bob | Comments (0) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: IIS | FTP | Extensibility
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Adding Custom FTP Providers with the IIS Configuration Editor - Part 1

I've written a lot of walkthroughs and blog posts about creating custom FTP providers over the past several years, and I usually include instructions for adding these custom providers to IIS. When you create a custom FTP authentication provider, IIS has a user interface for adding that provider to FTP. But if you are adding a custom home directory or logging provider, there is no dedicated user interface for adding those types of FTP providers. In addition, if you create a custom FTP provider that requires settings that are stored in your IIS configuration, there is no user interface to add or manage those settings.

With this in mind, I include instructions in my blogs and walkthroughs that describe how to add those type of providers by using AppCmd.exe from a command line. For example, if you take a look at my How to Use Managed Code (C#) to Create an FTP Authentication and Authorization Provider using an XML Database walkthrough, I include the following instructions:

Adding the Provider

  1. Determine the assembly information for the extensibility provider:
    • In Windows Explorer, open your "C:\Windows\assembly" path, where C: is your operating system drive.
    • Locate the FtpXmlAuthorization assembly.
    • Right-click the assembly, and then click Properties.
    • Copy the Culture value; for example: Neutral.
    • Copy the Version number; for example: 1.0.0.0.
    • Copy the Public Key Token value; for example: 426f62526f636b73.
    • Click Cancel.
  2. Using the information from the previous steps, add the extensibility provider to the global list of FTP providers and configure the options for the provider:
    • At the moment there is no user interface that enables you to add properties for custom authentication or authorization modules, so you will have to use the following command line:

      cd %SystemRoot%\System32\Inetsrv

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpXmlAuthorization',type='FtpXmlAuthorization,FtpXmlAuthorization,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpXmlAuthorization']" /commit:apphost

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpXmlAuthorization'].[key='xmlFileName',value='C:\Inetpub\XmlSample\Users.xml']" /commit:apphost
    • Note: The file path that you specify in the xmlFileName attribute must match the path where you saved the "Users.xml" file on your computer in the earlier in this walkthrough.

This example adds a custom FTP provider, and then it adds a custom setting for that provider that is stored in your IIS configuration settings.

That being said, there is actually a way to add custom FTP providers with settings like the ones that I have just described through the IIS interface by using the IIS Configuration Editor. This feature was first available through the IIS Administration Pack for IIS 7.0, and is built-in for IIS 7.5 and IIS 8.0.

Before I continue, if would probably be prudent to take a look at the settings that we are trying to add, because these settings might help you to understand the rest of steps in this blog. Here is an example from my applicationhost.config file for three custom FTP authentication providers; the first two providers are installed with the FTP service, and the third provider is a custom provider that I created with a single provider-specific configuration setting:

<system.ftpServer>
  <providerDefinitions>
    <add name="IisManagerAuth" type="Microsoft.Web.FtpServer.Security.IisManagerAuthenticationProvider, Microsoft.Web.FtpServer, version=7.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="AspNetAuth" type="Microsoft.Web.FtpServer.Security.AspNetFtpMembershipProvider, Microsoft.Web.FtpServer, version=7.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add name="FtpXmlAuthorization" type="FtpXmlAuthorization, FtpXmlAuthorization, version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73" />
    <activation>
      <providerData name="FtpXmlAuthorization">
        <add key="xmlFileName" value="C:\inetpub\FtpUsers\Users.xml" />
      </providerData>
    </activation>
  </providerDefinitions>
</system.ftpServer>

With that in mind, in part 1 of this blog series, I will show you how to use the IIS Configuration Editor to add a custom FTP provider with provider-specific configuration settings.

Step 1 - Open the IIS Manager and click on the Configuration Editor at feature the server level:

Step 2 - Click the Section drop-down menu, expand the the system.ftpServer collection, and then highlight the providerDefinitions node:

Step 3 - A default installation IIS with the FTP service should show a Count of 2 providers in the Collection row, and no settings in the activation row:

Step 4 - If you click on the Collection row, an ellipsis [...] will appear, and when you click that, IIS will display the Collection Editor dialog for FTP providers. By default you should see just the two built-in providers for the IisManagerAuth and AspNetAuth providers:

Step 5 - When you click Add in the Actions pane, you can enter the registration information for your provider. At a minimum you must provide a name for your provider, but you will need to enter either the clsid for a COM-based provider or the type for a managed-code provider:

Step 6 - When you close the Collection Editor dialog, the Count of providers in the Collection should now reflect the provider that we just added; click Apply in the Actions pane to save the changes:

Step 7 - If you click on the activation row, an ellipsis [...] will appear, and when you click that, IIS will display the Collection Editor dialog for provider data; this is where you will enter provider-specific settings. When you click Add in the Actions pane, you must specify the name for your provider's settings, and this name must match the exact name that you provided in Step 5 earlier:

Step 8 - If you click on the Collection row, an ellipsis [...] will appear, and when you click that, IIS will display the Collection Editor dialog for the activation data for an FTP provider. At a minimum you must provide a key for your provider, which will depend on the settings that your provider expects to retrieve from your configuration settings. (For example, in the XML file that I provided earlier, my FtpXmlAuthorization provider expects to retrieve the path to an XML that contains a list of users, roles, and authorization rules.) You also need to enter the either the value or encryptedValue for your provider; although you can specify either setting, should generally specify the value when the settings are not sensitive in nature, and specify the encryptedValue for settings like usernames and passwords:

Step 9 - When you close the Collection Editor dialog for the activation data, the Count of key/value pairs in the Collection should now reflect the value that we just added:

Step 10 - When you close the Collection Editor dialog for the provider data, the Count of provider data settings in the activation row should now reflect the custom settings that we just added; click Apply in the Actions pane to save the changes:

That's all that there is to adding a custom FTP provider with provider-specific settings; I admit that it might seem like a lot of steps until you get the hang of it.

In the next blog for this series, I will show you how to add custom providers to FTP sites by using the IIS Configuration Editor.

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Posted: Mar 31 2013, 16:03 by Bob | Comments (0) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: Extensibility | FTP | IIS
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Using Classic ASP and URL Rewrite for Dynamic SEO Functionality

I had another interesting situation present itself recently that I thought would make a good blog: how to use Classic ASP with the IIS URL Rewrite module to dynamically generate Robots.txt and Sitemap.xml files.

Overview

Here's the situation: I host a website for one of my family members, and like everyone else on the Internet, he wanted some better SEO rankings. We discussed a few things that he could do to improve his visibility with search engines, and one of the suggestions that I gave him was to keep his Robots.txt and Sitemap.xml files up-to-date. But there was an additional caveat - he uses two separate DNS names for the same website, and that presents a problem for absolute URLs in either of those files. Before anyone points out that it's usually not a good idea to host multiple DNS names on the same content, there are times when this is acceptable; for example, if you are trying to decide which of several DNS names is the best to use, you might want to bind each name to the same IP address and parse your logs to find out which address is getting the most traffic.

In any event, the syntax for both Robots.txt and Sitemap.xml files is pretty easy, so I wrote a couple of simple Classic ASP Robots.asp and Sitemap.asp pages that output the correct syntax and DNS-specific URLs for each domain name, and I wrote some simple URL Rewrite rules that rewrite inbound requests for Robots.txt and Sitemap.xml files to the ASP pages, while blocking direct access to the Classic ASP pages themselves.

All of that being said, there are a couple of quick things that I would like to mention before I get to the code:

  • First of all, I chose Classic ASP for the files because it allows the code to run without having to load any additional framework; I could have used ASP.NET or PHP just as easily, but either of those would require additional overhead that isn't really required.
  • Second, the specific website for which I wrote these specific examples consists of all static content that is updated a few times a month, so I wrote the example to parse the physical directory structure for the website's URLs and specified a weekly interval for search engines to revisit the website. All of these options can easily be changed; for example, I reused this code a little while later for a website where all of the content was created dynamically from a database, and I updated the code in the Sitemap.asp file to create the URLs from the dynamically-generated content. (That's really easy to do, but outside the scope of this blog.)

That being said, let's move on to the actual code.

Creating the Required Files

There are three files that you will need to create for this example:

  1. A Robots.asp file to which URL Rewrite will send requests for Robots.txt
  2. A Sitemap.asp file to which URL Rewrite will send requests for Sitemap.xml
  3. A Web.config file that contains the URL Rewrite rules

Step 1 - Creating the Robots.asp File

You need to save the following code sample as Robots.asp in the root of your website; this page will be executed whenever someone requests the Robots.txt file for your website. This example is very simple: it checks for the requested hostname and uses that to dynamically create the absolute URL for the website's Sitemap.xml file.

<%
    Option Explicit
    On Error Resume Next
    
    Dim strUrlRoot
    Dim strHttpHost
    Dim strUserAgent

    Response.Clear
    Response.Buffer = True
    Response.ContentType = "text/plain"
    Response.CacheControl = "public"

    Response.Write "# Robots.txt" & vbCrLf
    Response.Write "# For more information on this file see:" & vbCrLf
    Response.Write "# http://www.robotstxt.org/" & vbCrLf & vbCrLf

    strHttpHost = LCase(Request.ServerVariables("HTTP_HOST"))
    strUserAgent = LCase(Request.ServerVariables("HTTP_USER_AGENT"))
    strUrlRoot = "http://" & strHttpHost

    Response.Write "# Define the sitemap path" & vbCrLf
    Response.Write "Sitemap: " & strUrlRoot & "/sitemap.xml" & vbCrLf & vbCrLf

    Response.Write "# Make changes for all web spiders" & vbCrLf
    Response.Write "User-agent: *" & vbCrLf
    Response.Write "Allow: /" & vbCrLf
    Response.Write "Disallow: " & vbCrLf
    Response.End
%>

Step 2 - Creating the Sitemap.asp File

The following example file is also pretty simple, and you would save this code as Sitemap.asp in the root of your website. There is a section in the code where it loops through the file system looking for files with the *.html file extension and only creates URLs for those files. If you want other files included in your results, or you want to change the code from static to dynamic content, this is where you would need to update the file accordingly.

<%
    Option Explicit
    On Error Resume Next
    
    Response.Clear
    Response.Buffer = True
    Response.AddHeader "Connection", "Keep-Alive"
    Response.CacheControl = "public"
    
    Dim strFolderArray, lngFolderArray
    Dim strUrlRoot, strPhysicalRoot, strFormat
    Dim strUrlRelative, strExt

    Dim objFSO, objFolder, objFile

    strPhysicalRoot = Server.MapPath("/")
    Set objFSO = Server.CreateObject("Scripting.Filesystemobject")
    
    strUrlRoot = "http://" & Request.ServerVariables("HTTP_HOST")
    
    ' Check for XML or TXT format.
    If UCase(Trim(Request("format")))="XML" Then
        strFormat = "XML"
        Response.ContentType = "text/xml"
    Else
        strFormat = "TXT"
        Response.ContentType = "text/plain"
    End If

    ' Add the UTF-8 Byte Order Mark.
    Response.Write Chr(CByte("&hEF"))
    Response.Write Chr(CByte("&hBB"))
    Response.Write Chr(CByte("&hBF"))
    
    If strFormat = "XML" Then
        Response.Write "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf
        Response.Write "<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">" & vbCrLf
    End if
    
    ' Always output the root of the website.
    Call WriteUrl(strUrlRoot,Now,"weekly",strFormat)

    ' --------------------------------------------------
    ' This following section contains the logic to parse
    ' the directory tree and return URLs based on the
    ' static *.html files that it locates. This is where
    ' you would change the code for dynamic content.
    ' -------------------------------------------------- 
    strFolderArray = GetFolderTree(strPhysicalRoot)

    For lngFolderArray = 1 to UBound(strFolderArray)
        strUrlRelative = Replace(Mid(strFolderArray(lngFolderArray),Len(strPhysicalRoot)+1),"\","/")
        Set objFolder = objFSO.GetFolder(Server.MapPath("." & strUrlRelative))
        For Each objFile in objFolder.Files
            strExt = objFSO.GetExtensionName(objFile.Name)
            If StrComp(strExt,"html",vbTextCompare)=0 Then
                If StrComp(Left(objFile.Name,6),"google",vbTextCompare)<>0 Then
                    Call WriteUrl(strUrlRoot & strUrlRelative & "/" & objFile.Name, objFile.DateLastModified, "weekly", strFormat)
                End If
            End If
        Next
    Next

    ' --------------------------------------------------
    ' End of file system loop.
    ' --------------------------------------------------     
    If strFormat = "XML" Then
        Response.Write "</urlset>"
    End If
    
    Response.End

    ' ======================================================================
    '
    ' Outputs a sitemap URL to the client in XML or TXT format.
    ' 
    ' tmpStrFreq = always|hourly|daily|weekly|monthly|yearly|never 
    ' tmpStrFormat = TXT|XML
    '
    ' ======================================================================

    Sub WriteUrl(tmpStrUrl,tmpLastModified,tmpStrFreq,tmpStrFormat)
        On Error Resume Next
        Dim tmpDate : tmpDate = CDate(tmpLastModified)
        ' Check if the request is for XML or TXT and return the appropriate syntax.
        If tmpStrFormat = "XML" Then
            Response.Write " <url>" & vbCrLf
            Response.Write " <loc>" & Server.HtmlEncode(tmpStrUrl) & "</loc>" & vbCrLf
            Response.Write " <lastmod>" & Year(tmpLastModified) & "-" & Right("0" & Month(tmpLastModified),2) & "-" & Right("0" & Day(tmpLastModified),2) & "</lastmod>" & vbCrLf
            Response.Write " <changefreq>" & tmpStrFreq & "</changefreq>" & vbCrLf
            Response.Write " </url>" & vbCrLf
        Else
            Response.Write tmpStrUrl & vbCrLf
        End If
    End Sub

    ' ======================================================================
    '
    ' Returns a string array of folders under a root path
    '
    ' ======================================================================

    Function GetFolderTree(strBaseFolder)
        Dim tmpFolderCount,tmpBaseCount
        Dim tmpFolders()
        Dim tmpFSO,tmpFolder,tmpSubFolder
        ' Define the initial values for the folder counters.
        tmpFolderCount = 1
        tmpBaseCount = 0
        ' Dimension an array to hold the folder names.
        ReDim tmpFolders(1)
        ' Store the root folder in the array.
        tmpFolders(tmpFolderCount) = strBaseFolder
        ' Create file system object.
        Set tmpFSO = Server.CreateObject("Scripting.Filesystemobject")
        ' Loop while we still have folders to process.
        While tmpFolderCount <> tmpBaseCount
            ' Set up a folder object to a base folder.
            Set tmpFolder = tmpFSO.GetFolder(tmpFolders(tmpBaseCount+1))
              ' Loop through the collection of subfolders for the base folder.
            For Each tmpSubFolder In tmpFolder.SubFolders
                ' Increment the folder count.
                tmpFolderCount = tmpFolderCount + 1
                ' Increase the array size
                ReDim Preserve tmpFolders(tmpFolderCount)
                ' Store the folder name in the array.
                tmpFolders(tmpFolderCount) = tmpSubFolder.Path
            Next
            ' Increment the base folder counter.
            tmpBaseCount = tmpBaseCount + 1
        Wend
        GetFolderTree = tmpFolders
    End Function
%>

Note: There are two helper methods in the preceding example that I should call out:

  • The GetFolderTree() function returns a string array of all the folders that are located under a root folder; you could remove that function if you were generating all of your URLs dynamically.
  • The WriteUrl() function outputs an entry for the sitemap file in either XML or TXT format, depending on the file type that is in use. It also allows you to specify the frequency that the specific URL should be indexed (always, hourly, daily, weekly, monthly, yearly, or never).

Step 3 - Creating the Web.config File

The last step is to add the URL Rewrite rules to the Web.config file in the root of your website. The following example is a complete Web.config file, but you could merge the rules into your existing Web.config file if you have already created one for your website. These rules are pretty simple, they rewrite all inbound requests for Robots.txt to Robots.asp, and they rewrite all requests for Sitemap.xml to Sitemap.asp?format=XML and requests for Sitemap.txt to Sitemap.asp?format=TXT; this allows requests for both the XML-based and text-based sitemaps to work, even though the Robots.txt file contains the path to the XML file. The last part of the URL Rewrite syntax returns HTTP 404 errors if anyone tries to send direct requests for either the Robots.asp or Sitemap.asp files; this isn't absolutely necesary, but I like to mask what I'm doing from prying eyes. (I'm kind of geeky that way.)

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rewriteMaps>
        <clear />
        <rewriteMap name="Static URL Rewrites">
          <add key="/robots.txt" value="/robots.asp" />
          <add key="/sitemap.xml" value="/sitemap.asp?format=XML" />
          <add key="/sitemap.txt" value="/sitemap.asp?format=TXT" />
        </rewriteMap>
        <rewriteMap name="Static URL Failures">
          <add key="/robots.asp" value="/" />
          <add key="/sitemap.asp" value="/" />
        </rewriteMap>
      </rewriteMaps>
      <rules>
        <clear />
        <rule name="Static URL Rewrites" patternSyntax="ECMAScript" stopProcessing="true">
          <match url=".*" ignoreCase="true" negate="false" />
          <conditions>
            <add input="{Static URL Rewrites:{REQUEST_URI}}" pattern="(.+)" />
          </conditions>
          <action type="Rewrite" url="{C:1}" appendQueryString="false" redirectType="Temporary" />
        </rule>
        <rule name="Static URL Failures" patternSyntax="ECMAScript" stopProcessing="true">
          <match url=".*" ignoreCase="true" negate="false" />
          <conditions>
            <add input="{Static URL Failures:{REQUEST_URI}}" pattern="(.+)" />
          </conditions>
          <action type="CustomResponse" statusCode="404" subStatusCode="0" />
        </rule>
        <rule name="Prevent rewriting for static files" patternSyntax="Wildcard" stopProcessing="true">
          <match url="*" />
          <conditions>
             <add input="{REQUEST_FILENAME}" matchType="IsFile" />
          </conditions>
          <action type="None" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

Summary

That sums it up for this blog; I hope that you get some good ideas from it.

For more information about the syntax in Robots.txt and Sitemap.xml files, see the following URLs:

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Dec 31 2012, 08:09 by Bob | Comments (0) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: IIS | URL Rewrite | SEO | Classic ASP
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Error: Class Not Registered (0x80040154) when Querying FTP Runtime State

I had a great question from a customer earlier today, and I thought that it was worth blogging about. The problem that he was running into was that he was seeing the following error when he was trying to query the runtime state for the FTP service in an application that he was writing:

Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG))

He was using Visual Basic, and his code looked okay to me, so for the moment I was stumped.

I'm more of a C# guy, and I remembered that I had written the following blog many years ago:

Viewing current FTP7 sessions using C#

I copied the code from that blog into a new Visual Studio project, and I got the same error that he was seeing when I ran my code - this had me a little more confused. Have you ever said to yourself, "Darn - I know that worked the other day...?" ;-]

I knew that there is more than one way to access the runtime state, so I rewrote my sample application using two different approaches:

Method #1:

AppHostAdminManager objAdminManager = new AppHostAdminManager();
IAppHostElement objSitesElement =
  objAdminManager.GetAdminSection("system.applicationHost/sites",
  "MACHINE/WEBROOT/APPHOST");
uint intSiteCount = objSitesElement.Collection.Count;
for (int intSite = 0; intSite < intSiteCount; ++intSite)
{
    IAppHostElement objFtpSite = objSitesElement.Collection[intSite];
    Console.WriteLine("Name: " + objFtpSite.Properties["name"].StringValue);
    IAppHostElement objFtpSiteElement = objFtpSite.ChildElements["ftpServer"];
    IAppHostPropertyCollection objProperties = objFtpSiteElement.Properties;
    try
    {
        IAppHostProperty objState = objProperties["state"];
        string ftpState = objState.StringValue;
        Console.WriteLine("State: " + ftpState);
    }
    catch (System.Exception ex)
    {
        Console.WriteLine("\r\nError: {0}", ex.Message);
    }
}

Method #2:

ServerManager manager = new ServerManager();
foreach (Site site in manager.Sites)
{
    Console.WriteLine("Name: " + site.Name);
    ConfigurationElement ftpServer = site.GetChildElement("ftpServer");
    try
    {
        foreach (ConfigurationAttribute attrib in ftpServer.Attributes)
        {
            Console.WriteLine(attrib.Name + ": " + attrib.Value);
        }
    }
    catch (System.Exception ex)
    {
        Console.WriteLine("\r\nError: {0}", ex.Message);
    }
}

Both of these methods returned the same COM error, so this was getting weird for me. Hmm...

The FTP runtime state is exposed through a COM interface, and that is implemented in a DLL that is named "ftpconfigext.dll". That file should be registered when you install IIS, and I re-registered it on my system just for good measure, but that didn't resolve the issue.

I had a brief conversation with one of my coworkers, Eok Kim, about the error that I was seeing. He also suggested re-registering the DLL, but something else that he said about searching the registry for the InprocServer32 entry made me wonder if the whole problem was related to the bitness of my application.

To make a long story short - that was the whole problem.

Both the customer and I were creating 32-bit .NET applications, and the COM interface for the FTP runtime state is implemented in a 64-bit-only DLL. Once we both changed our projects to compile for 64-bit platforms, we were both able to get the code to run. (Coincidentally, all I had was a 32-bit system when I wrote my original blog, so I probably would have run into this sooner if I had owned a 64-bit system way back then. ;-])

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Posted: Jun 29 2012, 12:01 by Bob | Comments (0) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: FTP | IIS
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Configuring FTP Client Certificate Authentication in FTP 7

We had a customer question the other day about configuring FTP Client Certificate Authentication in FTP 7.0 and  in FTP 7.5. It had been a while since the last time that I had configured those settings on an FTP server, so I thought that it would be great to re-familiarize myself with that feature. To my initial dismay, it was a little more difficult than I had remembered, because there are a lot of parts to be configured.

That being said, there are a few primary activities that you need to know about and configure correctly:

I will explain each of those in this blog, although I will defer some of the details for Active Directory mapping to an excellent blog series that I discovered by Vivek Kumbhar.

Configuring the FTP Service

There are several settings that you need to configure for the FTP server; unfortunately there is no user interface for those settings, so you might want to familiarize yourself with the following settings:

At first I had made a batch file that was configuring these settings by using AppCmd, but I eventually abandoned that script and wrote the following VBScript code to configure all of the settings at one time - the only parts that you need to change is your site name and the hash value your SSL certificate, which are highlighted in yellow:

Set adminManager = CreateObject("Microsoft.ApplicationHost.WritableAdminManager")
adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST"
Set sitesSection = adminManager.GetAdminSection("system.applicationHost/sites", "MACHINE/WEBROOT/APPHOST")
Set sitesCollection = sitesSection.Collection

siteElementPos = FindElement(sitesCollection, "site", Array("name", "ftp.contoso.com"))
If (addElementPos = -1) Then
   WScript.Echo "Element not found!"
   WScript.Quit
End If
Set siteElement = sitesCollection.Item(siteElementPos)

Set ftpServerElement = siteElement.ChildElements.Item("ftpServer")
Set securityElement = ftpServerElement.ChildElements.Item("security")

Set sslClientCertificatesElement = securityElement.ChildElements.Item("sslClientCertificates")
sslClientCertificatesElement.Properties.Item("clientCertificatePolicy").Value = "CertRequire"
sslClientCertificatesElement.Properties.Item("useActiveDirectoryMapping").Value = True

Set authenticationElement = securityElement.ChildElements.Item("authentication")
Set clientCertAuthenticationElement = authenticationElement.ChildElements.Item("clientCertAuthentication")
clientCertAuthenticationElement.Properties.Item("enabled").Value = True

Set sslElement = securityElement.ChildElements.Item("ssl")
sslElement.Properties.Item("serverCertHash").Value = "57686f6120447564652c2049495320526f636b73"
sslElement.Properties.Item("controlChannelPolicy").Value = "SslRequire"
sslElement.Properties.Item("dataChannelPolicy").Value = "SslRequire"

adminManager.CommitChanges

Function FindElement(collection, elementTagName, valuesToMatch)
   For i = 0 To CInt(collection.Count) - 1
      Set element = collection.Item(i)
      If element.Name = elementTagName Then
         matches = True
         For iVal = 0 To UBound(valuesToMatch) Step 2
            Set property = element.GetPropertyByName(valuesToMatch(iVal))
            value = property.Value
            If Not IsNull(value) Then
               value = CStr(value)
            End If
            If Not value = CStr(valuesToMatch(iVal + 1)) Then
               matches = False
               Exit For
            End If
         Next
         If matches Then
            Exit For
         End If
      End If
   Next
   If matches Then
      FindElement = i
   Else
      FindElement = -1
   End If
End Function

Once you have configured your FTP settings, you should have an FTP site that resembles the following in your ApplicationHost.config file:

<site name="ftp.contoso.com" id="2">
   <application path="/">
      <virtualDirectory path="/" physicalPath="c:\inetpub\ftproot" />
   </application>
   <bindings>
      <binding protocol="ftp" bindingInformation="*:21:" />
   </bindings>
   <ftpServer>
      <security>
         <ssl serverCertHash="57686f6120447564652c2049495320526f636b73"  ssl128="false"  controlChannelPolicy="SslRequire"  dataChannelPolicy="SslRequire" />
         <authentication>
            <basicAuthentication enabled="false" />
            <anonymousAuthentication enabled="false" />
            <clientCertAuthentication enabled="true" />
         </authentication>
         <sslClientCertificates  clientCertificatePolicy="CertRequire"  useActiveDirectoryMapping="true" />
      </security>
   </ftpServer>
</site>

More details about these settings can be found in the configuration reference articles that I mentioned in the beginning of this blog post, and additional information about configuring FTP over SSL can be found in the following walkthrough:

Configuring Active Directory Mapping

The next part of this process is kind of tricky; you need to accomplish all of the following:

  • Obtain and install a client certificate on the system where your FTP client is installed. Hare some additional notes to consider:
    • This may involve setting up your client system to trust the CA that issued your client certificate.
    • This may also involve setting up your FTP server to trust the CA that issued both your client certificate and the server certificate that you are using for your FTP site.
  • Configure Active Directory to map the client certificate to an Active Directory account.
  • Configure your FTP client to use a client certificate when connecting to your FTP server.

That makes it all sound so easy, but it can be very tricky. That being said, as I mentioned earlier, as I was putting together my notes to write this blog, I stumbled across a great blog series by Vivek Kumbhar, where he goes into great detail when describing all of the steps to set up the Active Directory mapping. With that in mind, instead of trying to rewrite what Vivek has already documented, I will include links to his blog series:

I have to give Vivek full credit where it's due - he wrote a truly great blog series, and he included a lot more detail in his blog series than I had originally planned to include in this blog. (In my humble opinion, Vivek's blog series is the best documentation that I have seen for this feature.)

Configuring your FTP Client

To test out client certificates, I used both the SmartFTP GUI-based FTP client and the MOVEit-Freely command-line FTP client; both of which I discussed in my FTP Clients blog series some time ago.

Using the SmartFTP Client

To configure the SmartFTP client, I just needed to enable and specify the correct client certificate in the properties for my connection:

Using the MOVEit-Freely FTP Client

For the MOVEit-Freely FTP client, I just needed to specify the correct parameters on the command line:

ftps.exe -z -e:on -pfxfile:administrator.pfx -pfxpw:"P@ssw0rd" -user:anonymous -password:"someone@contoso.com"

The important settings are the pfxfile and pfxpw values, where pfxfile is the name of the PFX file that holds your client certificate, and pfxpw is the password for the PFX file. (The username and password values will be ignored for the most part, because you will actually be logged in through your client certificate, so you can leave those as anonymous.)

Client Recap

For more information about these two FTP clients, see the following blog posts:

Summary

FTP client certificates are definitely a bit of a challenge to configure correctly, but it's not an impossible task to get this feature working.

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Apr 25 2012, 21:36 by Bob | Comments (0) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: FTP | Scripting
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Programmatically Flushing FTP Logs

I had a great question from Scott Forsyth earlier today about programmatically flushing the logs for an FTP site. Scott had noticed that there was a FlushLog method listed on the following page in the IIS Configuration Reference:

http://www.iis.net/ConfigReference/system.applicationHost/sites/site/ftpServer

Unfortunately there wasn't a code sample for that method; but as luck would have it, I had already written some code to do just that. (I love synchronicity...) With that in mind, I though that I'd post the code in a blog. In keeping with the cross-language samples that I wrote for the topics in the Configuration Reference, I thought that's I'd include several languages in this blog to make it easier for someone else to copy and paste.

C#

using System;
using System.Text;
using Microsoft.Web.Administration;

internal static class Sample
{
private static void Main()
{
using (ServerManager serverManager = new ServerManager())
{
Configuration config = serverManager.GetApplicationHostConfiguration();
// Retrieve the sites collection.
ConfigurationSection sitesSection = config.GetSection("system.applicationHost/sites");
ConfigurationElementCollection sitesCollection = sitesSection.GetCollection();

// Locate a specific site.
ConfigurationElement siteElement = FindElement(sitesCollection,"site","name",@"ftp.contoso.com");
if (siteElement == null) throw new InvalidOperationException("Element not found!");

// Create an object for the ftpServer element.
ConfigurationElement ftpServerElement = siteElement.GetChildElement("ftpServer");
// Create an instance of the FlushLog method.
ConfigurationMethodInstance FlushLog = ftpServerElement.Methods["FlushLog"].CreateInstance();
// Execute the method to flush the logs for the FTP site.
FlushLog.Execute();
}
}

// Locate and return the index for a specific element in a collection.
private static ConfigurationElement FindElement(ConfigurationElementCollection collection, string elementTagName, params string[] keyValues)
{
foreach (ConfigurationElement element in collection)
{
if (String.Equals(element.ElementTagName, elementTagName, StringComparison.OrdinalIgnoreCase))
{
bool matches = true;
for (int i = 0; i < keyValues.Length; i += 2)
{
object o = element.GetAttributeValue(keyValues[i]);
string value = null;
if (o != null)
{
value = o.ToString();
}
if (!String.Equals(value, keyValues[i + 1], StringComparison.OrdinalIgnoreCase))
{ matches = false;
break;
}
}
if (matches)
{
return element;
}
}
}
return null;
}
}

VB.NET

Imports System
Imports System.Text
Imports Microsoft.Web.Administration

Module Sample
Sub Main()
Dim serverManager As ServerManager = New ServerManager
Dim config As Configuration = serverManager.GetApplicationHostConfiguration
' Retrieve the sites collection.
Dim sitesSection As ConfigurationSection = config.GetSection("system.applicationHost/sites")
Dim sitesCollection As ConfigurationElementCollection = sitesSection.GetCollection

' Locate a specific site.
Dim siteElement As ConfigurationElement = FindElement(sitesCollection,"site","name","ftp.contoso.com")
If (siteElement Is Nothing) Then
Throw New InvalidOperationException("Element not found!")
End If

' Create an object for the ftpServer element.
Dim ftpServerElement As ConfigurationElement = siteElement.GetChildElement("ftpServer")
' Create an instance of the FlushLog method.
Dim FlushLog As ConfigurationMethodInstance = ftpServerElement.Methods("FlushLog").CreateInstance()
' Execute the method to flush the logs for the FTP site.
FlushLog.Execute()

End Sub

' Locate and return the index for a specific element in a collection.
Private Function FindElement(ByVal collection As ConfigurationElementCollection, ByVal elementTagName As String, ByVal ParamArray keyValues() As String) As ConfigurationElement
For Each element As ConfigurationElement In collection
If String.Equals(element.ElementTagName, elementTagName, StringComparison.OrdinalIgnoreCase) Then
Dim matches As Boolean = True
Dim i As Integer
For i = 0 To keyValues.Length - 1 Step 2
Dim o As Object = element.GetAttributeValue(keyValues(i))
Dim value As String = Nothing
If (Not (o) Is Nothing) Then
value = o.ToString
End If
If Not String.Equals(value, keyValues((i + 1)), StringComparison.OrdinalIgnoreCase) Then
matches = False
Exit For
End If
Next
If matches Then
Return element
End If
End If
Next
Return Nothing
End Function

End Module

JavaScript

// Create a Writable Admin Manager object.
var adminManager = new ActiveXObject('Microsoft.ApplicationHost.WritableAdminManager');
adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST";

// Retrieve the sites collection.
var sitesSection = adminManager.GetAdminSection("system.applicationHost/sites","MACHINE/WEBROOT/APPHOST");
var sitesCollection = sitesSection.Collection;

// Locate a specific site.
var siteElementPos = FindElement(sitesCollection,"site",["name","ftp.contoso.com"]);
if (siteElementPos == -1) throw "Element not found!";

// Retrieve the site element.
var siteElement = sitesCollection.Item(siteElementPos);
// Create an object for the ftpServer element.
var ftpServerElement = siteElement.ChildElements.Item("ftpServer");
// Create an instance of the FlushLog method.
var FlushLog = ftpServerElement.Methods.Item("FlushLog").CreateInstance();
// Execute the method to flush the logs for the FTP site.
FlushLog.Execute();

// Locate and return the index for a specific element in a collection.
function FindElement(collection, elementTagName, valuesToMatch) {
for (var i = 0; i < collection.Count; i++) {
var element = collection.Item(i);
if (element.Name == elementTagName) {
var matches = true;
for (var iVal = 0; iVal < valuesToMatch.length; iVal += 2) {
var property = element.GetPropertyByName(valuesToMatch[iVal]);
var value = property.Value;
if (value != null) {
value = value.toString();
}
if (value != valuesToMatch[iVal + 1]) {
matches = false;
break;
}
}
if (matches) {
return i;
}
}
}
return -1;
}

VBScript

' Create a Writable Admin Manager object.
Set adminManager = CreateObject("Microsoft.ApplicationHost.WritableAdminManager")
adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST"

' Retrieve the sites collection.
Set sitesSection = adminManager.GetAdminSection("system.applicationHost/sites","MACHINE/WEBROOT/APPHOST")
Set sitesCollection = sitesSection.Collection

' Locate a specific site.
siteElementPos = FindElement(sitesCollection,"site",Array("name","ftp.contoso.com"))
If siteElementPos = -1 Then
WScript.Echo "Element not found!"
WScript.Quit
End If

' Retrieve the site element.
Set siteElement = sitesCollection.Item(siteElementPos)
' Create an object for the ftpServer element.
Set ftpServerElement = siteElement.ChildElements.Item("ftpServer")
' Create an instance of the FlushLog method.
Set FlushLog = ftpServerElement.Methods.Item("FlushLog").CreateInstance()
' Execute the method to flush the logs for the FTP site.
FlushLog.Execute()

' Locate and return the index for a specific element in a collection.
Function FindElement(collection, elementTagName, valuesToMatch)
For i = 0 To CInt(collection.Count) - 1
Set element = collection.Item(i)
If element.Name = elementTagName Then
matches = True
For iVal = 0 To UBound(valuesToMatch) Step 2
Set property = element.GetPropertyByName(valuesToMatch(iVal))
value = property.Value
If Not IsNull(value) Then
value = CStr(value)
End If
If Not value = CStr(valuesToMatch(iVal + 1)) Then
matches = False
Exit For
End If
Next
If matches Then
Exit For
End If
End If
Next
If matches Then
FindElement = i
Else
FindElement = -1 End If
End Function

Summary

Hopefully this gives you an idea of how to call the FlushLog method. You can also use these examples to call the Start and Stop methods for FTP sites; you just need to substitute the correct method in place of the FlushLog method.


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Posted: Feb 03 2012, 10:21 by Bob | Comments (1) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: IIS | Scripting
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Using URL Rewrite to Insert Different Scripts Based on Browser Type

I just stumbled across a piece of sample code that I had written several months ago for a coworker, and I thought that I'd share it with everyone.

Here's the scenario: my coworker asked me if it was possible to have different client-side scripts inserted dynamically depending on the type of web browser that is being used. If the application was written in ASP.NET or some other dynamic language, then it would be trivial to determine the browser type and return the correct HTML <script> block to the client. Unfortunately, he needed the script for both static and dynamic pages, and he didn't want to modify all of his pages unless absolutely necessary.

This sounded to me like a job for a pair of outbound rules in URL Rewrite.

With the above design requirements in mind, I wrote the following sample configuration for URL Rewrite that accomplishes the following tasks:

  • If you’re using Internet Explorer, it inserts VBScript to display a popup message.
  • If you’re not using Internet Explorer, it inserts JavaScript to display a different popup message.

In order to use this sample code, you need to open the web.config file for your application and add the following code for the URL Rewrite rules. (Note: You need to disable compression in order to avoid an HTTP 500.52 error, and the following sample code does just that.)

<system.webServer>
  <rewrite>
    <outboundRules>
      <rule name="Add JavaScript" preCondition="IsNotInternetExplorer" patternSyntax="ExactMatch">
        <match filterByTags="None" pattern="&lt;/body>" />
        <action type="Rewrite" value="&lt;script language=&quot;javascript&quot;>alert('You are not using Internet Explorer!');&lt;/script>&lt;/body>" />
      </rule>
      <rule name="Add VBScript" preCondition="IsInternetExplorer" patternSyntax="ExactMatch">
        <match filterByTags="None" pattern="&lt;/body>" />
        <action type="Rewrite" value="&lt;script language=&quot;vbscript&quot;>MsgBox &quot;You are using Internet Explorer!&quot;&lt;/script>&lt;/body>" />
      </rule>
      <preConditions>
        <preCondition name="IsInternetExplorer">
          <add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
          <add input="{HTTP_USER_AGENT}" pattern=".*MSIE" negate="false" />
        </preCondition>
        <preCondition name="IsNotInternetExplorer">
          <add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
          <add input="{HTTP_USER_AGENT}" pattern=".*MSIE" negate="true" />
        </preCondition>
      </preConditions>
    </outboundRules>
  </rewrite>
  <urlCompression doStaticCompression="false" doDynamicCompression="false" />
</system.webServer>

The above example is what I sent to my coworker, and I intended it as an easy place to start when you just want a simple chunk of script to be inserted. It works well, but a better example would be to have it dynamically insert code for an external script file based on the browser type. This is illustrated in the following example:

<system.webServer>
  <rewrite>
    <outboundRules>
      <rule name="For Other Browsers" preCondition="IsNotInternetExplorer" patternSyntax="ExactMatch">
        <match filterByTags="None" pattern="&lt;/body>" />
        <action type="Rewrite" value="&lt;script language=&quot;javascript&quot; src=&quot;other.js&quot;>&lt;/script>&lt;/body>" />
      </rule>
      <rule name="For Internet Explorer" preCondition="IsInternetExplorer" patternSyntax="ExactMatch">
        <match filterByTags="None" pattern="&lt;/body>" />
        <action type="Rewrite" value="&lt;script language=&quot;javascript&quot; src=&quot;msie.js&quot;>&lt;/script>&lt;/body>" />
      </rule>
      <preConditions>
        <preCondition name="IsInternetExplorer">
          <add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
          <add input="{HTTP_USER_AGENT}" pattern=".*MSIE" negate="false" />
        </preCondition>
        <preCondition name="IsNotInternetExplorer">
          <add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
          <add input="{HTTP_USER_AGENT}" pattern=".*MSIE" negate="true" />
        </preCondition>
      </preConditions>
    </outboundRules>
  </rewrite>
  <urlCompression doStaticCompression="false" doDynamicCompression="false" />
</system.webServer>

The above sample dynamically inserts an HTML <script> block, and specifies one script file ("msie.js") for Internet Explorer a different script  file ("other.js") for all other browsers.

A simple script for a simple task - just the way I like it. ;-]

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Feb 02 2012, 21:14 by Bob | Comments (0) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: IIS | Scripting
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Changing the Identity of the FTP 7 Extensibility Process

Many IIS 7 FTP developers may not have noticed, but all custom FTP 7 extensibility providers execute through COM+ in a DLLHOST.exe process, which runs as NETWORK SERVICE by default. That being said, NETWORK SERVICE does not always have the right permissions to access some of the areas on your system where you may be attempt to implement custom functionality. What this means is, some of the custom features that you try to implement may not work as expected.

For example, if you look at the custom FTP logging provider in following walkthrough, the provider may not have sufficient permissions to create log files in the folder that you specify:

How to Use Managed Code (C#) to Create a Simple FTP Logging Provider

There are a couple of ways that you can resolve this issue:

  1. First of all, you could grant NETWORK SERVICE permissions to the destination folder.
  2. Second, you could change the identity of the FTP extensibility process so that it runs as a user that has permissions for the destination folder.

For what it's worth, I usually change the identity of the FTP 7 extensibility process on my servers so that I can set custom permissions for situations like this.

Here's how you do that:

  • Create a user account that is only a member of the built-in Guests group, that way you're always using an extremely low-privileged account on your system. (You can also set custom security policies for that account, but that's outside the cope of this blog.)
  • Open Administrative Tools on your Windows system and double-click Component Services.

  • Expand Component Services, then expand Computers, then My Computer, and then highlight COM+ Applications.

  • Right-click Microsoft FTP Publishing Service Extensibility Host and then click Properties.

  • Click the Identity tab, and then click the This userradio button.

  • Enter the credentials for the low-privileged user account that you created earlier, and then click OK.

Once you have done this, you can set permissions for this account whenever you need to specify permissions for situations like I described earlier.

Personally, I prefer to change the identity of the FTP 7 extensibility process instead of granting NETWORK SERVICE more permissions than it probably needs.

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Posted: Dec 01 2011, 06:22 by Bob | Comments (0) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: FTP | IIS
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

How to use Managed Code (C#) to create an FTP Home Directory Provider that is based on the Remote Client IP Address

I recently had an interesting scenario that was presented to me by a customer: they had a business requirement where they needed to give the same username and password to a group of people, but they didn't want any two people to be able to see anyone else's files. This seemed like an unusual business requirement to me; the whole point of keeping users separate is one of the reasons why we added user isolation to the FTP service.

With that in mind, my first suggestion was - of course - to rethink their business requirement, assign different usernames and passwords to everyone, and use FTP user isolation. But that wasn't going to work for them; their business requirement for giving out the same username and password could not be avoided. So I said that I would get back to them, and I spent the next few days experimenting with a few ideas.

One of my early ideas that seemed somewhat promising was to write a custom home directory provider that dynamically created unique home directories that were based on the session IDs for the individual FTP sessions, and the provider would use those directories to isolate the users. That seemed like a good idea, but when I analyzed the results I quickly saw that it wasn't going to work; as each user logged in, they would get a new session ID, and they wouldn't see their files from their last session. On top of that, the FTP server would rapidly start to collect a large number of session-based directories, with no garbage collection. So it was back to the drawing board for me.

After some discussions with the customer, we reasoned that the best suggestion for their particular environment was to leverage some of the code that I had written for my session-based home directory provider in order to create home directory provider that dynamically created home directories that are based on the remote IP of the FTP client.

I have to stress, however, that this solution will not work in all situations. For example:

  • If multiple FTP clients are accessing your FTP server through the same firewall, their remote IP might appear to be the same.
  • If an FTP client is moving between geographic locations, such as traveling with a laptop, then the remote IP address will change, and the client will not see their files from their previous session.

That being said, the customer felt that those limitations were acceptable for their environment, so I created a home directory provider that dynamically created home directories that were based on the remote IP address of their FTP clients. I agree that it's not a perfect solution, but their business requirement made this scenario considerably difficult to work around.

Note: I wrote and tested the steps in this blog using both Visual Studio 2010 and Visual Studio 2008; if you use an different version of Visual Studio, some of the version-specific steps may need to be changed.

In This Blog

Prerequisites

The following items are required to complete the procedures in this blog:

  1. The following version of IIS must be installed on your Windows computer, and the Internet Information Services (IIS) Manager must also be installed:
    • IIS 7.0 must be installed on Windows Server 2008
    • IIS 7.5 must be installed on Windows Server 2008 R2 or Windows 7
  2. The new FTP 7.5 service must be installed. To install FTP 7.5, follow the instructions in the following topic:
  3. You must have FTP publishing enabled for a site. To create a new FTP site, follow the instructions in the following topic:
  4. Set the content permissions to allow access for the COM+ process identity that handles extensibility:
    • Open a command prompt.
    • Type the following command:
      ICACLS "%SystemDrive%\inetpub\ftproot" /Grant "Network Service":M /T
      Where "%SystemDrive%\inetpub\ftproot" is the home directory for your FTP site.
    • Close the command prompt.
    Note: This last step is necessary for the custom home directory provider to create the isolation directories.

Step 1: Set up the Project Environment

In this step, you will create a project in Microsoft Visual Studio for the demo provider.

  1. Open Visual Studio 2008 or Visual Studio 2010.
  2. Click the File menu, then New, then Project.
  3. In the New Projectdialog box:
    • Choose Visual C# as the project type.
    • Choose Class Library as the template.
    • Type FtpRemoteIPHomeDirectory as the name of the project.
    • Click OK.
  4. When the project opens, add a reference path to the FTP extensibility library:
    • Click Project, and then click FtpRemoteIPHomeDirectory Properties.
    • Click the Reference Paths tab.
    • Enter the path to the FTP extensibility assembly for your version of Windows, where C: is your operating system drive.
      • For Windows Server 2008 and Windows Vista:
        • C:\Windows\assembly\GAC_MSIL\Microsoft.Web.FtpServer\7.5.0.0__31bf3856ad364e35
      • For 32-bit Windows 7 and Windows Server 2008 R2:
        • C:\Program Files\Reference Assemblies\Microsoft\IIS
      • For 64-bit Windows 7 and Windows Server 2008 R2:
        • C:\Program Files (x86)\Reference Assemblies\Microsoft\IIS
    • Click Add Folder.
  5. Add a strong name key to the project:
    • Click Project, and then click FtpRemoteIPHomeDirectory Properties.
    • Click the Signing tab.
    • Check the Sign the assembly check box.
    • Choose <New...> from the strong key name drop-down box.
    • Enter FtpRemoteIPHomeDirectoryKey for the key file name.
    • If desired, enter a password for the key file; otherwise, clear the Protect my key file with a password check box.
    • Click OK.
  6. Note: FTP 7.5 Extensibility does not support the .NET Framework 4.0; if you are using Visual Studio 2010, or you have changed your default framework version, you may need to change the framework version for this project. To do so, use the following steps:
    • Click Project, and then click FtpRemoteIPHomeDirectory Properties.
    • Click the Application tab.
    • Choose .NET Framework 3.5 in the Target framework drop-down menu.
    • Save, close, and re-open the project.
  7. Optional: You can add a custom build event to add the DLL automatically to the Global Assembly Cache (GAC) on your development computer:
    • Click Project, and then click FtpRemoteIPHomeDirectory Properties.
    • Click the Build Events tab.
    • Enter the appropriate commands in the Post-build event command linedialog box, depending on your version of Visual Studio:
      • If you are using Visual Studio 2010:
        net stop ftpsvc
        call "%VS100COMNTOOLS%\vsvars32.bat">null
        gacutil.exe /if "$(TargetPath)"
        net start ftpsvc
      • If you are using Visual Studio 2008:
        net stop ftpsvc
        call "%VS90COMNTOOLS%\vsvars32.bat">null
        gacutil.exe /if "$(TargetPath)"
        net start ftpsvc
      Note: You need to be logged in as an administrator in order to restart the FTP service and add the dll to the Global Assembly Cache.
  8. Save the project.

Step 2: Create the Extensibility Class

In this step, you will implement the extensibility interfaces for the demo provider.

  1. Add the necessary references to the project:
    • Click Project, and then click Add Reference...
    • On the .NET tab, click Microsoft.Web.FtpServer.
    • Click OK.
  2. Add the code for the authentication class:
    • In Solution Explorer, double-click the Class1.cs file.
    • Remove the existing code.
    • Paste the following code into the editor:
      using System;
      using System.Collections.Generic;
      using System.Collections.Specialized;
      using System.IO;
      using Microsoft.Web.FtpServer;

      public class FtpRemoteIPHomeDirectory :
      BaseProvider,
      IFtpHomeDirectoryProvider,
      IFtpLogProvider
      {
      // Create a dictionary object that will contain
      // session IDs and remote IP addresses.
      private static Dictionary<string, string> _sessionList = null;

      // Store the path to the default FTP folder.
      private static string _defaultDirectory = string.Empty;

      // Override the default initialization method.
      protected override void Initialize(StringDictionary config)
      {
      // Test if the session dictionary has been created.
      if (_sessionList == null)
      {
      // Create the session dictionary.
      _sessionList = new Dictionary<string, string>();
      }
      // Retrieve the default directory path from configuration.
      _defaultDirectory = config["defaultDirectory"];
      // Test for the default home directory (Required).
      if (string.IsNullOrEmpty(_defaultDirectory))
      {
      throw new ArgumentException(
      "Missing default directory path in configuration.");
      }
      }

      // Define the home directory provider method.
      string IFtpHomeDirectoryProvider.GetUserHomeDirectoryData(
      string sessionId,
      string siteName,
      string userName)
      {
      // Create a string with the folder name.
      string _sessionDirectory = String.Format(
      @"{0}\{1}", _defaultDirectory,
      _sessionList[sessionId]);
      try
      {
      // Test if the folder already exists.
      if (!Directory.Exists(_sessionDirectory))
      {
      // Create the physical folder. Note: NETWORK SERVICE
      // needs write permissions to the default folder in
      // order to create each remote IP's home directory.
      Directory.CreateDirectory(_sessionDirectory);
      }
      }
      catch (Exception ex)
      {
      throw ex;
      }
      // Return the path to the session folder.
      return _sessionDirectory;
      }
      // Define the log provider method.
      public void Log(FtpLogEntry logEntry)
      {
      // Test if the USER command was entered.
      if (logEntry.Command.Equals(
      "USER",
      StringComparison.InvariantCultureIgnoreCase))
      {
      // Reformat the remote IP address.
      string _remoteIp = logEntry.RemoteIPAddress
      .Replace(':', '-')
      .Replace('.', '-');
      // Add the remote IP address to the session dictionary.
      _sessionList.Add(logEntry.SessionId, _remoteIp);
      }
      // Test if the command channel was closed (end of session).
      if (logEntry.Command.Equals(
      "CommandChannelClosed",
      StringComparison.InvariantCultureIgnoreCase))
      {
      // Remove the closed session from the dictionary.
      _sessionList.Remove(logEntry.SessionId);
      }
      }
      }
  3. Save and compile the project.

Note: If you did not use the optional steps to register the assemblies in the GAC, you will need to manually copy the assemblies to your IIS 7 computer and add the assemblies to the GAC using the Gacutil.exe tool. For more information, see the following topic on the Microsoft MSDN Web site:

Global Assembly Cache Tool (Gacutil.exe)

Step 3: Add the Demo Provider to FTP

In this step, you will add your provider to the global list of custom providers for your FTP service, configure your provider's settings, and enable your provider for an FTP site.

Adding your Provider to FTP

  1. Determine the assembly information for your extensibility provider:
    • In Windows Explorer, open your "C:\Windows\assembly" path, where C: is your operating system drive.
    • Locate the FtpRemoteIPHomeDirectory assembly.
    • Right-click the assembly, and then click Properties.
    • Copy the Culture value; for example: Neutral.
    • Copy the Version number; for example: 1.0.0.0.
    • Copy the Public Key Token value; for example: 426f62526f636b73.
    • Click Cancel.
  2. Add the extensibility provider to the global list of FTP authentication providers:
    • Open the Internet Information Services (IIS) Manager.
    • Click your computer name in the Connections pane.
    • Double-click FTP Authentication in the main window.
    • Click Custom Providers... in the Actions pane.
    • Click Register.
    • Enter FtpRemoteIPHomeDirectory for the provider Name.
    • Click Managed Provider (.NET).
    • Enter the assembly information for the extensibility provider using the information that you copied earlier. For example:
      FtpRemoteIPHomeDirectory,FtpRemoteIPHomeDirectory,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73
    • Click OK.
    • Clear the FtpRemoteIPHomeDirectory check box in the providers list.
    • Click OK.

Note: If you prefer, you could use the command line to add the provider to FTP by using syntax like the following example:

cd %SystemRoot%\System32\Inetsrv

appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpRemoteIPHomeDirectory',type='FtpRemoteIPHomeDirectory,FtpRemoteIPHomeDirectory,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost

Configuring your Provider's Settings

At the moment there is no user interface that allows you to configure properties for a custom home directory provider, so you will have to use the following command line:

cd %SystemRoot%\System32\Inetsrv

appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpRemoteIPHomeDirectory']" /commit:apphost

appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpRemoteIPHomeDirectory'].[key='defaultDirectory',value='C:\Inetpub\ftproot']" /commit:apphost

Note: The highlighted area contains the value that you need to update with the root directory of your FTP site.

Enabling your Provider for an FTP site

At the moment there is no user interface that allows you to enable a custom home directory provider for an FTP site, so you will have to use the following command line:

cd %SystemRoot%\System32\Inetsrv

appcmd.exe set config -section:system.applicationHost/sites /+"[name='My FTP Site'].ftpServer.customFeatures.providers.[name='FtpRemoteIPHomeDirectory']" /commit:apphost

appcmd.exe set config -section:system.applicationHost/sites /"[name='My FTP Site'].ftpServer.userIsolation.mode:Custom" /commit:apphost

Note: The highlighted areas contain the name of the FTP site where you want to enable the custom home directory provider.

Summary

In this blog I showed you how to:

  • Create a project in Visual Studio 2010 or Visual Studio 2008 for a custom FTP home directory provider.
  • Implement the extensibility interface for custom FTP home directories.
  • Add a custom home directory provider to your FTP service.

When users connect to your FTP site, the FTP service will create a directory that is based on their remote IP address, and it will drop their session in the corresponding folder for their remote IP address. They will not be able to change to the root directory, or a directory for a different remote IP address.

For example, if the root directory for your FTP site is "C:\Inetpub\ftproot" and a client connects to your FTP site from 192.168.0.100, the FTP home directory provider will create a folder that is named "C:\Inetpub\ftproot\192-168-0-100", and the FTP client's sessions will be isolated in that directory; the FTP client will not be able to change directory to "C:\Inetpub\ftproot" or the home directory for another remote IP.

Once again, there are limitations to this approach, and I agree that it's not a perfect solution in all scenarios; but this provider works as expected when you have to use the same username and password for all of your FTP clients, and you know that your FTP clients will use unique remote IP addresses.

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Nov 02 2011, 08:02 by Bob | Comments (0) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: FTP | IIS
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Day 4 of IIS at TechEd IT Forum in Barcelona

Well, today was the last day, and the Microsoft Tech∙Ed: IT Forum 2006 in Barcelona has come to an end. I started off the day doing a presentation about using LogParser 2.2 with IIS. Matthew Boettcher took the following photo during the presentation:

Following that presentation, Matthew and I hosted a Chalk & Talk session with Paul Wright and Chad Kraykovic from microsoft.com and Sergei Anatov from the IIS product team. Matthew started off by giving a small recap of the topics that had been covered in the Connected Systems Infrastructure track at Tech∙Ed, then we opened the floor for the next hour and a half to answer any questions. We had around 20 or so people, and between the various attendees we had a great discussion.

A new question that's cropped up with customers over the past few days is whether there will be a web version for the Longhorn Core SKU. I don't have an answer on that, but I wanted to mention that it seems like a lot of customers are interested.

Here's a shot of the exhibition hall around the lunch break:

I worked at the IIS booth for the next three hours after the Chalk & Talk session, then I said my goodbyes to Matthew, Ivan, and Sergei, and my part of this conference was over.

My thanks to all the customers that dropped by and gave us such great feedback!

Posted: Nov 17 2006, 17:41 by Bob | Comments (0) RSS comment feed |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: IIS
Tags: , ,
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us