coding tips

posted:2014.07.14

[c#] FTP over SSLによるファイル操作

FTP over SSLによるファイル制御方法を紹介します。

ファイルのアップロード、ダウンロード、削除、ディレクトリの存在確認、ディレクトリの作成を一つのクラスに実装します。

サンプルをダウンロード
from GitHub

FTPについて

クライアントからサーバーへファイルを送る通信方式としてFTPがあります。 サーバへのアップロードやダウンロードをはじめ、様々なファイル操作を行うことができます。

C#でFTP通信を実現するための実装例を挙げます。

FTP管理クラス

FTP通信を制御するためのクラスを作り、機能を集約してみましょう。

ここでは次の機能について述べていきます。

FTP over SSLに対応していますが、この実装をそのまま使うと証明書名の不一致をスルーしてしまいます。

これは私が試した環境によるものですが、レンタルサーバーを使っているとFTP over SSLの通信を行う際に共有SSL証明書を使用する場合があります。

この共有SSL証明書の識別名(コモンネーム)が独自ドメインを使うと不一致になる場合があります

私の場合、エックスサーバーを使用していますが、2014年7月14日現在このFTP over SSLで使われるのは共有SSL証明書のようです。 私はRapidSSLの独自SSL証明書を導入していますが、サポートに確認したところ、これはwebアクセスのみ対応しているもののようです。

SSL通信ができることには変わりはないので、証明書名の不一致については許容するようにServerCertificateValidationCallbackに実装しています。 使用する際は、証明書の内容を確認してください。

証明書の不一致もチェックする場合はFtpConfigのEnableSslPolicyErrorsRemoteCertificateNameMismatchにtrueを設定します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.Net.Security;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace SampleFTP
{
    // FTP設定
    public class FtpConfig
    {
        private static string ftpUser;
        private static string ftpPassword;
        private static string ftpHost;
        private static bool enableSslPolicyErrorsRemoteCertificateNameMismatch;

        static FtpConfig()
        {
            // 接続情報
            FtpConfig.FtpUser = "user";  // FTPユーザー
            FtpConfig.FtpPassword = "password"; // FTPパスワード
            FtpConfig.ftpHost = "example.com";   // FTPホスト(ドメイン)
            // オプション
            FtpConfig.EnableSslPolicyErrorsRemoteCertificateNameMismatch = false;   // SSL証明書名の不一致をチェックするか?
        }

        // FTPユーザー
        public static string FtpUser
        {
            get { return ftpUser; }
            private set { ftpUser = value; }
        }

        // FTPパスワード
        public static string FtpPassword
        {
            get { return ftpPassword; }
            private set { ftpPassword = value; }
        }

        // ホストのルートURI
        public static string FtpRoot
        {
            get { return "ftp://" + ftpHost; }
        }

        // SSL証明書名の不一致をエラーとするか
        public static bool EnableSslPolicyErrorsRemoteCertificateNameMismatch
        {
            get { return enableSslPolicyErrorsRemoteCertificateNameMismatch; }
            set { enableSslPolicyErrorsRemoteCertificateNameMismatch = value; }
        }
    }

    // FTP非同期通信の状態管理
    public class FtpState
    {
        private ManualResetEvent wait;
        string status;
        private FtpWebRequest request;
        private string filePath;
        private Exception operationException = null;

        public FtpState() { wait = new ManualResetEvent(false); }

        // シグナル状態管理
        public ManualResetEvent OperationComplete { get { return wait; } }

        // ステータス記述
        public string StatusDescription
        {
            get { return status; }
            set { status = value; }
        }

        // FTPオブジェクト
        public FtpWebRequest Request
        {
            get { return request; }
            set { request = value; }
        }

        // ローカルファイルパス
        public string FilePath
        {
            get { return filePath; }
            set { filePath = value; }
        }

        // 例外
        public Exception OperationException
        {
            get { return operationException; }
            set { operationException = value; }
        }
    }

    // FTP管理
    public class FtpManager
    {
        //証明書の内容を表示
        private static void PrintCertificate(X509Certificate certificate)
        {
            StringBuilder sb = new StringBuilder();
            StringWriter sw = new StringWriter(sb);

            sw.WriteLine("▼証明書の内容");
            sw.WriteLine("サブジェクトの識別名(Subject):{0}", certificate.Subject);
            sw.WriteLine("発行した証明機関の名前(Issuer):{0}", certificate.Issuer);
            sw.WriteLine("形式の名前(GetFormat):{0}", certificate.GetFormat());
            sw.WriteLine("失効日(GetExpirationDateString):{0}", certificate.GetExpirationDateString());
            sw.WriteLine("発効日(GetEffectiveDateString):{0}", certificate.GetEffectiveDateString());
            sw.WriteLine("文字列形式のキー アルゴリズム情報(GetKeyAlgorithm):{0}", certificate.GetKeyAlgorithm());
            sw.WriteLine("16進数文字列形式の公開キー(GetPublicKeyString):{0}", certificate.GetPublicKeyString());
            sw.WriteLine("16進数文字列形式のシリアル番号(GetSerialNumberString):{0}", certificate.GetSerialNumberString());
            sw.WriteLine("▲証明書の内容");

            Console.WriteLine(sb);
        }

        // SSL証明書の信頼性を確認
        private static bool OnRemoteCertificateValidationCallback(
          Object sender,
          X509Certificate certificate,
          X509Chain chain,
          SslPolicyErrors sslPolicyErrors)
        {
            if (sslPolicyErrors == SslPolicyErrors.None)
            {
                Console.WriteLine("SSL のポリシー エラーはありません");
            }
            else
            {
                if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) ==
                    SslPolicyErrors.RemoteCertificateChainErrors)
                {
                    Console.WriteLine("【ERROR】ChainStatusが、空でない配列を返しました");
                } else if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) ==
                    SslPolicyErrors.RemoteCertificateNameMismatch)
                {
                    // 証明書情報を確認
                    PrintCertificate(certificate);

                    if (!FtpConfig.EnableSslPolicyErrorsRemoteCertificateNameMismatch)
                    {
                        Console.WriteLine("【WARNING】設定により証明書名の不一致を容認しました");
                        return true;
                    }
                    Console.WriteLine("【ERROR】証明書の名前が一致していません");
                }
                else if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) ==
                  SslPolicyErrors.RemoteCertificateNotAvailable)
                {
                    Console.WriteLine("【ERROR】証明書が利用できません");
                }
                else {
                    Console.WriteLine("【ERROR】予期しないエラーが発生しました");
                }

                return false;
            }
            return true;
        }

        // FTPのリクエスト用オブジェクト作成
        private static FtpWebRequest CreateFtpWebRequest(string webRequestMethod, string requestUri)
        {
            FtpWebRequest request = null;
            string uri = FtpConfig.FtpRoot + requestUri;

            try
            {
                // アップロード先URI
                Uri targetUri = new Uri(uri);

                request = (FtpWebRequest)WebRequest.Create(targetUri);
                request.Credentials = new NetworkCredential(FtpConfig.FtpUser, FtpConfig.FtpPassword);
                request.Method = webRequestMethod;
                request.KeepAlive = false;
                request.UseBinary = true;
                request.UsePassive = true;

                // SSL通信設定
                ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(OnRemoteCertificateValidationCallback);
                request.EnableSsl = true;
            }
            catch (UriFormatException ex)
            {
                Console.WriteLine("【ERROR】無効なURIを検出 - {0}, uri:{1}", ex.Message, uri);
                throw;
            }
            return request;
        }

        // アップロード(非同期)
        public static void UploadAsync(string requestUri, string filePath)
        {
            FtpWebRequest request = null;
            try
            {
                // 状態管理クラスをオブジェクト化
                FtpState state = new FtpState();

                // FTPのリクエスト用オブジェクト作成
                request = CreateFtpWebRequest(WebRequestMethods.Ftp.UploadFile, requestUri);

                // 非同期通信管理オブジェクト設定
                state.Request = request;
                state.FilePath = filePath;

                // 待機オブジェクト
                ManualResetEvent waitObject = state.OperationComplete;

                // 非同期リクエスト開始
                request.BeginGetRequestStream(new AsyncCallback(EndGetStreamCallback), state);

                // 処理完了まで待機
                waitObject.WaitOne();

                // 処理完了
                if (state.OperationException != null)
                {
                    // 例外発生
                    throw state.OperationException;
                }
                else
                {
                    Console.WriteLine("アップロード処理が正常に終了しました - {0}", state.StatusDescription);
                }
            }
            catch (WebException ex)
            {
                Console.WriteLine("【ERROR】アップロード処理 - {0}", ex.Message);
                throw;
            }
            finally
            {
                request = null;
            }
        }

        // 非同期通信のコールバック
        private static void EndGetStreamCallback(IAsyncResult ar)
        {
            // 通信状態管理オブジェクトを取得
            FtpState state = (FtpState)ar.AsyncState;
            Stream requestStream = null;
            FileStream stream = null;
            try
            {
                // ファイルをストリームに書き込む
                requestStream = state.Request.EndGetRequestStream(ar);
                byte[] buffer = new byte[1024];
                int count = 0;
                int readBytes = 0;
                stream = File.OpenRead(state.FilePath);
                do
                {
                    readBytes = stream.Read(buffer, 0, buffer.Length);
                    requestStream.Write(buffer, 0, readBytes);
                    count += readBytes;
                }
                while (readBytes != 0);
                Console.WriteLine("{0} byteストリームに書き込みました", count);

                state.Request.BeginGetResponse(new AsyncCallback(EndGetResponseCallback), state);
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine("【ERROR】アップロード処理 ファイルがありません。 - {0}", ex.Message);
                state.OperationException = ex;
                state.OperationComplete.Set();
                return;
            }
            catch (Exception ex)
            {
                Console.WriteLine("【ERROR】アップロード処理 - {0}", ex.Message);
                state.OperationException = ex;
                state.OperationComplete.Set();
                return;
            }
            finally
            {
                if (stream != null) stream.Close();
                if (requestStream != null) requestStream.Close();
            }
        }

        // 非同期通信完了処理
        private static void EndGetResponseCallback(IAsyncResult ar)
        {
            FtpState state = (FtpState)ar.AsyncState;
            FtpWebResponse response = null;

            try
            {
                response = (FtpWebResponse)state.Request.EndGetResponse(ar);
                state.StatusDescription = response.StatusDescription;
                state.OperationComplete.Set();
            }
            catch (Exception ex)
            {
                Console.WriteLine("【ERROR】アップロード処理 - {0}", ex.Message);
                state.OperationException = ex;
                state.OperationComplete.Set();
            }
            finally
            {
                if (response != null) response.Close();
            }
        }

        // ファイルを削除
        public static void Delete(string requestUri)
        {
            FtpWebRequest request = null;
            FtpWebResponse response = null;

            try
            {
                // リクエスト用オブジェクト作成
                request = CreateFtpWebRequest(WebRequestMethods.Ftp.DeleteFile, requestUri);
                // レスポンス用オブジェクト取得
                response = (FtpWebResponse)request.GetResponse();
                Console.WriteLine("削除処理が正常に終了しました - {0}", response.StatusDescription);
            }
            catch (WebException ex)
            {
                Console.WriteLine("【ERROR】削除処理 - {0}", ex.Message);
                throw;
            }
            finally
            {
                if (response != null) response.Close();
                request = null;
            }
        }

        /* ダウンロード */
        public static void Download(string requestUri, string filePath)
        {
            FtpWebRequest request = null;
            FtpWebResponse response = null;
            Stream responseStream = null;
            FileStream stream = null;
            try
            {
                // リクエスト用オブジェクト作成
                request = CreateFtpWebRequest(WebRequestMethods.Ftp.DownloadFile, requestUri);
                // レスポンス用オブジェクト取得
                response = (FtpWebResponse)request.GetResponse();

                // ストリームに書き込み
                responseStream = response.GetResponseStream();
                stream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
                byte[] buffer = new byte[1024];
                while (true)
                {
                    int readSize = responseStream.Read(buffer, 0, buffer.Length);
                    if (readSize == 0) break;
                    stream.Write(buffer, 0, readSize);
                }
                Console.WriteLine("ダウンロード処理が正常に終了しました - {0}", response.StatusDescription);
            }
            catch (WebException ex)
            {
                Console.WriteLine("【ERROR】ダウンロード処理 - {0}", ex.Message);
                throw;
            }
            finally
            {
                if (responseStream != null) responseStream.Close();
                if (stream != null) stream.Close();
                if (response != null) response.Close();
                request = null;
            }
        }

        /* ディレクトリ存在チェック */
        public static bool ExistsDirectory(string requestUri)
        {
            FtpWebRequest request = null;
            FtpWebResponse response = null;

            try
            {
                // リクエスト用オブジェクト作成
                request = CreateFtpWebRequest(WebRequestMethods.Ftp.ListDirectory, requestUri);
                // レスポンス用オブジェクト取得
                response = (FtpWebResponse)request.GetResponse();
            }
            catch (WebException ex)
            {
                if (ex.Status == WebExceptionStatus.ProtocolError)
                {
                    FtpWebResponse r = (FtpWebResponse)ex.Response;
                    if (r.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable)
                    {
                        Console.WriteLine("ディレクトリ無");
                        return false;
                    }
                }
                Console.WriteLine("【ERROR】ディレクトリ存在チェック処理 - {0}", ex.Message);
                throw;
            }
            finally
            {
                if (response != null) response.Close();
                request = null;
            }

            Console.WriteLine("ディレクトリ有");
            return true;
        }

        /* ディレクトリを作成 */
        public static void MakeDirectory(string requestUri)
        {
            FtpWebRequest request = null;
            FtpWebResponse response = null;

            try
            {
                // 既にディレクトリが存在していたら何もしない
                if (ExistsDirectory(requestUri)) return;
                // リクエスト用オブジェクト作成
                request = CreateFtpWebRequest(WebRequestMethods.Ftp.MakeDirectory, requestUri);
                // レスポンス用オブジェクト取得
                response = (FtpWebResponse)request.GetResponse();
                Console.WriteLine("ディレクトリ作成処理が正常に終了しました - {0}", response.StatusDescription);
            }
            catch (WebException ex)
            {
                Console.WriteLine("【ERROR】ディレクトリを作成処理 - {0}", ex.Message);
                throw;
            }
            finally
            {
                if (response != null) response.Close();
                request = null;
            }
        }
    }
}

