Compartir a través de


Creación de un proveedor de autenticación FTP con restricciones de IP dinámicas

por Robert McMurray

Microsoft ha creado un nuevo servicio FTP que se ha reescrito completamente para Windows Server® 2008. Este nuevo servicio FTP incorpora muchas características nuevas que permiten a los autores web publicar contenido de una forma más sencilla y ofrece a los administradores web más opciones de seguridad e implementación.

El nuevo servicio FTP 7.5 admite extensibilidad que le permite ampliar la funcionalidad integrada que se incluye con el servicio FTP. En concreto, FTP 7.5 admite la creación de sus propios proveedores de autenticación. También puede crear proveedores para el registro de FTP personalizado y para determinar la información del directorio principal para los usuarios de FTP.

Este tutorial le llevará a través de los pasos para usar código administrado en un proveedor de autenticación FTP que proporciona compatibilidad con restricciones de IP dinámicas que usan una base de datos de SQL Server para almacenar información de la cuenta. Este proveedor implementa esta lógica registrando el número de fallos procedentes de direcciones IP remotas y después usando esta información para bloquear las direcciones IP que no logran iniciar sesión en el servidor dentro de una trama temporal determinada.

Importante

La última versión del servicio FTP 7.5 debe estarinstalada para poder usar el proveedor de este tutorial. El 3 de agosto de 2009 se publicó una versión FTP 7.5 que abordaba una incidencia en la que las direcciones IP local y remota en el método IFtpLogProvider.Log() eran incorrectas. Por ello, usar una versión anterior del servicio FTP impedirá que este proveedor funcione.

Requisitos previos

Los siguientes elementos son necesarios para completar los procedimientos de esta sección:

  1. Debe tener instalado IIS 7.0 o superior en su servidor de Windows Server 2008 y también debe tener instalado el Administrador de Internet Information Services (IIS).

  2. El nuevo servicio FTP 7.5 debe estar instalado.

    Importante

    Como se mencionó anteriormente en este tutorial, la versión más reciente del servicio FTP 7.5 debe estar instalada para poder usar el proveedor en este tutorial. El 3 de agosto de 2009 se publicó una versión FTP 7.5 que abordaba una incidencia en la que las direcciones IP local y remota en el método IFtpLogProvider.Log() eran incorrectas. Por ello, usar una versión anterior del servicio FTP impedirá que este proveedor funcione.

  3. Debe tener habilitada la publicación FTP para un sitio.

  4. Debe usar Visual Studio 2008.

    Nota:

    Si usa una versión anterior de Visual Studio, es posible que algunos de los pasos de este tutorial no sean correctos.

  5. Debe usar una base de datos de SQL Server para la lista de cuentas de usuario y las listas de restricciones asociadas, este ejemplo no se puede usar con la autenticación básica de FTP. La sección "Información adicional" de este tutorial contiene un script para SQL Server que crea las tablas necesarias para este ejemplo.

  6. Necesitará Gacutil.exe en el equipo IIS; esto es necesario para agregar los ensamblados a la caché global de ensamblados (GAC).

Importante

Para ayudar a mejorar el rendimiento de las solicitudes de autenticación, el servicio FTP almacena en caché las credenciales para los inicios de sesión correctos durante 15 minutos de forma predeterminada. Este proveedor de autenticación denegará las solicitudes de un atacante inmediatamente, pero si el atacante pudo adivinar correctamente la contraseña de un usuario que ha iniciado sesión recientemente, puede obtener acceso a través de las credenciales almacenadas en caché. Esto puede tener la consecuencia involuntaria de permitir que un usuario malintencionado ataque su servidor después de que este proveedor haya bloqueado su dirección IP. Para mitigar esta posible vía de ataque, debe deshabilitar el almacenamiento en caché de credenciales para el servicio FTP. Para ello, siga estos pasos:

  1. Abra un símbolo del sistema.

  2. Escriba los siguientes comandos:

    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. Cierre el símbolo del sistema.

