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/

Advanced Log Parser Charts Part 4 - Adding Custom Input Formats

In Part 4 of this series, I'll show you how you can do a couple of cool things:

  • Create a new input format for Log Parser (and I'll use FTP RSCA data as an example)
  • Create charts from your custom input format

For the data source for my custom plug-in, I thought that it would be cool to consume the data from FTP 7's Runtime Status (RSCA). If you've followed some of my old blogs, you would have seen that around five years ago I wrote the following pair of blogs about programmatically viewing FTP 7 sessions:

I'm going to recycle some of the FTP RSCA concepts from those blogs in order to create my COM plug-in.

Log Parser Input Formats

If you're like me, you already realize that the existing features of Log Parser simply rock. But what most people don't realize is that Log Parser lets you extend the functionality by adding new input formats, so you can consume the data from any place where you feel compelled to sit down and write your own Log Parser module.

As a quick reminder, Log Parser supports the following built-in input formats:

  • IIS Log File Input Formats
    • IISW3C: parses IIS log files in the W3C Extended Log File Format.
    • IIS: parses IIS log files in the Microsoft IIS Log File Format.
    • BIN: parses IIS log files in the Centralized Binary Log File Format.
    • IISODBC: returns database records from the tables logged to by IIS when configured to log in the ODBC Log Format.
    • HTTPERR: parses HTTP error log files generated by Http.sys.
    • URLSCAN: parses log files generated by the URLScan IIS filter.
  • Generic Text File Input Formats
    • CSV: parses comma-separated values text files.
    • TSV: parses tab-separated and space-separated values text files.
    • XML: parses XML text files.
    • W3C: parses text files in the W3C Extended Log File Format.
    • NCSA: parses web server log files in the NCSA Common, Combined, and Extended Log File Formats.
    • TEXTLINE: returns lines from generic text files.
    • TEXTWORD: returns words from generic text files.
  • System Information Input Formats
    • EVT: returns events from the Windows Event Log and from Event Log backup files (.evt files).
    • FS: returns information on files and directories.
    • REG: returns information on registry values.
    • ADS: returns information on Active Directory objects.
  • Special-purpose Input Formats
    • NETMON: parses network capture files created by NetMon.
    • ETW: parses Enterprise Tracing for Windows trace log files and live sessions.
    • COM: provides an interface to Custom Input Format COM plug-ins.

This last input format, COM, is how you interface with Log Parser in order to create your own input formats. When you install Log Parser, there are a few COM-based samples in the Log Parser directory, and you can take a look at those when you get the chance.

A Brief Introduction to Creating a COM plug-ins for Log Parser

To start with, your COM plug-in has to support a few public methods - and each of these will be more clear when I create my plug-in later:

Method Name Description
OpenInput Opens your data source and sets up any initial environment settings.
GetFieldCount Returns the number of fields that your plug-in will provide.
GetFieldName Returns the name of a specified field.
GetFieldType Returns the datatype of a specified field.
GetValue Returns the value of a specified field.
ReadRecord Reads the next record from your data source.
CloseInput Closes your data source and cleans up any environment settings.

After you've created and registered your COM plug-in, you will call it by using something like the following syntax:

logparser "SELECT * FROM FOO" -i:COM -iProgID:BAR

In this example, FOO is some data source that makes sense to your plug-in, and BAR is the COM class name for your plug-in.

Creating the COM plug-in for FTP RSCA Data

I'm going to demonstrate how to create a COM component as a scriptlet, and then I'll call that from Log Parser to process the data. I chose to use a scriptlet for this demo because they are quick to design and they're easily portable. Since no compilation is required, updates take place on the fly. All of that being said, if I were writing a real COM plug-in for Log Parser, I would use C# or C++.

To create the sample COM plug-in, copy the following code into a text file, and save that file as "MSUtil.LogQuery.FtpRscaScriptlet.sct" to your computer. (Note: The *.SCT file extension tells Windows that this is a scriptlet file.)

<SCRIPTLET>
  <registration
    Description="FTP RSCA for Log Parser Scriptlet"
    Progid="MSUtil.LogQuery.FtpRscaScriptlet"
    Classid="{4e616d65-6f6e-6d65-6973-526f62657274}"
    Version="1.00"
    Remotable="False" />
  <comment>
  EXAMPLE 1: logparser "SELECT * FROM ftp.example.com" -i:COM -iProgID:MSUtil.LogQuery.FtpRscaScriptlet
  EXAMPLE 2: logparser "SELECT * FROM 1" -i:COM -iProgID:MSUtil.LogQuery.FtpRscaScriptlet
  </comment>
  <implements id="Automation" type="Automation">
    <method name="OpenInput">
      <parameter name="strValue"/>
    </method>
    <method name="GetFieldCount" />
    <method name="GetFieldName">
      <parameter name="intFieldIndex"/>
    </method>
    <method name="GetFieldType">
      <parameter name="intFieldIndex"/>
    </method>
    <method name="ReadRecord" />
    <method name="GetValue">
      <parameter name="intFieldIndex"/>
    </method>
    <method name="CloseInput">
      <parameter name="blnAbort"/>
    </method>
  </implements>
  <SCRIPT LANGUAGE="VBScript">

Option Explicit

Dim objAdminManager,objSessionDictionary
Dim objSitesSection,objSitesCollection
Dim objSiteElement,objFtpServerElement
Dim objSessionsElement,objSessionElement
Dim intSiteElementPos,intSession,intRecordIndex
Dim clsSession

intRecordIndex = -1

' --------------------------------------------------------------------------------
' Open an input session that reads FTP RSCA data and stores it in a dictionary object.
' --------------------------------------------------------------------------------

Public Function OpenInput(strValue)
  Set objSessionDictionary = CreateObject("Scripting.Dictionary")
  Set objAdminManager = CreateObject("Microsoft.ApplicationHost.WritableAdminManager")
  objAdminManager.CommitPath = "MACHINE/WEBROOT/APPHOST"
  Set objSitesSection = objAdminManager.GetAdminSection("system.applicationHost/sites", "MACHINE/WEBROOT/APPHOST")
  Set objSitesCollection = objSitesSection.Collection
  If IsNumeric(strValue) Then
    intSiteElementPos = FindElement(objSitesCollection, "site", Array("id", strValue))
  Else
    intSiteElementPos = FindElement(objSitesCollection, "site", Array("name", strValue))
  End If
  If intSiteElementPos > -1 Then
    Set objSiteElement = objSitesCollection.Item(intSiteElementPos)
    Set objFtpServerElement = objSiteElement.ChildElements.Item("ftpServer")
    Set objSessionsElement = objFtpServerElement.ChildElements.Item("sessions").Collection
    For intSession = 0 To CLng(objSessionsElement.Count)-1
       Set objSessionElement = objSessionsElement.Item(intSession)
       Set clsSession = New Session
       clsSession.CurrentDateTime = GetUtcDate()
       clsSession.ClientIp = objSessionElement.GetPropertyByName("clientIp").Value
       clsSession.SessionId = objSessionElement.GetPropertyByName("sessionId").Value
       clsSession.SessionStartTime = objSessionElement.GetPropertyByName("sessionStartTime").Value
       clsSession.UserName = objSessionElement.GetPropertyByName("userName").Value
       clsSession.CurrentCommand = objSessionElement.GetPropertyByName("currentCommand").Value
       clsSession.PreviousCommand = objSessionElement.GetPropertyByName("previousCommand").Value
       clsSession.CommandStartTime = objSessionElement.GetPropertyByName("commandStartTime").Value
       clsSession.BytesSent = objSessionElement.GetPropertyByName("bytesSent").Value
       clsSession.BytesReceived = objSessionElement.GetPropertyByName("bytesReceived").Value
       clsSession.LastErrorStatus = objSessionElement.GetPropertyByName("lastErrorStatus").Value
       objSessionDictionary.Add intSession,clsSession
    Next
  End If
End Function

' --------------------------------------------------------------------------------
' Close the input session.
' --------------------------------------------------------------------------------

Public Function CloseInput(blnAbort)
  intRecordIndex = -1
  objSessionDictionary.RemoveAll
End Function

' --------------------------------------------------------------------------------
' Return the count of fields.
' --------------------------------------------------------------------------------

Public Function GetFieldCount()
  GetFieldCount = 11
End Function

' --------------------------------------------------------------------------------
' Return the specified field's name.
' --------------------------------------------------------------------------------

Public Function GetFieldName(intFieldIndex)
  Select Case intFieldIndex
    Case 0
      GetFieldName = "currentDateTime"
    Case 1
      GetFieldName = "clientIp"
    Case 2
      GetFieldName = "sessionId"
    Case 3
      GetFieldName = "sessionStartTime"
    Case 4
      GetFieldName = "userName"
    Case 5
      GetFieldName = "currentCommand"
    Case 6
      GetFieldName = "previousCommand"
    Case 7
      GetFieldName = "commandStartTime"
    Case 8
      GetFieldName = "bytesSent"
    Case 9
      GetFieldName = "bytesReceived"
    Case 10
      GetFieldName = "lastErrorStatus"
  End Select
End Function

' --------------------------------------------------------------------------------
' Return the specified field's type.
' --------------------------------------------------------------------------------

Public Function GetFieldType(intFieldIndex)
  Const TYPE_INTEGER   = 1
  Const TYPE_REAL      = 2
  Const TYPE_STRING    = 3
  Const TYPE_TIMESTAMP = 4
  Const TYPE_NULL      = 5
  
  Select Case intFieldIndex
    Case 0
      GetFieldType = TYPE_STRING
    Case 1
      GetFieldType = TYPE_STRING
    Case 2
      GetFieldType = TYPE_STRING
    Case 3
      GetFieldType = TYPE_STRING
    Case 4
      GetFieldType = TYPE_STRING
    Case 5
      GetFieldType = TYPE_STRING
    Case 6
      GetFieldType = TYPE_STRING
    Case 7
      GetFieldType = TYPE_STRING
    Case 8
      GetFieldType = TYPE_INTEGER
    Case 9
      GetFieldType = TYPE_INTEGER
    Case 10
      GetFieldType = TYPE_INTEGER
  End Select
End Function

' --------------------------------------------------------------------------------
' Return the specified field's value.
' --------------------------------------------------------------------------------

Public Function GetValue(intFieldIndex)
  If objSessionDictionary.Count > 0 Then
    Select Case intFieldIndex
      Case 0
        GetValue = objSessionDictionary(intRecordIndex).CurrentDateTime
      Case 1
        GetValue = objSessionDictionary(intRecordIndex).ClientIp
      Case 2
        GetValue = objSessionDictionary(intRecordIndex).SessionId
      Case 3
        GetValue = objSessionDictionary(intRecordIndex).SessionStartTime
      Case 4
        GetValue = objSessionDictionary(intRecordIndex).UserName
      Case 5
        GetValue = objSessionDictionary(intRecordIndex).CurrentCommand
      Case 6
        GetValue = objSessionDictionary(intRecordIndex).PreviousCommand
      Case 7
        GetValue = objSessionDictionary(intRecordIndex).CommandStartTime
      Case 8
        GetValue = objSessionDictionary(intRecordIndex).BytesSent
      Case 9
        GetValue = objSessionDictionary(intRecordIndex).BytesReceived
      Case 10
        GetValue = objSessionDictionary(intRecordIndex).LastErrorStatus
    End Select
  End If
End Function
  
' --------------------------------------------------------------------------------
' Read the next record, and return true or false if there is more data.
' --------------------------------------------------------------------------------

Public Function ReadRecord()
  If objSessionDictionary.Count > 0 Then
    If intRecordIndex < (objSessionDictionary.Count-1) Then
      intRecordIndex = intRecordIndex + 1
      ReadRecord = True
    Else
      ReadRecord = False
    End If
  End If
End Function

' --------------------------------------------------------------------------------
' Return the current UTC date/time.
' --------------------------------------------------------------------------------

Private Function GetUtcDate()
  Dim dtmNow,dtmUtc,strUtc
  Dim objShell,lngActiveTimeBias
  dtmNow = Now()
  Set objShell = CreateObject("WScript.Shell")
  lngActiveTimeBias = CLng(objShell.RegRead("HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\TimeZoneInformation\ActiveTimeBias"))
  dtmUtc = DateAdd("n",lngActiveTimeBias,dtmNow)
  strUtc = Year(dtmUtc) & "-" & _
    Right("0" & Month(dtmUtc),2) & "-" & _
    Right("0" & Day(dtmUtc),2) & "T" & _
    Right("0" & Hour(dtmUtc),2) & ":" & _
    Right("0" & Minute(dtmUtc),2) & ":" & _
    Right("0" & Second(dtmUtc),2) & ".000Z"
  GetUtcDate = strUtc
End Function

' --------------------------------------------------------------------------------
' Return an element's position in a collection.
' --------------------------------------------------------------------------------

Private Function FindElement(objCollection, strElementTagName, arrValuesToMatch)
  Dim i,elem,matches,j,prop,value
  For i = 0 To CInt(objCollection.Count) - 1
    Set elem = objCollection.Item(i)
    If elem.Name = strElementTagName Then
      matches = True
      For j = 0 To UBound(arrValuesToMatch) Step 2
        Set prop = elem.GetPropertyByName(arrValuesToMatch(j))
        value = prop.Value
        If Not IsNull(value) Then
          value = CStr(value)
        End If
        If Not value = CStr(arrValuesToMatch(j + 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

' --------------------------------------------------------------------------------
' Define a generic class for holding session data.
' --------------------------------------------------------------------------------

Class Session
  Public CurrentDateTime
  Public ClientIp
  Public SessionId
  Public SessionStartTime
  Public UserName
  Public CurrentCommand
  Public PreviousCommand
  Public CommandStartTime
  Public BytesSent
  Public BytesReceived
  Public LastErrorStatus
End Class

  </SCRIPT>

</SCRIPTLET>

After you've saved the scriptlet code to your computer, you will register it by using the following syntax:

regsvr32 MSUtil.LogQuery.FtpRscaScriptlet.sct

At the very minimum, you can now use the COM plug-in with Log Parser by using syntax like the following:

logparser "SELECT * FROM ftp.example.com" -i:COM -iProgID:MSUtil.LogQuery.FtpRscaScriptlet

Next, we'll analyze what the scriptlet does.

Examining the COM plug-in in Detail

Here are the different parts of the scriptlet and what they do:

  • The <registration> section of the scriptlet sets up the COM registration information; you'll notice the COM component class name and GUID, as well as version information and a general description.
  • The <implements> section declares the public methods that the COM plug-in has to support.
  • The <script> section contains the actual implementation:
    • The first part of the script section declares the global variables that will be used.
    • The second part of the script contains the required methods:
      • The OpenInput() method opens the FTP RSCA data for a specific FTP site:
        • The site will be specified in your Log Parser query, and the COM plug-in supports using either the site name or the site's numeric ID:
          • "SELECT * FROM ftp.example.com"
          • "SELECT * FROM 1"
        • All of the RSCA data for the site in stored in classes that will be cached in a global dictionary for quick access
      • The  CloseInput() method doesn't do much in this script, but your COM plug-ins may require more clean up depending on your data source.
      • The GetFieldCount() method simply returns the number of data fields in each record of your data.
      • The GetFieldName() method returns the name of a field that is passed to the method as a number.
      • The GetFieldType() method returns the data type of a field that is passed to the method as a number; Log Parser supports the following five data types for COM plug-ins:
        • TYPE_INTEGER
        • TYPE_REAL
        • TYPE_STRING
        • TYPE_TIMESTAMP
        • TYPE_NULL
      • The GetValue() method returns the data value of a field that is passed to the method as a number.
      • The ReadRecord() method moves to the next record in your data set; this method returns True if there is data to read, or False when the end of data is reached.
    • The third part of the script contains some helper features:
      • The GetUtcDate() method returns the current date and time in Universal Coordinated Time (UTC) format.
      • The FindElement() method locates a specified element's position within an IIS collection, or -1 if the element cannot be found. This method is used to determine the specified FTP site within the IIS configuration.
      • The Session class is a generic construct to hold the information for a single FTP RSCA data record.

This wraps up the description of how the scriptlet works as a COM plug-in, in the next part of my blog we'll look at how to actually use it.

Using the COM plug-in with Log Parser

Earlier I showed you how you can use the COM plug-in with Log Parser by using syntax like the following:

logparser "SELECT * FROM ftp.example.com" -i:COM -iProgID:MSUtil.LogQuery.FtpRscaScriptlet

This will return output that resembles something like the following:

currentDateTime clientIp sessionId sessionStartTime userName currentCommand previousCommand commandStartTime bytesSent bytesReceived lastErrorStatus
---------------- -------- --------- ---------------- -------- -------------- --------------- ---------------- --------- ------------- ---------------
2012-05-25T11:42:11.000Z 10.121.75.26 3950d1e5-3e94-4734-a89a-9768c52aa924 2012-05-25T10:08:09.861Z robert PASS USER 2012-05-25T11:42:06.080Z 6049 1193 0
2012-05-25T11:42:11.000Z 10.121.75.26 d1591fa8-3b09-4afd-b2c0-950421ba79fe 2012-05-25T10:08:18.184Z robert RETR NLST 2012-05-25T11:42:07.172Z 5887 1169 0
2012-05-25T11:42:11.000Z 10.121.75.26 0f92b5ed-920a-441d-a15d-39056a36f2a4 2012-05-25T10:08:22.327Z robert NOOP NLST 2012-05-25T11:41:40.917Z 5857 1163 0
2012-05-25T11:42:11.000Z 10.121.75.26 16925f0d-1fc5-4cb7-be19-ab33face2da9 2012-05-25T10:08:48.756Z   NLST SYST 2012-05-25T11:41:44.770Z 6026 1192 0
2012-05-25T11:42:11.000Z 10.121.75.26 aeb68389-869b-4afc-8c81-47b578e74824 2012-05-25T10:08:54.214Z   USER HOST 2012-05-25T11:41:42.087Z 5864 1168 0
2012-05-25T11:42:11.000Z 10.121.75.26 4ed55569-ee25-47d1-8388-12cdb90a1c07 2012-05-25T10:12:31.555Z alice RETR NLST 2012-05-25T11:42:01.789Z 5780 1138 0
2012-05-25T11:42:11.000Z 10.121.75.26 d6b16bb4-cb65-492d-a9fa-fbd6b72de0f3 2012-05-25T10:12:54.591Z bob NOOP NLST 2012-05-25T11:41:46.563Z 5748 1130 0
                     
Statistics:                    
-----------                    
Elements processed: 7                  
Elements output: 7                  
Execution time: 0.12 seconds                  

That information is something of a jumbled mess, and we can clean that up a bit by simply choosing the fields that we might be interested in:

userName currentCommand commandStartTime
-------- -------------- ----------------
robert PASS 2012-05-25T11:42:06.080Z
robert RETR 2012-05-25T11:42:07.172Z
robert NOOP 2012-05-25T11:41:40.917Z
  NLST 2012-05-25T11:41:44.770Z
  USER 2012-05-25T11:41:42.087Z
alice RETR 2012-05-25T11:42:01.789Z
bob NOOP 2012-05-25T11:41:46.563Z
     
Statistics:    
-----------    
Elements processed: 7  
Elements output: 7  
Execution time: 0.12 seconds  

Now let's look at some interesting data - one of the main focuses for this blog series is charting  with Log Parser, so let's look at doing something useful with the data. To start with, here's how to create a pie chart that counts the number of sessions by user name:

logparser "SELECT CASE UserName WHEN '' THEN 'anonymous' ELSE TO_LOWERCASE(UserName) END AS User,COUNT(*) AS Sessions INTO SessionCountByUser.gif FROM 1 GROUP BY User" -i:COM -iProgID:MSUtil.LogQuery.FtpRscaScriptlet -chartType:PieExploded -o:CHART -values:on -categories:off -legend:on -chartTitle:"User Sessions"

This will generate a chart like the following:

Here's a variation on that script that illustrates how to create a pie chart that counts the number of authenticated sessions versus anonymous sessions:

logparser "SELECT CASE UserName WHEN '' THEN 'Anonymous' ELSE 'Authenticated' END AS AuthType,COUNT(*) AS Sessions INTO AuthenticatedSessions.gif FROM 1 GROUP BY AuthType" -i:COM -iProgID:MSUtil.LogQuery.FtpRscaScriptlet -chartType:PieExploded -o:CHART -values:on -categories:off -legend:on -chartTitle:"Authenticated Sessions"

This will generate a chart like the following:

We can also do line, bar, and column charts with the data:

logparser "SELECT CASE UserName WHEN '' THEN 'anonymous' ELSE TO_LOWERCASE(UserName) END AS User,COUNT(*) AS Sessions INTO SessionCountByUser.gif FROM 1 GROUP BY User" -i:COM -iProgID:MSUtil.LogQuery.FtpRscaScriptlet -chartType:Column3D -o:CHART -values:on -legend:off -chartTitle:"User Sessions"

The above code sample will generate a chart like the following:

There's a lot more that we could do with this, but eventually I have to get some sleep, so I think that's enough fun for the day.

Summary

In this blog post, I've shown you how to add your own custom input format to Log Parser by creating scriptlet as a COM plug-in. I hope that you take this information and create some great Log Parser plug-ins of your own.

;-]

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/

Extensibility Updates in the FTP 8.0 Service

A few years ago I wrote a blog that was titled "FTP 7.5 Service Extensibility References", in which I discussed the extensibility APIs that we added in FTP 7.5. Over the next couple of years I followed that initial blog with a series of walkthroughs on IIS.net and several related blog posts. Here are just a few examples:

In today's blog I'd like to discuss some of the extensibility features that we added in FTP 8.0, and show you how you can use those in your FTP providers.

Custom FTP Authorization

In FTP 7.5 we provided interfaces for IFtpAuthenticationProvider and IFtpRoleProvider, which respectively allowed developers to create FTP providers that performed user and role lookups. In FTP 8.0 we added a logical extension to that API set with IFtpAuthorizationProvider interface, which allows developers to create FTP providers that perform authorization tasks.

With that in mind, I wrote the following walkthrough on the IIS.net web site:

The title pretty much says it all: the provider that I describe in that walkthrough will walk you through the steps that are required to create an FTP provider that provides custom user authentication, verification of role memberships, and authorization lookups on a per-path basis.

Custom FTP Event Handling

In FTP 7.5 if you wanted your provider to respond to specific user activity, the best way to do so was to implement the IFtpLogProvider.Log() interface and use that to provide a form of pseudo-event handling. In FTP 8.0 we add two event handling interfaces, IFtpPreprocessProvider and IFtpPostprocessProvider, which respectively allow developers to write providers that implement functionality before or after events have occurred.

With that in mind, I wrote the following walkthrough on the IIS.net web site:

Once again, the title says it all: the provider that I describe in that walkthrough will walk you through the steps that are required to create an FTP provider that prevents FTP clients from downloading more files per-session than you have allowed in your configuration settings.

Happy coding!


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

FTP Logon Restrictions in IIS 8

One of the biggest asks from our customers over the years was to provide a way to prevent brute-force password attacks on the FTP service. On several of the FTP sites that I host, I used to see a large number of fraudulent logon requests from hackers that were trying to guess a username/password combination. My first step in trying to prevent these kinds of attacks, like most good administrators, was to implement strong password requirements and password lockout policies. This was a good first step, but there is an unfortunate downside to password lockout policies - once a hacker locks out a user account, that means that a valid user is locked out of their account. What's more, a hacker can continue your server.

The FTP service has had a feature to block IP addresses, but this required something of a manual process to discover malicious behavior. To accomplish this, you had to query your log files for excessive activity, and then added the IP addresses from potential hackers to your blacklist of banned IP addresses. Besides the manual nature of this process, another big drawback to this approach is the fact that it isn't real-time, so a malicious client could be attacking your system for some time before you discover their activity.

With that in mind, my next step was to go after the hackers and block their IP addresses from accessing my server. To that end, I created the custom authentication provider for the FTP 7.5 service that I documented in the following walkthrough:

How to Use Managed Code (C#) to Create an FTP Authentication Provider with Dynamic IP Restrictions

That was pretty effective, but it was really intended to be a stop-gap measure while we were working on a built-in feature for the FTP service that ships with IIS 8, which allows you to block malicious logon attempts.

Here's the way this feature works - at the server level, you configure the maximum number of failed logon attempts that you will allow within a given time period; if someone fails to logon within that time frame, the FTP service will drop the connection, and the client will be blocked from accessing your server until the time frame has passed.

Additional details are available in the walkthrough that I wrote at the following URL:

IIS 8.0 FTP Logon Attempt Restrictions

If you'd like to try out the new FTP Logon Restrictions feature, you can download the Windows Server 8 Beta from the following URL:

http://www.microsoft.com/en-us/server-cloud/windows-server/v8-default.aspx


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

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 attempting 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/

FTP Clients - Recap

Having written 10 blog posts in my series about FTP clients, I decided that it might be a good idea to recap some of the information that I have presented thus far. With that in mind, here is a quick recap of the entire series to date:

What I'd like to do in the rest of this blog is recap the scorecard information for the FTP clients that I've looked at. With one exception: I'm going to skip the information that I included about the FTP experience for various web browsers, which I discussed in Part 1 of this blog series, but only because web browsers aren't supposed to be first-class FTP clients.

That being said, I'm presenting the information for the remaining FTP clients that I have reviewed in alphabetical order, which is not necessarily by order of preference. ;-]


Core FTP LE 2.1

Original Blog Post: FTP Clients - Part 6: Core FTP LE

Client
Name
Directory
Browsing
Explicit
FTPS
Implicit
FTPS
Virtual
Hosts
True
HOSTs
Site
Manager
Extensibility
Core FTP LE 2.1 Rich Yes Yes Yes Partial1 Yes No

Footnotes:

  1. Core FTP can support true FTP HOSTs by configuring pre-login commands in the Site Manager.

Expression Web 4

Original Blog Post: FTP Clients - Part 9: Expression Web 4

Client
Name
Directory
Browsing
Explicit
FTPS
Implicit
FTPS
Virtual
Hosts
True
HOSTs
Site
Manager
Extensibility
Expression Web 4 Rich Yes Yes Yes1 No2 Partial3 Yes

Footnotes:

  1. EW4 supports virtual hosts, but some earlier versions of Expression Web did not.
  2. EW4 has no way to send a HOST command, so true FTP HOSTs are not supported.
  3. EW4 has only basic Site Manager functionality; it lacks most of the features that are available in many of the GUI-based FTP clients.

FileZilla 3.1.6

Original Blog Post: FTP Clients - Part 4: FileZilla

Client
Name
Directory
Browsing
Explicit
FTPS
Implicit
FTPS
Virtual
Hosts
True
HOSTs
Site
Manager
Extensibility
FileZilla 3.1.61 Rich Yes Yes Yes No2 Yes Yes3

Footnotes:

  1. My original post was for FileZilla 3.1.6; I have upgraded to 3.5.1 since then, but there are no changes as far as the information in my blog was concerned.
  2. FileZilla has no way to send a HOST command, so true FTP HOSTs are not supported.
  3. FileZilla is an Open Source project, so you can modify the source and recompile the application; see http://filezilla-project.org/ for more information.

FTP Voyager

Original Blog Post: FTP Clients - Part 10: FTP Voyager

Client
Name
Directory
Browsing
Explicit
FTPS
Implicit
FTPS
Virtual
Hosts
True
HOSTs
Site
Manager
Extensibility
FTP Voyager Rich Yes Yes Yes Yes1 Yes Yes

Footnotes:

  1. FTP Voyager fully supports the FTP HOST command, and is enabled by default for new connections.

Kermit FTP Client 2.1.3

Original Blog Post: FTP Clients - Part 7: Kermit FTP Client

Client
Name
Directory
Browsing
Explicit
FTPS
Implicit
FTPS
Virtual
Hosts
True
HOSTs
Site
Manager
Extensibility
Kermit FTP Client 2.1.3 No Yes No Yes Partial1 Yes Yes

Footnotes:

  1. True FTP HOSTs can be implemented by using Kermit's "ftp quote HOST ftp.example.com" syntax.

MOVEit Freely 5.0.0.0

Original Blog Post: FTP Clients - Part 5: MOVEit Freely Command-Line Secure FTP Client

Client
Name
Directory
Browsing
Explicit
FTPS
Implicit
FTPS
Virtual
Hosts
True
HOSTs
Site
Manager
Extensibility
MOVEit Freely 5.0.0.0 n/a Yes Yes Yes Partial1 No No

Footnotes:

  1. True FTP HOSTs can be implemented by using MOVEit Freely's "quote HOST ftp.example.com" syntax.

SmartFTP Ultimate 4.0

Original Blog Post: FTP Clients - Part 8: SmartFTP Client

Client
Name
Directory
Browsing
Explicit
FTPS
Implicit
FTPS
Virtual
Hosts
True
HOSTs
Site
Manager
Extensibility
SmartFTP Ultimate 4.0 Rich Yes Yes Yes Yes1 Yes Yes

Footnotes:

  1. SmartFTP fully supports the FTP HOST command, but you need to configure the SmartFTP Client to send the FEAT command before logging in.

That wraps it up for my recap of the FTP clients that I've reviewed so far; but rest assured, I have a few more FTP clients that I'm waiting to review.

;-]


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

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/

FTP Clients - Part 10: FTP Voyager

For this installment in my series about FTP Clients, I'd like to take a look at FTP Voyager from Rhino Software. For this blog I used FTP Voyager 15.2.0.17, and it is available from the following URL:

http://www.ftpvoyager.com/

FTP Voyager is a great FTP client that supports a wide array of features and connection options, but I shouldn't get ahead of myself and talk about everything in my introduction. ;-]

Fig. 1 - FTP Voyager Splash Screen

At the time of this blog post, FTP Voyager is a for-retail product that is available in two different versions:

  • FTP Voyager
  • FTP Voyager Secure

You should take a look at the FTP Voyager Versions page for a description of the features that are available in each version.

FTP Voyager Overview

The FTP Voyager user interface is uncluttered, easy to understand, and allows you to customize which panes you want to see displayed.

Fig. 2 - FTP Voyager's Default Panes and Options

If you want a really uncluttered display, FTP Voyager offers a Simple Mode, which narrows down the number of panes that are displayed. (Sometimes this is a handy feature to have.)

Fig. 3 - FTP Voyager's Simple Mode

FTP Voyager doesn't have a command-line interface, but it has web browser integration; and it has a really cool scheduler, which allows you to configure FTP jobs to run at scheduled times.

Fig. 4 - FTP Voyager Scheduler's Task Settings

FTP Voyager also supports sending custom FTP commands, and it has an extensibility interface for creating add-ons. I didn't experiment with creating any add-ons, but you can find details about creating your own add-ons through RhinoSoft's FTP Voyager Add-Ons page.

FTP connections are created and edited through FTP Voyager's Site Profile Manager, which is comparable to the site management features that I have found in many of the better GUI-based FTP clients.

Fig. 5 - FTP Voyager's Site Profile Manager

That concludes my summary for some of the general features - so now we'll take a look at the FTP7-specific features that I've discussed in my other FTP client blog posts.

Using FTP Voyager with FTP over SSL (FTPS)

FTP Voyager supports both Implicit and Explicit FTPS, so the choice is up to you to decide which method to use. As I have mentioned in my previous blogs, the FTPS method in FTP7 is specified by the port number that you choose when you are creating your bindings. Once again, I realize that I have posted the following information in almost all of my posts in this FTP client series, but it needs to be mentioned that the following rules apply for FTP7 when determining whether you are using Implicit or Explicit FTPS:

  • If you enable SSL in FTP7 and you assign the FTP site to port 990, you are using Implicit FTPS.
  • If you enable SSL in FTP7 and you assign the FTP site to any port other than port 990, you are using Explicit FTPS.

To configure the security options for a connection in FTP Voyager, you need to open the Advanced Settings dialog for the connection in FTP Voyager's Site Profile Manager.

Fig. 6 - FTP Voyager's Security Options

The additional security options in FTP Voyager's Security Options allow you to configure the SSL environment to match FTP7's Advanced SSL Policy settings.

Fig. 7 - FTP7's Advanced SSL Policy Settings

Note: I was able to use FTP Voyager's FTPS features with FTP7's virtual host names, but I should mention that I had to configure a Global Listener FTP Site in order to get that to work.

Using FTP Voyager with True FTP Hosts

FTP Voyager has built-in for the HOST command, so you can use true FTP host names when using FTP Voyager to connect to FTP7 sites that are configured with host names. This feature is enabled by default, but if you needed to disable it for some reason, that feature can be accessed through FTP Voyager's Advanced Settings dialog.

Fig. 7 - FTP Voyager's Advanced Connection Settings

The following excerpt from the Log Pane of an FTP Voyager session shows the HOST command in action:

STATUS:> Connecting to "ftp.contoso.com" on port 21.
  220 Microsoft FTP Service
STATUS:> Connected. Logging into the server
COMMAND:> HOST ftp.contoso.com
  220 Host accepted.
COMMAND:> USER robert
  331 Password required for robert.
COMMAND:> PASS **********
  230 User logged in.
STATUS:> Login successful

Using FTP Voyager with FTP Virtual Host Names

FTP Voyager's login settings allow you to specify the virtual host name as part of the user credentials by using syntax like "ftp.example.com|username" or "ftp.example.com\username", but since FTP Voyager allows you to use true FTP hosts this is really a moot point. Just the same, there's nothing to stop you from disabling the HOST command for a connection and specifying an FTP virtual host as part of your username, although I'm not sure why you would want to do that.

Fig. 9 - Specifying a Virtual Host in FTP Voyager

Scorecard for FTP Voyager

This concludes our quick look at some of the FTP features that are available with FTP Voyager, and here are the scorecard results:

Client
Name
Directory
Browsing
Explicit
FTPS
Implicit
FTPS
Virtual
Hosts
True
HOSTs
Site
Manager
Extensibility
FTP Voyager 15.2 Rich Y Y Y Y Y Y
As noted earlier, FTP Voyager supports the FTP HOST command, and is enabled by default for new connections.

In closing, FTP Voyager is a great GUI-based FTP client that has first-class support for all of the features that I have been examining in detail throughout my FTP client blog series. But that being said, I included the following disclaimer in all of my preceding posts, so this post will be no exception: there are a great number of additional features that FTP Voyager provides - but once again I only focused on a few specific topic areas that apply to FTP7. ;-]


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

How to determine if FTP clients are using FTPS

One of my colleagues here at Microsoft, Emmanuel Boersma, just reminded me of an email thread that we had several weeks ago, where a customer had asked him how they could tell if FTPS was being used on their FTP server. He had pointed out that when he looks at his FTP log files, the port number was always 21, so it wasn't as easy as looking at a website's log files and looking for port 80 for HTTP versus port 443 for HTTPS. I had sent him the following notes, and I thought that they might make a good blog. ;-)

As I mentioned earlier, we had discussed the control channel is typically over port 21 for both FTP and FTPS, so you can't rely on the port. But having said that, I mentioned that you will see certain verbs in your FTP logs that will let you know when FTPS is being used, and that’s a reliable way to check.

With that in mind, I suggested the following two methods that you can use to determine if FTPS is being used:

  • If the port number is something other than 990, and you see the following verbs being used (and succeeding), then Explicit FTPS is being used:
    • AUTH
    • PBSZ
    • PROT
  • If the port is 990, then Implicit FTPS is being used. (This means the FTPS is always on.)

For example, see the highlighted data in following FTP log file excerpts:

Explicit FTPS over port 21:

#Fields: date time c-ip cs-username cs-host s-ip s-port cs-method cs-uri-stem sc-status sc-win32-status sc-substatus sc-bytes cs-bytes time-taken
2011-06-30 22:11:24 ::1 - - ::1 21 ControlChannelOpened - - 0 0 0 0 0
2011-06-30 22:11:24 ::1 - - ::1 21 AUTH TLS 234 0 0 31 10 16
2011-06-30 22:11:27 ::1 - - ::1 21 PBSZ 0 200 0 0 69 8 0
2011-06-30 22:11:27 ::1 - - ::1 21 PROT P 200 0 0 69 8 0
2011-06-30 22:11:36 ::1 - - ::1 21 USER robert 331 0 0 69 13 0
2011-06-30 22:11:42 ::1 robert - ::1 21 PASS *** 230 0 0 53 15 2808

Implicit FTPS over port 990:

#Fields: date time c-ip cs-username cs-host s-ip s-port cs-method cs-uri-stem sc-status sc-win32-status sc-substatus sc-bytes cs-bytes time-taken
2011-06-30 22:16:55 ::1 - - ::1 990 ControlChannelOpened - - 0 0 0 0 0
2011-06-30 22:16:58 ::1 - - ::1 990 USER robert 331 0 0 69 13 0
2011-06-30 22:16:58 ::1 robert - ::1 990 PASS *** 230 0 0 53 15 78
2011-06-30 22:16:58 ::1 robert - ::1 990 SYST - 500 5 51 1005 6 0
2011-06-30 22:16:58 ::1 robert - ::1 990 FEAT - 211 0 0 313 6 0
2011-06-30 22:16:58 ::1 robert - ::1 990 OPTS UTF8+ON 200 0 0 85 14 0
2011-06-30 22:16:58 ::1 robert - ::1 990 PBSZ 0 200 0 0 69 8 0
2011-06-30 22:16:58 ::1 robert - ::1 990 PROT P 200 0 0 69 8 0

FWIW – An explanation about Implicit FTPS and Explicit FTPS can be found in the following articles:


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