Help us understand the problem. What is going on with this article?

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

通信の仕組みとしては、まず暗号化しない状態で接続した後に、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を使ってサーバーを構築したのですが、設定や認証の仕組みとか、まだ漠然としていて理解が足りていないので、もっと勉強します。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした