このページでは、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 -> コマンドポート -> |
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; |
実際に、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. |