[PHP]FTPS接続：暗号化モード(Explicit/Implicit)による実装方法の違い

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](https://ja.wikipedia.org/wiki/FTPS)

通信の仕組みとしては、まず暗号化しない状態で接続した後に、AUTHコマンドという認証コマンドを実行した後に、暗号化通信が始まる。認証コマンドで明示的に暗号化した通信を始めるので、Explicit（明示的な）モード。Explicitモードで使われる制御コネクションは21番。

### Implicitモード

>サーバの990/tcpポートに接続した直後にSSLまたはTLSによるハンドシェイクがおこなわれる。 Implicitモードで動作するサーバに接続する場合、クライアントはサーバが採用している暗号化プロトコルに適合したFTPSクライアントソフトを使用する必要がある。 また、データ転送チャネル（PORTまたはPASVコマンドで作成されるチャネル）での通信を暗号化する場合、PROTコマンドを用いて保護レベルをP (Private) に設定する必要がある。
[FTPS - Wikipedia](https://ja.wikipedia.org/wiki/FTPS)

それに対してImplicitモードは、最初から暗号化した状態で接続をする。制御コネクションには990番ポートが使われる。

## PHPでFTPSに接続する

PHPでFTPSへ接続する際に、`ftp_connect`の代わりに`ftp_ssl_connect`を使えばFTPの時のように接続できるだろうと思っていたら、この関数はExplicitモードにしか対応していないらしい。
[PHP: ftp_ssl_connect - Manual](https://www.php.net/manual/ja/function.ftp-ssl-connect.php)

なのでPHPでFTPSへ接続するプログラムを実装する場合、

#### Explicitモード
- `ftp_ssl_connect`使う
- Curlを使う

#### Implicitモード
- Curl使う

と、暗号化モードによって実装の方法が異なってくる。

[参考] Implicitモードがゆえ、`ftp_ssl_connect`が使えない系エラー
[php - ftp_ssl_connect with implicit ftp over tls - Stack Overflow](https://stackoverflow.com/questions/6589730/ftp-ssl-connect-with-implicit-ftp-over-tls)
[PHPでFTPS接続でなぜかタイムアウトして辛かった - Qiita](https://qiita.com/ir-yk/items/5c9277bc6a657d59e239)

### PHPでFTPSに接続する：Explicitモード(ftp_ssl_connect)

これはFTPの時とほとんど変わらないのでわかりやすい。downloadディレクトリ内のファイル一覧をダウンロードする例。（エラーハンドリングなどは省略）

```php
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を使ってやってみる。

```php
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`でポートを指定した。

```php
// (略)

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](https://stackoverflow.com/questions/32491790/php-curl-ftpes-w-explicit-tls-ssl
)

## まとめ

PHPでFTPS通信する際に、暗号化モードによって実装の方法を使い分けなければいけないことを知らなかったので、勉強になった。

ローカルでテストする際にvsftpdを使ってサーバーを構築したのですが、設定や認証の仕組みとか、まだ漠然としていて理解が足りていないので、もっと勉強します。