Command-Line Utility to Create BlogEngine.NET Password Hashes

I ran into an interesting predicament the other day, and I thought that both the situation and my solution were worth sharing. Here's the scenario: I host websites for several family members and friends, and one of my family member's uses BlogEngine.NET for her blog. (As you may have seen in my previous blogs, I'm a big fan of BlogEngine.NET.) In any event, she forgot her password, so I logged into the admin section of her website, only to discover that there was no way for me to reset her password – I could only reset my password. Since it's my webserver, I have access to the physical files, so I decided to write a simple utility that can create the requisite SHA256/BASE64 password hashes that BlogEngine.NET uses, and then I can manually update the Users.xml file with new password hashes as I create them.

With that in mind, here is the code for the command-line utility:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace BlogEnginePasswordHash
{
  class Program
  {
    static void Main(string[] args)
    {
      // Verify that a single argument was passed to the application...
      if (args.Length != 1)
      {
        // ...if not, reply with generic help message.
        Console.WriteLine("\nUSAGE: BlogEnginePasswordHash <password>\n");
      }
      // ...otherwise...
      else
      {
        // Retrieve a sequence of bytes for the password argument.
        var passwordBytes = Encoding.UTF8.GetBytes(args[0]);
        // 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);
          // Display the password and it's hash.
          Console.WriteLine("\nPassword: {0}\nHash: {1}\n", args[0], passwordHash);
        }
      }
    }
  }
}

That code snippet should be pretty self-explanatory; the application takes a single argument, which is the password to hash. Once you enter a password and hit enter, the password and it's respective hash will be displayed.

Here are a few examples:

C:\>BlogEnginePasswordHash.exe "This is my password"

Password: This is my password
Hash: 6tV+IGzvN4gaQ0vmCWNHSQ0UQ0WgW4+ThJuhpXR6Z3c=

C:\>BlogEnginePasswordHash.exe Password1

Password: Password1
Hash: GVE/3J2k+3KkoF62aRdUjTyQ/5TVQZ4fI2PuqJ3+4d0=

C:\>BlogEnginePasswordHash.exe Password2

Password: Password2
Hash: G+AiJ1Cq84iauVtdWTuhLk/xBGR0cC1rR3n0tScwWyM=

C:\>

Once you have created password hashes, you can paste those into the Users.xml file for your website:

<Users>
  <User>
    <UserName>Alice</UserName>
    <Password>GVE/3J2k+3KkoF62aRdUjTyQ/5TVQZ4fI2PuqJ3+4d0=</Password>
    <Email>alice@fabrikam.com</Email>
    <LastLoginTime>2015-01-31 01:52:00</LastLoginTime>
  </User>
  <User>
    <UserName>Bob</UserName>
    <Password>G+AiJ1Cq84iauVtdWTuhLk/xBGR0cC1rR3n0tScwWyM=</Password>
    <Email>bob@fabrikam.com</Email>
    <LastLoginTime>2015-01-31 01:53:00</LastLoginTime>
  </User>
</Users>

That's all there is to do. Pretty simple stuff.


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

Simple Utility to Calculate File Hashes

I have to download various files from time-to-time, and it's nice when websites provide checksum hashes so I can validate that the file I just downloaded matches the version on the server. (ON a related note, I wrote a blog several years ago which showed how to create a provider for the IIS FTP service which automatically creates checksum files when files are uploaded to a server; see my Automatically Creating Checksum Files for FTP Uploads blog post for the details.)