フォームの設計と実装

画面には該当機能を記したボタンを配置します。

try catchを使って例外に対応します。

画面イメージ
図1. 画面イメージ
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace SampleFTP
{
    public partial class FormMain : Form
    {
        public FormMain()
        {
            InitializeComponent();
        }

        // アップロード
        private void buttonUpload_Click(object sender, EventArgs e)
        {
            try
            {
                FtpManager.UploadAsync("/upload.txt", @"C:\ftptest\upload.txt");
                MessageBox.Show("アップロードしました", "アップロード", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        // 削除
        private void buttonDelete_Click(object sender, EventArgs e)
        {
            try
            {
                FtpManager.Delete("/upload.txt");
                MessageBox.Show("削除しました", "削除", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        // ダウンロード
        private void buttonDownload_Click(object sender, EventArgs e)
        {
            try
            {
                FtpManager.Download("/upload.txt", @"C:\ftptest\uploadDownLoad.txt");
                MessageBox.Show("ダウンロードしました", "ダウンロード", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        // ディレクトリ存在確認
        private void buttonExistsDirectory_Click(object sender, EventArgs e)
        {
            try
            {
                if (FtpManager.ExistsDirectory("/test/"))
                {
                    MessageBox.Show("ディレクトリがありました", "ディレクトリ存在確認", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                else
                {
                    MessageBox.Show("ディレクトリはありません", "ディレクトリ存在確認", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        // ディレクトリ作成
        private void buttonMakeDirectory_Click(object sender, EventArgs e)
        {
            try
            {
                FtpManager.MakeDirectory("/testMake/");
                MessageBox.Show("ディレクトリを作成しました", "ディレクトリ作成", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }
}
動作確認環境