Una vez realizados estos cambios, el proveedor de autenticación de este ejemplo podrá denegar todas las solicitudes de un posible atacante inmediatamente.

Descripción del proveedor

Este tutorial contiene varios puntos que requieren cierta discusión. Los ataques basados en Internet suelen aprovechar un servidor FTP en el intento de obtener el nombre de usuario y la contraseña de una cuenta en un sistema. Detectar este comportamiento es posible a través del análisis de los registros de actividad de FTP y examinando las direcciones IP que están siendo usadas para atacar su sistema y bloqueando esas direcciones para futuros accesos. Por desgracia, se trata de un proceso algo manual, e incluso si se automatiza no será en tiempo real.

El servicio FTP contiene una característica para restringir las conexiones en función de las direcciones IP, pero la lista de direcciones IP se almacena en los archivos de configuración de IIS y requiere acceso administrativo para su actualización. El proceso de extensión del servicio FTP se ejecuta como una cuenta con privilegios hacia abajo que no tiene permisos para actualizar la configuración necesaria los archivos de configuración de IIS. Podría escribir un proveedor de registro FTP que detecte las inundaciones de nombres de usuario y escriba esa información en un almacén de datos y un servicio independiente que se ejecute como una cuenta con privilegios superiores que pueda actualizar los archivos de configuración de IIS, pero eso requiere un mayor conocimiento de la arquitectura del sistema y exige una serie de detalles de difícil implementación. Por ello, es necesario un almacén de datos alternativo.

Una base de datos constituye una opción ideal debido a la facilidad de acceso a los datos y a la disponibilidad general de las herramientas disponibles para manipular los datos de una base de datos. El siguiente reto es usar las interfaces de extensibilidad FTP existentes para implementar la lógica necesaria para detectar la inundación de inicios de sesión que usaría un atacante. A modo de repaso, las interfaces de extensibilidad disponibles son:

Podría escribir fácilmente un proveedor que aprovechara todas estas interfaces para reforzar la seguridad en mayor medida, pero el proveedor de este tutorial solo usará las siguientes interfaces:

  • IFtpAuthenticationProvider: el proveedor usará esta interfaz para permitir o denegar el acceso al servidor FTP.
  • IFtpLogProvider: el proveedor usará esta interfaz como agente de escucha de eventos genérico.

El servicio FTP no tiene notificaciones de eventos reales para las que un proveedor puede registrarse, pero puede escribir proveedores que usen el método IFtpLogProvider.Log() para proporcionar procesamiento posterior al evento. Por ejemplo, los intentos de inicio de sesión con errores registrarán el comando "PASS" con un código de estado distinto de "230", que es el código de estado para un inicio de sesión FTP correcto. Al capturar información adicional sobre el intento de inicio de sesión erróneo, como la dirección IP del cliente que no pudo iniciar sesión, es posible usar esta información para proporcionar funcionalidad adicional, como bloquear el acceso al servidor FTP en el futuro.

Arquitectura y lógica del proveedor