In order to calculate hashes for files that I have downloaded, several years ago I wrote a simple command-line application for Windows which uses several of the built-in algorithms in .NET's System.Security.Cryptography. And while I realize that there are probably other tools that provide this same functionality, I have used this little utility for years, and I've had several people ask me for copies. With that in mind, I thought that it might make a nice blog topic if I shared the code with everyone. (Note: It's a really simple sample; the .NET framework does all the real work for this application.)

Without further fanfare, here's the source code. In order to use this code sample, you need to create a new C# project in Visual Studio and choose the Console Application template. When the new project opens, replace the template's code with the following:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Security.Cryptography;

class Hash
{
    static void Main(string[] args)
    {
        // Verify the correct number of command-line arguments.
        if (args.Length != 2)
        {
            // Show the help message if an incorrect number of arguments was specified.
            ShowHelp();
            return;
        }
        else
        {
            byte[] hashValue = null;
            // Verify that the specified file exists.
            if (!File.Exists(args[1]))
            {
                // Show the help message if a non-existent filename was specified.
                ShowHelp();
                return;
            }
            else
            {
                try
                {
                    // Create a fileStream for the file.
                    FileStream fileStream = File.OpenRead(args[1]);
                    // Be sure it's positioned to the beginning of the stream.
                    fileStream.Position = 0;
                    // Use the specified hash algorithm.
                    switch (args[0].ToUpper())
                    {
                        case "MD5":
                            // Compute the MD5 hash of the fileStream.
                            hashValue = MD5.Create().ComputeHash(fileStream);
                            break;
                        case "SHA1":
                            // Compute the SHA1 hash of the fileStream.
                            hashValue = SHA1.Create().ComputeHash(fileStream);
                            break;
                        case "SHA256":
                            // Compute the SHA256 hash of the fileStream.
                            hashValue = SHA256.Create().ComputeHash(fileStream);
                            break;
                        case "SHA384":
                            // Compute the SHA384 hash of the fileStream.
                            hashValue = SHA384.Create().ComputeHash(fileStream);
                            break;
                        case "SHA512":
                            // Compute the SHA512 hash of the fileStream.
                            hashValue = SHA512.Create().ComputeHash(fileStream);
                            break;
                        case "BASE64":
                            // Compute the BASE64 hash of the fileStream.
                            byte[] binaryData = new Byte[fileStream.Length];
                            long bytesRead = fileStream.Read(binaryData, 0, (int)fileStream.Length);
                            if (bytesRead != fileStream.Length)
                            {
                                throw new Exception(String.Format("Number of bytes read ({0}) does not match file size ({1}).", bytesRead, fileStream.Length));
                            }
                            string base64String = System.Convert.ToBase64String(binaryData, 0, binaryData.Length);
                            Console.WriteLine("File: {0}\r\nBASE64 Hash: {1}", fileStream.Name, base64String);
                            hashValue = null;
                            break;
                        default:
                            // Display the help message if an unrecognized hash algorithm was specified.
                            ShowHelp();
                            return;
                    }
                    if (hashValue != null)
                    {
                        // Write the hash value to the Console.
                        PrintHashData(args[0].ToUpper(), fileStream.Name, hashValue);
                    }
                    // Close the file.
                    fileStream.Close();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error: {0}", ex.Message);
                }
            }
        }
    }

    // Display the help message.
    private static void ShowHelp()
    {/>        Console.WriteLine("HASH.exe <hash algorithm> <file name>\n\n" +
            "\tWhere <hash algorithm> is one of the following:\n" +
            "\t\tBASE64\n\t\tMD5\n\t\tSHA1\n\t\tSHA256\n\t\tSHA384\n\t\tSHA512\n");
    }

    // Print the hash data in a readable format.
    private static void PrintHashData(string algorithm, string fileName, byte[] array)
    {
        Console.Write("File: {0}\r\n{1} Hash: ", fileName,algorithm);
        for (int i = 0; i < array.Length; i++)
        {
            Console.Write(String.Format("{0:X2}", array[i]));
        }
        Console.WriteLine();
    }/>}

When you compile and run the application, you will see following help message when you specify no command-line parameters:


HASH.exe <hash algorithm> <file name> Where <hash algorithm> is one of the following: BASE64 MD5 SHA1 SHA256 SHA384 SHA512

When you specify one of the supported hashing algorithms and a filename, the application will display something like the following example:


C:\>hash.exe SHA1 foobar.zip File: C:\foobar.zip SHA1 Hash: 57686F6120447564652C20426F6220526F636B73

That's all there is to it. As I mentioned earlier, it's a pretty simple sample. ;-]


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

Custom Post-Build Events for Compiling FTP Providers

I've written a lot of walkthroughs and blog posts about creating custom FTP providers over the past several years, and I usually include instructions like the following example for adding a custom post-build event that will automatically register your extensibility provider in the Global Assembly Cache (GAC) on your development computer:

  • Click Project, and then click the menu item your project's properties.
  • Click the Build Events tab.
  • Enter the following in the Post-build event command line dialog box:
    net stop ftpsvc
    call "%VS100COMNTOOLS%\vsvars32.bat">nul
    gacutil.exe /if "$(TargetPath)"
    net start ftpsvc

