ASH | サーバ | セキュリティ | Linux | FreeBSD | DB | Web | CGI | Perl | Java | XML | プログラム | ネットワーク | 標準 | Tips集

Perlでファイル転送(FTP)

 このページでは、Perlを使って、簡単なFTPコマンドを作成しています。

概要

 FTPプロトコルとは、FileTransferProtocolの略で、ファイル転送するときに利用しているプロトコルです。 FTPプロトコルは、TCP/IP上のプロトコルで、通常21番ポートと20番ポートを使ってアクセスします。 21番ポートは、最初に接続するポートで、FTPのコマンド転送用に利用します。 これに対して20番ポートは、データ転送用に利用しています。 これらのポート番号は、サーバの設定で変更することもできます。
 FTPに関する詳細な定義は、以下のRFCで定義されています。

 FTPは、データ転送用のポートに接続する場合に、FTPサーバ側からFTPクライアントに接続します。 このモードをActiveモードと呼んでいます。 しかし、ファイアウォールなどがある場合は、Activeモードでは転送できません。

●ActiveモードでのFTPプロトコルの例

FTPクライアント                        FTPサーバ
USER           ->  コマンドポート  ->
PASS           ->  コマンドポート  ->
SYST           ->  コマンドポート  ->
PORT $port     ->  コマンドポート  ->
LIST           ->  コマンドポート  ->
               <-   データポート   <-  connect $port
               <-   データポート   <-  ファイル一覧データ
QUIT           ->  コマンドポート  ->

 そのため、FTPには、Passiveモードがあります。 Passiveモードでは、PASVコマンドで受信したサーバのポートに対して、クライアント側から接続します。 Activeモードに対して、connectの向きが逆になります。

●PassiveモードでのFTPプロトコルの例

FTPクライアント                        FTPサーバ
USER           ->  コマンドポート  ->
PASS           ->  コマンドポート  ->
SYST           ->  コマンドポート  ->
PASV           ->  コマンドポート  ->
               <-  コマンドポート  <-  $port
LIST           ->  コマンドポート  ->
connect $port  ->   データポート   ->
               <-   データポート   <-  ファイル一覧データ
QUIT           ->  コマンドポート  ->

FTPクライアントのソース作成

 Perlを使って、PassiveモードでFTP転送するソースをSocketモジュールを使って作成してみます。 Socketモジュールは標準モジュールですから、特にインストールする必要はありません。 モジュールの詳細は、perldoc Socketを参照してください。
 ftp.plコマンドの使い方は、基本的に、UNIXのFTPコマンドの-dオプションに準拠しています。 パラメータは、ホスト名とポート番号を受け付けます。 省略した場合は、ホスト名(localhost)と、ポート番号(21)となります。
 ユーザ名とパスワードによるユーザ認証した後、systコマンドを実行してコマンド待ちになります。 コマンドは、ls, cd, pwd, binary, ascii, get, put, bye(quit)コマンドを受け付けます。 !による、コマンドエスケープもできます。 lcdコマンドは、FTPプロトコルは使わないので実装していませんが、あると便利ですね。
 ソースを公開していますので、その他のコマンドも、簡単に作成できるでしょう。 また、strictモードで作成していますし、下位のSocketモジュールを使っていますので、C言語への移植も簡単にできるでしょう。
 性能や信頼性など、実用性を考えるなら、Net::FTPモジュールを使うべきだと思います。

ftp.pl
#!/usr/bin/perl
# @(#)ftp.pl Copyright (C)2001 ASH. http://ash.jp/
#
# 簡易ファイル転送スクリプト(FTP)
#   Usage: ftp.pl [host [port]]
#

use strict;
use Socket;
use FileHandle;

require 'ftp_sub.pl'; # FTPプロトコル処理関数
require 'ftp_cmd.pl'; # FTPコマンド処理関数

&main();