En las descripciones siguientes se resume el comportamiento de este proveedor de autenticación:

  • Al registrar el proveedor en el sistema, especifique la conexión de base de datos que se va a usar y los valores del número de intentos de inicio de sesión erróneos y el tiempo de espera de inundación en los archivos de configuración de IIS.

  • Cuando el servicio FTP carga el proveedor, proporciona los valores de los archivos de configuración de IIS al método Initialize() del proveedor. Después de almacenar estos valores en la configuración global, el método Initialize() realiza alguna recolección de elementos no utilizados inicial para limpiar cualquier información de las sesiones FTP anteriores que podrían estar en la base de datos.

  • Cuando un cliente FTP se conecta al servidor FTP, el servicio FTP envía el mensaje "ControlChannelOpened" al método Log() del proveedor. El método Log() comprueba la base de datos para ver si se ha bloqueado la dirección IP del cliente; si es así, marca la sesión en la base de datos.

  • Cuando el usuario escribe su nombre de usuario y contraseña, el servicio FTP llama al método AuthenticateUser() del proveedor, que comprueba si la sesión está marcada. Si se marca la sesión, el proveedor devuelve false, lo que indica que el usuario no pudo iniciar sesión. Si la sesión no está marcada, el nombre de usuario y la contraseña se comprueban con la base de datos para ver si son válidos. Si son válidos, el método devuelve true, lo que indica que el usuario es válido y puede iniciar sesión.

  • Si el usuario no puede escribir un nombre de usuario y una contraseña válidos, el servicio FTP llama al método Log() y el método ejecuta la recolección periódica de elementos no utilizados para asegurarse de que el número de errores es menor que el tiempo de espera de inundación. A continuación, el método comprueba si el número restante de errores es menor que el número máximo de errores:

    • Si no se ha alcanzado el número máximo de errores, el método agrega una notificación de error para la dirección IP del cliente a la base de datos.
    • Si se ha alcanzado el número máximo de errores, el método agrega la dirección IP del cliente a la lista de direcciones IP bloqueadas de la base de datos.
  • Cuando un cliente FTP se desconecta del servidor, el servicio FTP llama al método Log() del proveedor y envía el mensaje "ControlChannelClosed". El método Log() aprovecha esta notificación para realizar la recolección de elementos no utilizados para la sesión.

Notas adicionales

  • Este proveedor expone la funcionalidad para la validación de usuarios y direcciones IP, pero no proporciona una implementación para la búsqueda de roles. Dicho esto, sería relativamente fácil agregar una tabla adicional para las asignaciones de usuario a rol y agregar el método IFtpRoleProvider.IsUserInRole() al proveedor, pero eso está fuera del ámbito de este tutorial.
  • Este proveedor realiza un pequeño número de llamadas al servidor de base de datos SQL durante el proceso de autenticación. Mediante la consolidación de algunas de las instrucciones SQL en consultas compuestas únicas o procedimientos almacenados, podría reducir aún más el número de recorridos de ida y vuelta a la base de datos, pero eso está fuera del ámbito de este tutorial.

Paso 1: configurar el entorno del proyecto

En este paso, creará un proyecto en Visual Studio 2008 para el proveedor de demostración.

  1. Abra Microsoft Visual Studio 2008.

  2. Haga clic en el menú Archivo, Nuevo y, luego, en Proyecto.

  3. En el cuadro de diálogo Nuevo proyecto:

    • Elija Visual C# como tipo de proyecto.
    • Elija Biblioteca de clases como plantilla.
    • Escriba FtpAddressRestrictionAuthentication como nombre del proyecto.
    • Haga clic en OK.
  4. Cuando se abra el proyecto, agregue una ruta de acceso de referencia a la biblioteca de extensibilidad de FTP:

    • Haga clic en Proyecto y después haga clic en Propiedades de FtpAddressRestrictionAuthentication.

    • Haga clic en la pestaña Rutas de acceso de referencia.

    • Escriba la ruta al ensamblado de extensibilidad FTP para la versión de Windows, donde C: es la unidad del sistema operativo.

      • Para Windows Server 2008 y Windows Vista:

        C:\Windows\assembly\GAC_MSIL\Microsoft.Web.FtpServer\7.5.0.0__31bf3856ad364e35
        
      • Para Windows 7:

        C:\Program Files\Reference Assemblies\Microsoft\IIS
        
    • Haga clic en Agregar carpeta.

  5. Agregue al proyecto una clave de nombre seguro:

    • Haga clic en Proyecto y después haga clic en Propiedades de FtpAddressRestrictionAuthentication.
    • Haga clic en la pestaña Firma.
    • Marque la casilla Firmar el ensamblado.
    • Elija <Nuevo...> en el cuadro desplegable de nombre de clave segura.
    • Escriba FtpAddressRestrictionAuthenticationKey como nombre de archivo de clave.
    • Si lo desea, escriba una contraseña para el archivo de clave; de lo contrario, desactive la casilla Proteger mi archivo de clave con una contraseña.
    • Haga clic en OK.
  6. Opcional: puede agregar un evento de compilación personalizado para agregar el archivo DLL automáticamente a la caché global de ensamblados (GAC) en el equipo de desarrollo:

    • Haga clic en Proyecto y después haga clic en Propiedades de FtpAddressRestrictionAuthentication.

    • Haga clic en la pestaña Eventos de compilación.

    • En el cuadro de diálogo Línea de comandos del evento posterior a la compilación, escriba el siguiente comando:

      net stop ftpsvc
      call "%VS90COMNTOOLS%\vsvars32.bat">null
      gacutil.exe /if "$(TargetPath)"
      net start ftpsvc
      
  7. Guarde el proyecto.

