| 前へ << FTP クライアントを作ってみよう (4) | モジュールを使ってみよう (1) >> 次へ |
% ftptrans GET hoge@foo.bar.com:file.tar.gz -password secretとします。すると、指定のファイルを GET し、標準入力に書き出します。
% ftptrans GET hoge@foo.bar.com:remote.file -password secret -in local.fileとします。
% ftptrans ftp://ftp.foo.com/pub/file.tar.gzとします。指定のファイルを GET し、標準入力に書き出します。
-password オプションでパスワードを指定しなかった場合、
Password:というプロンプトを表示し、ユーザにパスワードの入力を促します。 パスワードの入力部分はエコーバックしません (それっぽいでしょ?)。 なお、このときパスワードは標準入力から読んでいるだけなので、
% echo YourPassword > password-file % chmod 600 password-file % cat password-file | ftptrans GET user@host:dir/fileとすることもできます。
% ftptrans GET hoge@foo.bar.com:/dir/ -password secret % ftptrans ftp://ftp.foo.com/pub/dir/などと、ファイルの最後を `/' にすると、 ファイルでなくディレクトリの一覧を取得します。 具体的には、RETR コマンドでなく LIST コマンドを送信するわけです。
% ftptrans GET hoge@foo.bar.com:/home/hoge/public_html/index.html -password secretと、絶対パスで指定しても構いませんが、ログイン直後に カレントディレクトリがホームディレクトリになるなら、
% ftptrans GET hoge@foo.bar.com:public_html/index.html -password secretでもいいです。
241: sub send_command {
242: $verbose && print STDERR "$_[0]";
243: print COMMAND $_[0];
244: }
引数で指定された文字列を、コマンドコネクション用のソケットに出力します。
251: sub read_response {
252: $buf = <COMMAND>;
253: $verbose && print STDERR "--> $buf";
254:
255: # 複数行のレスポンス。
256: if ( $buf =~ m/^(\d\d\d)-/ ){
257: $return_code = $1;
258: while (<COMMAND>){
259: $verbose && print STDERR "--> $_";
260: $buf .= $_;
261: if ( /^$return_code / ){
262: last;
263: }
264: }
265: }
266: return $buf;
267: }
FTP サーバからのレスポンスを受け取ります。
前ページ で説明したように、
複数行のレスポンスが返された場合は、
全てのレスポンスを受け取ります。
この関数は受け取ったレスポンスを返します。 ユーザ認証のときなどは、この戻り値によって 認証が成功したか失敗したのかを判断します。
274: sub client_work {
275: ($SOCK, $host, $port) = @_;
276:
277: # ホスト名を、IP アドレスの構造体に変換
278: unless ( $iaddr = inet_aton($host) ){
279: die "$host は存在しないホストです。$!";
280: }
281:
282: # ポート番号と IP アドレスをまとめて構造体に変換
283: $sock_addr = pack_sockaddr_in($port, $iaddr);
284:
285: # ソケット生成
286: socket($SOCK, PF_INET, SOCK_STREAM, 0)
287: || die "ソケットを生成できません。\n";
288:
289: # 指定のホストの指定の port に接続
290: connect($SOCK, $sock_addr)
291: || die "$hostname のポート $port に接続できません。\n";
292:
293: # ファイルハンドルをバッファリングしない
294: select($SOCK); $|=1; select(STDOUT);
295: }
ソケット名、ホスト、ポート番号を渡すと、
指定のホスト・ポートに接続します。例えば、
client_work(SOCKET,'foo.bar.com',80)
とすると、foo.bar.com の WWW サーバに接続します。
この関数は、
301: sub server_work {
302:
303: # ソケット生成
304: socket(DATA_WAITING, PF_INET, SOCK_STREAM, 0)
305: || die "ソケットを生成できません。\n";
306:
307: # ソケットオプション設定
308: setsockopt(DATA_WAITING, SOL_SOCKET, SO_REUSEADDR, 1)
309: || die "setsockopt でエラーが発生しました。\n";
310:
311: # ソケットにアドレス(=名前)を割り付ける
312: bind(DATA_WAITING, pack_sockaddr_in(0, INADDR_ANY))
313: || die "bind に失敗しました。$!";
314:
315: # OSに、クライアントと接続し待ち行列に入れるよう指示
316: listen(DATA_WAITING, SOMAXCONN)
317: || die "listen に失敗しました。$!";
318:
319: # ローカルホストの IP アドレス・ポート番号を取得
320: $local_sock_addr = getsockname(COMMAND);
321: ($tmp_port, $local_addr) = unpack_sockaddr_in($local_sock_addr);
322: $local_ip = inet_ntoa($local_addr);
323: $local_ip =~ s/\./,/g;
324:
325: # データコネクションのポート番号を取得
326: $local_sock_addr = getsockname(DATA_WAITING);
327: ($local_port, $tmp_addr) = unpack_sockaddr_in($local_sock_addr);
328:
329: return ($local_ip, $local_port);
330: }
ポートを listen します。ポート番号の選択は OS にまかせます。
戻り値として、ローカルホストの IP アドレスとポート番号を返します。
この関数は、Active モードのときに呼ばれ、 FTP サーバからの接続を待つための下準備をします。
6: BEGIN {
7: $0=~s|^\S+ (\S+)|$1|;
8: }
$0 を書き換えます。例えば
% ftptrans GET user@host:file -password SECRETと実行したとき、たまたま他のユーザが ps コマンドを実行すると、
% ps axww 5812 p1 S+ 0:00.18 /usr/local/bin/perl ftptrans.pl get user@host:file -password SECRET (perl5.00404)と、パスワードが丸見えになってしまいます。これではまずいので、 $0 eq 'ftptrans.pl' となるように書き換えるのです。 できるだけ早くこの処理を行いたいため、BEGIN ブロックで囲んでいます。
28: if ( ! @ARGV ){
29: &usage;
30: }
31:
32: for ( $i=0 ; $i<@ARGV ; $i++ ){
33: $_ = $ARGV[$i];
34:
35: # anonymous FTP
36: if ( m|^ftp://(.*)|i ){
37: if ( $1 =~ m|^([a-zA-Z0-9\.\-\_]+)(/?.*)$| ){
38: $hostname = $1;
39: $target_file = $2 || '/';
40: } else {
41: &usage;
42: }
43:
44: $mode = 'get';
45: $username = 'anonymous';
46: # パスワードを作成するために、
47: # ユーザ名とホスト名を取得
48: $local_username = `whoami`; chop $local_username;
49: $local_hostname = `hostname`; chop $local_hostname;
50: $password = "$local_username\@$local_hostname";
51:
52: } elsif ( m/^GET|PUT$/i ){
53: $mode = $_;
54: $mode =~ tr/A-Z/a-z/;
55:
56: $_ = $ARGV[++$i];
57:
58: if ( m|^(.*?)@([a-zA-Z0-9\.\-\_]+):(.*)$| ){
59: ($username, $hostname, $target_file) = ($1, $2, $3);
60: } else {
61: &usage;
62: }
63:
64: } elsif ( m/^-password$/i ){
65: $password = $ARGV[++$i] || &usage;
66:
67: } elsif ( m/^-in$/i ){
68: $infile = $ARGV[++$i] || &usage;
69:
70: } elsif ( m/^-out$/i ){
71: $outfile = $ARGV[++$i] || &usage;
72:
73: } elsif ( m/^-timeout$/i ){
74: $timeout = $ARGV[++$i] || &usage;
75: &usage unless ( $timeout =~ m/^\d+$/ );
76:
77: } elsif ( m/^-passive$/i ){
78: $passive = 1;
79:
80: } elsif ( m/^-v$/i ){
81: $verbose = 1;
82:
83: } else {
84: &usage;
85: }
86: }
@ARGV を先頭から順に見ていきます。
ftp://... という引数が指定されていたら、 anonymous FTP とみなします。 ユーザ名として anonymous、パスワードとしてメールアドレスを使うのですが、 このとき whoami コマンド・hostname コマンドを使います。 hostname コマンドの実行結果が FQDN である保証はなく、あくまで 「メールアドレスらしきもの」を取得することしかできません。
引数に GET か PUT が指定されると、その次に続く文字列を取得します。 後に数値やファイル名などを受けるオプション (-password や -in など) は
64: } elsif ( m/^-password$/i ){
65: $password = $ARGV[++$i] || &usage;
66:
という書き方をしています。もし、-password の後に続く引数がなければ
$ARGV[++$i] が偽になり、`||' の後に書いてある &usage が実行されます。
95: while ( $password eq "" ){
96: print "Password: ";
97: system("stty -echo >/dev/null 2>&1");
98: $password = <STDIN>;
99: chomp $password;
100: system("stty echo >/dev/null 2>&1");
101: print "\n";
102: }
パスワード入力時には、stty -echo でエコーバックを OFF にし、
パスワード入力が終わると stty echo で元に戻します。
% cat password_file | ftptrans ...とすると stty がエラーを出力してしまうので、 stty >/dev/null 2>&1 として、全ての出力 (標準出力と標準エラー出力) を捨てています。
120: if ( $timeout ){
121: $SIG{ALRM} = sub {
122: if ( defined $pid ){
123: kill 'TERM', $pid;
124: }
125: die "TIMEOUT! ($timeout secs)";
126: };
127: alarm($timeout);
128: }
もし $timeout が真なら (コマンドラインで -timeout オプションが
指定されたなら)、シグナルの設定をします。
UNIX にはシグナルという仕組みがあり、任意のプロセスに シグナルを送ることで、非同期の動作をさせることができます。 alarm はそのシグナルを利用した仕組みで、例えば alarm(10) とすると、そのときから 10 秒後に 自分自身に SIGALRM というシグナルが飛んできます。
シグナルが飛んできたときに任意の関数を実行するように設定することができます。 C では signal(3) を使いますが、perl では %SIG ハッシュを使います。
$SIG{ALRM} = \&function_name;
とすると、SIGALRM が飛んできたら 関数 function_name が実行されます。
シグナルが飛んできたときに実行される関数をシグナルハンドラといいます。
このプログラムでは、シグナルハンドラに名前のない関数を使っています。 基本的な仕事は die で終了するだけなのですが、この後 fork で 子プロセスを生成しますので、子プロセスのプロセス番号を入れておく $pid が 定義されていたら、子プロセスに SIGTERM を送って終了させます。
133: $port = getservbyname('ftp', 'tcp');
134: &client_work(COMMAND, $hostname, $port);
135: &read_response;
FTP サーバに接続します。このソケット COMMAND は、
以後コマンドコネクション用として使われます。
接続すると、FTP サーバからレスポンスが返ってくるはずなので、
それを読みます。
137: &send_command("USER $username\r\n");
138: &read_response;
139:
140: &send_command("PASS $password\r\n");
141: $ret = &read_response;
142:
143: if ( $ret =~ m/^5\d\d/ ){
144: print STDERR "ログインできませんでした。\n";
145: exit;
146: }
147:
148: &send_command("TYPE I\r\n");
149: &read_response;
ユーザ認証を行います。
まず USER コマンドを送信し、レスポンスを受け取ります。
次に PASS コマンドを送信し、レスポンスを受け取ります。
PASS コマンドのレスポンスコードが 5xx なら、
ログイン失敗ということなので、終了します。
ログインに成功したら、「TYPE I」を送信し、 バイナリモードにします。 これもレスポンスを受け取ります。
154: if ( ! $passive ){ # Active モード
155: ($local_ip, $local_port) = &server_work;
156:
157: # コマンドコネクションに PORT コマンドを送信
158: &send_command(
159: sprintf("PORT $local_ip,%d,%d\r\n", $local_port/256, $local_port%256)
160: );
161: &read_response;
162:
163: } else { # Passive モード
164: &send_command("PASV\r\n");
165: $ret = &read_response;
166:
167: # レスポンスから情報を取得
168: if ( $ret =~ m/^2\d\d .*\((\d+,\d+,\d+,\d+),(\d+),(\d+)\)/ ){
169: $data_connection_port = $2*256+$3;
170: $data_connection_host = $1;
171: $data_connection_host =~ s/,/\./g;
172:
173: } else {
174: print STDERR $ret;
175: exit;
176: }
177: }
データコネクションを確立します。
Active モードなら (-passive オプションが指定されていなければ)…
Passive モードなら (-passive オプションが指定されていたら)…
$data_connection_port = 40000 (156*256+64) $data_connection_host = "192.168.1.1"というふうに変数に代入します。
185: if ( $mode eq 'put' ){
186: &send_command("STOR $target_file\r\n");
187: } elsif ( $target_file =~ m|/$| ){
188: &send_command("LIST $target_file\r\n");
189: } else {
190: &send_command("RETR $target_file\r\n");
191: }
PUT なら STOR コマンドを、GET なら RETR コマンドを送信します。
ただし、GET でファイル名を「user@host:dir/」などと
最後を `/' とした場合は、ファイル一覧を取得するため LIST コマンドを送信します。
ここでは、まだ FTP サーバからのレスポンスは受け取りません。 例えば存在するファイルを取得しようとして「RETR filename」とすると、
しかし、存在しないファイルを RETR しようとすると、
つまり、ファイルが存在するかどうかがわからないため、 RETR コマンドで送信した後、次のアクションは
そのため、
そこで fork して2つのプロセスが 役割を分担するか、4引数 select を使う必要があります。 このプログラムでは fork を使っています。
196: # 親プロセス
197: if ( $pid = fork() ){
198: $ret = &read_response;
199: # エラー。子プロセスを殺す
200: if ( $ret =~ m/^5/ ){
201: print STDERR $ret;
202: kill 'TERM', $pid;
203: } else {
204: &read_response;
205: }
206: # 子プロセス。データコネクションから読み込む
207: } else {
208: if ( $passive ){
209: &client_work(DATA, $data_connection_host, $data_connection_port);
210: } else {
211: accept(DATA, DATA_WAITING);
212: }
213:
214: if ( $mode eq 'put' ){
215: open(IN, $infile) || die "$infile: $!";
216: while (<IN>){
217: print DATA $_;
218: }
219: close(IN);
220: } else {
221: open(OUT, ">$outfile") || die "$outfile: $!";
222: print OUT <DATA>;
223: close(OUT);
224: }
225: close(DATA);
226: exit;
227: }
fork して子プロセスを生成します。
親プロセスはコマンドコネクション担当で、
FTP サーバからのレスポンスを受け取ります。
子プロセスはデータコネクション担当で、
214: if ( $mode eq 'put' ){
215: open(IN, $infile) || die "$infile: $!";
216: while (<IN>){
217: print DATA $_;
218: }
219: close(IN);
220: } else {
221: open(OUT, ">$outfile") || die "$outfile: $!";
222: print OUT <DATA>;
223: close(OUT);
224: }
子プロセスが担当するファイル転送の部分も説明しておきます。
$mode の値により、ファイルハンドル DATA に書き込むのか (送信)、
読み込むのか (受信) を決めます。
$infile と $outfile の初期値は
19: $infile = '-'; # デフォルトは '-' ('-' は標準入力)
20: $outfile = '-'; # デフォルトは '-' ('>-' は標準出力)
となっています。perl ではファイル名として `-' を指定すると標準入出力を
表しますので、-in オプション、-out オプションで指定しなかった場合は、
| 前へ << FTP クライアントを作ってみよう (4) | モジュールを使ってみよう (1) >> 次へ |
ご意見・ご指摘は Twitter: @68user までお願いします。