Microsoft Bob

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

Be sure to check out my non-technical blog at www.bobsbasement.net.

Month List

Automatically Creating Checksum Files for FTP Uploads

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

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

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

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

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

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

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

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

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

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

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

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

  // This method is almost 100% from scripts that were created
  // by the IIS Manager Configuration Editor admin pack tool.
  private static ConfigurationElement FindElement(
    ConfigurationElementCollection collection,
    string elementTagName,
    params string[] keyValues)
  {
    foreach (ConfigurationElement element in collection)
    {
      if (String.Equals(element.ElementTagName,
        elementTagName,
        StringComparison.OrdinalIgnoreCase))
      {
        bool matches = true;

        for (int i = 0; i < keyValues.Length; i += 2)
        {
          object o = element.GetAttributeValue(keyValues[i]);
          string value = null;
          if (o != null)
          {
            value = o.ToString();
          }

          if (!String.Equals(value,
            keyValues[i + 1],
            StringComparison.OrdinalIgnoreCase))
          {
            matches = false;
            break;
          }
        }
        if (matches)
        {
          return element;
        }
      }
    }
    return null;
  }
}

That wraps it up for today's post.

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

Merging FTP Extensibility Walkthroughs

Over the past several months I’ve been publishing a series of walkthroughs that use the extensibility in FTP 7.5 to create a several custom providers for a variety of scenarios, and today I posted my most recent entry in the series:

How to Use Managed Code to Create an FTP Authentication Provider using an XML Database

As a piece of behind-the-scenes trivia, some of these walkthroughs were based off custom providers that I had actually written for my FTP servers, and I used the samples that I wrote for some of the other walkthroughs as a starting point for custom providers that I currently use. With that in mind, I’d like to use today’s blog to talk about some of the ways that I combine what you see in a few of these walkthroughs into some useful scenarios.

One of the common providers that I use is a combination of the code that you see in these two walkthroughs:

Here's the way that I create the provider - I start with a single provider class that implements the IFtpHomeDirectoryProvider, IFtpAuthenticationProvider, and IFtpRoleProvider interfaces, and I create a few global variables that I'll use later.

public class FtpXmlAuthentication : BaseProvider,
    IFtpHomeDirectoryProvider,
    IFtpAuthenticationProvider,
    IFtpRoleProvider
{
    private string _XmlFileName;

    private string _HomeDirectory;

    private Dictionary<string, XmlUserData> _XmlUserData =
        new Dictionary<string, XmlUserData>(
            StringComparer.InvariantCultureIgnoreCase);
}

I add an Initialize() method to the class, where I load the values named xmlFileName and homeDirectory from the configuration settings.

protected override void Initialize(StringDictionary config)
{
    _XmlFileName = config["xmlFileName"];
    _HomeDirectory = config["homeDirectory"];
    if (string.IsNullOrEmpty(_XmlFileName))
    {
        throw new ArgumentException("Missing xmlFileName value in configuration.");
    }
}

I recycle the provider across a bunch of different FTP sites, and I don't always use the custom home directory feature, so my GetUserHomeDirectoryData() method has to accommodate for that. (Note: this means that your FTP site has to use a method of User Isolation other than "Custom". You can find more information about User Isolation on the FTP User Isolation Page.)

string IFtpHomeDirectoryProvider.GetUserHomeDirectoryData(
    string sessionId,
    string siteName,
    string userName)
{
    if (string.IsNullOrEmpty(_HomeDirectory))
    {
        throw new ArgumentException("Missing homeDirectory value in configuration.");
    }
    return _HomeDirectory;
}

(Note: While it may seem that I could throw the ArgumentException() in the Initialize() method, since I don't always need this value for providers that don't implement the home directory lookup it's best to throw the exception in the GetUserHomeDirectoryData() method.)

The last thing that I do for the provider is to copy the AuthenticateUser(), IsUserInRole(), ReadXmlDataStore(), GetInnerText() methods and XmlUserData class from the How to Use Managed Code to Create an FTP Authentication Provider using an XML Database walkthrough. This gives me a custom FTP authentication provider that provides user, role, and home directory lookups. This means the XML file for the provider registration has to vary a little from the walkthroughs in order to define settings for the xmlFileName and homeDirectory values. Here's an example of that that might look like:

<system.ftpServer>
    <providerDefinitions>
        <add name="ContosoXmlAuthentication" type="FtpXmlAuthentication,FtpXmlAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73" />
        <activation>
            <providerData name="ContosoXmlAuthentication">
                <add key="xmlFileName" value="C:\Inetpub\www.contoso.com\Users.xml" />
                <add key="homeDirectory" value="C:\Inetpub\www.contoso.com\ftproot" />
            </providerData>
        </activation>
    </providerDefinitions>

    <!-- Other XML goes here -->

</system.ftpServer>

The last thing that you need to do is to create the XML file that contains the usernames and passwords, which you can copy from the How to Use Managed Code to Create an FTP Authentication Provider using an XML Database walkthrough.

I use this provider on multiple FTP sites, so I simply re-register the provider under a different name and specify different values for the xmlFileName and homeDirectory values:

<system.ftpServer>
    <providerDefinitions>
        <add name="ContosoXmlAuthentication" type="FtpXmlAuthentication,FtpXmlAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73" />
        <add name="FabrikamXmlAuthentication" type="FtpXmlAuthentication,FtpXmlAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73" />
        <add name="WingTipToysXmlAuthentication" type="FtpXmlAuthentication,FtpXmlAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73" />
        <activation>
            <providerData name="ContosoXmlAuthentication">
                <add key="xmlFileName" value="C:\Inetpub\www.Contoso.com\Users.xml" />
                <add key="homeDirectory" value="C:\Inetpub\www.Contoso.com\ftproot" />
            </providerData>
            <providerData name="FabrikamXmlAuthentication">
                <add key="xmlFileName" value="C:\Inetpub\www.Fabrikam.com\Users.xml" />
                <add key="homeDirectory" value="C:\Inetpub\www.Fabrikam.com\ftproot" />
            </providerData>
            <providerData name="WingTipToysXmlAuthentication">
                <add key="xmlFileName" value="C:\Inetpub\www.WingTipToys.com\Users.xml" />
                <add key="homeDirectory" value="C:\Inetpub\www.WingTipToys.com\ftproot" />
            </providerData>
        </activation>
    </providerDefinitions>

    <!-- Other XML goes here -->

</system.ftpServer>

So in the end I have a provider that provides unique users, roles, and home directory for each FTP site. I point the FTP root to a path that is outside of the HTTP root, so my users can upload files for an application like a photo gallery that I provide them, but they can't access the actual ASP.NET files for the application. Since they're using accounts from the XML file, I don't have to hand out physical accounts on my servers or my domain. (The security-paranoid side of my personality really likes that.)

For some sites I use the XML file for ASP.NET membership by following the instructions in the How to use the Sample Read-Only XML Membership and Role Providers with IIS 7.0 walkthrough. In those cases, I move the XML file into the App_Data folder of the web site. Once again, since the FTP root is different than the HTTP root, this prevents any of my FTP users from accessing the XML file and making changes to it. (Although you could do that if you wanted to allow one of your users to update the list of FTP users for their site. But as you can imagine, the security-paranoid side of my personality really does not like that.)

All that being said, I hope that this helps you to get an idea for other ways that you can use some of the walkthroughs that I've been writing. I have several additional providers and walkthroughs that I’m working on for the IIS.NET web site, but I’ll keep those as a secret for now. ;-]

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