And I usually include instructions like the following example for determining 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 FtpXmlAuthorization assembly.
  • Right-click the assembly, and then click Properties.
  • Copy the Culture value; for example: Neutral.
  • Copy the Version number; for example: 1.0.0.0.
  • Copy the Public Key Token value; for example: 426f62526f636b73.
  • Click Cancel.

Over time I have changed the custom post-build event that I use when I am creating custom FTP providers, and my changes make it easier to register custom FTP providers. With that in mind, I thought that my changes would make a good blog subject.

First of all, if you take a look at my How to Use Managed Code (C#) to Create a Simple FTP Authentication Provider walkthrough, you will see that I include instructions like my earlier examples to create a custom post-build event and retrieve the assembly information for your extensibility provider.

That being said, instead of using the custom post-build event in that walkthrough, I have started using the following custom post-build event:

net stop ftpsvc
call "$(DevEnvDir)..\Tools\vsvars32.bat"
gacutil.exe /uf "$(TargetName)"
gacutil.exe /if "$(TargetPath)"
gacutil.exe /l "$(TargetName)"
net start ftpsvc

This script should resemble the following example when entered into Visual Studio:

This updated script performs the following actions:

  1. Stops the FTP service (this will allow any copies of your DLL to unload)
  2. Loads the Visual Studio environment variables (this will add gacutil.exe to the path)
  3. Calls gacutil.exe to forcibly unregister any previous version of your FTP provider
  4. Calls gacutil.exe to forcibly register the newly-compiled version of your FTP provider
  5. Calls gacutil.exe to list the GAC information for your FTP provider (this will be used to register your DLL with IIS)
  6. Starts the FTP service

Let's say that you created a simple FTP authentication provider which contained code like the following example:

using System;
using System.Text;
using Microsoft.Web.FtpServer;

public class FtpTestProvider :
    BaseProvider,
    IFtpAuthenticationProvider
{
    private string _username = "test";
    private string _password = "password";
    
    public bool AuthenticateUser(
        string sessionId,
        string siteName,
        string userName,
        string userPassword,
        out string canonicalUserName)
    {
        canonicalUserName = userName;
        if (((userName.Equals(_username,
            StringComparison.OrdinalIgnoreCase)) == true) &&
            userPassword == _password)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

When you compile your provider in Visual Studio, the output window should show the results of the custom post-build event:

When you examine the output information in detail, the highlighted area in the example below should be of particular interest, because it contains the assembly information for your extensibility provider:

------ Rebuild All started: Project: FtpTestProvider, Configuration: Debug Any CPU ------
FtpTestProvider -> c:\users\foobar\documents\visual studio 2012\Projects\FtpTestProvider\bin\Debug\FtpTestProvider.dll
The Microsoft FTP Service service is stopping..
The Microsoft FTP Service service was stopped successfully.

Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.17929
Copyright (c) Microsoft Corporation. All rights reserved.

Assembly successfully added to the cache
Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.17929
Copyright (c) Microsoft Corporation. All rights reserved.

The Global Assembly Cache contains the following assemblies:
FtpTestProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=eb763c2ec0efff75, processorArchitecture=MSIL

Number of items = 1
The Microsoft FTP Service service is starting.
The Microsoft FTP Service service was started successfully.

========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

Once you have that information, you simply need to reformat it as "FtpTestProvider, FtpTestProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=eb763c2ec0efff75" in order to enter it into the FTP Custom Authentication Providers dialog box in the IIS Manager, or by following the steps in my FTP Walkthroughs or my Adding Custom FTP Providers with the IIS Configuration Editor blogs.

That wraps it up for today's post. As always, let me know if you have any questions. ;-]


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

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

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

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

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

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

Viewing current FTP7 sessions using C#

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

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

Method #1:

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

Method #2:

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

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

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

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

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

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


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

Extensibility Updates in the FTP 8.0 Service

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

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

Custom FTP Authorization

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

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

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

Custom FTP Event Handling

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

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

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

Happy coding!


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

Programmatically Flushing FTP Logs

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

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

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

C#

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

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

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

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

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

VB.NET

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

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

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

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

End Sub

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

End Module

JavaScript

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

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

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

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

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

VBScript

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

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

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

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

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

Summary

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



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

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/

Sending WebDAV Requests in .NET Revisited

I recently spoke with a great customer in India, and he was experimenting with the code from my Sending WebDAV Requests in .NET blog post. He had a need to send the WebDAV LOCK/UNLOCK commands, so I wrote a quick addition to the code in my original blog post to send those commands, and I thought that I'd share that code in an updated blog post.

Using WebDAV Locks

First of all, you may need to enable WebDAV locks on your server. To do so, follow the instructions in the following walkthrough:

How to Use WebDAV Locks
http://learn.iis.net/page.aspx/596/how-to-use-webdav-locks/

If you were writing a WebDAV client, sending the LOCK/UNLOCK commands would help to avoid two clients attempting to author the same resource. So if your WebDAV client was editing a file named "foo.txt", the flow of events would be something like the following:

  1. LOCK foo.txt
  2. GET foo.txt
  3. [make some changes to foo.txt]
  4. PUT foo.txt
  5. UNLOCK foo.txt

Description for the Sample Application

The updated code sample in this blog post shows how to send most of the common WebDAV requests using C# and common .NET libraries. In addition to adding the LOCK/UNLOCK commands to this version, I also changed the sample files to upload/download Classic ASP pages instead of text files; I did this so you can see that the WebDAV requests are correctly accessing the source code of the ASP pages instead of the translated output.

Having said that, I need to mention once again that I create more objects than are necessary for each section of the sample, which creates several intentional redundancies; I did this because I wanted to make each section somewhat self-sufficient, which helps you to copy and paste a little easier. I present the WebDAV methods the in the following order:

WebDAV MethodNotes
PUT This section of the sample writes a string as a text file to the destination server as "foobar1.asp". Sending a raw string is only one way of writing data to the server, in a more common scenario you would probably open a file using a steam object and write it to the destination. One thing to note in this section of the sample is the addition of the "Overwrite" header, which specifies that the destination file can be overwritten.
LOCK This section of the sample sends a WebDAV request to lock the "foobar1.asp" before downloading it with a GET request.
GET This section of the sample sends a WebDAV-specific form of the HTTP GET method to retrieve the source code for the destination URL. This is accomplished by sending the "Translate: F" header and value, which instructs IIS to send the source code instead of the processed URL. In this specific sample I am using Classic ASP, but if the requests were for ASP.NET or PHP files you would also need to specify the "Translate: F" header/value pair.
PUT This section of the sample sends an updated version of the "foobar1.asp" script to the server, which overwrites the original file. The purpose of this PUT command is to simulate creating a WebDAV client that can update files on the server.
GET This section of the sample retrieves the updated version of the "foobar1.asp" script from the server, just to show that the updated version was saved successfully.
UNLOCK This section of the sample uses the lock token from the earlier LOCK request to unlock the "foobar1.asp"
COPY This section of the sample copies the file from "foobar1.asp" to "foobar2.asp", and uses the "Overwrite" header to specify that the destination file can be overwritten. One thing to note in this section of the sample is the addition of the "Destination" header, which obviously specifies the destination URL. The value for this header can be a relative path or an FQDN, but it may not be an FQDN to a different server.
MOVE This section of the sample moves the file from "foobar2.asp" to "foobar1.asp", thereby replacing the original uploaded file. As with the previous two sections of the sample, this section of the sample uses the "Overwrite" and "Destination" headers.
DELETE This section of the sample deletes the original file, thereby removing the sample file from the destination server.
MKCOL This section of the sample creates a folder named "foobar3" on the destination server; as far as WebDAV on IIS is concerned, the MKCOL method is a lot like the old DOS MKDIR command.
DELETE This section of the sample deletes the folder from the destination server.

Source Code for the Sample Application

Here is the source code for the updated sample application:

using System;
using System.Net;
using System.IO;
using System.Text;

class WebDavTest
{
  static void Main(string[] args)
  {
    try
    {
      // Define the URLs.
      string szURL1 = @"http://localhost/foobar1.asp";
      string szURL2 = @"http://localhost/foobar2.asp";
      string szURL3 = @"http://localhost/foobar3";

      // Some sample code to put in an ASP file.
      string szAspCode1 = @"<%=Year()%>";
      string szAspCode2 = @"<%=Time()%>";

      // Some XML to put in a lock request.
      string szLockXml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
        "<D:lockinfo xmlns:D='DAV:'>" +
        "<D:lockscope><D:exclusive/></D:lockscope>" +
        "<D:locktype><D:write/></D:locktype>" +
        "<D:owner><D:href>mailto:someone@example.com</D:href></D:owner>" +
        "</D:lockinfo>";

      // Define username, password, and lock token strings.
      string szUsername  = @"username";
      string szPassword  = @"password";
      string szLockToken = null;

      // --------------- PUT REQUEST #1 --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpPutRequest1 =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpPutRequest1.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpPutRequest1.PreAuthenticate = true;

      // Define the HTTP method.
      httpPutRequest1.Method = @"PUT";

      // Specify that overwriting the destination is allowed.
      httpPutRequest1.Headers.Add(@"Overwrite", @"T");

      // Specify the content length.
      httpPutRequest1.ContentLength = szAspCode1.Length;

      // Optional, but allows for larger files.
      httpPutRequest1.SendChunked = true;

      // Retrieve the request stream.
      Stream putRequestStream1 =
         httpPutRequest1.GetRequestStream();

      // Write the string to the destination as text bytes.
      putRequestStream1.Write(
         Encoding.UTF8.GetBytes((string)szAspCode1),
         0, szAspCode1.Length);

      // Close the request stream.
      putRequestStream1.Close();

      // Retrieve the response.
      HttpWebResponse httpPutResponse1 =
         (HttpWebResponse)httpPutRequest1.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"PUT Response #1: {0}",
         httpPutResponse1.StatusDescription);

      // --------------- LOCK REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpLockRequest =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpLockRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpLockRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpLockRequest.Method = @"LOCK";

      // Specify the request timeout.
      httpLockRequest.Headers.Add(@"Timeout", "Infinite");

      // Specify the request content type.
      httpLockRequest.ContentType = "text/xml; charset=\"utf-8\"";

      // Retrieve the request stream.
      Stream lockRequestStream =
         httpLockRequest.GetRequestStream();

      // Write the lock XML to the destination.
      lockRequestStream.Write(
         Encoding.UTF8.GetBytes((string)szLockXml),
         0, szLockXml.Length);

      // Close the request stream.
      lockRequestStream.Close();

      // Retrieve the response.
      HttpWebResponse httpLockResponse =
         (HttpWebResponse)httpLockRequest.GetResponse();

      // Retrieve the lock token for the request.
      szLockToken = httpLockResponse.GetResponseHeader("Lock-Token");

      // Write the response status to the console.
      Console.WriteLine(
         @"LOCK Response: {0}",
         httpLockResponse.StatusDescription);
      Console.WriteLine(
         @" LOCK Token: {0}",
         szLockToken);

      // --------------- GET REQUEST #1 --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpGetRequest1 =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpGetRequest1.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpGetRequest1.PreAuthenticate = true;

      // Define the HTTP method.
      httpGetRequest1.Method = @"GET";

      // Specify the request for source code.
      httpGetRequest1.Headers.Add(@"Translate", "F");

      // Retrieve the response.
      HttpWebResponse httpGetResponse1 =
         (HttpWebResponse)httpGetRequest1.GetResponse();

      // Retrieve the response stream.
      Stream getResponseStream1 =
         httpGetResponse1.GetResponseStream();

      // Create a stream reader for the response.
      StreamReader getStreamReader1 =
         new StreamReader(getResponseStream1, Encoding.UTF8);

      // Write the response status to the console.
      Console.WriteLine(
         @"GET Response #1: {0}",
         httpGetResponse1.StatusDescription);
      Console.WriteLine(
         @" Response Length: {0}",
         httpGetResponse1.ContentLength);
      Console.WriteLine(
         @" Response Text: {0}",
         getStreamReader1.ReadToEnd());

      // Close the response streams.
      getStreamReader1.Close();
      getResponseStream1.Close();

      // --------------- PUT REQUEST #2 --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpPutRequest2 =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpPutRequest2.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpPutRequest2.PreAuthenticate = true;

      // Define the HTTP method.
      httpPutRequest2.Method = @"PUT";

      // Specify that overwriting the destination is allowed.
      httpPutRequest2.Headers.Add(@"Overwrite", @"T");

      // Specify the lock token.
      httpPutRequest2.Headers.Add(@"If",
        String.Format(@"({0})",szLockToken));

      // Specify the content length.
      httpPutRequest2.ContentLength = szAspCode1.Length;

      // Optional, but allows for larger files.
      httpPutRequest2.SendChunked = true;

      // Retrieve the request stream.
      Stream putRequestStream2 =
         httpPutRequest2.GetRequestStream();

      // Write the string to the destination as a text file.
      putRequestStream2.Write(
         Encoding.UTF8.GetBytes((string)szAspCode2),
         0, szAspCode1.Length);

      // Close the request stream.
      putRequestStream2.Close();

      // Retrieve the response.
      HttpWebResponse httpPutResponse2 =
         (HttpWebResponse)httpPutRequest2.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"PUT Response #2: {0}",
         httpPutResponse2.StatusDescription);

      // --------------- GET REQUEST #2 --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpGetRequest2 =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpGetRequest2.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpGetRequest2.PreAuthenticate = true;

      // Define the HTTP method.
      httpGetRequest2.Method = @"GET";

      // Specify the request for source code.
      httpGetRequest2.Headers.Add(@"Translate", "F");

      // Retrieve the response.
      HttpWebResponse httpGetResponse2 =
         (HttpWebResponse)httpGetRequest2.GetResponse();

      // Retrieve the response stream.
      Stream getResponseStream2 =
         httpGetResponse2.GetResponseStream();

      // Create a stream reader for the response.
      StreamReader getStreamReader2 =
         new StreamReader(getResponseStream2, Encoding.UTF8);

      // Write the response status to the console.
      Console.WriteLine(
         @"GET Response #2: {0}",
         httpGetResponse2.StatusDescription);
      Console.WriteLine(
         @" Response Length: {0}",
         httpGetResponse2.ContentLength);
      Console.WriteLine(
         @" Response Text: {0}",
         getStreamReader2.ReadToEnd());

      // Close the response streams.
      getStreamReader2.Close();
      getResponseStream2.Close();
      
      // --------------- UNLOCK REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpUnlockRequest =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpUnlockRequest.Credentials = 
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpUnlockRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpUnlockRequest.Method = @"UNLOCK";

      // Specify the lock token.
      httpUnlockRequest.Headers.Add(@"Lock-Token", szLockToken);

      // Retrieve the response.
      HttpWebResponse httpUnlockResponse =
         (HttpWebResponse)httpUnlockRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(
         @"UNLOCK Response: {0}",
         httpUnlockResponse.StatusDescription);

      // --------------- COPY REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpCopyRequest =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpCopyRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpCopyRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpCopyRequest.Method = @"COPY";

      // Specify the destination URL.
      httpCopyRequest.Headers.Add(@"Destination", szURL2);

      // Specify that overwriting the destination is allowed.
      httpCopyRequest.Headers.Add(@"Overwrite", @"T");

      // Retrieve the response.
      HttpWebResponse httpCopyResponse =
         (HttpWebResponse)httpCopyRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"COPY Response: {0}",
         httpCopyResponse.StatusDescription);

      // --------------- MOVE REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpMoveRequest =
         (HttpWebRequest)WebRequest.Create(szURL2);

      // Set up new credentials.
      httpMoveRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpMoveRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpMoveRequest.Method = @"MOVE";

      // Specify the destination URL.
      httpMoveRequest.Headers.Add(@"Destination", szURL1);

      // Specify that overwriting the destination is allowed.
      httpMoveRequest.Headers.Add(@"Overwrite", @"T");

      // Retrieve the response.
      HttpWebResponse httpMoveResponse =
         (HttpWebResponse)httpMoveRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"MOVE Response: {0}",
         httpMoveResponse.StatusDescription);

      // --------------- DELETE FILE REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpDeleteFileRequest =
         (HttpWebRequest)WebRequest.Create(szURL1);

      // Set up new credentials.
      httpDeleteFileRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpDeleteFileRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpDeleteFileRequest.Method = @"DELETE";

      // Retrieve the response.
      HttpWebResponse httpDeleteFileResponse =
         (HttpWebResponse)httpDeleteFileRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"DELETE File Response: {0}",
         httpDeleteFileResponse.StatusDescription);

      // --------------- MKCOL REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpMkColRequest =
         (HttpWebRequest)WebRequest.Create(szURL3);

      // Set up new credentials.
      httpMkColRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpMkColRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpMkColRequest.Method = @"MKCOL";

      // Retrieve the response.
      HttpWebResponse httpMkColResponse =
         (HttpWebResponse)httpMkColRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"MKCOL Response: {0}",
         httpMkColResponse.StatusDescription);

      // --------------- DELETE FOLDER REQUEST --------------- //

      // Create an HTTP request for the URL.
      HttpWebRequest httpDeleteFolderRequest =
         (HttpWebRequest)WebRequest.Create(szURL3);

      // Set up new credentials.
      httpDeleteFolderRequest.Credentials =
         new NetworkCredential(szUsername, szPassword);

      // Pre-authenticate the request.
      httpDeleteFolderRequest.PreAuthenticate = true;

      // Define the HTTP method.
      httpDeleteFolderRequest.Method = @"DELETE";

      // Retrieve the response.
      HttpWebResponse httpDeleteFolderResponse =
         (HttpWebResponse)httpDeleteFolderRequest.GetResponse();

      // Write the response status to the console.
      Console.WriteLine(@"DELETE Folder Response: {0}",
         httpDeleteFolderResponse.StatusDescription);
    }
    catch (Exception ex)
    {
      Console.WriteLine(ex.Message);
    }
  }
}

