はじめに
昔は、ファイル転送として、大変お世話になった FTP ですが、WEB API で事足りるので、しばらく利用する機会がありませんでした。
しかし、数年前、案件対応として、FTPS でのファイル転送を C# で実装するケースがありました。
今回は、FTP / FTPS / SFTP クライアントについて記載したいと思います。
テスト環境
ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
FTP / FTPS / SFTP サーバは、Ubuntu 24.04 上に構築しました。
こちらは主題ではないので、ペアでの確認用に、メモ程度に残しておきます。
と言いながら、こちらの情報量のほうが多いかも、、、
※テスト用の設定なので、実運用としては、相応しくない可能性があります。
FTP/FTPS/SFTP
まずは、FTP / FTPS / SFTP の概要説明をします。
- FTP
- FTP(File Transfer Protocol)は、1970年代から利用されている、ファイル転送プロトコルで、制御チャネルとデータ転送チャネルの 2つのチャネル(デュアルチャネル)を利用します。
- FTP には、Active(アクティブ:能動的)モードと Passive(パッシブ:受動的)モードが存在します。双方ともに、最初は、クライアントからサーバのポート番号 21 に接続して制御チャネルを確立します。
- Active モードでは、クライアントからサーバに接続ポート番号を伝えて、サーバがポート番号 20 からクライアントの対象ポートに接続して、データチャネルを確立します。
- Passive モードでは、サーバからクライアントに、サーバで割り当てらた範囲内のポート番号を伝えて、クライアントからサーバの対象ポートに接続して、データ転送チャネルを確立します。
- メリット
- 迅速、かつ、シンプルです。
- デメリット
- 暗号化されないため、セキュリティリスクがあります。
- FTPS
- FTPS(FTP Secure、もしくは、FTP over SSL/TLS)は、FTP で送受信するデータを、SSL/TLS で暗号化する通信プロトコルで、FTP と同様に、制御チャネルとデータ転送チャネルの 2つのチャネルを利用します。(Active モードと Passive モードが存在)
- FTPS には、暗号化モードとして、Explicit(エクスプリシット:明示的)モード と Implicit(インプリシット:暗黙的)モードが存在します。
- Explicit モードは、クライアントからサーバのポート番号 21 に接続して制御チャネルを確立します。通信開始時は暗号化せず、クライアントが AUTH コマンドを実行することで、途中から通信内容を暗号化します。データ転送チャネルの利用ポート番号については、Active モード / Passive モードともに FTP と同様です。
- Implicit モードは、クライアントからサーバのポート番号 990 に接続して制御チャネルを確立して、初めから暗号化通信を行います。データ転送チャネルの利用ポート番号については、Active モードの場合、サーバはポート番号 20 ではなく 989 を利用します。Passive モードの場合、FTP と同様です。
- メリット
- 既存の FTP インフラを活用しながらセキュリティを強化できるため、システムの大幅な変更が不要です。
- SSL/TLS で、認証情報、および、転送データの暗号化を行うことで、セキュリティリスクが軽減されます。
- デメリット
- ファイアウォールに妨げられることがあるので、ポート番号、もしくは、FTPS サーバ / クライアントソフトの例外登録が必要となります。
- SFTP
- SFTP(SSH File Transfer Protocol)は、SSH(Secure Shell)を利用したファイル転送プロトコルです。SSH の暗号化を利用することで、高いセキュリティを確保します。また、FTP とは異なり、シングルチャネル(制御とデータ転送を同じチャネルで実施)実装となり、ポート番号 22 を利用します。
- メリット
- すべての通信が暗号化され、認証も SSH の強力な仕組みに基づいて行われます。
- シングルチャネルのため、設定やファイアウォールの構成が容易です。
- デメリット
- SSH が標準利用できる環境でない場合には、各種ソフトの導入が必要となります。
上記に記載したポート番号は一般的なポート番号です。
システムによっては、異なるポート番号を利用するケースもあります。
上記以外に、SSH を使用したファイル転送プロトコル SCP(Secure Copy Protocol)もありますが、今回は割愛させて頂きます。
事前準備
Ubuntu
下記目的で ftpusers というグループを作成します。
- FTP / FTPS で vsftpd.conf - umask を 002 として、対象グループで更新可とする
- SFTP 接続を対象グループで許可して umask を 002 として、対象グループで更新可とする
$ sudo groupadd ftpusers
ftpuser1, ftpuser2 というユーザを ftpusers グループで作成して、パスワードを設定します。
$ sudo useradd -m -g ftpusers ftpuser1
$ sudo passwd sftpuser1
新しい パスワード:
新しい パスワードを再入力してください:
passwd: パスワードは正しく更新されました
$ sudo useradd -m -g ftpusers ftpuser2
$ sudo passwd sftpuser2
新しい パスワード:
新しい パスワードを再入力してください:
passwd: パスワードは正しく更新されました
共用公開ディレクトリとして /var/ftproot を作成して、サンプルとして hoge1.txt を配置します。
$ sudo -i
# mkdir /var/ftproot
# chown root:ftpusers /var/ftproot
# chmod 0775 /var/ftproot
# echo "HogeHoge" > /var/ftproot/hoge1.txt
# chown ftpuser2:ftpusers /var/ftproot/hoge1.txt
# chmod 0664 /var/ftproot/hoge1.txt
$
C# クライアント
FTP / FTPS は、FtpWebRequest を用いた実装が可能でしたが、FtpWebRequest 基底クラス WebRequest が .NET 6 で非推奨となり、FtpWebRequest マニュアルに「新しい開発に FtpWebRequest を使用することはお勧めしません」との記載がされました。
このため、NuGet Gallery | FluentFTP を利用することとします。
SFTP については、NuGet Gallery | SSH.NET を利用します。
PM> NuGet\Install-Package FluentFTP
PM> NuGet\Install-Package SSH.NET
FTP
FTP - Passive をサンプルとします。
Ubuntu
vsftpd(3.0.5)をインストールして、起動されていることを確認します。
$ sudo apt update
$ sudo apt install vsftpd
$ sudo systemctl status vsftpd
vsftpd の設定ファイルで、接続時のルートディレクトリ、Passive モードなどを設定します。
$ sudo vi /etc/vsftpd.conf
# vsftpd.conf 既存項目
listen=YES
listen_ipv6=NO
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=002
use_localtime=YES
connect_from_port_20=NO
ascii_upload_enable=YES
ascii_donwload_enable=YES
chroot_local_user=YES
# 接続可能ユーザ制限
userlist_enable=YES
userlist_deny=NO
userlist_file=/etc/vsftpd.user_list
# 接続時のルートディレクトリ
local_root=/var/ftproot
# 書き込み権限があるとchroot出来ない機能を無効
allow_writeable_chroot=YES
# Passive モード関係
pasv_enable=YES
pasv_min_port=50000
pasv_max_port=50050
接続可能ユーザを vsftpd.user_list に記載します。
$ sudo vi /etc/vsftpd.user_list
ftpuser1
ftpuser2
FireWall の状態を確認、非アクティブでしたので FireWall 設定はスキップします。
※本記事は Ubuntu サーバ構築ではなく、FTP/FTPS/SFTP クライアントが主題のため。
$ sudo ufw status
状態:非アクティブ
vsftpd を再起動します。
$ sudo systemctl restart vsftpd
C# クライアント
FluentFTP は GitHub で公開されていて、CSharpExamples もあります。
以前、利用した時は 37.0 でしたが、RELEASES.md を見ると、40.0 で大幅な改修がされて、設定情報が Config に集約されたようです。
FtpClient(同期)、AsyncFtpClient(非同期)が用意されています。
FtpClient での 接続 / 一覧取得 / ダウンロード / アップロード / 切断 サンプルコードを記載します。
using FluentFTP;
// FtpClient は、IDisposable なので、using もしくは Dispose() が必要
// IP_OR_HOST:接続先、ftpuser1:ユーザ名、PASSWORD:パスワード、21:接続ポート
using (var client = new FluentFTP.FtpClient("IP_OR_HOST", "ftpuser1", "PASSWORD", 21))
{
// FTP - Passive
client.Config.EncryptionMode = FtpEncryptionMode.None;
client.Config.DataConnectionType = FtpDataConnectionType.AutoPassive;
// タイムアウト値 (msec)
client.Config.ConnectTimeout = 5000;
// 必要に応じて下記も設定
// client.Config.DataConnectionConnectTimeout
// client.Config.DataConnectionReadTimeout
// 接続
try
{
client.Connect();
}
catch (System.TimeoutException)
{
// タイムアウト - TODO
}
catch (FluentFTP.Exceptions.FtpAuthenticationException)
{
// 認証情報不正、および、FTP接続許可されていないユーザ - TODO
}
// 公開フォルダの一覧取得
FtpListItem[] items = client.GetListing();
foreach (var item in items)
{
var target = item.Name;
// TODO
}
// ダウンロード
string localPath = System.IO.Path.GetTempPath() + "hoge1.txt";
string remotePath = "hoge1.txt";
FtpStatus sts = client.DownloadFile(localPath, remotePath, FtpLocalExists.Overwrite);
if (sts != FtpStatus.Success)
{
// ERROR - TODO
}
// アップロード
remotePath = "hoge2.txt";
sts = client.UploadFile(localPath, remotePath, FtpRemoteExists.Overwrite);
if (sts != FtpStatus.Success)
{
// ERROR - TODO
}
// 切断
client.Disconnect();
}
FluentFTP.FtpClient、上記以外の主な機能を記載します。
メソッド | 内容 |
---|---|
FileExists | リモートファイル存在確認 |
DeleteFile | リモートファイル削除 |
DirectoryExists | リモートディレクトリ存在確認 |
CreateDirectory | リモートディレクトリ作成 |
Rename | リモートファイル名称変更 |
FTPS
FTPS - Implicit Passive をサンプルとします。
Ubuntu
【vsftpd】FTPをSSL/TLSで暗号化する設定 #ftps - Qiita
vsftpdでchrootを使って複数ユーザでFTPS接続させる
vsftpd.conf(5) — Arch manual pages
前述、FTP サーバとして構築した環境を FTPS - Implicit に再構築します。
まず、自己署名証明書を作成します。
vsftpd.key 作成で、画面表示に従いパスワードを設定します。
$ sudo -i
# cd /etc/ssl/private
# openssl genrsa -aes256 4096 > vsftpd.key
Generating RSA private key, 4096 bit long modulus (2 primes)
........++++
.......................++++
e is 65537 (0x010001)
Enter pass phrase:
Verifying - Enter pass phrase:
vsftpd.conf では、rsa_private_key_file に対するパスワード設定項目がないので、パスワードを削除した後、vsftpd.pem を作成して、ファイルモードを変更します。
# openssl rsa -in vsftpd.key -out vsftpd.key
Enter pass phrase for vsftpd.key:
writing RSA key
# openssl req -new -key vsftpd.key -x509 -days 365 -out vsftpd.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Tokyo
Organization Name (eg, company) [Internet Widgits Pty Ltd]:hoge
Organizational Unit Name (eg, section) []:hoge
Common Name (e.g. server FQDN or YOUR name) []:hoge
Email Address []:hoge@example.com
# chmod 0666 vsftpd.key vsftpd.pem
# exit
$
自己署名証明書なので、 vsftpd.key と vsftpd.pem から、Windows 用の vsftpd.pfx 作成は割愛させて頂きます。
vsftpd の設定ファイルに下記内容を更新します。
$ sudo vi /etc/vsftpd.conf
# vsftpd.conf 既存項目をコメントアウト
#rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
#rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
#ssl_enable=NO
# SSL/TLSを有効化
ssl_enable=YES
# 使用プロトコル
ssl_sslv2=NO
ssl_sslv3=NO
ssl_tlsv1=NO
ssl_tlsv11=NO
ssl_tlsv12=YES
ssl_tlsv13=YES
# 暗号化方式
ssl_ciphers=HIGH
# サーバ証明書、秘密鍵
rsa_cert_file=/etc/ssl/private/vsftpd.pem
rsa_private_key_file=/etc/ssl/private/vsftpd.key
# Implicit モード設定
implicit_ssl=YES
listen_port=990
vsftpd.conf 記載項目でつまずいたら、Ubuntu – Ubuntu パッケージ検索 で vsftpd を検索、vsftpd_3.0.5.orig.tar.gz をダウンロード、解凍した parseconf.c を確認という手もあります。
vsftpd を再起動します。
$ sudo systemctl restart vsftpd
C# クライアント
FTP サンプルコードの一部を更新して、FTPS - Implicit Passive とします。
- ポート番号 990 利用
- FtpEncryptionMode.Implicit 指定
- ValidateCertificate コールバック
// FtpClient は、IDisposable なので、using もしくは Dispose() が必要
// IP_OR_HOST:接続先、ftpuser1:ユーザ名、PASSWORD:パスワード、990:接続ポート
using (var client = new FluentFTP.FtpClient("IP_OR_HOST", "ftpuser1", "PASSWORD", 990))
{
// FTPS - Implicit Passive
client.Config.EncryptionMode = FtpEncryptionMode.Implicit;
client.Config.DataConnectionType = FtpDataConnectionType.AutoPassive;
// 証明書の内容を確認しない(自己署名証明書なので)
client.ValidateCertificate += (control, e) => { e.Accept = true; };
// タイムアウト値 (msec)
client.Config.ConnectTimeout = 5000;
<以降は FTP と同一>
正規の認証局が発行する証明書がある場合には、下記手順で証明書を利用します。
// 証明書
string pfxPath = "PATH_TO_PFX"; // TODO - pfxファイルのパス
var certificate = new X509Certificate2(pfxPath, "PASSWORD"); // TODO - pfxのパスワード
client.Config.ClientCertificates.Add(certificate);
client.ValidateCertificate += (control, e) => {
if (e.PolicyErrors == System.Net.Security.SslPolicyErrors.None)
{
e.Accept = true;
}
else
{
// TODO
e.Accept = false;
}
};
SFTP
パスワード認証と公開鍵認証がありますが、今回は、パスワード認証とします。
Ubuntu
SSHサーバとしてOpen SSH Serverをインストールして、起動されていることを確認します。
$ sudo apt update
$ sudo apt install openssh-server
$ sudo systemctl status ssh
sshd_config を修正します。
$ sudo vi /etc/ssh/sshd_config
# ssh 無効、sftp のみ有効
#Subsystem sftp /usr/lib/openssh/sftp-server
Subsystem sftp internal-sftp
# パスワード認証無効、公開鍵認証有効
PasswordAuthentication no
PubkeyAuthentication yes
# ftpusers グループはパスワード認証有効、/var/ftproot を初期ディレクトリ
Match Group ftpusers
X11Forwarding no
AllowTcpForwarding no
PasswordAuthentication yes
ForceCommand internal-sftp -d /var/ftproot -u 002
ForceCommand internal-sftp 引数は、sftp-server(8) - Linux manual page 参照してください。
FireWall の状態を確認、非アクティブでしたので FireWall 設定はスキップします。
※本記事は Ubuntu サーバ構築ではなく、FTP/FTPS/SFTP クライアントが主題のため。
$ sudo ufw status
状態:非アクティブ
SSHサーバを再起動します。
$ sudo systemctl restart ssh
C# クライアント
GitHub - sshnet/SSH.NET: SSH.NET is a Secure Shel
C#でSFTPでファイル転送やってみる
SftpClient での 接続 / 一覧取得 / ダウンロード / アップロード / 切断 サンプルコードを記載します。
// SftpClient は、IDisposable なので、using もしくは Dispose() が必要
// IP_OR_HOST:接続先、22:接続ポート、ftpuser1:ユーザ名、PASSWORD:パスワード
using (var client = new SftpClient("IP_OR_HOST", 22, "ftpuser1", "PASSWORD"))
{
// タイムアウト値 (TimeSpan)
client.ConnectionInfo.Timeout = TimeSpan.FromSeconds(5);
// 必要に応じて下記も設定
// client.OperationTimeout
// 接続
try
{
client.Connect();
}
catch (Renci.SshNet.Common.SshOperationTimeoutException)
{
// タイムアウト - TODO
}
catch(Renci.SshNet.Common.SshAuthenticationException)
{
// 認証情報不正、および、対象ユーザはパスワード認証無効 - TODO
}
// 公開フォルダの一覧取得
foreach (ISftpFile file in client.ListDirectory("."))
{
var name = file.Name;
if (name.StartsWith("."))
{
// "."、".." などをスキップ
continue;
}
// TODO
var size = file.Length;
}
// ダウンロード
string localPath = System.IO.Path.GetTempPath() + "hoge1.txt";
string remotePath = "hoge1.txt";
using (var fs = System.IO.File.OpenWrite(localPath))
{
client.DownloadFile(remotePath, fs);
}
// アップロード
remotePath = "hoge2.txt";
using (var fs = System.IO.File.OpenRead(localPath))
{
client.UploadFile(fs, remotePath, true); // true:上書き
}
// 切断
client.Disconnect();
}
SftpClient、および、ISftpFile、上記以外の主な機能を記載します。
メソッド | 内容 |
---|---|
SftpClient.CreateDirectory | ディレクトリ作成 |
SftpClient.Exist | 存在確認 |
ISftpFile.Delete | 削除 |
ISftpFile.MoveTo | リネーム |
対象がディレクトリかの確認は、プロパティで可能です。
プロパティ | 内容 |
---|---|
ISftpFile.IsDirectory | ディレクトリ確認 |