C Sharpとネットワーク - FTP

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

FTP (File Transfer Protocol) は、インターネットを介してファイルを転送するために使用される標準的なネットワークプロトコルである。
1971年に開発され、現在でも広く使用されている。

FTPの主な目的は、クライアントとサーバ間でのファイルの送受信を可能にすることである。
これは、Webサイトの更新、大容量ファイルの共有、バックアップの作成等の用途に利用されている。

FTPの動作原理は、クライアント-サーバモデルに基づいており、
クライアントがFTPサーバに接続および認証を行った後、ファイルの転送やディレクトリの操作等のコマンドを実行する。

一般的に、FTPは2つの接続を使用する。

  • 制御接続
    コマンドの送信
  • データ接続
    ファイル転送


セキュリティの観点から見ると、従来のFTPでは、データが平文で送信されるため、盗聴のリスクがある。
この問題に対処するため、FTPSやSFTP等の暗号化された代替プロトコルが開発された。

しかし、FTPは柔軟性が高く、様々な転送モードをサポートしている。
アスキーモードはテキストファイル、バイナリモードは画像や実行可能ファイルに適している。

多くのFTPクライアントソフトウェアが存在しており、グラフィカルなインターフェースを提供しているため、一般ユーザでも簡単に使用することができる。
また、コマンドラインインターフェースでも使用可能であり、高度なユーザや自動化スクリプトに適している。

FTPは長年にわたり使用されてきたが、セキュリティ上の懸念やより新しい代替手段の登場により、その使用は徐々に減少している。
しかし、特定の状況下では依然として有効なプロトコルとして認識されている。


アクティブモードとパッシブモード

FTPには、アクティブモードとパッシブモードの2つの接続モードが存在する。

アクティブモード

クライアントがデータ接続のためのポートを開き、サーバに接続を要求する。

ファイアウォールの問題が発生しやすく、特にNAT環境では問題が起きやすい。

パッシブモード

サーバがデータ接続のためのポートを開き、クライアントがそのポートに接続する。

ファイアウォールやNAT環境での問題が少なく、より信頼性が高い。

一般的に、パッシブモードの使用が推奨される。
特に、クライアントがファイアウォールの内側にある場合やインターネットを介して接続する場合に有効である。

ただし、特定のネットワーク環境や要件によっては、アクティブモードが必要になる場合もある。

パッシブモードのメリットを以下に示す。

  • クライアント側のファイアウォール設定が簡単である。
  • NATやプロキシを通した接続が容易である。
  • 多くの現代的なFTPクライアントのデフォルト設定である。


※注意
サーバ側で追加のポート開放が必要な場合がある。
一部の古いFTPサーバではサポートされていない可能性がある。

C#での使用例

FtpWebRequestクラスのUsePassiveプロパティを設定することにより、パッシブモードの可否を指定する。

  • trueに指定する場合
    パッシブモードを使用する。
  • falseに指定する場合
    アクティブモードを使用する。


 FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUrl);
 request.UsePassive = true;  // パッシブモードを有効化



FTP : データの送信

以下の例では、FTPを使用して、ASCIIモード (テキストファイル) とバイナリモード (画像ファイル等) でファイルを送信している。

FTP接続にはIPアドレスも使用可能である。
IPアドレスを使用するメリットと注意点を以下に示す。

  • IPアドレスを使用する場合のメリット
    • 直接接続
      DNSルックアップが不要なため、接続が少し速くなる可能性がある。
  • ネットワークトラブルシューティング
    • DNSの問題を切り分けることができる。
    • ファイアウォール設定
      特定のIPアドレスのみを許可するようなセキュリティ設定が容易である。

  • IPアドレスを使用する場合の注意点
    • SSL / TLS証明書
      セキュアな接続 (FTPS) を使用する場合、証明書がIPアドレスに対して発行されていないと警告が出る可能性がある。
    • 柔軟性
      サーバのIPアドレスが変更された場合、クライアント側の設定も変更が必要になる。
    • 仮想ホスティング
      1つのIPアドレスで複数のFTPサーバをホストしている場合、正しいサーバに接続できない可能性がある。


