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

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

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

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

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

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

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

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

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

Step 1 - Create the project

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

Step 2 - Merge global variables

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Step 7 - Register the provider and configure your settings

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

cd %SystemRoot%\System32\Inetsrv

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

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

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

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

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

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

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

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

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

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

Step 8 - Add the provider to a site

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

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

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

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

Summary

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