PHPからFTPSへ接続するプログラムを実装する機会があったので、その時に学んだことのメモ。
FTPについて
**FTPはFile Transfer Protocolの略で、「ファイル(file)を転送する(transfer)ためのプロトコル(protocol:決まりごと)」**のことをいう。
FTPの通信では、FTPサーバー(インターネット側)とFTPクライアント(コンピュータ側)の間でファイルを転送されている。例えば、インターネットにWEBサイトを公開するために、手元のPCからHTMLファイルをサーバーにアップロードしたり、逆にサーバーにあるファイルをダウンロードしたり。
FTPの通信の仕組み
FTPでは**20番(データコネクション)と21番のポート(制御コネクション)**が使われる。
それぞれのポートには役割があって、データコネクションはデータを転送するために使われるポート。制御コネクションは、データを送るための始まりと終わりの合図のようなものを送るために使われるポート。
FTP通信を行う時には、21番ポート経由で「今からFTPでファイル送るよー、ログインするよー」とサーバーに合図する。サーバーから「認証したから送っていいよー」とOKもらったら、20番ポート経由でファイルのデータを送る。データ送ったらまた21番ポート経由で「終わったよー」って合図する。みたいなイメージ。
FTPのデメリットとして、ユーザー名やパスワード、データを暗号化せずに通信してしまうので、セキュリティ的にあまり好ましくない。そのため、今ではFTPSやSFTPといった通信が使われることのほうが多い。
FTPSについて
FTPS (File Transfer Protocol over SSL/TLS) は、FTPで送受信するデータをTLSまたはSSLで暗号化する通信プロトコル。FTPの延長版みたいなもので、FTPで暗号化していなかったデータを、暗号化して送るようにした仕組み。延長版なので、データ送る流れは上のFTPの時とほぼ同じ。
FTPSには2つの暗号化モードがある。一つは、Explicit(明示的な)モード、もう一つはImplicit(暗黙的な)モード。それぞれのモードで、接続の仕方が異なるのに加えて、制御コネクションとして使われるポートが違う。
Explicitモード
サーバの21/tcpポートに接続した後にクライアントがAUTHコマンドを実行して、使用するプロトコル(SSLまたはTLS)のネゴシエーションをおこない、適合したプロトコルでのハンドシェイク完了後に暗号化された通信がおこなわれる。 つまりExplicitモードの場合、クライアントがAUTHコマンドを実行しなければ通常のFTPとして機能する
FTPS - Wikipedia
通信の仕組みとしては、まず暗号化しない状態で接続した後に、AUTHコマンドという認証コマンドを実行した後に、暗号化通信が始まる。認証コマンドで明示的に暗号化した通信を始めるので、Explicit(明示的な)モード。Explicitモードで使われる制御コネクションは21番。
Implicitモード
サーバの990/tcpポートに接続した直後にSSLまたはTLSによるハンドシェイクがおこなわれる。 Implicitモードで動作するサーバに接続する場合、クライアントはサーバが採用している暗号化プロトコルに適合したFTPSクライアントソフトを使用する必要がある。 また、データ転送チャネル(PORTまたはPASVコマンドで作成されるチャネル)での通信を暗号化する場合、PROTコマンドを用いて保護レベルをP (Private) に設定する必要がある。
FTPS - Wikipedia
それに対してImplicitモードは、最初から暗号化した状態で接続をする。制御コネクションには990番ポートが使われる。
PHPでFTPSに接続する
PHPでFTPSへ接続する際に、ftp_connect
の代わりにftp_ssl_connect
を使えばFTPの時のように接続できるだろうと思っていたら、この関数はExplicitモードにしか対応していないらしい。
PHP: ftp_ssl_connect - Manual
なのでPHPでFTPSへ接続するプログラムを実装する場合、
Explicitモード
-
ftp_ssl_connect
使う - Curlを使う
Implicitモード
- Curl使う
と、暗号化モードによって実装の方法が異なってくる。
[参考] Implicitモードがゆえ、ftp_ssl_connect
が使えない系エラー
php - ftp_ssl_connect with implicit ftp over tls - Stack Overflow
PHPでFTPS接続でなぜかタイムアウトして辛かった - Qiita
PHPでFTPSに接続する:Explicitモード(ftp_ssl_connect)
これはFTPの時とほとんど変わらないのでわかりやすい。downloadディレクトリ内のファイル一覧をダウンロードする例。(エラーハンドリングなどは省略)
public function downloadFiles()
{
$host = 'hostname';
$username = 'ftpuser';
$userpass = 'password';
$remoteDir = 'download';
$localDir = '/home/user/download';
$stream = ftp_ssl_connect($host);
ftp_login($stream, $username, $userpass);
// パッシブモードON
ftp_pasv($stream, true);
// ディレクトリ内のファイル一覧を取得
$remoteFilePaths = ftp_nlist($stream, $remoteDir);
foreach ($remoteFilePaths as $filePath) {
$localFilePath = $localDir . '/' . basename($filePath);
// ファイルをダウンロード
$result = ftp_get($stream, $localFilepath, $filepath, FTP_BINARY);
}
ftp_close($stream);
}
PHPでFTPSに接続する:Explicitモード(Curl)
上と同じ処理をCurlを使ってやってみる。
private $host;
private $username;
private $userpass;
public __construct()
{
$this->host = 'hostname';
$this->username = 'ftpuser';
$this->userpass = 'password';
}
public function downloadFiles()
{
$remoteDir = 'download';
$localDir = '/home/user/download';
// ディレクトリ内のファイル一覧を取得
$remoteFilePaths = $this->_getFiles($remoteDir);
foreach ($remoteFilePaths as $filePath) {
$localFilePath = $localDir . '/' . basename($filePath);
// ファイルをダウンロード
$result = $this->_download($localFilePath, $remoteDir);
}
}
private function _getFiles($dir)
{
$url = 'ftp://ftptest.com/' . $dir . '/'; // ディレクトリ指定の場合、最後の'/'を忘れずに
$options = array(
CURLOPT_URL => $url,
CURLOPT_USERPWD => $this->username . ':' . $this->userpass,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FTP_SSL => CURLFTPSSL_ALL,
CURLOPT_FTPSSLAUTH => CURLFTPAUTH_DEFAULT,
CURLOPT_UPLOAD => false,
CURLOPT_FTPLISTONLY => true,
CURLOPT_RETURNTRANSFER => true,
);
$ch = curl_init();
curl_setopt_array($ch, $options);
$result = curl_exec($ch);
$fileList = explode("\n", trim($result));
curl_close($ch);
return $fileList;
}
private function _download($localFilepath, $dir)
{
$url = 'ftp://ftptest.com/' . $dir . '/' . basename($localFilepath);
$file = fopen($localFilepath, "w");
$options = array(
CURLOPT_URL => $url, // ダウンロードするファイルパス
CURLOPT_USERPWD => $this->username . ':' . $this->userpass,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FTP_SSL => CURLFTPSSL_ALL,
CURLOPT_FTPSSLAUTH => CURLFTPAUTH_DEFAULT,
CURLOPT_UPLOAD => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FILE => $file, // ローカルの出力先ファイル
);
$ch = curl_init();
curl_setopt_array($ch, $options);
$result = curl_exec($ch);
curl_close($ch);
fclose($file);
return $result;
}
CurlでExplicitモードで通信する場合、CURLOPT_URL
でセットするURLはftp://
で始まる値になる。
PHPでFTPSに接続する:Implicitモード(Curl)
一方、Implicitモードで通信する場合、CURLOPT_URL
でセットするURLはftps://
で始まる値になる。他は上と一緒。
ポートは指定しない場合990番で自動的に繋がる気がするけど、一応CURLOPT_PORT
でポートを指定した。
// (略)
private function _getFiles($dir)
{
$url = 'ftps://ftptest.com/' . $dir . '/'; // ディレクトリ指定の場合、最後の'/'を忘れずに
$options = array(
CURLOPT_URL => $url,
CURLOPT_USERPWD => $this->username . ':' . $this->userpass,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FTP_SSL => CURLFTPSSL_ALL,
CURLOPT_FTPSSLAUTH => CURLFTPAUTH_DEFAULT,
CURLOPT_UPLOAD => false,
CURLOPT_FTPLISTONLY => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PORT => 990,
);
// (略)
private function _download($localFilepath, $dir)
{
$url = 'ftps://ftptest.com/' . $dir . '/' . basename($localFilepath);
$file = fopen($localFilepath, "w");
$options = array(
CURLOPT_URL => $url, // ダウンロードするファイルパス
CURLOPT_USERPWD => $this->username . ':' . $this->userpass,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FTP_SSL => CURLFTPSSL_ALL,
CURLOPT_FTPSSLAUTH => CURLFTPAUTH_DEFAULT,
CURLOPT_UPLOAD => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FILE => $file, // ローカルの出力先ファイル
CURLOPT_PORT => 990,
);
// (略)
こちらのライブラリが参考になりました。
https://github.com/nalindaDJ/php-FTP-implicit-ssl-tls
[参考]
PHP curl FTPes w/ explicit TLS/SSL - Stack Overflow
まとめ
PHPでFTPS通信する際に、暗号化モードによって実装の方法を使い分けなければいけないことを知らなかったので、勉強になった。
ローカルでテストする際にvsftpdを使ってサーバーを構築したのですが、設定や認証の仕組みとか、まだ漠然としていて理解が足りていないので、もっと勉強します。