Microsoft Bob

Just a short, simple blog for Bob to share some tips and tricks.

Be sure to check out my non-technical blog at www.geekybob.com.

Month List

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/
Posted: Dec 01 2011, 07:22 by Bob | Comments (0)
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: FTP | Extensibility
Tags:
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)
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: IIS | FTP | Extensibility
Tags:
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

How to Use Managed Code (C#) to Create an FTP Home Directory Provider for the Days of the Week

I had a question from someone that had an interesting scenario: they had a series of reports that their manufacturing company generates on a daily basis, and they wanted to automate uploading those files over FTP from their factory to their headquarters. Their existing automation created report files with names like Widgets.log, Sprockets.log, Gadgets.log, etc.

But they had an additional request: they wanted the reports dropped into folders based on the day of the week. People in their headquarters could retrieve the reports from a share on their headquarters network where the FTP server would drop the files, and anyone could look at data from anytime within the past seven days.

This seemed like an extremely trivial script for me to write, so I threw together the following example batch file for them:

@echo off
pushd "C:\Reports"
for /f "usebackq delims= " %%a in (`date /t`) do (
  echo open MyServerName>ftpscript.txt
  echo MyUsername>>ftpscript.txt
  echo MyPassword>>ftpscript.txt
  echo mkdir %%a>>ftpscript.txt
  echo cd %%a>>ftpscript.txt
  echo asc>>ftpscript.txt
  echo prompt>>ftpscript.txt
  echo mput *.log>>ftpscript.txt
  echo bye>>ftpscript.txt
)
ftp.exe -s:ftpscript.txt
del ftpscript.txt
popd

This would have worked great for most scenarios, but they pointed out a few problems in their specific environment: manufacturing and headquarters were in different geographical regions of the world, therefore in different time zones, and they wanted the day of the week to be based on the day of the week where their headquarters was located. They also wanted to make sure that if anyone logged in over FTP, they would only see the reports for the current day, and they didn't want to take a chance that something might go wrong with the batch file and they might overwrite the logs from the wrong day.

With all of those requirements in mind, this was beginning to look like a problem for a custom home directory provider to tackle. Fortunately, this was a really easy home directory provider to write, and I thought that it might make a good blog.

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. You need to create the folders for the days of the week under your FTP root directory; for example, Sunday, Monday, Tuesday, etc.

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 FtpDayOfWeekHomeDirectory 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 FtpDayOfWeekHomeDirectory 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 FtpDayOfWeekHomeDirectory Properties.
    • Click the Signing tab.
    • Check the Sign the assembly check box.
    • Choose <New...> from the strong key name drop-down box.
    • Enter FtpDayOfWeekHomeDirectoryKey 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 FtpDayOfWeekHomeDirectory 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 FtpDayOfWeekHomeDirectory 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 Microsoft.Web.FtpServer;
      
      public class FtpDayOfWeekHomeDirectory :
          BaseProvider,
          IFtpHomeDirectoryProvider
      {
          // 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)
          {
              // 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)
          {
              // Return the path to the folder for the day of the week.
              return String.Format(
                  @"{0}\{1}",
                  _defaultDirectory,
                  DateTime.Today.DayOfWeek);
          }
      }
  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 FtpDayOfWeekHomeDirectory 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 FtpDayOfWeekHomeDirectory 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:
      FtpDayOfWeekHomeDirectory,FtpDayOfWeekHomeDirectory,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73
    • Click OK.
    • Clear the FtpDayOfWeekHomeDirectory 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='FtpDayOfWeekHomeDirectory',type='FtpDayOfWeekHomeDirectory,FtpDayOfWeekHomeDirectory,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='FtpDayOfWeekHomeDirectory']" /commit:apphost

appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpDayOfWeekHomeDirectory'].[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='FtpDayOfWeekHomeDirectory']" /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 drop their session in the corresponding folder for the day of the week under the home directory for your FTP site, and they will not be able to change to the root directory or a directory for a different day of the week.

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Sep 29 2011, 13:23 by Bob | Comments (0)
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: FTP | Extensibility
Tags:
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

FTP and LDAP - Part 2: How to Set Up an Active Directory Lightweight Directory Services (AD LDS) Server

This blog is designed as a complement to my FTP and LDAP - Part 1: How to Use Managed Code (C#) to Create an FTP Authentication Provider that uses an LDAP Server blog post. In this second blog, I'll walk you through the steps to set up an Active Directory Lightweight Directory Services (AD LDS) server, which you can use with the custom FTP LDAP Authentication provider that I discussed in my last blog.

In This Blog

Step 1: Installing AD LDS

The following steps will walk you through installing Active Directory Lightweight Directory Services on a computer that is running Windows Server 2008.

Adding the AD LDS Role

  1. Open the Windows Server 2008 Server Manager, click Roles in the navigation pane, and then click Add Roles.
  2. Check the box for Active Directory Lightweight Directory Services, and then click Next.
  3. Read the information on the Introduction to Active Directory Lightweight Directory Services page, and then click Next.
  4. Verify the Confirmation Installation Settings, and then click Next.
  5. The installation will start; this may take several minutes to complete.
  6. When the installation has completed, click Close.

Creating an AD LDS instance

Note: Before completing these steps I created a local user account named "LdapAdmin" that I would specify the administrative account for managing my LDAP instance. This user account was only a member of the local "Users" group, and not a member of the local "Administrators" group.

  1. Click Start, then click Administrative Tools, and then click Active Directory Lightweight Directory Services Setup Wizard.
  2. When the Active Directory Lightweight Directory Services Setup Wizard appears, click Next.
  3. Select A unique instance, and then click Next.
  4. Enter a name for your instance, for example "MyTestInstance," and then click Next.
  5. Verify the port numbers for LDAP connections, and then click Next.
  6. Choose Yes, create an application directory partition, and then enter a unique partition name by using X.500 path syntax. For example: "CN=MyServer,DC=MyDomain,DC=local". When you have finished entering your partition name, click Next.
  7. Verify the paths to the AD LDS files for this instance, and then click Next.
  8. Choose an account for your service account. (Note: Because I was creating a standalone LDAP server, I chose to use the network service account.) Once you have chosen an account, click Next.
  9. If you choose to use the network service account, the AD LDS wizard will prompt you about replication. Click Yes to continue.
  10. Choose an account as your AD LDS administrator. (Note: In my situation I chose the LdapAdmin account that I had created earlier; I did this so that I wouldn't be storing the credentials for an administrative account.) Once you have chosen an account, click Next.
  11. Choose one of the following LDIF files to import; these will be used to create user accounts.
    • MS-User.LDF
    • MS-InetOrgPerson.LDF
    Note: I tested my FTP LDAP authentication provider with both LDIF files.
  12. Verify your installation options, and then click Next.
  13. When prompted for your AD LDS credentials, enter the credentials for the account that you chose to administer your AD LDS instance.
  14. The wizard will begin to install the requisite files and create your instance; this may take several minutes to complete.

  15. When the wizard has completed, click Finish.

Step 2: Using ADSI Edit to add Users and Groups

Connecting to your AD LDS Server

  1. Click Start, and then Administrative Tools, and then ADSI Edit.
  2. Click Action, and then click Connect to...
  3. When the Connection Settingsdialog box is displayed:
    • Enter the LDAP path for your AD LDS server in the Select or type a Distinguished Name or Naming Contexttext box. For example:
      • CN=MyServer,DC=MyDomain,DC=local
    • Enter the server name and port in the Select or type a domain or servertext box. For example:
      • MYSERVER:389
    • The preceding steps should create the following path in the Pathtext box:
      • LDAP://MYSERVER:389/CN=MyServer,DC=MyDomain,DC=local
       
    • Click the Advanced button; when the Advanceddialog box is displayed:
      • Check the Specify Credentials box.
      • Enter the user name and password for your AD LDS server.
      • Click OK.
  4. Click OK.

Adding a User Object

  1. Expand the tree until you have highlighted the correct LDAP path for your server. For example:
    • CN=MyServer,DC=MyDomain,DC=local.
  2. Click Action, and then New, and then Object...
  3. Highlight the appropriate user class, and then click Next.
  4. Enter the common name for your user, and then click Next. For example: enter FtpUser for the common name.
  5. Click Finish.
  6. Right-click the user that you created, and then click Properties.
  7. Select msDS-UserAccountDisabled in the list of attributes, and then click Edit.
  8. Select False, and then click OK.
  9. Select userPrincipalName in the list of attributes, and then click Edit.
  10. Enter your user's common name for the value, and then click OK. For example: enter FtpUser for the common name.
  11. Click OK to close the user properties dialog box.
  12. Right-click the user that you created, and then click Reset Password...
  13. Enter and confirm the password for your user.
  14. Click OK.

Adding Users to Groups

  1. Retrieve the Distinguished Name (DN) for a user:
    • Right-click the user that you created, and then click Properties.
    • Select distinguishedName in the list of attributes, and then click View.
    • Copy the value, and then click OK. For example: CN=FtpUser,CN=MyServer,DC=MyDomain,DC=local.
    • Click OK to close the user's properties dialog box.
  2. Add the user to a group:
    • Expand the tree until you have highlighted a group in your server. For example, you could use the built-in CN-Users group.
    • Right-click the group, and then click Properties.
    • Select member in the list of attributes, and then click Edit.
    • When the editor dialog box is displayed, click Add DN...
    • When the Add Distinguished Name (DN) dialog box appears, paste the user DN syntax that you copied earlier. For example: CN=FtpUser,CN=MyServer,DC=MyDomain,DC=local.
    • Click OK to close the Add DN dialog box.
    • Click OK to close the group's properties dialog box.

More Information

For additional information about working with AD LDS instances, see the following URLs:

Enabling the Custom FTP LDAP Authentication Provider for an FTP site

While this is technically outside the scope of setting up the LDAP server, I'm reposting the notes from my last blog about adding the FTP LDAP Authentication provider and adding authorization rules for FTP users or groups.

  1. Add the custom authentication provider for an FTP site:
    • Open an FTP site in the Internet Information Services (IIS) Manager.
    • Double-click FTP Authentication in the main window.
    • Click Custom Providers... in the Actions pane.
    • Check FtpLdapAuthentication in the providers list.
    • Click OK.
  2. Add an authorization rule for the authentication provider:
    • Double-click FTP Authorization Rules in the main window.
    • Click Add Allow Rule... in the Actions pane.
    • You can add either of the following authorization rules:
      • For a specific user:
        • Select Specified users for the access option.
        • Enter a user name that you created in your AD LDS partition.
      • For a role or group:
        • Select Specified roles or user groups for the access option.
        • Enter the role or group name that you created in your AD LDS partition.
      • Select Read and/or Write for the Permissions option.
    • Click OK.

Once these settings are configured and users connect to your FTP site, the FTP service will attempt to authenticate users from your LDAP server by using the custom FTP LDAP Authentication provider.

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

FTP and LDAP - Part 1: How to Use Managed Code (C#) to Create an FTP Authentication Provider that uses an LDAP Server

Over the past few years I've created a series of authentication providers for the FTP 7.5 service that ships with Windows Server 2008 R2 and Windows 7, and is available for download for Windows Server 2008. Some of these authentication providers are available on the http://learn.iis.net/page.aspx/590/developing-for-ftp-75/ website, while others have been in my blog posts.

With that in mind, I had a question a little while ago about using an LDAP server to authenticate users for the FTP service, and it seemed like that would make a great subject for another custom FTP authentication provider blog post.

The steps in this blog will lead you through the steps to use managed code to create an FTP authentication provider that uses a server running Active Directory Lightweight Directory Services (AD LDS) that is located on your local network.

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. You must have an AD LDS server available on your local network. Note: See my How to Set Up an Active Directory Lightweight Directory Services (AD LDS) Server blog post for more information.

Note: To test this blog, I used AD LDS on Windows Server 2008; if you use a different LDAP server, you may need to change some of the LDAP syntax in the code samples. To get started using AD LDS, see the following topics:

I tested this blog by using the user objects from both the MS-User.LDF and MS-InetOrgPerson.LDF Lightweight Directory interchange Format (LDIF) files.

Important

To help improve the performance for authentication requests, the FTP service caches the credentials for successful logins for 15 minutes by default. This means that if you change the password in your AD LDS server, this change may not be reflected for the cache duration. To alleviate this, you can disable credential caching for the FTP service. To do so, use the following steps:

  1. Open a command prompt.
  2. Type the following commands:
    cd /d "%SystemRoot%\System32\Inetsrv"
    Appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost
    Net stop FTPSVC
    Net start FTPSVC
  3. Close the command prompt.

Step 1: Set up the Project Environment

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

  1. Open Microsoft Visual Studio 2008.
  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 FtpLdapAuthentication 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 FtpLdapAuthentication 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 FtpLdapAuthentication Properties.
    • Click the Signing tab.
    • Check the Sign the assembly check box.
    • Choose <New...> from the strong key name drop-down box.
    • Enter FtpLdapAuthenticationKey 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. To do so, use the following steps:
    • Click Project, and then click FtpLdapAuthentication 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 FtpLdapAuthentication 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 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 authentication and role 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.
    • Repeat the above steps to add the following references to the project:
      • System.Configuration
      • System.DirectoryServices
      • System.DirectoryServices.AccountManagement
  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.Specialized;
      using System.Configuration.Provider;
      using System.DirectoryServices;
      using System.DirectoryServices.AccountManagement;
      using Microsoft.Web.FtpServer;
      
      public class FtpLdapAuthentication :
        BaseProvider,
        IFtpAuthenticationProvider,
        IFtpRoleProvider
      {
        private static string _ldapServer = string.Empty;
        private static string _ldapPartition = string.Empty;
        private static string _ldapAdminUsername = string.Empty;
        private static string _ldapAdminPassword = string.Empty;
      
        // Override the default initialization method.
        protected override void Initialize(StringDictionary config)
        {
          // Retrieve the provider settings from configuration.
          _ldapServer = config["ldapServer"];
          _ldapPartition = config["ldapPartition"];
          _ldapAdminUsername = config["ldapAdminUsername"];
          _ldapAdminPassword = config["ldapAdminPassword"];
      
          // Test for the LDAP server name (Required).
          if (string.IsNullOrEmpty(_ldapServer) || string.IsNullOrEmpty(_ldapPartition))
          {
            throw new ArgumentException(
              "Missing LDAP server values in configuration.");
          }
        }
      
        public bool AuthenticateUser(
          string sessionId,
          string siteName,
          string userName,
          string userPassword,
          out string canonicalUserName)
        {
          canonicalUserName = userName;
          // Attempt to look up the user and password.
          return LookupUser(true, userName, string.Empty, userPassword);
        }
      
        public bool IsUserInRole(
          string sessionId,
          string siteName,
          string userName,
          string userRole)
        {
          // Attempt to look up the user and role.
          return LookupUser(false, userName, userRole, string.Empty);
        }
      
        private static bool LookupUser(
          bool isUserLookup,
          string userName,
          string userRole,
          string userPassword)
        {
          PrincipalContext _ldapPrincipalContext = null;
          DirectoryEntry _ldapDirectoryEntry = null;
      
          try
          {
            // Create the context object using the LDAP connection information.
            _ldapPrincipalContext = new PrincipalContext(
              ContextType.ApplicationDirectory,
              _ldapServer,
      
              _ldapPartition,
              ContextOptions.SimpleBind,
              _ldapAdminUsername,
              _ldapAdminPassword);
      
            // Test for LDAP credentials.
            if (string.IsNullOrEmpty(_ldapAdminUsername) || string.IsNullOrEmpty(_ldapAdminPassword))
            {
              // If LDAP credentials do not exist, attempt to create an unauthenticated directory entry object.
              _ldapDirectoryEntry = new DirectoryEntry("LDAP://" + _ldapServer + "/" + _ldapPartition);
            }
            else
            {
              // If LDAP credentials exist, attempt to create an authenticated directory entry object.
              _ldapDirectoryEntry = new DirectoryEntry("LDAP://" + _ldapServer + "/" + _ldapPartition,
                _ldapAdminUsername, _ldapAdminPassword, AuthenticationTypes.Secure);
            }
      
            // Create a DirectorySearcher object from the cached DirectoryEntry object.
            DirectorySearcher userSearcher = new DirectorySearcher(_ldapDirectoryEntry);
            // Specify the the directory searcher to filter by the user name.
            userSearcher.Filter = String.Format("(&(objectClass=user)(cn={0}))", userName);
            // Specify the search scope.
            userSearcher.SearchScope = SearchScope.Subtree;
            // Specify the directory properties to load.
            userSearcher.PropertiesToLoad.Add("distinguishedName");
            // Specify the search timeout.
            userSearcher.ServerTimeLimit = new TimeSpan(0, 1, 0);
            // Retrieve a single search result.
            SearchResult userResult = userSearcher.FindOne();
            // Test if no result was found.
            if (userResult == null)
            {
              // Return false if no matching user was found.
              return false;
            }
            else
            {
              if (isUserLookup == true)
              {
                try
                {
                  // Attempt to validate credentials using the username and password.
                  return _ldapPrincipalContext.ValidateCredentials(userName, userPassword, ContextOptions.SimpleBind);
                }
                catch (Exception ex)
                {
                  // Throw an exception if an error occurs.
                  throw new ProviderException(ex.Message);
                }
              }
              else
              {
                // Retrieve the distinguishedName for the user account.
                string distinguishedName = userResult.Properties["distinguishedName"][0].ToString();
      
                // Create a DirectorySearcher object from the cached DirectoryEntry object.
                DirectorySearcher groupSearcher = new DirectorySearcher(_ldapDirectoryEntry);
                // Specify the the directory searcher to filter by the group/role name.
                groupSearcher.Filter = String.Format("(&(objectClass=group)(cn={0}))", userRole);
                // Specify the search scope.
                groupSearcher.SearchScope = SearchScope.Subtree;
                // Specify the directory properties to load.
                groupSearcher.PropertiesToLoad.Add("member");
                // Specify the search timeout.
                groupSearcher.ServerTimeLimit = new TimeSpan(0, 1, 0);
                // Retrieve a single search result.
                SearchResult groupResult = groupSearcher.FindOne();
      
                // Loop through the member collection.
                for (int i = 0; i < groupResult.Properties["member"].Count; ++i)
                {
                  string member = groupResult.Properties["member"][i].ToString();
                  // Test if the current member contains the user's distinguished name.
                  if (member.IndexOf(distinguishedName, StringComparison.OrdinalIgnoreCase) > -1)
                  {
                    // Return true (role lookup succeeded) if the user is found.
                    return true;
                  }
                }
                // Return false (role lookup failed) if the user is not found for the role.
                return false;
              }
            }
          }
          catch (Exception ex)
          {
            // Throw an exception if an error occurs.
            throw new ProviderException(ex.Message);
          }
        }
      }
  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 list of providers for your FTP service, configure your provider for your LDAP server, and enable your provider to authenticate users 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 FtpLdapAuthentication 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 FtpLdapAuthentication 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:
      FtpLdapAuthentication,FtpLdapAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73
    • Click OK.
    • Clear the FtpLdapAuthentication check box in the providers list.
    • Click OK.

Configuring your Provider's Settings

  1. Determine the connection information for your LDAP server; there are four pieces of information that you will need to know in order to configure the extensibility provider to talk to your LDAP server:
    • Server Name and TCP/IP Port: This is the name (or IP address) of the server that is hosting your LDAP service; the port is usually 389. These will be added to your provider using the "SERVERNAME:PORT" syntax.
    • LDAP Partition: This is the LDAP path within your LDAP service to your data, for example: "CN=ServerName,DC=DomainName,DC=DomainExtension."
    • LDAP Username: This is a username that has access to your LDAP server; this is not the name of an account that you will use for FTP access, and it does not have to be a Windows account.
    • LDAP Password: This is the password that is associated with the LDAP username.
  2. Using the information from the previous steps, configure the options for the provider:
    • At the moment there is no user interface that enables you to add properties for a custom authentication module, so you will have to use the following command line. You will need to update the highlighted areas with the information from the previous steps and the information for your LDAP server:
      cd %SystemRoot%\System32\Inetsrv

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

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpLdapAuthentication'].[key='ldapServer',value='MYSERVER:389']" /commit:apphost

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpLdapAuthentication'].[key='ldapPartition',value='CN=MyServer,DC=MyDomain,DC=local']" /commit:apphost

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpLdapAuthentication'].[key='ldapAdminUsername',encryptedValue='MyAdmin']" /commit:apphost

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpLdapAuthentication'].[key='ldapAdminPassword',encryptedValue='MyPassword1']" /commit:apphost
    • Note: The highlighted areas are the values for the ldapServer, ldapPartition, ldapAdminUsername, and ldapAdminPassword settings, which configure your network environment for your LDAP server.

Enabling your Provider for an FTP site

  1. Add the custom authentication provider for an FTP site:
    • Open an FTP site in the Internet Information Services (IIS) Manager.
    • Double-click FTP Authentication in the main window.
    • Click Custom Providers... in the Actions pane.
    • Check FtpLdapAuthentication in the providers list.
    • Click OK.
  2. Add an authorization rule for the authentication provider:
    • Double-click FTP Authorization Rules in the main window.
    • Click Add Allow Rule... in the Actions pane.
    • You can add either of the following authorization rules:
      • For a specific user:
        • Select Specified users for the access option.
        • Enter a user name that you created in your AD LDS partition.
      • For a role or group:
        • Select Specified roles or user groups for the access option.
        • Enter the role or group name that you created in your AD LDS partition.
      • Select Read and/or Write for the Permissions option.
    • Click OK.

Summary

In this blog I showed you how to:

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

When users connect to your FTP site, the FTP service will attempt to authenticate users from your LDAP server by using your custom authentication provider.

Additional Information

The PrincipalContext.ValidateCredentials() method will validate the user name in the userName parameter with the value of the userPrincipalName attribute of the user object in AD LDS. Because of this, the userPrincipalName attribute for a user object is expected to match the name of the user account that an FTP client will use to log in, which will should be the same value as the cn attribute for the user object. Therefore, when you create a user object in AD LDS, you will need to set the corresponding userPrincipalName attribute for the user object. In addition, when you create a user object in AD LDS, the msDS-UserAccountDisabled attribute is set to TRUE by default, so you will need to change the value of that attribute to FALSE before you attempt to log in.

For more information, see my follow-up blog that is titled FTP and LDAP - Part 2: How to Set Up an Active Directory Lightweight Directory Services (AD LDS) Server.

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

How to Create an Authentication Provider for FTP 7.5 using BlogEngine.NET's XML Membership Files

I ran into an interesting situation recently with BlogEngine.NET that I thought would make a good blog post.

Here's the background for the environment: I host several blog sites for friends of mine, and they BlogEngine.NET for their blogging engine. From a security perspective this works great for me, because I can give them accounts for blogging that are kept in the XML files for each of their respective blogs that aren't real user accounts on my Windows servers.

The problem that I ran into: BlogEngine.NET has great support for uploading files to your blog, but it doesn't provide a real way to manage the files that have been uploaded. So when one of my friends mentioned that they wanted to update one of their files, I was left in a momentary quandary.

My solution: I realized that I could write a custom FTP provider that would solve all of my needs. For my situation the provider needed to do three things:

  1. The provider needed to perform username/password validation.
  2. The provider needed to perform role lookups.
  3. The provider needed to return a custom home directory.

Here's why item #3 was so important - my users have no idea about the underlying functionality for their blog, so I didn't want to simply enable FTP publishing for their website and give them access to their ASP.NET files - there's no telling what might happen. Since all of their files are kept in the path ~/App_Data/files, it made sense to have the custom FTP provider return home directories for each of their websites that point to their files instead of the root folders of their websites.

Prerequisites

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

  1. The following version of IIS must be installed on your Windows server, 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
  2. The 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:

Step 1: Set up the Project Environment

Note: I used Visual Studio 2008 when I created my custom provider and wrote the steps that appear in this blog, although since then I have upgraded to Visual Studio 2010, and I have successfully recompiled my provider using that version. In any event, the steps should be similar whether you are using Visual Studio 2008 or Visual Studio 2010.;-]

In this step, you will create a project inVisual Studio 2008for the demo provider.

  1. Open MicrosoftVisual Studio 2008.
  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 FtpBlogEngineNetAuthentication 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 FtpBlogEngineNetAuthentication 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 Windows Server 2008 R2 and Windows 7:
        • C:\Program Files\Reference Assemblies\Microsoft\IIS
    • Click Add Folder.
  5. Add a strong name key to the project:
    • Click Project, and then click FtpBlogEngineNetAuthentication Properties.
    • Click the Signing tab.
    • Check the Sign the assembly check box.
    • Choose <New...> from the strong key name drop-down box.
    • Enter FtpBlogEngineNetAuthenticationKey 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. 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 FtpBlogEngineNetAuthentication Properties.
    • Click the Build Events tab.
    • Enter the following in the Post-build event command linedialog box for your version of Visual Studio:
      • If you have Visual Studio 2008:
        net stop ftpsvc
        call "%VS90COMNTOOLS%\vsvars32.bat">nul
        gacutil.exe /if "$(TargetPath)"
        net start ftpsvc
      • If you have Visual Studio 2010:
        net stop ftpsvc
        call "%VS100COMNTOOLS%\vsvars32.bat">nul
        gacutil.exe /if "$(TargetPath)"
        net start ftpsvc
  7. Save the project.

Step 2: Create the Extensibility Class

In this step, you will implement the logging extensibility interface 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.
      Note: If Microsoft.Web.FtpServer does not show up on the .NETtab, then use the following steps:
      • Click the Browse tab.
      • Navigate to the reference path where Microsoft.Web.FtpServer.dll is located. (See the paths that were listed earlier in Step #1 for the location.)
      • Highlight Microsoft.Web.FtpServer.dll.
    • Click OK.
    • Repeat the above steps to add the following reference to the project:
      • System.Configuration
  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.Specialized;
      using System.Collections.Generic;
      using System.Configuration.Provider;
      using System.IO;
      using System.Security.Cryptography;
      using System.Text;
      using System.Xml;
      using System.Xml.XPath;
      using Microsoft.Web.FtpServer;
      
      public class FtpBlogEngineNetAuthentication : BaseProvider,
          IFtpAuthenticationProvider,
          IFtpRoleProvider,
          IFtpHomeDirectoryProvider
      {
          // Create strings to store the paths to the XML files that store the user and role data.
          private string _xmlUsersFileName;
          private string _xmlRolesFileName;
      
          // Create a string to store the FTP home directory path.
          private string _ftpHomeDirectory;
      
          // Create a file system watcher object for change notifications.
          private FileSystemWatcher _xmlFileWatch;
      
          // Create a dictionary to hold user data.
          private Dictionary<string, XmlUserData> _XmlUserData =
            new Dictionary<string, XmlUserData>(
              StringComparer.InvariantCultureIgnoreCase);
      
          // Override the Initialize method to retrieve the configuration settings.
          protected override void Initialize(StringDictionary config)
          {
              // Retrieve the paths from the configuration dictionary.
              _xmlUsersFileName = config[@"xmlUsersFileName"];
              _xmlRolesFileName = config[@"xmlRolesFileName"];
              _ftpHomeDirectory = config[@"ftpHomeDirectory"];
      
              // Test if the path to the users or roles XML file is empty.
              if ((string.IsNullOrEmpty(_xmlUsersFileName)) || (string.IsNullOrEmpty(_xmlRolesFileName)))
              {
                  // Throw an exception if the path is missing or empty.
                  throw new ArgumentException(@"Missing xmlUsersFileName or xmlRolesFileName value in configuration.");
              }
              else
              {
                  // Test if the XML files exist.
                  if ((File.Exists(_xmlUsersFileName) == false) || (File.Exists(_xmlRolesFileName) == false))
                  {
                      // Throw an exception if the file does not exist.
                      throw new ArgumentException(@"The specified XML file does not exist.");
                  }
              }
      
              try
              {
                  // Create a file system watcher object for the XML file.
                  _xmlFileWatch = new FileSystemWatcher();
                  // Specify the folder that contains the XML file to watch.
                  _xmlFileWatch.Path = _xmlUsersFileName.Substring(0, _xmlUsersFileName.LastIndexOf(@"\"));
                  // Filter events based on the XML file name.
                  _xmlFileWatch.Filter = @"*.xml";
                  // Filter change notifications based on last write time and file size.
                  _xmlFileWatch.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size;
                  // Add the event handler.
                  _xmlFileWatch.Changed += new FileSystemEventHandler(this.XmlFileChanged);
                  // Enable change notification events.
                  _xmlFileWatch.EnableRaisingEvents = true;
              }
              catch (Exception ex)
              {
                  // Raise an exception if an error occurs.
                  throw new ProviderException(ex.Message,ex.InnerException);
              }
          }
      
          // Define the event handler for changes to the XML files.
          public void XmlFileChanged(object sender, FileSystemEventArgs e)
          {
              // Verify that the changed file is one of the XML data files.
              if ((e.FullPath.Equals(_xmlUsersFileName,
                  StringComparison.OrdinalIgnoreCase)) ||
                  (e.FullPath.Equals(_xmlRolesFileName,
                  StringComparison.OrdinalIgnoreCase)))
              {
                  // Clear the contents of the existing user dictionary.
                  _XmlUserData.Clear();
                  // Repopulate the user dictionary.
                  ReadXmlDataStore();
              }
          }
      
          // Override the Dispose method to dispose of objects.
          protected override void Dispose(bool IsDisposing)
          {
              if (IsDisposing)
              {
                  _xmlFileWatch.Dispose();
                  _XmlUserData.Clear();
              }
          }
      
          // Define the AuthenticateUser method.
          bool IFtpAuthenticationProvider.AuthenticateUser(
                 string sessionId,
                 string siteName,
                 string userName,
                 string userPassword,
                 out string canonicalUserName)
          {
              // Define the canonical user name.
              canonicalUserName = userName;
      
              // Validate that the user name and password are not empty.
              if (String.IsNullOrEmpty(userName) || String.IsNullOrEmpty(userPassword))
              {
                  // Return false (authentication failed) if either are empty.
                  return false;
              }
              else
              {
                  try
                  {
                      // Retrieve the user/role data from the XML file.
                      ReadXmlDataStore();
                      // Create a user object.
                      XmlUserData user = null;
                      // Test if the user name is in the dictionary of users.
                      if (_XmlUserData.TryGetValue(userName, out user))
                      {
                          // Retrieve a sequence of bytes for the password.
                          var passwordBytes = Encoding.UTF8.GetBytes(userPassword);
                          // Retrieve a SHA256 object.
                          using (HashAlgorithm sha256 = new SHA256Managed())
                          {
                              // Hash the password.
                              sha256.TransformFinalBlock(passwordBytes, 0, passwordBytes.Length);
                              // Convert the hashed password to a Base64 string.
                              string passwordHash = Convert.ToBase64String(sha256.Hash);
                              // Perform a case-insensitive comparison on the password hashes.
                              if (String.Compare(user.Password, passwordHash, true) == 0)
                              {
                                  // Return true (authentication succeeded) if the hashed passwords match.
                                  return true;
                              }
                          }
                      }
                  }
                  catch (Exception ex)
                  {
                      // Raise an exception if an error occurs.
                      throw new ProviderException(ex.Message,ex.InnerException);
                  }
              }
              // Return false (authentication failed) if authentication fails to this point.
              return false;
          }
      
          // Define the IsUserInRole method.
          bool IFtpRoleProvider.IsUserInRole(
               string sessionId,
               string siteName,
               string userName,
               string userRole)
          {
              // Validate that the user and role names are not empty.
              if (String.IsNullOrEmpty(userName) || String.IsNullOrEmpty(userRole))
              {
                  // Return false (role lookup failed) if either are empty.
                  return false;
              }
              else
              {
                  try
                  {
                      // Retrieve the user/role data from the XML file.
                      ReadXmlDataStore();
                      // Create a user object.
                      XmlUserData user = null;
                      // Test if the user name is in the dictionary of users.
                      if (_XmlUserData.TryGetValue(userName, out user))
                      {
                          // Search for the role in the list.
                          string roleFound = user.Roles.Find(item => item == userRole);
                          // Return true (role lookup succeeded) if the role lookup was successful.
                          if (!String.IsNullOrEmpty(roleFound)) return true;
                      }
                  }
                  catch (Exception ex)
                  {
                      // Raise an exception if an error occurs.
                      throw new ProviderException(ex.Message,ex.InnerException);
                  }
              }
              // Return false (role lookup failed) if role lookup fails to this point.
              return false;
          }
      
          // Define the GetUserHomeDirectoryData method.
          public string GetUserHomeDirectoryData(string sessionId, string siteName, string userName)
          {
              // Test if the path to the home directory is empty.
              if (string.IsNullOrEmpty(_ftpHomeDirectory))
              {
                  // Throw an exception if the path is missing or empty.
                  throw new ArgumentException(@"Missing ftpHomeDirectory value in configuration.");
              }
              // Return the path to the home directory.
              return _ftpHomeDirectory;
          }
      
          // Retrieve the user/role data from the XML files.
          private void ReadXmlDataStore()
          {
              // Lock the provider while the data is retrieved.
              lock (this)
              {
                  try
                  {
                      // Test if the dictionary already has data.
                      if (_XmlUserData.Count == 0)
                      {
                          // Create an XML document object and load the user data XML file
                          XPathDocument xmlUsersDocument = GetXPathDocument(_xmlUsersFileName);
                          // Create a navigator object to navigate through the XML file.
                          XPathNavigator xmlNavigator = xmlUsersDocument.CreateNavigator();
                          // Loop through the users in the XML file.
                          foreach (XPathNavigator userNode in xmlNavigator.Select("/Users/User"))
                          {
                              // Retrieve a user name.
                              string userName = GetInnerText(userNode, @"UserName");
                              // Retrieve the user's password.
                              string password = GetInnerText(userNode, @"Password");
                              // Test if the data is empty.
                              if ((String.IsNullOrEmpty(userName) == false) && (String.IsNullOrEmpty(password) == false))
                              {
                                  // Create a user data class.
                                  XmlUserData userData = new XmlUserData(password);
                                  // Store the user data in the dictionary.
                                  _XmlUserData.Add(userName, userData);
                              }
                          }
      
                          // Create an XML document object and load the role data XML file
                          XPathDocument xmlRolesDocument = GetXPathDocument(_xmlRolesFileName);
                          // Create a navigator object to navigate through the XML file.
                          xmlNavigator = xmlRolesDocument.CreateNavigator();
                          // Loop through the roles in the XML file.
                          foreach (XPathNavigator roleNode in xmlNavigator.Select(@"/roles/role"))
                          {
                              // Retrieve a role name.
                              string roleName = GetInnerText(roleNode, @"name");
                              // Loop through the users for the role.
                              foreach (XPathNavigator userNode in roleNode.Select(@"users/user"))
                              {
                                  // Retrieve a user name.
                                  string userName = userNode.Value;
                                  // Create a user object.
                                  XmlUserData user = null;
                                  // Test if the user name is in the dictionary of users.
                                  if (_XmlUserData.TryGetValue(userName, out user))
                                  {
                                      // Add the role name for the user.
                                      user.Roles.Add(roleName);
                                  }
                              }
                          }
                      }
                  }
                  catch (Exception ex)
                  {
                      // Raise an exception if an error occurs.
                      throw new ProviderException(ex.Message,ex.InnerException);
                  }
              }
          }
      
          // Retrieve an XPathDocument object from a file path.
          private static XPathDocument GetXPathDocument(string path)
          {
              Exception _ex = null;
              // Specify number of attempts to create an XPathDocument.
              for (int i = 0; i < 8; ++i)
              {
                  try
                  {
                      // Create an XPathDocument object and load the user data XML file
                      XPathDocument xPathDocument = new XPathDocument(path);
                      // Return the XPathDocument if successful. 
                      return xPathDocument;
                  }
                  catch (Exception ex)
                  {
                      // Save the exception for later.
                      _ex = ex;
                      // Pause for a brief interval.
                      System.Threading.Thread.Sleep(250);
                  }
              }
              // Throw the last exception if the function fails to this point.
              throw new ProviderException(_ex.Message,_ex.InnerException);
          }
      
          // Retrieve data from an XML element.
          private static string GetInnerText(XPathNavigator xmlNode, string xmlElement)
          {
              string xmlText = string.Empty;
              try
              {
                  // Test if the XML element exists.
                  if (xmlNode.SelectSingleNode(xmlElement) != null)
                  {
                      // Retrieve the text in the XML element.
                      xmlText = xmlNode.SelectSingleNode(xmlElement).Value.ToString();
                  }
              }
              catch (Exception ex)
              {
                  // Raise an exception if an error occurs.
                  throw new ProviderException(ex.Message,ex.InnerException);
              }
              // Return the element text.
              return xmlText;
          }
      }
      
      // Define the user data class.
      internal class XmlUserData
      {
          // Create a private string to hold a user's password.
          private string _password = string.Empty;
          // Create a private string array to hold a user's roles.
          private List<String> _roles = null;
      
          // Define the class constructor requiring a user's password.
          public XmlUserData(string Password)
          {
              this.Password = Password;
              this.Roles = new List<String>();
          }
      
          // Define the password property.
          public string Password
          {
              get { return _password; }
              set
              {
                  try { _password = value; }
                  catch (Exception ex)
                  {
                      throw new ProviderException(ex.Message,ex.InnerException);
                  }
              }
          }
      
          // Define the roles property.
          public List<String> Roles
          {
              get { return _roles; }
              set
              {
                  try { _roles = value; }
                  catch (Exception ex)
                  {
                      throw new ProviderException(ex.Message,ex.InnerException);
                  }
              }
          }
      }
  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 custom FTP provider to IIS

In this step, you will add the provider to your FTP service. These steps obviously assume that you are using BlogEngine.NET on your Default Web Site, but these steps can be easily amended for any other website where BlogEngine.NET is installed.

  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 FtpBlogEngineNetAuthentication 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 a custom authentication module, so you will have to use the following command line:
      cd %SystemRoot%\System32\Inetsrv

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

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

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication'].[key='xmlUsersFileName',value='C:\inetpub\wwwroot\App_Data\Users.xml']" /commit:apphost

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication'].[key='xmlRolesFileName',value='C:\inetpub\wwwroot\App_Data\Roles.xml']" /commit:apphost

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpBlogEngineNetAuthentication'].[key='ftpHomeDirectory',value='C:\inetpub\wwwroot\App_Data\files']" /commit:apphost
    • Note: You will need to update the values for the xmlUsersFileName, xmlRolesFileName, and ftpHomeDirectory settings for your environment.

Step 4: Use the Custom Authentication Provider with your BlogEngine.NET Website

Just like the steps that I listed earlier, these steps assume that you are using BlogEngine.NET on your Default Web Site, but these steps can be easily amended for any other website where BlogEngine.NET is installed.

Add FTP publishing to your BlogEngine.NET website

  1. In the IIS 7 Manager, in the Connections pane, expand the Sites node in the tree, then highlight the Default Web Site.
  2. Click Add FTP Publishing in the Actions pane.
  3. When the Add FTP Site Publishingwizard appears:
    • Choose an IP address for your FTP site from the IP Address drop-down, or choose to accept the default selection of "All Unassigned."
    • Accept the default port of 21 for the FTP site, or enter a custom TCP/IP port in the Port box.
    • Click Next.
  4. Do no choose any authentication or authorization options for now; you will set those later.
  5. Click Finish.
  6. Hit F5 to refresh the view in IIS 7 Manager.

Specify the custom authentication provider for your BlogEngine.NET website

  1. Double-click FTP Authentication in the main window for your website.
  2. Click Custom Providers... in the Actions pane.
  3. Check FtpBlogEngineNetAuthentication in the providers list.
  4. Click OK.

Add authorization rules for the authentication provider

  1. Double-click FTP Authorization Rules in the main window for your website.
  2. Click Add Allow Rule... in the Actions pane.
  3. You can add either of the following authorization rules:
    • For a specific user from your BlogEngine.NET website:
      • Select Specified users for the access option.
      • Enter a user name that you created in your BlogEngine.NET website.
    • For a role or group from your BlogEngine.NET website:
      • Select Specified roles or user groups for the access option.
      • Enter the role or group name that you created in your BlogEngine.NET website.
    • Select Read and/or Write for the Permissions option.
  4. Click OK.

Specify a custom home directory provider for your BlogEngine.NET website

At the moment there is no user interface that enables you to add custom home directory providers, so you will have to use the following command line:

cd %SystemRoot%\System32\Inetsrv

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

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

Additional Information

To help improve the performance for authentication requests, the FTP service caches the credentials for successful logins for 15 minutes by default. This means that if you change your passwords, this change may not be reflected for the cache duration. To alleviate this, you can disable credential caching for the FTP service. To do so, use the following steps:

  1. Open a command prompt.
  2. Type the following commands:
    cd /d "%SystemRoot%\System32\Inetsrv"
    Appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost
    Net stop FTPSVC
    Net start FTPSVC
  3. Close the command prompt.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Jun 30 2011, 12:13 by Bob | Comments (0)
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Tags:
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Credential Caching in FTP 7.0 and FTP 7.5

I've seen a few situations where people that are using the FTP 7.0 and FTP 7.5 service have noticed that it takes a while for their password changes to be reflected by the FTP service. To put this another way, here are the typical symptoms that people describe to me:

  • A user successfully logs into their FTP site using their username and password
  • The user logs out of their FTP site
  • The user changes their password
  • The user attempts to log into their FTP site using their username and their new password, but this fails
  • The user can successfully log into their FTP site using their username and their old password
  • (Note: As a corollary, restarting the FTP service fixes the symptoms)

Here's why this happens: to help improve the performance for authentication requests, the FTP service caches the credentials for successful logins. (The cache duration is set to 15 minutes by default.) This means that if you change your password, your changes may not be reflected for the cache duration.

The good news is, the FTP credential cache settings can be changed easily, and I have documented all of the settings for FTP caching in the IIS configuration reference at the following URLs:

Quoting and paraphrasing the above documentation, there are the two settings that you can configure on the <credentialsCache> element:

AttributeDescription
enabled Optional Boolean attribute.

true if credential caching is enabled; otherwise, false.

The default value is true.
flushInterval Optional uint attribute.

Specifies the cache lifetime, in seconds, for credentials that are stored in the cache.

Note: This value must be between 5 and 604,800 seconds.

The default value is 900.

What this means to you is - you can completely disable credential caching, or you can specify a different timeout. For example, on several of my development servers I often disable credential caching; this allows me to change passwords whenever I want, which is very useful when I am creating custom authentication providers. For my production servers I tend to stick with the default values, although I might change those values when I'm troubleshooting a problem.

I usually configure the settings from a command line or a batch file, although the articles that I listed earlier have steps for using the IIS Manager to change the settings for FTP credential caching. Just the same, here are some examples for setting the values by using appcmd.exe:

How to Disable FTP Credential Caching

cd /d "%SystemRoot%\System32\Inetsrv"
appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost
net stop FTPSVC
net start FTPSVC

How to Specify a Custom Timeout for FTP Credential Caching

cd /d "%SystemRoot%\System32\Inetsrv"
appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"True" /commit:apphost
appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.flushInterval:"300" /commit:apphost
net stop FTPSVC
net start FTPSVC

I hope this helps. ;-]

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

FTP 7.5 Extensibility and Visual Studio Express Editions

In earlier blog posts I have mentioned that I written the several walkthroughs to help developers get started writing providers for the FTP 7.5 service, all of which available on Microsoft's learn.iis.net Web site under the "Developing for FTP 7.5" section. In each of these walkthroughs I wrote the steps as if you were using Visual Studio 2008.

Following up on that, I received a great question yesterday from a customer, Paul Dowdle, who wondered if it was possible to write an extensibility provider for the FTP 7.5 service using one of the Visual Studio Express Editions. By way of coincidence, I used to install Visual C# Express Edition on my laptop when I was traveling around the world to speak at events like TechEd. I usually did this because the Express Edition took up less hard drive space than a full installation of Visual Studio, and I was only writing code in C# on my laptop.

To answer Paul's question, the short answer is - yes, you can use Visual Studio Express Editions to develop custom providers for the FTP 7.5 service, with perhaps a few small changes from my walkthroughs.

For example, if you look at my "How to Use Managed Code (C#) to Create a Simple FTP Authentication Provider" walkthrough, in the section that is titled "Step 1: Set up the Project Environment", there is an optional step 6 for adding a custom build event to register the DLL automatically in the Global Assembly Cache (GAC) on your development computer.

When I installed Microsoft Visual C# 2008 Express Edition on a new computer, I didn't have the "%VS90COMNTOOLS%" environment variable or the "vsvars32.bat" file, so I had to update the custom build event to the following:

net stop ftpsvc
"%ProgramFiles%\Microsoft SDKs\Windows\v6.0A\bin\gacutil.exe" /if "$(TargetPath)"
net start ftpsvc

Once I made that change, the rest of the walkthrough worked as written.

So, to reiterate my earlier statement - you can use Visual Studio Express Editions to develop custom providers for the FTP 7.5 service. My thanks to Paul for the great question!

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Sep 17 2009, 04:19 by Bob | Comments (0)
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: IIS | FTP | Extensibility
Tags:
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Merging FTP Extensibility Walkthroughs - Part 2

I had not intended to do a series on this subject when I wrote my original Merging FTP Extensibility Walkthroughs blog post, but I came up with a scenario that I felt was worth sharing. I recently posted the following walkthrough on the learn.iis.net web site:

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

We have had many customer requests for a dynamic IP restrictions provider for the FTP server, and I wanted to get that out to customers as soon as I could. That being said, like several of my extensibility walkthroughs in the past, I wrote and tested the provider in that walkthrough on one of the servers that I manage. To show how effective it was, within the first couple of hours the provider had caught and blocked its first script kiddie who was attempting a brute force attack on my FTP server. Over the next few days the provider caught its next hacker, and over the past few weeks it has continued to do so.

That being said, I thought that it might be nice to know when an IP address was blocked, and I had already written the following walkthrough:

How to Use Managed Code (C#) to Create an FTP Provider that Sends an Email when Files are Uploaded

With that in mind, merging the two walkthroughs seemed like a simple thing to do.

Before continuing I need to reiterate the notice that I added to the dynamic IP restrictions walkthrough:

IMPORTANT NOTE: The latest version of the FTP 7.5 service must be installed in order to use the provider in this walkthrough. A version FTP 7.5 was released on August 3, 2009 that addressed an issue where the local and remote IP addresses in the IFtpLogProvider.Log() method were incorrect. Because of this, using an earlier version of the FTP service will prevent this provider from working.

With that warning out of the way, here are the steps that you need to follow in order to merge the two walkthroughs:

Step 1 - Create the project

Create a new C# project following all of the steps in the How to Use Managed Code (C#) to Create an FTP Authentication Provider with Dynamic IP Restrictions walkthrough.

Step 2 - Merge global variables

In this step you need to merge the global variables from the two walkthroughs. In my provider this looked like the following:

// Define the default values - these are only
// used if the configuration settings are not set.
const int defaultLogonAttempts = 5;
const int defaultFloodSeconds = 30;
const int defaultSmtpPort = 25;

// Define a connection string with no default.
private static string _connectionString;

// Initialize the private variables with the default values.
private static int _logonAttempts = defaultLogonAttempts;
private static int _floodSeconds = defaultFloodSeconds;

// Flag the application as uninitialized.
private static bool _initialized = false;

// Define a list that will contain the list of flagged sessions.
private static List<string> _flaggedSessions;

private string _smtpServerName;
private string _smtpFromAddress;
private string _smtpToAddress;
private int _smtpServerPort;
Step 3 - Merge the Initialize() methods

In this step you need to merge the Initialize() methods from the two walkthroughs so that all of the settings are retrieved from the IIS configuration file when the provider is loaded by the FTP service. In my provider this looked like the following:

// Initialize the provider.
protected override void Initialize(StringDictionary config)
{
    // Test if the application has already been initialized.
    if (_initialized == false)
    {
        // Create the flagged sessions list.
        _flaggedSessions = new List<string>();
        
        // Retrieve the connection string for the database connection.
        _connectionString = config["connectionString"];
        if (string.IsNullOrEmpty(_connectionString))
        {
            // Raise an exception if the connection string is missing or empty.
            throw new ArgumentException(
                "Missing connectionString value in configuration.");
        }
        else
        {
            // Determine whether the database is a Microsoft Access database.
            if (_connectionString.Contains("Microsoft.Jet"))
            {
                // Throw an exception if the database is a Microsoft Access database.
                throw new ProviderException("Microsoft Access databases are not supported.");
            }
        }
        
        // Retrieve the number of failures before an IP
        // address is locked out - or use the default value.
        if (int.TryParse(config["logonAttempts"], out _logonAttempts) == false)
        {
            // Set to the default if the number of logon attempts is not valid.
            _logonAttempts = defaultLogonAttempts;
        }
        
        // Retrieve the number of seconds for flood
        // prevention - or use the default value.
        if (int.TryParse(config["floodSeconds"], out _floodSeconds) == false)
        {
            // Set to the default if the number of logon attempts is not valid.
            _floodSeconds = defaultFloodSeconds;
        }
        
        // Test if the number is a positive integer and less than 10 minutes.
        if ((_floodSeconds <= 0) || (_floodSeconds > 600))
        {
            // Set to the default if the number of logon attempts is not valid.
            _floodSeconds = defaultFloodSeconds;
        }
        
        // Retrieve the email settings from configuration.
        _smtpServerName = config["smtpServerName"];
        _smtpFromAddress = config["smtpFromAddress"];
        _smtpToAddress = config["smtpToAddress"];
        
        // Detect and handle any mis-configured settings.
        if (!int.TryParse(config["smtpServerPort"], out _smtpServerPort))
        {
            _smtpServerPort = defaultSmtpPort;
        }
        if (string.IsNullOrEmpty(_smtpServerName))
        {
            throw new ArgumentException(
                "Missing smtpServerName value in configuration.");
        }
        if (string.IsNullOrEmpty(_smtpFromAddress))
        {
            throw new ArgumentException(
                "Missing smtpFromAddress value in configuration.");
        }
        if (string.IsNullOrEmpty(_smtpToAddress))
        {
            throw new ArgumentException(
                "Missing smtpToAddress value in configuration.");
        }
        
        // Initial garbage collection.
        GarbageCollection(true);
        
        // Flag the provider as initialized.
        _initialized = true;
    }
}
Step 4 - Add a SendEmail() method

For this step I copied some of my code from the email walkthrough and used it as the foundation for a new SendEmail() method that I added to the provider. In my provider this looked like the following:

private void SendEmail(string emailSubject, string emailMessage)
{
    // Create an SMTP message.
    SmtpClient smtpClient = new SmtpClient(_smtpServerName, _smtpServerPort);
    MailAddress mailFromAddress = new MailAddress(_smtpFromAddress);
    MailAddress mailToAddress = new MailAddress(_smtpToAddress);
    
    using (MailMessage mailMessage = new MailMessage(mailFromAddress, mailToAddress))
    {
        try
        {
            // Format the SMTP message as UTF8.
            mailMessage.BodyEncoding = Encoding.UTF8;
            // Add the subject.
            mailMessage.Subject = emailSubject;
            // Add the body.
            mailMessage.Body = emailMessage;
            // Send the email message.
            smtpClient.Send(mailMessage);
        }
        catch (SmtpException ex)
        {
            // Send an exception message to the debug
            // channel if the email fails to send.
            Debug.WriteLine(ex.Message);
        }
    }
}

Note: This uses the settings that you store in your IIS applicationHost.config file and are loaded by the Initialize() method.

Step 5 - Add email functionality to the BanAddress() method

In this step you add the functionality to send an email whenever an IP address is added to the list of banned IP addresses. In my provider this looked like the following:

// Mark an IP address as banned.
private void BanAddress(string ipAddress)
{
    // Check if the IP address is already banned.
    if (IsAddressBanned(ipAddress) == false)
    {
        // Ban the IP address if it is not already banned.
        InsertDataIntoTable("[BannedAddresses]",
            "[IPAddress]", "'" + ipAddress + "'");
        // Send an email for the banned address.
        SendEmail("Banned IP Address",
            "The IP address " + ipAddress + " was banned.");
    }
}
Step 6 - Methods that are not changed

I need to point out that there are several methods that require no changes. These methods are listed here for reference:

  • Dispose()
  • AuthenticateUser()
  • Log()
  • IsValidUser()
  • IsAddressBanned()
  • IsSessionFlagged()
  • FlagSession()
  • GarbageCollection
  • GetRecordCountByCriteria()
  • InsertDataIntoTable()
  • DeleteRecordsByCriteria()
  • ExecuteQuery()

Note: You could easily add the email functionality to the FlagSession() method so you will see when a banned IP address is trying to access your server, but depending on the number of sessions that are flagged on your server you might receive more emails than you really need.

Step 7 - Register the provider and configure your settings

In this last step you add the provider to your IIS configuration settings using the AppCmd utility, and you specify the values for the various settings that the provider requires:

cd %SystemRoot%\System32\Inetsrv

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

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

AppCmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='smtpServerName',value='localhost']" /commit:apphost

AppCmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='smtpServerPort',value='25']" /commit:apphost

AppCmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='smtpFromAddress',value='someone@contoso.com']" /commit:apphost

AppCmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='smtpToAddress',value='someone@contoso.com']" /commit:apphost

AppCmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='connectionString',value='Server=localhost;Database=FtpAuthentication;User ID=FtpLogin;Password=P@ssw0rd']" /commit:apphost

AppCmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='logonAttempts',value='5']" /commit:apphost

AppCmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='floodSeconds',value='30']" /commit:apphost

