このページでは、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. |