Paso 2: Creación de la clase de extensibilidad

En este paso, implementará la interfaz de extensibilidad de inicio de sesión para el proveedor de demostración.

  1. Agregue una referencia a la biblioteca de extensibilidad FTP para el proyecto:

    • Haga clic en Proyecto y después haga clic en Agregar referencia...
    • En la pestaña .NET, haga clic en Microsoft.Web.FtpServer.
    • Haga clic en OK.
  2. Agregue una referencia a System.Web para el proyecto:

    • Haga clic en Proyecto y después haga clic en Agregar referencia...
    • En la pestaña .NET, haga clic en System.Web.
    • Haga clic en OK.
  3. Agregue una referencia a System.Configuration para el proyecto:

    • Haga clic en Proyecto y después haga clic en Agregar referencia...
    • En la pestaña .NET, haga clic en System.Configuration.
    • Haga clic en OK.
  4. Agregue una referencia a System.Data para el proyecto:

    • Haga clic en Proyecto y después haga clic en Agregar referencia.
    • En la pestaña .NET, haga clic en System.Data.
    • Haga clic en OK.
  5. Agregue el código para la clase de autenticación:

    • En el Explorador de soluciones, haga doble clic en el archivo Class1.cs.

    • Elimine el código existente.

    • Pegue el siguiente código en el editor:

      using System;
      using System.Collections.Generic;
      using System.Collections.Specialized;
      using System.Configuration.Provider;
      using System.Data;
      using System.Data.SqlClient;
      using System.Text;
      using Microsoft.Web.FtpServer;
      
      public class FtpAddressRestrictionAuthentication :
        BaseProvider,
        IFtpLogProvider,
        IFtpAuthenticationProvider
      {
        // Define the default values - these are only
        // used if the configuration settings are not set.
        const int defaultLogonAttempts = 5;
        const int defaultFloodSeconds = 30;
      
        // 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;
      
        // 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;
            }
      
            // Initial garbage collection.
            GarbageCollection(true);
            // Flag the provider as initialized.
            _initialized = true;
          }
        }
      
        // Dispose of the provider.
        protected override void Dispose(bool disposing)
        {
          base.Dispose(disposing);
      
          // Test if the application has already been uninitialized.
          if (_initialized == true)
          {
            // Final garbage collection.
            GarbageCollection(true);
            // Flag the provider as uninitialized.
            _initialized = false;
          }
        }
      
        // Authenticate a user.
        bool IFtpAuthenticationProvider.AuthenticateUser(
          string sessionId,
          string siteName,
          string userName,
          string userPassword,
          out string canonicalUserName)
        {
          // Define the canonical user name.
          canonicalUserName = userName;
      
          // Check if the session is flagged.
          if (IsSessionFlagged(sessionId) == true)
          {
            // Return false (authentication failed) if the session is flagged.
            return false;
          }
      
          // Check the user credentials and return the status.
          return IsValidUser(userName, userPassword);
        }
      
        // Implement custom actions by using the Log() method.
        void IFtpLogProvider.Log(FtpLogEntry loggingParameters)
        {
          // Test if the control channel was opened or the USER command was sent.
          if ((String.Compare(loggingParameters.Command,
            "ControlChannelOpened", true) == 0)
            || (String.Compare(loggingParameters.Command,
            "USER", true) == 0))
          {
            // Check if the IP address is banned.
            if (IsAddressBanned(loggingParameters.RemoteIPAddress) == true)
            {
              // If the IP is banned, flag the session.
              FlagSession(loggingParameters.SessionId);
              return;
            }
          }
          // Test if the PASS command was sent.
          if (String.Compare(loggingParameters.Command,
            "PASS", true) == 0)
          {
            // Check for password failures (230 is a success).
            if (loggingParameters.FtpStatus != 230)
            {
              // Periodic garbage collection - remove authentication
              // failures that are older than the flood timeout.
              GarbageCollection(false);
      
              // Test if the existing number of failures exceeds the maximum logon attempts.
              if (GetRecordCountByCriteria("[Failures]",
                "[IPAddress]='" + loggingParameters.RemoteIPAddress +
                "'") < _logonAttempts)
              {
                // Add the failure to the list of failures.
                InsertDataIntoTable("[Failures]",
                  "[IPAddress],[FailureDateTime]",
                  "'" + loggingParameters.RemoteIPAddress +
                  "','" + DateTime.Now.ToString() + "'");
              }
              else
              {
                // Ban the IP address if authentication has failed
                // from that IP more than the defined number of failures.
                BanAddress(loggingParameters.RemoteIPAddress);
                FlagSession(loggingParameters.SessionId);
              }
              return;
            }
          }
          // Test if the control channel was closed.
          if (String.Compare(loggingParameters.Command,
            "ControlChannelClosed", true) == 0)
          {
            // Session-based garbage collection - remove the
            // current session from the list of flagged sessions.
            _flaggedSessions.Remove(loggingParameters.SessionId);
            return;
          }
        }
      
        // Check for a valid username/password.
        private static bool IsValidUser(
          string userName,
          string userPassword)
        {
          // Define the initial status as the credentials are not valid.
          try
          {
            // Create a new SQL connection object.
            using (SqlConnection connection = new SqlConnection(_connectionString))
            {
              // Create a new SQL command object.
              using (SqlCommand command = new SqlCommand())
              {
                // Specify the connection for the command object.
                command.Connection = connection;
                // Specify a text command type.
                command.CommandType = CommandType.Text;
      
                // Specify the SQL text for the command object.
                command.CommandText = "SELECT COUNT(*) AS [NumRecords] " +
                  "FROM [Users] WHERE [UID]=@UID AND [PWD]=@PWD AND [Locked]=0";
      
                // Add parameters for the user name and password.
                command.Parameters.Add("@UID", SqlDbType.NVarChar).Value = userName;
                command.Parameters.Add("@PWD", SqlDbType.NVarChar).Value = userPassword;
      
                // Open the database connection.
                connection.Open();
                // Return the valid status for the credentials.
                return ((int)command.ExecuteScalar() > 0);
              }
            }
          }
          catch (Exception ex)
          {
            // Raise an exception if an error occurs.
            throw new ProviderException(ex.Message);
          }
        }
      
        // Check if the IP is banned.
        private bool IsAddressBanned(string ipAddress)
        {
          // Return whether the IP address was found in the banned addresses table.
          return (GetRecordCountByCriteria("[BannedAddresses]",
            "[IPAddress]='" + ipAddress + "'") != 0);
        }
      
        // Check if the session is flagged.
        private bool IsSessionFlagged(string sessionId)
        {
          // Return whether the session ID was found in the flagged sessions table.
          return _flaggedSessions.Contains(sessionId);
        }
      
        // Mark a session as flagged.
        private void FlagSession(string sessionId)
        {
          // Check if the session is already flagged.
          if (IsSessionFlagged(sessionId) == false)
          {
            // Flag the session if it is not already flagged.
            _flaggedSessions.Add(sessionId);
          }
        }
      
        // 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 + "'");
          }
        }
      
        // Perform garbage collection tasks.
        private void GarbageCollection(bool deleteSessions)
        {
          // Remove any authentication failures that are older than the flood timeout.
          DeleteRecordsByCriteria("[Failures]",
            String.Format("DATEDIFF(second,[FailureDateTime],'{0}')>{1}",
            DateTime.Now.ToString(),_floodSeconds.ToString()));
      
          // Test if flagged sessions should be deleted.
          if (deleteSessions == true)
          {
            // Remove any sessions from the list of flagged sessions.
            _flaggedSessions.Clear();
          }
        }
      
        // Retrieve the count of records based on definable criteria.
        private int GetRecordCountByCriteria(
          string tableName,
          string criteria)
        {
          // Create a SQL string to retrieve the count of records 
          // that are found in a table based on the criteria.
          StringBuilder sqlString = new StringBuilder();
          sqlString.Append("SELECT COUNT(*) AS [NumRecords]");
          sqlString.Append(String.Format(
            " FROM {0}",tableName));
          sqlString.Append(String.Format(
            " WHERE {0}",criteria));
          // Execute the query.
          return ExecuteQuery(true, sqlString.ToString());
        }
      
        // Insert records into a database table.
        private void InsertDataIntoTable(
          string tableName,
          string fieldNames,
          string fieldValues)
        {
          // Create a SQL string to insert data into a table.
          StringBuilder sqlString = new StringBuilder();
          sqlString.Append(String.Format(
            "INSERT INTO {0}",tableName));
          sqlString.Append(String.Format(
            "({0}) VALUES({1})",fieldNames, fieldValues));
          // Execute the query.
          ExecuteQuery(false, sqlString.ToString());
        }
      
        // Remove records from a table based on criteria.
        private void DeleteRecordsByCriteria(
          string tableName,
          string queryCriteria)
        {
          // Create a SQL string to delete data from a table.
          StringBuilder sqlString = new StringBuilder();
          sqlString.Append(String.Format(
            "DELETE FROM {0}",tableName));
          // Test if any criteria is specified.
          if (string.IsNullOrEmpty(queryCriteria) == false)
          {
            // Append the criteria to the SQL string.
            sqlString.Append(String.Format(
              " WHERE {0}",queryCriteria));
          }
          // Execute the query.
          ExecuteQuery(false, sqlString.ToString());
        }
      
        // Execute SQL queries.
        private int ExecuteQuery(bool returnRecordCount, string sqlQuery)
        {
          try
          {
            // Create a new SQL connection object.
            using (SqlConnection connection =
              new SqlConnection(_connectionString))
            {
              // Create a new SQL command object.
              using (SqlCommand command =
                new SqlCommand(sqlQuery, connection))
              {
                // Open the connection.
                connection.Open();
                // Test whether the method should return a record count.
                if (returnRecordCount == true)
                {
                  // Run the database query.
                  SqlDataReader dataReader = command.ExecuteReader();
                  // Test if data reader has returned any rows.
                  if (dataReader.HasRows)
                  {
                    // Read a single row.
                    dataReader.Read();
                    // Return the number of records.
                    return ((int)dataReader["NumRecords"]);
                  }
                }
                else
                {
                  // Run the database query.
                  command.ExecuteNonQuery();
                }
              }
            }
            // Return a zero record count.
            return 0;
          }
          catch (Exception ex)
          {
            // Raise an exception if an error occurs.
            throw new ProviderException(ex.Message);
          }
        }
      }
      
  6. Guarde el proyecto y compílelo.