#
# FTPプログラムメイン処理
#
sub main {
  my ($host, $port, $user, $pass);
  my ($in, $cmd, @opt, $buf);
  my ($comd);

  ($host, $port) = @ARGV;
  if ($host eq '') { $host = 'localhost'; }
  if ($port eq '') { $port = getservbyname('ftp', 'tcp'); }

  $comd = new FileHandle;

  # FTP接続開始
  &ftp_open($comd, $host, $port);
  &ftp_recv($comd);

  # ユーザ認証
  print "Username: "; chomp($user=<STDIN>);
  if ($user eq '') { $user = 'ftp'; }
  &ftp_send($comd, 'USER', $user);
  $buf = &ftp_recv($comd);

  # パスワード認証
  system("stty -echo >/dev/null 2>&1");
  print "Password: "; chomp($pass=<STDIN>);
  system("stty echo >/dev/null 2>&1");
  print "\n";
  &ftp_send($comd, 'PASS', $pass);
  $buf = &ftp_recv($comd);
  if ($buf =~ /^5/) { exit; }

  # SYSTコマンド送信
  &ftp_send($comd, 'SYST');
  $buf = &ftp_recv($comd);

  while (-1) {
    # FTPコマンド入力
    print "ftp> "; chomp($in=<STDIN>);
    ($cmd, @opt) = split(' ', $in);

    # コマンド呼出し処理
    if ($in =~ /^ls/i){ # ls files
      &cmd_ls($comd, $cmd, @opt);

    } elsif ($in =~ /^cd/i) { # cd directory
      # CWDコマンド送信
      &ftp_send($comd, 'CWD', @opt);
      $buf = &ftp_recv($comd);

    } elsif ($in =~ /^pw/i) { # pwd
      # PWDコマンド送信
      &ftp_send($comd, 'PWD');
      $buf = &ftp_recv($comd);

    } elsif ($in =~ /^bi/i) { # binary
      # TYPEコマンド送信
      &ftp_send($comd, 'TYPE', 'I');
      $buf = &ftp_recv($comd);

    } elsif ($in =~ /^as/i) { # ascii
      # TYPEコマンド送信
      &ftp_send($comd, 'TYPE', 'A');
      $buf = &ftp_recv($comd);

    } elsif ($in =~ /^get/i) { # get file
      &cmd_get($comd, $cmd, @opt);

    } elsif ($in =~ /^put/i) { # put file
      &cmd_put($comd, $cmd, @opt);

    } elsif ($in =~ /^!(.*)/) { # escape shell
      system("$1");

    } elsif ($in =~ /^bye|^quit/i) { # quit
      last;

    } else {
      print "? Invalid command\n";
    }
  }

  # FTP接続終了
  &ftp_send($comd, 'QUIT');
  &ftp_recv($comd);
  &ftp_close($comd);
}

 FTPプロトコルを簡単に扱うため、以下の関数を作成しています。

関数機能
&ftp_open($sock, $host, $port)FTPソケットオープン
&ftp_close($sock)FTPソケットクローズ
&ftp_send($sock, $cmd, @opt)FTPコマンド送信
&ftp_recv($sock)FTPレスポンス受信と表示
($host, $port) = &get_pasv_port($buf)PASVコマンドによるポート番号の取得

 ftp_openと、ftp_close関数では、socket関数でソケットを生成し、connect関数で接続した後、ソケットハンドルを経由して、入出力を行います。 ソケットもファイルと同様に扱うことができますので、print関数や、<>に対して、ファイルハンドルの代わりにソケット識別子が使えます。 また、Passiveモードを使っていますので、listenなどのサーバとしての処理は組み込んでいません。 Passiveモードですから、get_pasv_port関数を使って、PASVコマンドによるポート番号の取得を行っています。
 基本的な流れは、ネットワークプログラミングと同じです。 注意する点としては、ソケットをautoflushモードにする必要がある点です。 autoflushモードにしないとデータがバッファリングされてしまい、応答が返って来なくなります。 autoflushモードにするために、FileHandleモジュールを使っています。

 ftp_sendでは、FTPコマンドを送信します。 結果は、ftp_recvで、レスポンスを受信します。
 FTPプロトコルでは、レスポンスの先頭は、3桁の数字となっています。

 また、数字の後に'-'が付いている場合には、複数のレスポンスが返却されます。 ただし、ftp_recv関数では、レスポンスを標準出力に出力しているため、最後のレスポンスしか返却しません。

ftp_sub.pl
#
# FTPプロトコル処理関数
#

# FTP接続開始
sub ftp_open {
  my ($sock, $host, $port) = @_;
  my ($ip, $sockaddr);

  # ソケット生成
  $ip = inet_aton($host) || die "host($host) not found.\n";
  $sockaddr = pack_sockaddr_in($port, $ip);
  socket($sock, PF_INET, SOCK_STREAM, 0) || die "socket error.\n";

  # 接続
  connect($sock, $sockaddr) || die "connect $host $port error.\n";
  autoflush $sock (1);
}

# FTP接続終了
sub ftp_close {
  my ($sock) = @_;

  close($sock);
}

# FTPコマンド送信
sub ftp_send {
  my ($sock, $cmd, @opt) = @_;

  if ($#opt) {
    print "---> $cmd\n";
    print $sock "$cmd\n";
  } else {
    if ($cmd eq 'PASS') {
      print "---> $cmd XXXX\n";
    } else {
      print "---> $cmd @opt\n";
    }
    print $sock "$cmd @opt\n";
  }
}

# FTPレスポンス受信
sub ftp_recv {
  my ($sock) = @_;
  my ($buf, $rc, $cont, $msg);

  $rc = 0;
  while (chomp($buf=<$sock>)) {
    print "$buf\n";
    $buf =~ /^(\d\d\d)([ |-])(.*)/;
    $rc = $1; $cont = $2; $msg = $3;
    if ($cont ne '-') { last; }
  }
  return($buf);
}

sub get_pasv_port {
  my ($buf) = @_;
  my ($host, $port);

  $buf =~ /^2\d\d .*\((\d+,\d+,\d+,\d+),(\d+),(\d+)\)/;
  $host = $1;
  $port = $2 * 256 + $3;
  $host =~ s/,/\./g;

  return($host, $port);
}
1;

 FTPコマンド関数は、入力されたコマンド毎に呼ばれる関数です。 パラメータ形式は、ソケット識別子、コマンド名、オプションの配列で統一されています。

