マネージ コード (C#) を使用して Dynamic IP Restrictions が備わった FTP 認証プロバイダーを作成する方法

公開日: 2009 年 8 月 6 日 (作業者: robmcm (英語))

更新日: 2009 年 9 月 11 日 (作業者: robmcm (英語))

マイクロソフトでは、Windows Server® 2008 用に完全に書き換えた新しい FTP サービスを作成しました。この新しい FTP サービスでは多くの新機能が追加され、Web サイト作成者は簡単にコンテンツを発行でき、Web 管理者は多くのセキュリティ オプションと展開オプションを使用できます。

この新しい FTP 7.5 サービスでは、FTP サービスに付属の組み込み機能の拡張を可能にする拡張機能がサポートされます。具体的には、FTP 7.5 で、独自の認証プロバイダーの作成がサポートされます。さらに、カスタム FTP ログ作成および FTP ユーザーのホーム ディレクトリ情報の判定を行うためのプロバイダーを作成することもできます。

このチュートリアルでは、動的な IP 制限 (アカウント情報の格納に SQL Server データベースを使用) をサポートする FTP 認証プロバイダーにマネージ コードを使用するステップを説明します。このプロバイダーでは、リモート IP アドレスからの失敗の数をログに記録し、次にこの情報を使用して一定時間内にサーバーへのログインに失敗した IP アドレスをブロックすることで、このロジックを実装しています。

重要なメモ: このチュートリアル内のプロバイダーを使用するには、FTP 7.5 サービスの最新バージョンが必ずインストールされている必要があります。IFtpLogProvider.Log() メソッド内のローカルおよびリモート アドレスが不正確であった問題を修正する FTP 7.5 のバージョンが、2009 年 8 月 3 日にリリースされました。したがって、この FTP サービスよりも古いバージョンでは、このプロバイダーは動作しません。

このチュートリアルの内容

  • プロバイダーの説明
  • ステップ 1: プロジェクト環境のセットアップ
  • ステップ 2: 拡張機能クラスの作成
  • ステップ 3: FTP へのデモ プロバイダーの追加
  • ステップ 4: FTP 7.5 でのプロバイダーの使用
  • 追加情報
  • まとめ

必要条件

この記事の手順を完了するには、以下の項目が必須です。

  1. Windows Server 2008 サーバーに IIS 7 がインストールされている必要があります。また、インターネット インフォメーション サービス (IIS) マネージャーもインストールされている必要があります。
  2. 新しい FTP 7.5 サービスがインストールされている必要があります。FTP 7.5 サービスは、Web サイト (https://www.iis.net/(英語)) からダウンロードしてインストールできます。次のいずれかのリンクを使用してください。
    • IIS 7 用 FTP 7.5 (x86)
    • IIS 7 用 FTP 7.5 (x64)
    • 重要なメモ: 前述のように、このチュートリアル内のプロバイダーを使用するには、FTP 7.5 サービスの最新バージョンが必ずインストールされている必要があります。IFtpLogProvider.Log() メソッド内のローカルおよびリモート アドレスが不正確であった問題を修正する FTP 7.5 のバージョンが、2009 年 8 月 3 日にリリースされました。したがって、この FTP サービスよりも古いバージョンでは、このプロバイダーは動作しません。
  3. サイトで FTP 発行が有効になっている必要があります。
  4. Visual Studio 2008 を使用する必要があります(メモ: Visual Studio の旧バージョンを使用すると、このチュートリアルのステップの一部が不正確になる可能性があります)。
  5. ユーザー アカウントのリストおよび関連付けられた制限リスト用に SQL Server データベースを使用する必要があります。この例は FTP 基本認証と一緒には使用できません。このチュートリアルの「追加情報」セクションには、このサンプルに必要なテーブルを作成する SQL Server スクリプトが記載されています。
  6. IIS 7 コンピューターに Gacutil.exe があることを確認してください。Gacutil.exe は、アセンブリをグローバル アセンブリ キャッシュ (GAC) に追加するのに必要なツールです。

重要

認証要求のパフォーマンスの向上のために、既定では、成功したログインの資格情報が FTP サービスに 15 分間キャッシュされます。この認証プロバイダーは、攻撃者からの要求を即座に拒否しますが、攻撃者が最近ログインしたユーザーのパスワードを類推できる場合、キャッシュされた資格情報を使用してアクセス権を取得する可能性があります。つまり、このプロバイダーが攻撃者の IP アドレスをブロックした後でサーバー攻撃を許すという、意図しない結果を引き起こすことがあります。この潜在的な攻撃方法を回避するには、FTP サービスの資格情報キャッシュを無効にする必要があります。これを実行するには、以下の手順に従ってください。

  1. コマンド プロンプト ウィンドウを開きます。

  2. 次のコマンドを入力します。

    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. コマンド プロンプト ウィンドウを閉じます。

これらの変更を行うと、この例での認証プロバイダーは、潜在的な攻撃者からの要求を即座にすべて拒否できるようになります。

プロバイダーの説明

このチュートリアルには、少し解説が必要な点がいくつか含まれています。FTP サーバーはしばしば、システム上のアカウントのユーザー名とパスワードを入手しようとするインターネットを使用した攻撃の標的となります。この行為は、FTP アクティビティ ログの分析で検出可能で、システムへの攻撃に使用されている IP アドレスを調査すれば、これらのアドレスからの以後のアクセスをブロックできます。しかし、これは少し手のかかる手動処理です。また、この処理を自動化しても、リアルタイムには処理できません。

FTP サービスには IP アドレスに基づいて接続を制限する機能が含まれていますが、IP アドレスの一覧は IIS 構成ファイルに格納されていて、更新するには管理者のアクセス権が必要となります。FTP サービスの拡張機能プロセスは、IIS 構成ファイルの必要な設定を更新するアクセス権がない低い権限のアカウントで実行されます。ユーザー名フラッディング攻撃を検出し、その情報をデータストア、および IIS 構成ファイルを更新できる高い権限のアカウントで実行される別のサービスに書き込む FTP ログ プロバイダーを記述できます。しかし、これを実行するにはシステム アーキテクチャについての深い知識が必要で、困難を伴う多くの詳細な実装が必要です。上記の理由で、代替のデータ ストアが必要となります。

データベースは理想的な選択肢です。データ アクセスが簡単で、データベース内のデータを操作するためのツールも広く入手できます。次の課題は、既存の FTP 拡張機能インターフェイスを使用して、攻撃者が使用するログイン フラッディングを検出するために必要なロジックを実装することです。再確認のため、使用可能な拡張機能インターフェイスを以下に示します。

セキュリティを強固にするために、これらすべてのインターフェイスの利点を活用するプロバイダーを簡単に記述できますが、このチュートリアルのプロバイダーでは、以下のインターフェイスのみを使用します。

  • IFtpAuthenticationProvider - プロバイダーは、このインターフェイスを使用して FTP サーバーへのアクセスを許可または拒否します。
  • IFtpLogProvider - プロバイダーは、このインターフェイスを汎用イベント リスナーとして使用します。

FTP サービスには、プロバイダーが登録できる実際のイベント通知はありませんが、IFtpLogProvider.Log() メソッドを使用してイベント後処理を実行するプロバイダーを記述できます。たとえば、失敗したログイン試行すべてについて、"230" (FTP ログイン成功) 以外の状態コードが付いた PASS コマンドがログに記録されます。失敗したログイン試行についての追加情報 (ログインに失敗したクライアントの IP アドレスなど) をキャプチャすることで、その情報を使用し、該当 IP アドレスからの以降の FTP サーバーへのアクセスをブロックするなどの追加機能を使用できます。

プロバイダーのアーキテクチャとロジック

以下の説明は、この認証プロバイダーの動作をまとめたものです。

  • システムにプロバイダーを登録するときに、使用するデータベース接続、およびログイン試行の失敗回数とフラッド タイムアウトの値を IIS 構成ファイルに指定します。
  • FTP サービスがプロバイダーを読み込むときに、IIS 構成ファイルからの値がプロバイダーの Initialize() メソッドに提供されます。これらの値がグローバル設定内に格納された後で、Initialize() メソッドは、データベース内に存在している可能性がある前の FTP セッションの情報を消去するために、初期化ガーベジ コレクションを実行します。
  • FTP クライアントが FTP サーバーに接続すると、FTP サービスからプロバイダーの Log() メソッドに "ControlChannelOpened" メッセージが送信されます。Log() メソッドは、そのクライアントの IP アドレスがブロックされているかどうかを確認するためにデータベースを調べます。ブロック対象の場合は、データベース内のセッションにフラグを付けます。
  • ユーザーがユーザー名とパスワードを入力すると、FTP サービスはプロバイダーの AuthenticateUser() メソッドを呼び出します。このメソッドは、セッションにフラグが付けられているかどうかを確認します。セッションにフラグが付いている場合は、プロバイダーは **false を返し、ユーザーがログインに失敗したことを知らせます。セッションにフラグが付いていない場合は、ユーザー名とパスワードが有効かどうかをデータベースで確認します。有効な場合は、このメソッドは **true を返し、ユーザーが有効でログインできることを示します。
  • ユーザーが有効なユーザー名とパスワードを入力しなかった場合は、FTP サービスによって Log() メソッドが呼び出されます。このメソッドは、定期的なガーベジ コレクションを実行し、失敗の回数がフラッド タイムアウトより少ないことを確認します。このメソッドは、次に、残っている失敗回数が失敗の最大回数より少ないことを確認します。
    • 失敗の最大回数に達していない場合、このメソッドは、該当クライアント IP アドレスの失敗通知をデータベースに追加します。
    • 失敗の最大回数に達している場合、このメソッドは、データベースのブロックする IP アドレスのリストに該当クライアント IP アドレスを追加します。
  • FTP クライアントが FTP サーバーとの接続を解除すると、FTP サービスはプロバイダーの Log() メソッドを呼び出して、"ControlChannelClosed" メッセージを送信します。Log() メソッドは、この通知を利用して、セッションのガーベジ コレクションを実行します。

追加メモ:

  • このプロバイダーでは、ユーザーおよび IP アドレスの検証機能が提供されていますが、役割の参照は実装されていません。ユーザーから役割へのマッピングのためのテーブルを追加し、プロバイダーに IFtpRoleProvider.IsUserInRole() メソッドを追加することは比較的簡単ですが、これはこのチュートリアルの範疇を超えています。
  • このプロバイダーは、認証プロセスの間に、SQL データベース サーバーへの呼び出しを何回か行います。いくつかの SQL ステートメントを単一の複合クエリまたはストアド プロシージャにまとめることで、データベースとのやり取りをさらに減らすことができますが、これはこのチュートリアルの範疇を超えています。

ステップ 1: プロジェクト環境のセットアップ

このステップでは、デモ プロバイダー用のプロジェクトを Visual Studio 2008 で作成します。

  1. Microsoft Visual Studio 2008 を開きます。
  2. [ファイル] メニューの [新規作成] をクリックし、[プロジェクト] をクリックします。
  3. [新しいプロジェクト] ダイアログ ボックスで以下の操作を行います。
    • プロジェクトの種類として [Visual C#] を選択します。
    • テンプレートとして [クラス ライブラリ] を選択します。
    • プロジェクト名として「FtpAddressRestrictionAuthentication」と入力します。
    • [OK] をクリックします。
  4. プロジェクトが開いたら、以下の操作を行って FTP 拡張機能ライブラリへの参照パスを追加します。
    • [プロジェクト] メニューの [FtpAddressRestrictionAuthentication のプロパティ] をクリックします。
    • [参照パス] タブをクリックします。
    • お使いの Windows バージョン用の FTP 拡張機能アセンブリへのパスを入力します。次は、オペレーティング システム ドライブが C: の場合です。
      • Windows Server 2008 および Windows Vista の場合:
        C:\Windows\assembly\GAC_MSIL\Microsoft.Web.FtpServer\7.5.0.0__31bf3856ad364e35
      • Windows 7 の場合:
        C:\Program Files\Reference Assemblies\Microsoft\IIS
    • [フォルダの追加] をクリックします。
  5. 厳密な名前のキーをプロジェクトに追加します。
    • [プロジェクト] メニューの [FtpAddressRestrictionAuthentication のプロパティ] をクリックします。
    • [署名] タブをクリックします。
    • [アセンブリの署名] チェック ボックスをオンにします。
    • 厳密なキー名を選択するドロップダウン ボックスから [<新規作成>] を選択します。
    • キー ファイルの名前として「FtpAddressRestrictionAuthenticationKey」と入力します。
    • 必要な場合、キー ファイル用のパスワードを入力します。不要な場合は、[キー ファイルをパスワードで保護する] チェック ボックスをオフにします。
    • [OK] をクリックします。
  6. オプション: カスタム ビルド イベントを追加して、開発用コンピューター上のグローバル アセンブリ キャッシュ (GAC) に DLL を自動的に追加できます。
    • [プロジェクト] メニューの [FtpAddressRestrictionAuthentication のプロパティ] をクリックします。

    • [ビルド イベント] タブをクリックします。

    • [ビルド後に実行するコマンド ライン] ダイアログ ボックスに、次を入力します。

      net stop ftpsvc
      call "%VS90COMNTOOLS%\vsvars32.bat">null
      gacutil.exe /if "$(TargetPath)"
      net start ftpsvc
      
  7. プロジェクトを保存します。

ステップ 2: 拡張機能クラスの作成

このステップでは、デモ プロバイダー用のログ拡張機能インターフェイスを実装します。

  1. プロジェクトに FTP 拡張機能ライブラリへの参照を追加します。

    • [プロジェクト] メニューの [参照の追加] をクリックします。
    • [.NET] タブで、[Microsoft.Web.FtpServer] をクリックします。
    • [OK] をクリックします。
  2. プロジェクトに System.Web への参照を追加します。

    • [プロジェクト] メニューの [参照の追加] をクリックします。
    • [.NET] タブで、[System.Web] をクリックします。
    • [OK] をクリックします。
  3. プロジェクトに System.Configuration への参照を追加します。

    • [プロジェクト] メニューの [参照の追加] をクリックします。
    • [.NET] タブで、[System.Configuration] をクリックします。
    • [OK] をクリックします。
  4. プロジェクトに System.Data への参照を追加します。

    • [プロジェクト] メニューの [参照の追加] をクリックします。
    • [.NET] タブで、[System.Data] をクリックします。
    • [OK] をクリックします。
  5. 認証クラスのコードを追加します。

    • [ソリューション エクスプローラー] で [Class1.cs] ファイルをダブルクリックします。

    • 既存のコードを削除します。

    • エディターに次のコードを貼り付けます。

      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
      {
      // 既定値を定義します。これらの既定値は構成設定に値が
      // 設定されていない場合にのみ使用されます。
      const int defaultLogonAttempts = 5;
      const int defaultFloodSeconds = 30;
      // 既定値なしで接続文字列を定義します。
      private static string _connectionString;
      // 既定値でプライベート変数を初期化します。
      private static int _logonAttempts = defaultLogonAttempts;
      private static int _floodSeconds = defaultFloodSeconds;
      // アプリケーションを未初期化としてフラグを付けます。
      private static bool _initialized = false;
      // フラグを付けられたセッションのリストを含むリストを定義します。
      private static List<string> _flaggedSessions;
      // プロバイダーを初期化します。
      protected override void Initialize(StringDictionary config)
      {
      // アプリケーションが初期化済みかどうかをテストします。
      if (_initialized == false)
      {
      // フラグが付けられたセッションのリストを作成します。
      _flaggedSessions = new List<string>();
      // データベース接続用の接続文字列を取得します。
      _connectionString = config["connectionString"];
      if (string.IsNullOrEmpty(_connectionString))
      {
      // 接続文字列がないか空である場合に例外をスローします。
      throw new ArgumentException(
      "Missing connectionString value in configuration.");
      }
      else
      {
      // データベースが Microsoft Access データベースかどうかを判断します。
      if (_connectionString.Contains("Microsoft.Jet"))
      {
      // データベースが Microsoft Access データベースの場合に例外をスローします。
      throw new ProviderException("Microsoft Access databases are not supported.");
      }
      }
      // IP アドレスがロックアウトされる前の失敗回数を取得します。
      // または既定値を使用します。
      if (int.TryParse(config["logonAttempts"], out _logonAttempts) == false)
      {
      // ログオン試行回数が有効でない場合は、既定値に設定します。
      _logonAttempts = defaultLogonAttempts;
      }
      // フラッドから保護する秒数を取得します。
      // または既定値を使用します。
      if (int.TryParse(config["floodSeconds"], out _floodSeconds) == false)
      {
      // ログオン試行回数が有効でない場合は、既定値に設定します。
      _floodSeconds = defaultFloodSeconds;
      }
      // 数が正の整数かつ 10 分より少ないかテストします。
      if ((_floodSeconds <= 0) || (_floodSeconds > 600))
      {
      // ログオン試行回数が有効でない場合は、既定値に設定します。
      _floodSeconds = defaultFloodSeconds;
      }
      // 初期化ガーベジ コレクションです。
      GarbageCollection(true);
      // プロバイダーを初期化済みとしてフラグを付けます。
      _initialized = true;
      }
      }
      // プロバイダーを破棄します。
      protected override void Dispose(bool disposing)
      {
      base.Dispose(disposing);
      // アプリケーションが未初期化かどうかをテストします。
      if (_initialized == true)
      {
      // 最終のガーベジ コレクションです。
      GarbageCollection(true);
      // プロバイダーを未初期化としてフラグを付けます。
      _initialized = false;
      }
      }
      // ユーザーを認証します。
      bool IFtpAuthenticationProvider.AuthenticateUser(
      string sessionId,
      string siteName,
      string userName,
      string userPassword,
      out string canonicalUserName)
      {
      // 正規ユーザー名を定義します。
      canonicalUserName = userName;
      // セッションにフラグが付けられているかどうかを確認します。
      if (IsSessionFlagged(sessionId) == true)
      {
      // セッションにフラグが付けられている場合に false (認証失敗) を返します。
      return false;
      }
      // ユーザー資格情報を確認して状態を返します。
      return IsValidUser(userName, userPassword);
      }
      // Log() メソッドを使用してカスタム アクションを実装します。
      void IFtpLogProvider.Log(FtpLogEntry loggingParameters)
      {
      // コントロール チャネルが開かれたか、または USER コマンドが送信されたかをテストします。
      if ((String.Compare(loggingParameters.Command,
      "ControlChannelOpened", true) == 0)
      || (String.Compare(loggingParameters.Command,
      "USER", true) == 0))
      {
      // IP アドレスが禁止されているかどうかを確認します。
      if (IsAddressBanned(loggingParameters.RemoteIPAddress) == true)
      {
      // IP が禁止されている場合、セッションにフラグを付けます。
      FlagSession(loggingParameters.SessionId);
      return;
      }
      }
      // PASS コマンドが送信されたかテストします。
      if (String.Compare(loggingParameters.Command,
      "PASS", true) == 0)
      {
      // パスワードに失敗したか確認します (230 は成功を示します)。
      if (loggingParameters.FtpStatus != 230)
      {
      // 定期的なガーベジ コレクションです。フラッド タイムアウトより古い
      // 認証失敗を削除します。
      GarbageCollection(false);
      // 既存の失敗回数が最大ログオン試行回数を超えているかテストします。
      if (GetRecordCountByCriteria("[Failures]",
      "[IPAddress]='" + loggingParameters.RemoteIPAddress +
      "'") < _logonAttempts)
      {
      // 失敗のリストに失敗を追加します。
      InsertDataIntoTable("[Failures]",
      "[IPAddress],[FailureDateTime]",
      "'" + loggingParameters.RemoteIPAddress +
      "','" + DateTime.Now.ToString() + "'");
      }
      else
      {
      // 定義された失敗回数を超えて認証に失敗した IP アドレスを
      // 禁止します。
      BanAddress(loggingParameters.RemoteIPAddress);
      FlagSession(loggingParameters.SessionId);
      }
      return;
      }
      }
      // コントロール チャネルが閉じられたかテストします。
      if (String.Compare(loggingParameters.Command,
      "ControlChannelClosed", true) == 0)
      {
      // セッションに基づくガーベジ コレクションです。フラグが付けられた
      // セッションのリストから現在のセッションを削除します。
      _flaggedSessions.Remove(loggingParameters.SessionId);
      return;
      }
      }
      // ユーザー名とパスワードが有効か確認します。
      private static bool IsValidUser(
      string userName,
      string userPassword)
      {
      // 資格情報が有効でないことを初期状態に定義します。
      try
      {
      // 新しい SQL 接続オブジェクトを作成します。
      using (SqlConnection connection = new SqlConnection(_connectionString))
      {
      // 新しい SQL コマンド オブジェクトを作成します。
      using (SqlCommand command = new SqlCommand())
      {
      // コマンド オブジェクト用の接続を指定します。
      command.Connection = connection;
      // テキスト コマンドの型を指定します。
      command.CommandType = CommandType.Text;
      // コマンド オブジェクト用の SQL テキストを指定します。
      command.CommandText = "SELECT COUNT(*) AS [NumRecords] " +
      "FROM [Users] WHERE [UID]=@UID AND [PWD]=@PWD AND [Locked]=0";
      // ユーザー名とパスワード用のパラメーターを追加します。
      command.Parameters.Add("@UID", SqlDbType.NVarChar).Value = userName;
      command.Parameters.Add("@PWD", SqlDbType.NVarChar).Value = userPassword;
      // データベース接続を開きます。
      connection.Open();
      // 資格情報の有効な状態を返します。
      return ((int)command.ExecuteScalar() > 0);
      }
      }
      }
      catch (Exception ex)
      {
      // エラーが発生した場合に例外をスローします。
      throw new ProviderException(ex.Message);
      }
      }
      // IP が禁止されているかどうかを確認します。
      private bool IsAddressBanned(string ipAddress)
      {
      // 禁止されたアドレスのテーブルに IP アドレスが存在するかどうかを返します。
      return (GetRecordCountByCriteria("[BannedAddresses]",
      "[IPAddress]='" + ipAddress + "'") != 0);
      }
      // セッションにフラグが付けられているかどうかを確認します。
      private bool IsSessionFlagged(string sessionId)
      {
      // フラグが付けられたセッションのテーブルにセッション ID が存在するかどうかを返します。
      return _flaggedSessions.Contains(sessionId);
      }
      // セッションにフラグが付けられているとしてマークします。
      private void FlagSession(string sessionId)
      {
      // セッションに既にフラグが付けられているか確認します。
      if (IsSessionFlagged(sessionId) == false)
      {
      // フラグがまだ付けられていない場合はセッションにフラグを付けます。
      _flaggedSessions.Add(sessionId);
      }
      }
      // IP アドレスを禁止されたものとしてマークします。
      private void BanAddress(string ipAddress)
      {
      // IP アドレスが既に禁止されているかどうかを確認します。
      if (IsAddressBanned(ipAddress) == false)
      {
      // まだ禁止されていない場合 IP アドレスを禁止します。
      InsertDataIntoTable("[BannedAddresses]",
      "[IPAddress]", "'" + ipAddress + "'");
      }
      }
      // ガーベジ コレクション タスクを実行します。
      private void GarbageCollection(bool deleteSessions)
      {
      // フラッド タイムアウトより古いすべての認証失敗を削除します。
      DeleteRecordsByCriteria("[Failures]",
      String.Format("DATEDIFF(second,[FailureDateTime],'{0}')>{1}",
      DateTime.Now.ToString(),_floodSeconds.ToString()));
      // フラグが付けられたセッションの削除が必要かテストします。
      if (deleteSessions == true)
      {
      // フラグが付けられたセッションのリストからセッションを削除します。
      _flaggedSessions.Clear();
      }
      }
      // 定義可能な条件に基づいてレコード数を取得します。
      private int GetRecordCountByCriteria(
      string tableName,
      string criteria)
      {
      // 条件に基づいてテーブルに存在するレコード数を取得するための 
      // SQL 文字列を作成します。
      StringBuilder sqlString = new StringBuilder();
      sqlString.Append("SELECT COUNT(*) AS [NumRecords]");
      sqlString.Append(String.Format(
      " FROM {0}",tableName));
      sqlString.Append(String.Format(
      " WHERE {0}",criteria));
      // クエリを実行します。
      return ExecuteQuery(true, sqlString.ToString());
      }
      // データベース テーブルにレコードを挿入します。
      private void InsertDataIntoTable(
      string tableName,
      string fieldNames,
      string fieldValues)
      {
      // テーブルにデータを挿入するための SQL 文字列を作成します。
      StringBuilder sqlString = new StringBuilder();
      sqlString.Append(String.Format(
      "INSERT INTO {0}",tableName));
      sqlString.Append(String.Format(
      "({0}) VALUES({1})",fieldNames, fieldValues));
      // クエリを実行します。
      ExecuteQuery(false, sqlString.ToString());
      }
      // 条件に基づいてテーブルからレコードを削除します。
      private void DeleteRecordsByCriteria(
      string tableName,
      string queryCriteria)
      {
      // テーブルからデータを削除するための SQL 文字列を作成します。
      StringBuilder sqlString = new StringBuilder();
      sqlString.Append(String.Format(
      "DELETE FROM {0}",tableName));
      // 条件が指定されているかテストします。
      if (string.IsNullOrEmpty(queryCriteria) == false)
      {
      // SQL 文字列に条件を追加します。
      sqlString.Append(String.Format(
      " WHERE {0}",queryCriteria));
      }
      // クエリを実行します。
      ExecuteQuery(false, sqlString.ToString());
      }
      // SQL クエリを実行します。
      private int ExecuteQuery(bool returnRecordCount, string sqlQuery)
      {
      try
      {
      // 新しい SQL 接続オブジェクトを作成します。
      using (SqlConnection connection =
      new SqlConnection(_connectionString))
      {
      // 新しい SQL コマンド オブジェクトを作成します。
      using (SqlCommand command =
      new SqlCommand(sqlQuery, connection))
      {
      // 接続を開きます。
      connection.Open();
      // メソッドがレコード数を返す必要があるかどうかをテストします。
      if (returnRecordCount == true)
      {
      // データベース クエリを実行します。
      SqlDataReader dataReader = command.ExecuteReader();
      // データ リーダーが行を返したかテストします。
      if (dataReader.HasRows)
      {
      // 単一行を読み取ります。
      dataReader.Read();
      // レコード数を返します。
      return ((int)dataReader["NumRecords"]);
      }
      }
      else
      {
      // データベース クエリを実行します。
      command.ExecuteNonQuery();
      }
      }
      }
      // レコード数ゼロを返します。
      return 0;
      }
      catch (Exception ex)
      {
      // エラーが発生した場合に例外をスローします。
      throw new ProviderException(ex.Message);
      }
      }
      }
      
  6. プロジェクトを保存してビルドします。

メモ: GAC にアセンブリを登録するオプションのステップを使用しなかった場合は、アセンブリを IIS 7 コンピューターに手動でコピーし、Gacutil.exe ツールを使用して GAC にアセンブリを追加する必要があります。詳細については、マイクロソフト MSDN Web サイトの次のトピックを参照してください。

グローバル アセンブリ キャッシュ ツール (Gacutil.exe)

ステップ 3: FTP へのデモ プロバイダーの追加

このステップでは、デモ プロバイダーを FTP サービスおよび既定の Web サイトに追加します。

  1. 拡張機能プロバイダー用のアセンブリ情報を判断します。
    • Windows Explorer で、パス C:\Windows\assembly を開きます。ここで、C: はオペレーティング システム ドライブです。
    • FtpAddressRestrictionAuthentication アセンブリを探します。
    • アセンブリを右クリックし、[プロパティ] をクリックします。
    • [カルチャ] の値 ([Neutral] など) をコピーします。
    • [バージョン] 番号 ([1.0.0.0] など) をコピーします。
    • [公開キー トークン] の値 ([426f62526f636b73] など) をコピーします。
    • [キャンセル] をクリックします。
  2. 前の手順で得た情報を使用して、FTP プロバイダーのグローバル リストに拡張機能プロバイダーを追加し、このプロバイダーのオプションを構成します。
    • この時点では、カスタム認証モジュールにプロパティを追加するためのユーザー インターフェイスがないので、次のコマンド ラインを使用する必要があります。

      cd %SystemRoot%\System32\Inetsrvappcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpAddressRestrictionAuthentication',type='FtpAddressRestrictionAuthentication,FtpAddressRestrictionAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphostappcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication']" /commit:apphostappcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='connectionString',value='Server=localhost;Database=FtpAuthentication;User ID=FtpLogin;Password=P@ssw0rd']" /commit:apphostappcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='logonAttempts',value='5']" /commit:apphostappcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='floodSeconds',value='30']" /commit:apphost

    • メモ: **connectionString 属性に指定した接続文字列は、データベース用の有効なログインである必要があります。

  3. サイトにカスタム プロバイダーを追加します。
    • この時点では、サイトにカスタム機能を追加するための UI がないので、次のコマンド ラインを使用する必要があります。

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

    • メモ: この構文は FTP 基本認証を無効化するので、この認証プロバイダーを使用するときは基本認証を無効にすることが重要です。そうしないと、攻撃者の IP アドレスがこの認証プロバイダーでブロックされていても、攻撃者は依然として基本認証を使用するアカウントを攻撃することができます。

  4. 認証プロバイダー用の承認規則を追加します。
    • メイン ウィンドウで [FTP 承認規則] をダブルクリックします。
    • [操作] ウィンドウで、[許可規則の追加] をクリックします。
    • アクセスのオプションで [指定されたユーザー] を選択します。
    • ユーザー名を入力します (メモ: この一連の手順以外で、データベースにユーザー名を入力する必要があります)。
    • [アクセス許可] オプションで、[読み取り] や [書き込み] を選択します。
    • [OK] をクリックします。

ステップ 4: FTP 7.5 でのプロバイダーの使用

FTP クライアントが FTP サイトに接続すると、FTP サービスはデータベースに格納されているアカウントを使用してカスタム認証プロバイダーでユーザーの認証を試行します。FTP クライアントが認証に失敗すると、プロバイダーは IP アドレスおよび失敗した日時をデータベース内で追跡します。特定の IP アドレスからの FTP クライアントのログインの失敗が logonAttempts 設定に指定されている回数に達し、かつfloodSeconds 設定に指定されている時間内であった場合、プロバイダーはその IP アドレスからの FTP サービスへのログインをブロックします。

メモ: このサンプル プロバイダーは、FTP サービスに認証ロジックを実装しますが、データベース内のデータを管理する管理モジュールは提供しません。たとえば、このプロバイダーを使用して、FTP ユーザー アカウント、禁止 IP アドレス、認証失敗などのリストを管理することはできません。IIS マネージャーを使用してデータを管理するために、IIS Database Manager を使用できます。詳細については、以下のトピックを参照してください。

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

追加情報

Microsoft SQL Server 用の次の SQL スクリプトを使用して、必要なデータベースおよびテーブルを作成できます。このスクリプトを使用するには、データベースの名前およびデータベース ファイルの場所を更新する必要があります。SQL Server で、このスクリプトを新しいクエリ ウィンドウで実行してから、接続文字列で使用するデータベース ログインを作成します。

メモ: c:\databases 以外の場所にデータベースを格納するには、SQL スクリプトに変更を加える必要があります。

/****** FtpAuthentication データベースの作成 ******/
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
/****** データベース テーブルの作成 ******/
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

まとめ

このチュートリアルでは、次について学びました。

  • カスタム FTP プロバイダー用に Visual Studio 2008 でプロジェクトを作成する方法
  • カスタム FTP プロバイダー用の拡張機能インターフェイスの実装方法
  • FTP サービスへの FTP カスタム プロバイダーの追加方法

関連コンテンツ

記事