Nota:

Si no ha usado los pasos opcionales para registrar los ensamblados en la GAC, deberá copiar manualmente los ensamblados en el equipo de IIS y agregar los ensamblados a la GAC mediante la herramienta Gacutil.exe. Para más información, consulte Gacutil.exe (Global Assembly Cache Tool).

Paso 3: Adición del proveedor de demostración a FTP

En este paso, agregará el proveedor de demostración al servicio FTP y el sitio web predeterminado.

  1. Determine la información del ensamblado para el proveedor de extensibilidad:

    • En el Explorador de Windows, abra la ruta C:\Windows\assembly, donde C: es la unidad del sistema operativo.
    • Busque el ensamblado FtpAddressRestrictionAuthentication.
    • Haga clic con el botón derecho en el ensamblado y, después, haga clic en Propiedades.
    • Copie el valor Cultura; por ejemplo: Neutro.
    • Copie el número de Versión; por ejemplo: 1.0.0.0.
    • Copie el valor de Token de clave pública; por ejemplo: 426f62526f636b73.
    • Haga clic en Cancelar.
  2. Con la información de los pasos anteriores, agregue el proveedor de extensibilidad a la lista global de proveedores de FTP y configure las opciones para el proveedor:

    • En este momento no hay ninguna interfaz de usuario que le permita agregar propiedades para un módulo de autenticación personalizado, por lo que tendrá que usar la siguiente línea de comandos:

      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='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
      

    Nota:

    La cadena de conexión que especifique en el atributo connectionString debe ser una conexión válida para su base de datos.

  3. Agregue el proveedor personalizado a un sitio:

    • En este momento no hay ninguna interfaz de usuario que le permita agregar características personalizadas a un sitio, por lo que tendrá que usar la siguiente línea de comandos:

      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
      

    Nota:

    Esta sintaxis deshabilita la autenticación básica de FTP y es importante deshabilitar la autenticación básica al usar este proveedor de autenticación. De lo contrario, cuando la dirección IP de un atacante ha sido bloqueada por este proveedor de autenticación, un atacante todavía sería capaz de atacar cuentas que usan la autenticación básica.

  4. Agregue una regla de autorización para el proveedor de autenticación:

    • Haga doble clic en Reglas de autorización de FTP en la ventana principal.

    • Haga clic en Agregar regla de permiso... en el panel Acciones.

    • Seleccione Usuarios especificados para la opción de acceso.

    • Escriba un nombre de usuario.

      Nota:

      El nombre de usuario deberá escribirse en la base de datos fuera de esta lista de pasos.

    • Seleccione Lectura o Escritura para la opción Permisos.

    • Haga clic en OK.