Running the Sample Application

When you run the code sample, if there are no errors you should see something like the following output:

PUT Response #1: Created
LOCK Response: OK
   LOCK Token: <opaquelocktoken:4e616d65-6f6e-6d65-6973-526f62657274.426f62526f636b73>
GET Response #1: OK
   Response Length: 11
   Response Text: <%=Year()%>
PUT Response #2: No Content
GET Response #2: OK
   Response Length: 11
   Response Text: <%=Time()%>
UNLOCK Response: No Content
COPY Response: Created
MOVE Response: No Content
DELETE File Response: OK
MKCOL Response: Created
DELETE Folder Response: OK

Press any key to continue . . .

If you looked at the IIS logs after running the sample application, you should see entries like the following example:

#Software: Microsoft Internet Information Services 7.5
#Version: 1.0
#Date: 2011-10-18 06:49:07
#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip sc-status sc-substatus sc-win32-status
2011-10-18 06:49:07 ::1 PUT /foobar1.asp - 80 - ::1 401 2 5
2011-10-18 06:49:07 ::1 PUT /foobar1.asp - 80 username ::1 201 0 0
2011-10-18 06:49:07 ::1 LOCK /foobar1.asp - 80 username ::1 200 0 0
2011-10-18 06:49:07 ::1 GET /foobar1.asp - 80 username ::1 200 0 0
2011-10-18 06:49:07 ::1 PUT /foobar1.asp - 80 username ::1 204 0 0
2011-10-18 06:49:07 ::1 GET /foobar1.asp - 80 username ::1 200 0 0
2011-10-18 06:49:07 ::1 UNLOCK /foobar1.asp - 80 username ::1 204 0 0
2011-10-18 06:49:07 ::1 COPY /foobar1.asp http://localhost/foobar2.asp 80 username ::1 201 0 0
2011-10-18 06:49:07 ::1 MOVE /foobar2.asp http://localhost/foobar1.asp 80 username ::1 204 0 0
2011-10-18 06:49:07 ::1 DELETE /foobar1.asp - 80 username ::1 200 0 0
2011-10-18 06:49:07 ::1 MKCOL /foobar3 - 80 username ::1 201 0 0
2011-10-18 06:49:07 ::1 DELETE /foobar3 - 80 username ::1 200 0 0

Closing Notes

Since the code sample cleans up after itself, you should not see any files or folders on the destination server when it has completed executing. To see the files and folders that are actually created and deleted on the destination server, you would need to step through the code in a debugger.

This updated version does not include examples of the WebDAV PROPPATCH/PROPFIND methods in this sample for the same reason that I did not do so in my previous blog - those commands require processing the XML responses, and that is outside the scope of what I wanted to do with this sample.

I hope this helps!


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

How to Use Managed Code (C#) to Create an FTP Home Directory Provider 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/

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/