Note: You need to update the above syntax using the managed type information for your provider and the configuration settings for your SMTP server, email addresses, and database connection string.

Step 8 - Add the provider to a site

In this last step you add the provider to a site. If you were adding the provider to your Default Web Site that would look like the following:

AppCmd.exe set config -section:system.applicationHost/sites /"[name='Default Web Site'].ftpServer.security.authentication.basicAuthentication.enabled:False" /commit:apphost

AppCmd.exe set config -section:system.applicationHost/sites /+"[name='Default Web Site'].ftpServer.security.authentication.customAuthentication.providers.[name='FtpAddressRestrictionAuthentication',enabled='True']" /commit:apphost

AppCmd set site "Default Web Site" /+ftpServer.customFeatures.providers.[name='FtpAddressRestrictionAuthentication',enabled='true'] /commit:apphost

Summary

That wraps it up for today's post, and I hope you find it useful. Smile

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Aug 19 2009, 13:36 by Bob | Comments (0)
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: FTP | Extensibility
Tags:
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

Automatically Creating Checksum Files for FTP Uploads

I had a great question in the publishing forums on forums.iis.net, where someone was asking if FTP 7 supported the XCRC command. The short answer is that the XCRC command is not supported, but I came up with a way to create an FTP provider that supports something like it. Since it was a rather fun code sample to write, I thought that I'd turn it into a blog.

The sample FTP provider code in this blog post will automatically calculate an MD5 checksum from a file that is uploaded and store it in a file with a "*.MD5.TXT" file name extension. You can then compare the uploaded checksum with a local checksum on the client to verify the uploaded file's integrity.

There are a few points that I need to discuss before I present the code sample:

  • I chose to use MD5 because it is built-in to the .NET System.Security.Cryptography namespace and I often like to use MD5 for file checksums. I could just have easily implemented SHA1, SHA256, or any of the other built-in hashing algorithms. Unfortunately, CRC32 is not a built-in algorithm for .NET, but a quick search around the Internet yielded several CRC32 samples in C# from various developers, so if you specifically need the CRC32 algorithm you can find it pretty quickly and substitute it for MD5 in my example. (You can click here to search for examples.) You could go one step further and have your provider support multiple checksum algorithms, but that's going way outside the scope of this blog.
  • There are a couple of security considerations for this provider:
    • The provider needs to calculate the path of the uploaded file, and to do so requires calling into the IIS configuration APIs. As I mention in the code remarks:
      • The FTP service will host the compiled assembly in the "Microsoft FTP Service Extensibility Host" COM+ package (DLLHOST.EXE), which runs by default as NETWORK SERVICE.
      • Also by default, the NETWORK SERVICE account does not have sufficient privileges to read the IIS configuration settings. As such, you must either grant READ permissions to NETWORK SERVICE for the IIS configuration files, or configure the COM+ package to run as a user that has at least READ access to the files in the InetSrv\config folder.
      • By default, the NETWORK SERVICE account may not have WRITE permission to the folder where your files are uploaded, so the checksum files cannot be written. As such, you will need to grant READ/WRITE access to the destination where the checksum files will be written.
    • The above steps are not generally recommended practices; but if you choose to grant NETWORK SERVICE permission to the configuration files, the remarks section in the code sample provides the details that you need.
    • Alternatively, you could skip the path lookup and always store the checksum files in a known location. This allows you to remove the MapSiteRootPath() and FindElement() methods from the code sample, and you need only grant the NETWORK SERVICE account permission for the known location.
  • The MapSiteRootPath() method in the provider sample calculates the path of the site's root, then uses the relative path of the uploaded file to compute the full path to the checksum file. This does not take into account any paths that include virtual directories; as such, you would need to accommodate for any virtual paths in your site's hierarchy. (That's too much code for this blog post.)
  • The provider defines a 1 GB constant for the maximum file size for computing checksums. I specified this value so that large files would not tie up your system's resources. You can increase or decrease that value, you could make that a parameter that is stored in the provider's settings, or you can remove the functionality completely. This provider runs synchronously, so larger files will obviously take more time. While it's outside the scope of this blog, you could implement some form of asynchronous functionality. (When discussing this provider with Daniel Vasquez Lopez, he suggested using MSMQ - but that's really going way beyond the scope of what I wanted to accomplish with this blog.)