Paso 4: Uso del proveedor con FTP 7.5

Cuando los clientes FTP se conectan al sitio FTP, el servicio FTP intentará autenticar a los usuarios con el proveedor de autenticación personalizado mediante cuentas almacenadas en la base de datos. Si un cliente FTP no se puede autenticar, el proveedor realizará un seguimiento de la dirección IP y la fecha y hora del error en la base de datos. Cuando un cliente FTP no puede iniciar sesión desde una dirección IP específica para el número de errores especificados en la configuración logonAttempts y dentro del período de tiempo especificado en la configuración de floodSeconds, el proveedor impedirá que la dirección IP inicie sesión en el servicio FTP.

Nota:

Este proveedor de ejemplo implementa la lógica de autenticación para el servicio FTP, pero no proporciona un módulo de administración para administrar los datos de la base de datos. Por ejemplo, no puede administrar la lista de cuentas de usuario FTP, direcciones IP prohibidas ni errores de autenticación mediante este proveedor. Para administrar los datos mediante el Administrador de IIS, puede usar el Administrador de bases de datos de IIS. Para obtener más información, consulte el siguiente tema:

https://www.iis.net/extensions/DatabaseManager

Información adicional

Puede usar el siguiente script SQL para Microsoft SQL Server para crear la base de datos y las tablas necesarias. Para usar este script, debe actualizar el nombre de la base de datos y la ubicación de los archivos de base de datos. En SQL Server, ejecutaría el script en una nueva ventana de consulta y, a continuación, crearía un inicio de sesión de base de datos que usará con la cadena de conexión.