cmd_コマンド名($sock, $cmd, @opt)

 FTPコマンドは、大きく分けて、コマンドを送信してレスポンスを受け取るだけのコマンドと、データポートを使ってデータ転送するコマンドがあります。 ls、get、putなどは、データポートを使って、データ転送します。 データポートを使う場合は、子プロセスを起動し、親プロセスで子プロセスの状態を監視します。
 PASVコマンドでは、IPアドレスは、','で区切られて返却されます。 また、ポート番号は、2バイトに分けられて返却されます。

ftp_cmd.pl
#
# FTPコマンド処理関数
#

# lsコマンド処理
sub cmd_ls {
  my ($sock, $cmd, @opt) = @_;
  my ($host, $port, $buf, $pid);
  my ($data);

  # PASVモードに変更
  &ftp_send($sock, 'PASV');
  $buf = &ftp_recv($sock);
  ($host, $port) = &get_pasv_port($buf);

  # LISTコマンド送信
  &ftp_send($sock, 'LIST', @opt);
  if ($pid = fork()) { # 親
    $buf = &ftp_recv($sock);
    if ($buf =~ /^5/) { kill 'TERM', $pid; return; }
    wait;
    $buf = &ftp_recv($sock);

  } else { # 子
    $data = new FileHandle;
    &ftp_open($data, $host, $port);
    while (chomp($buf=<$data>)) {
      print "$buf\n";
    }
    &ftp_close($data);
    exit(0);
  }
}

# getコマンド処理
sub cmd_get {
  my ($sock, $cmd, @opt) = @_;
  my ($host, $port, $buf, $pid, $file);
  my ($data);

  # PASVモードに変更
  &ftp_send($sock, 'PASV');
  $buf = &ftp_recv($sock);
  ($host, $port) = &get_pasv_port($buf);

  # ファイル名取得
  ($file) = @opt;

  # RETRコマンド送信
  &ftp_send($sock, 'RETR', $file);
  if ($pid = fork()) { # 親
    $buf = &ftp_recv($sock);
    if ($buf =~ /^5/) { kill 'TERM', $pid; return; }
    wait;
    $buf = &ftp_recv($sock);

  } else { # 子
    $data = new FileHandle;
    &ftp_open($data, $host, $port);

    open (FILE, ">$file") || exit(1);
    while (<$data>) {
      print FILE $_;
    }
    close (FILE);

    &ftp_close($data);
    exit(0);
  }
}

# putコマンド処理
sub cmd_put {
  my ($sock, $cmd, @opt) = @_;
  my ($host, $port, $buf, $pid, $file);
  my ($data);

  # PASVモードに変更
  &ftp_send($sock, 'PASV');
  $buf = &ftp_recv($sock);
  ($host, $port) = &get_pasv_port($buf);

  # ファイル名取得
  ($file) = @opt;

  # STORコマンド送信
  &ftp_send($sock, 'STOR', $file);
  if ($pid = fork()) { # 親
    $buf = &ftp_recv($sock);
    if ($buf =~ /^5/) { kill 'TERM', $pid; return; }
    wait;
    $buf = &ftp_recv($sock);

  } else { # 子
    $data = new FileHandle;
    &ftp_open($data, $host, $port);

    open (FILE, "$file") || exit(1);
    while (<FILE>) {
      print $data $_;
    }
    close (FILE);

    &ftp_close($data);
    exit(0);
  }
}
1;

FTPクライアントを実行

 実際に、PerlでFTPプロトコルを使って、ファイルの一覧と、ファイルを取得してみます。

Unix# ftp.pl
220 ftp.ash.jp FTP server (Version wu-2.4.2-VR16(1) Tue Aug 24 01:16:16 JST 1999) ready.
Username: ftp(anonymousFTPのユーザ名)
USER ftp
331 Guest login ok, send your complete e-mail address as password.
Password: joe@ash.jp(慣習として自分のメールアドレスを入力)
PASS XXXX
230 Guest login ok, access restrictions apply.
SYST
215 UNIX Type: L8

ftp> cd etc
CWD etc
250 CWD command successful.

ftp> ls
PASV
227 Entering Passive Mode (127,0,0,1,33,202)
LIST
150 Opening ASCII mode data connection for /bin/ls.
total 83
-rw-r--r--  1 14  5     23 Nov 17 03:11 ftpmotd
-rw-r--r--  1 14  5     37 Nov 17 03:10 group
-rw-------  1 14  5    216 Nov 17 03:06 master.passwd
-rw-r--r--  1 14  5  40960 Nov 17 03:07 pwd.db
-rw-------  1 14  5  40960 Nov 17 03:07 spwd.db
226 Transfer complete.

ftp> get group
PASV
227 Entering Passive Mode (127,0,0,1,33,203)
RETR group
150 Opening ASCII mode data connection for 'group' (37 bytes).
226 Transfer complete.

ftp> bye
QUIT
221 Goodbye.


Copyright (C)1995-2002 ASH multimedia lab.
mail : info@ash.jp