All of that being said, this provider follows the same development path as the provider in my How to Use Managed Code (C#) to Create a Simple FTP Logging Provider walkthrough, so if you follow the steps in that walkthrough and substitute "FtpUploadChecksumDemo" every place that you see "FtpLoggingDemo" and add a reference to Microsoft.Web.Administration, you should have all of the steps that you need in order to use this provider.

So without further discussion, here's the code for the provider:

using System;
using System.Configuration.Provider;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Web.Administration;
using Microsoft.Web.FtpServer;

// NOTE: This code is provided "as-is" and comes with the following security
// considerations. The FTP service will host the compiled assembly in the
// "Microsoft FTP Service Extensibility Host" COM+ package (DLLHOST.EXE),
// which runs by default as NETWORK SERVICE. By default, this account does not
// have sufficient privileges to read the IIS configuration settings. As such,
// you must either grant READ permissions to NETWORK SERVICE for the configuration
// files, or configure the COM+ package to run as a user that has at least READ
// access to the files in the InetSrv\config folder and READ/WRITE access to the
// destination where the checksum file will be written. However, these are not
// generally recommended practices.
//
// If you choose to grant NETWORK SERVICE permission to the configuration files,
// the following three commands should accomplish the requisite permissions:
//
//  cacls "%SystemRoot%\System32\inetsrv\config" /G "Network Service":R /E
//  cacls "%SystemRoot%\System32\inetsrv\config\redirection.config" /G "Network Service":R /E
//  cacls "%SystemRoot%\System32\inetsrv\config\applicationHost.config" /G "Network Service":R /E
//
// NOTE: You will need to do something similar for your content directory so that
// the checksum files can be created.

public sealed class FtpUploadChecksumDemo : BaseProvider, IFtpLogProvider
{
  // Implement the logging method.
  void IFtpLogProvider.Log(FtpLogEntry loggingParameters)
  {
    // Test for a successful file upload operation.
    if ((loggingParameters.Command == "STOR") && 
      (loggingParameters.FtpStatus == 226))
    {
      try
      {
        // Define a 1GB maximum length - to prevent system hogging.
        const long maxLength = 0x3fffffff;

        // Map the path to the site root.
        string fullPath = MapSiteRootPath(loggingParameters.SiteName);
        // Append the relative path of the uploaded file.
        fullPath += loggingParameters.FullPath;
        // Expand any environment variables.
        fullPath = Environment.ExpandEnvironmentVariables(fullPath);
        // Convert forward slashes to back slashes
        fullPath = fullPath.Replace(@"/", @"\");

        // Open the uploaded file to create a CRC.
        using (FileStream input = File.Open(
          fullPath,
          FileMode.Open,
          FileAccess.Read,
          FileShare.Read))
        {
          // Test the input file length.
          if (input.Length > maxLength)
          {
            // Throw an execption if the file is too big.
            throw new ProviderException(
              String.Format("Input file is too large: {0}",
              input.Length.ToString()));
          }
          else
          {
            // Open the hash file for output.
            using (StreamWriter output = new StreamWriter(
              fullPath + ".MD5.txt",
              false))
            {
              // Create an MD5 object.
              MD5 md5 = MD5.Create();
              // Retrieve the hash byte array.
              byte[] byteArray = md5.ComputeHash(input);
              // Create a new string builder for the ASCII hash string.
              StringBuilder stringBuilder =
                new StringBuilder(byteArray.Length * 2);
              // Loop through the hash.
              foreach (byte byteMember in byteArray)
              {
                // Append each ASCII hex byte to the hash string.
                stringBuilder.AppendFormat("{0:x2}", byteMember);
              }
              // Write the hash string to the output file.
              output.Write(stringBuilder);
            }
          }
        }
      }
      catch(Exception ex)
      {
        throw new ProviderException(ex.Message);
      }
    }
  }

  // This method is almost 100% from scripts that were created
  // by the IIS Manager Configuration Editor admin pack tool.
  private static string MapSiteRootPath(string siteName)
  {
    try
    {
      using (ServerManager serverManager = new ServerManager())
      {
        Configuration config =
          serverManager.GetApplicationHostConfiguration();
        ConfigurationSection sitesSection =
          config.GetSection("system.applicationHost/sites");
        ConfigurationElementCollection sitesCollection =
          sitesSection.GetCollection();
        ConfigurationElement siteElement =
          FindElement(sitesCollection, "site", "name", siteName);
        if (siteElement == null)
        {
          throw new InvalidOperationException("Element not found!");
        }
        else
        {
          ConfigurationElementCollection siteCollection =
            siteElement.GetCollection();
          ConfigurationElement applicationElement =
            FindElement(siteCollection,
            "application",
            "path", @"/");
          if (applicationElement == null)
          {
            throw new InvalidOperationException("Element not found!");
          }
          else
          {
            ConfigurationElementCollection applicationCollection =
              applicationElement.GetCollection();
            ConfigurationElement virtualDirectoryElement =
              FindElement(applicationCollection,
              "virtualDirectory",
              "path", @"/");
            if (virtualDirectoryElement == null)
            {
              throw new InvalidOperationException("Element not found!");
            }
            else
            {
              return virtualDirectoryElement["physicalPath"].ToString();
            }
          }
        }
      }
    }
    catch (Exception ex)
    {
      throw new ProviderException(ex.Message);
    }
  }

  // This method is almost 100% from scripts that were created
  // by the IIS Manager Configuration Editor admin pack tool.
  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;
  }
}

That wraps it up for today's post.

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Jul 30 2009, 11:01 by Bob | Comments (0)
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: FTP | Extensibility
Tags:
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us