セキュリティを向上させるために、可能であればFTPSやSFTP等の暗号化されたプロトコルを使用することを推奨する。

 using System;
 using System.IO;
 using System.Net;
 using System.Threading.Tasks;
 
 class AsyncFtpFileTransfer
 {
    private string host;
    private string username;
    private string password;
 
    // FTP接続に必要な情報
    public AsyncFtpFileTransfer(string host, string username, string password)
    {
       this.host     = host;
       this.username = username;
       this.password = password;
    }
 
    // ローカルファイルパス、リモートファイルパス、アスキーモードの可否を設定して、FTPサーバへ送信
    public async Task UploadFileAsync(string localFilePath, string remoteFilePath, bool useAscii)
    {
       try
       {
          FtpWebRequest request = (FtpWebRequest)WebRequest.Create($"ftp://{host}/{remoteFilePath}");
          request.Method      = WebRequestMethods.Ftp.UploadFile;  // ファイルの送信
          request.Credentials = new NetworkCredential(username, password);
          request.UsePassive  = true;       // パッシブモードを使用するかどうかの可否
          request.UseBinary   = !useAscii;  // ASCIIモードとバイナリモードの切り替え
          request.KeepAlive   = false;
 
          using (FileStream fileStream = File.OpenRead(localFilePath))
          using (Stream ftpStream = await request.GetRequestStreamAsync())
          {
             // ファイルは10[KB]のバッファを使用して読み取り、FTPストリームに書き込む
             byte[] buffer = new byte[10240];
             int read;
             while ((read = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
             {
                await ftpStream.WriteAsync(buffer, 0, read);
             }
          }
 
          Console.WriteLine($"Fileの送信に成功: {localFilePath} -> {remoteFilePath}");
       }
       catch (WebException ex)
       {
          FtpWebResponse response = (FtpWebResponse)ex.Response;
          Console.WriteLine($"FTP送信エラー: {response.StatusDescription}");
       }
       catch (Exception ex)
       {
          Console.WriteLine($"予期しないエラーが発生: {ex.Message}");
       }
    }
 
    public static async Task Main()
    {
       string host     = "<ホスト名またはIPアドレス  例1: ftp.example.com,  例2: 192.168.1.100>";
       string username = "<ユーザ名>";
       string password = "<パスワード>";
 
       AsyncFtpFileTransfer ftpTransfer = new AsyncFtpFileTransfer(host, username, password);
 
       // ASCIIモード送信 (テキストファイル向け)
       await ftpTransfer.UploadFileAsync("localTextFile.txt", "remoteTextFile.txt", true);
 
       // バイナルモード送信 (画像ファイル等のバイナリファイル向け)
       await ftpTransfer.UploadFileAsync("localImageFile.jpg", "remoteImageFile.jpg", false);
    }
 }



FTP : データの受信

以下の例では、FTPを使用して、ASCIIモード (テキストファイル) とバイナリモード (画像ファイル等) で複数のファイルを同時に受信している。

※注意
サーバによっては、同時接続数に制限がある場合があるため、FTPサーバの同時接続数制限に注意すること。

また、大量のファイルをダウンロードする場合は、メモリ使用量に注意すること。
必要に応じて、ダウンロードをバッチ処理することを検討する。

 using System;
 using System.IO;
 using System.Net;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 
 class AsyncFtpFileDownload
 {
    private string host;
    private string username;
    private string password;
 
    // FTP接続に必要な情報
    public AsyncFtpFileDownload(string host, string username, string password)
    {
       this.host = host;
       this.username = username;
       this.password = password;
    }
 
    // 単一のファイルを非同期でダウンロード
    public async Task DownloadFileAsync(string remoteFilePath, string localFilePath, bool useAscii)
    {
       try
       {
          // FTP要求の作成
          FtpWebRequest request = (FtpWebRequest)WebRequest.Create($"ftp://{host}/{remoteFilePath}");
          request.Method = WebRequestMethods.Ftp.DownloadFile;  // ファイルの受信
          request.Credentials = new NetworkCredential(username, password);
          request.UsePassive  = true;      // パッシブモードを使用するかどうかの可否
          request.UseBinary   = !useAscii;  // ASCIIモードとバイナリモードの切り替え
          request.KeepAlive   = false;
 
          using (FtpWebResponse response = (FtpWebResponse)await request.GetResponseAsync())
          using (Stream ftpStream = response.GetResponseStream())
          using (FileStream fileStream = File.Create(localFilePath))
          {
             // ファイルは10[KB]のバッファを使用して読み取り、FTPストリームに書き込む
             byte[] buffer = new byte[10240];
             int read;
             while ((read = await ftpStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
             {
                await fileStream.WriteAsync(buffer, 0, read);
             }
 
             Console.WriteLine($"ファイルの受信が完了: {remoteFilePath} -> {localFilePath}");
             Console.WriteLine($"ステータス: {response.StatusDescription}");
          }
       }
       catch (WebException ex)
       {
          FtpWebResponse response = (FtpWebResponse)ex.Response;
          if (response != null)
          {
             Console.WriteLine($"FTP受信エラー: {response.StatusDescription}");
          }
          else
          {
             Console.WriteLine($"Web例外が発生: {ex.Message}");
          }
       }
       catch (Exception ex)
       {
          Console.WriteLine($"予期せぬエラーが発生: {ex.Message}");
       }
    }
 
    // 複数のファイルを同時に非同期でダウンロードするメソッド
    public async Task DownloadMultipleFilesAsync(List<(string remoteFilePath, string localFilePath, bool useAscii)> files)
    {
       List<Task> downloadTasks = new List<Task>();
 
       // 各ファイルのダウンロードタスクを生成して、リストに追加
       foreach (var file in files)
       {
          downloadTasks.Add(DownloadFileAsync(file.remoteFilePath, file.localFilePath, file.useAscii));
       }
 
       // 全てのダウンロードタスクが完了するまで待機
       await Task.WhenAll(downloadTasks);
       Console.WriteLine("全てのファイルの受信が完了");
    }
 
    public static async Task Main()
    {
       // FTP接続情報
       string host     = "<ホスト名またはIPアドレス  例: ftp.example.com>";
       string username = "<ユーザ名>";
       string password = "<パスワード>";
 
       AsyncFtpFileDownload ftpDownload = new AsyncFtpFileDownload(host, username, password);
 
       // 受信するファイル群
       List<(string remoteFilePath, string localFilePath, bool useAscii)> filesToDownload = new List<(string, string, bool)>
       {
          ("remoteTextFile1.txt", "localTextFile1.txt", true),  // テキストファイル(ASCIIモード)
          ("remoteTextFile2.txt", "localTextFile2.txt", true),  // テキストファイル(ASCIIモード)
          ("remoteImageFile1.jpg", "localImageFile1.jpg", false),  // 画像ファイル(バイナリモード)
          ("remoteImageFile2.jpg", "localImageFile2.jpg", false)   // 画像ファイル(バイナリモード)
       };
 
       // 複数のファイルを同時に受信
       await ftpDownload.DownloadMultipleFilesAsync(filesToDownload);
    }
 }



FTPS : データの送信

以下の例では、FTPS (SSL / TLS証明書) を使用して、ASCIIモード (テキストファイル) とバイナリモード (画像ファイル等) で複数のファイルを非同期に送信している。

※注意
実務では、適切なSSL / TLS証明書の検証を行うこと。
ファイアウォールやネットワーク設定が、FTPSトラフィック (通常は、990番ポート) を許可していることを確認する。

また、大量のファイルや大きなファイルを送信する場合は、タイムアウト設定やメモリ使用量に注意する。

 using System;
 using System.IO;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using System.Net;
 using System.Net.Security;
 using System.Security.Cryptography.X509Certificates;
 
 class AsyncFtpsFileUpload
 {
    private string host;
    private string username;
    private string password;
    private int port;
 
    // FTPS接続に必要な情報
    public AsyncFtpsFileUpload(string host, string username, string password, int port = 990)
    {
       this.host     = host;
       this.username = username;
       this.password = password;
       this.port     = port;
    }
 
    // 単一のファイルを非同期でアップロード
    public async Task UploadFileAsync(string localFilePath, string remoteFilePath, bool useAscii)
    {
       try
       {
          // FTPS要求の作成
          FtpWebRequest request = (FtpWebRequest)WebRequest.Create($"ftps://{host}:{port}/{remoteFilePath}");
          request.Method = WebRequestMethods.Ftp.UploadFile;  // ファイルの送信
          request.Credentials = new NetworkCredential(username, password);
          request.UsePassive = true;      // パッシブモードを使用するかどうかの可否
          request.UseBinary = !useAscii;  // ASCIIモードとバイナリモードの切り替え
          request.KeepAlive = false;
          request.EnableSsl = true;  // SSLを有効化
 
          // SSL / TLS証明書の検証
          // 注意: 実務では適切に証明書を検証すること
          System.Net.ServicePointManager.ServerCertificateValidationCallback = ValidateServerCertificate;
 
          using (FileStream fileStream = File.OpenRead(localFilePath))
          using (Stream ftpStream = await request.GetRequestStreamAsync())
          {
             // ファイルは10[KB]のバッファを使用して読み取り、FTPストリームに書き込む
             byte[] buffer = new byte[10240];
             int read;
             while ((read = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
             {
                await ftpStream.WriteAsync(buffer, 0, read);
             }
          }
 
          using (FtpWebResponse response = (FtpWebResponse)await request.GetResponseAsync())
          {
             Console.WriteLine($"ファイルの送信が完了: {localFilePath} -> {remoteFilePath}");
             Console.WriteLine($"ステータス: {response.StatusDescription}");
          }
       }
       catch (WebException ex)
       {
          FtpWebResponse response = (FtpWebResponse)ex.Response;
          if (response != null)
          {
             Console.WriteLine($"FTPS送信エラー: {response.StatusDescription}");
          }
          else
          {
             Console.WriteLine($"Web例外が発生: {ex.Message}");
          }
       }
       catch (Exception ex)
       {
          Console.WriteLine($"予期せぬエラーが発生: {ex.Message}");
       }
    }
 
    // 複数のファイルを同時に非同期でアップロードするメソッド
    public async Task UploadMultipleFilesAsync(List<(string localFilePath, string remoteFilePath, bool useAscii)> files)
    {
       List<Task> uploadTasks = new List<Task>();
 
       // 各ファイルのアップロードタスクを生成して、リストに追加
       foreach (var file in files)
       {
          uploadTasks.Add(UploadFileAsync(file.localFilePath, file.remoteFilePath, file.useAscii));
       }
 
       // 全てのアップロードタスクが完了するまで待機
       await Task.WhenAll(uploadTasks);
       Console.WriteLine("全てのファイルの送信が完了");
    }
 
    public static async Task Main()
    {
       // FTPS接続情報
       string host     = "<ホスト名またはIPアドレス  例: ftps.example.com>";
       string username = "<ユーザ名>";
       string password = "<パスワード>";
       int port        = 990;  // FTPSの標準ポート
 
       AsyncFtpsFileUpload ftpsUpload = new AsyncFtpsFileUpload(host, username, password, port);
 
       // 送信するファイル群
       List<(string localFilePath, string remoteFilePath, bool useAscii)> filesToUpload = new List<(string, string, bool)>
       {
          ("localTextFile1.txt", "remoteTextFile1.txt", true),     // テキストファイル(ASCIIモード)
          ("localTextFile2.txt", "remoteTextFile2.txt", true),     // テキストファイル(ASCIIモード)
          ("localImageFile1.jpg", "remoteImageFile1.jpg", false),  // 画像ファイル(バイナリモード)
          ("localImageFile2.jpg", "remoteImageFile2.jpg", false)   // 画像ファイル(バイナリモード)
       };
 
       // 複数のファイルを同時に送信
       await ftpsUpload.UploadMultipleFilesAsync(filesToUpload);
    }
 }


 // ※注意
 // 実務では、使用環境に応じて、さらに厳密な検証ロジックを追加することを推奨する。
 // また、可能な限り信頼された認証局によって発行された証明書を使用することを推奨する。
 // 
 // 信頼された証明書のリストは定期的に更新して、不要になった証明書は速やかに削除すること。
 
 using System;
 using System.Net.Security;
 using System.Security.Cryptography.X509Certificates;
 
 public class SslCertificateValidator
 {
    // 信頼された証明書のサムプリント
    // 実際の環境に合わせて更新すること
    private static readonly string[] TrustedCertificates = new string[]
    {
       "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0",
       "B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1"
    };
 
    public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
    {
       if (sslPolicyErrors == SslPolicyErrors.None)
       {  // 証明書チェーンとポリシーエラーが無い場合
          LogMessage("情報", "証明書の検証に成功");
          return true;
       }
 
       LogMessage("警告", $"証明書エラー: {sslPolicyErrors}");
 
       if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors))
       {
          return HandleChainErrors(chain);
       }
 
       if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch))
       {
          LogMessage("エラー", "証明書の名前が不一致");
          return false;
       }
 
       if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
       {
          LogMessage("エラー", "リモート証明書が利用不可");
          return false;
       }
 
       // その他のエラーの場合
       return false;
    }
 
    private static bool HandleChainErrors(X509Chain chain)
    {
       foreach (X509ChainStatus status in chain.ChainStatus)
       {
          if (status.Status == X509ChainStatusFlags.UntrustedRoot)
          {
             X509Certificate2 rootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
             if (ValidateSelfSignedCertificate(rootCert))
             {
                LogMessage("情報", "信頼されたルート証明書を検証しました。");
                return true;
             }
          }
 
          LogMessage("エラー", $"証明書チェーンエラー: {status.StatusInformation}");
       }
 
       return false;
    }
 
    private static bool ValidateSelfSignedCertificate(X509Certificate certificate)
    {
       string certHash = certificate.GetCertHashString();
 
       if (TrustedCertificates.Contains(certHash, StringComparer.OrdinalIgnoreCase))
       {
          LogMessage("情報", "信頼された自己署名証明書を検証しました。");
          return true;
       }
 
       LogMessage("エラー", $"未知の自己署名証明書です。サムプリント: {certHash}");
       return false;
    }
 
    private static void LogMessage(string level, string message)
    {
       string logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
       Console.WriteLine(logMessage);
 
       // ファイルやデータベースへのログ記録処理等を追加
    }
 }