Nota:

Es posible que quiera modificar el script SQL para almacenar la base de datos en una ubicación distinta de c:\databases.

/****** Create the FtpAuthentication Database ******/

USE [master]
GO
CREATE DATABASE [FtpAuthentication] ON  PRIMARY 
( NAME = N'FtpAuthentication', FILENAME = N'c:\databases\FtpAuthentication.mdf' , SIZE = 2048KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
 LOG ON 
( NAME = N'FtpAuthentication_log', FILENAME = N'c:\databases\FtpAuthentication_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
 COLLATE SQL_Latin1_General_CP1_CI_AS
GO
EXEC dbo.sp_dbcmptlevel @dbname=N'FtpAuthentication', @new_cmptlevel=90
GO
IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [FtpAuthentication].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_NULL_DEFAULT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_NULLS OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_PADDING OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_WARNINGS OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ARITHABORT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_CLOSE OFF 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_CREATE_STATISTICS ON 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_SHRINK OFF 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_UPDATE_STATISTICS ON 
GO
ALTER DATABASE [FtpAuthentication] SET CURSOR_CLOSE_ON_COMMIT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET CURSOR_DEFAULT  GLOBAL 
GO
ALTER DATABASE [FtpAuthentication] SET CONCAT_NULL_YIELDS_NULL OFF 
GO
ALTER DATABASE [FtpAuthentication] SET NUMERIC_ROUNDABORT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET QUOTED_IDENTIFIER OFF 
GO
ALTER DATABASE [FtpAuthentication] SET RECURSIVE_TRIGGERS OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ENABLE_BROKER 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_UPDATE_STATISTICS_ASYNC OFF 
GO
ALTER DATABASE [FtpAuthentication] SET DATE_CORRELATION_OPTIMIZATION OFF 
GO
ALTER DATABASE [FtpAuthentication] SET TRUSTWORTHY OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ALLOW_SNAPSHOT_ISOLATION OFF 
GO
ALTER DATABASE [FtpAuthentication] SET PARAMETERIZATION SIMPLE 
GO
ALTER DATABASE [FtpAuthentication] SET READ_WRITE 
GO
ALTER DATABASE [FtpAuthentication] SET RECOVERY SIMPLE 
GO
ALTER DATABASE [FtpAuthentication] SET MULTI_USER 
GO
ALTER DATABASE [FtpAuthentication] SET PAGE_VERIFY CHECKSUM  
GO
ALTER DATABASE [FtpAuthentication] SET DB_CHAINING OFF 

/****** Create the Database Tables ******/

USE [FtpAuthentication]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[BannedAddresses]') AND type in (N'U'))
BEGIN
CREATE TABLE [BannedAddresses](
    [IPAddress] [nvarchar](50) NOT NULL
) ON [PRIMARY]
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Failures]') AND type in (N'U'))
BEGIN
CREATE TABLE [Failures](
    [IPAddress] [nvarchar](50) NOT NULL,
    [FailureDateTime] [datetime] NOT NULL
) ON [PRIMARY]
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Users]') AND type in (N'U'))
BEGIN
CREATE TABLE [Users](
    [UID] [nvarchar](50) NOT NULL,
    [PWD] [nvarchar](50) NOT NULL,
    [Locked] [bit] NOT NULL
) ON [PRIMARY]
END

Resumen

En este tutorial ha aprendido a:

  • Crear un proyecto en Visual Studio 2008 para un proveedor de FTP personalizado.
  • Implementar las interfaces de extensibilidad para un proveedor FTP personalizado.
  • Agregar un proveedor de FTP personalizado al servicio FTP.