;# ;# Copyright (c) 1995-1997 ;# Ikuo Nakagawa. All rights reserved. ;# ;# Redistribution and use in source and binary forms, with or without ;# modification, are permitted provided that the following conditions ;# are met: ;# ;# 1. Redistributions of source code must retain the above copyright ;# notice unmodified, this list of conditions, and the following ;# disclaimer. ;# 2. Redistributions in binary form must reproduce the above copyright ;# notice, this list of conditions and the following disclaimer in the ;# documentation and/or other materials provided with the distribution. ;# ;# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ;# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ;# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ;# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS ;# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, ;# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT ;# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR ;# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, ;# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE ;# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, ;# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ;# ;# $Id: FTP.pm,v 1.28 1999/10/28 10:32:19 ikuo Exp $ ;# ;# Description: ;# FTP.pm - FTP Class definitions ;# - Socket based operations. ;# - Passive mode transfer is supported. ;# - Multiple addresses for a single server are supported. ;# - Multiple sessions are supported. ;# ;# Usage: ;# $dir = "/pub/FreeBSD"; ;# chdir($dir); ;# $ftp = FTP->new( ;# ftp_user => "anonymous", ;# ftp_pass => "ikuo\@intec.co.jp", ;# ftp_gateway => "proxy.isl.intec.co.jp", ;# ftp_server => "localhost" ;# ); ;# $ftp->login || die("Can't login ftp server.\n"); ;# $ftp->cwd($dir) || die("Can't change directory to $dir.\n"); ;# scalar(@array = $ftp->stat(".")) || die("Can't get status.\n"); ;# for $i (@array) { ;# $ftp->get($i) or warn("Can't get $i\n"), last; ;# } ;# $ftp->quit || warn("QUIT failed.\n"); ;# ;# package Fan::FTP; use strict; use vars qw(@ISA $VERSION $LOG $ftp_port $hostname $sockaddr_in $n_session %stats); use Carp; use Socket; use Socket6; use Fan::TCP; use AutoLoader 'AUTOLOAD'; @ISA = qw(Fan::TCP); $VERSION = '0.03'; $LOG = 5 unless defined($LOG); $ftp_port = (getservbyname('ftp', 'tcp'))[2]; chomp($hostname = `hostname`); ;# A special marker for AutoSplit. 1; __END__ ;# Show statistics report. sub stats ($) { my $self = shift; my $name = $self->{ftp_server}; my $p; exists($stats{$name}) && ref($p = $stats{$name}) or return undef; my $cs = $p->{ctrl_sent} + 0; my $cr = $p->{ctrl_recv} + 0; my $ds = $p->{data_sent} + 0; my $dr = $p->{data_recv} + 0; 1 while $cs =~ s/(\d+)(\d\d\d)/$1,$2/; 1 while $cr =~ s/(\d+)(\d\d\d)/$1,$2/; 1 while $ds =~ s/(\d+)(\d\d\d)/$1,$2/; 1 while $dr =~ s/(\d+)(\d\d\d)/$1,$2/; my $len = 0; $len = length($cs) if $len < length($cs); $len = length($cr) if $len < length($cr); $len = length($ds) if $len < length($ds); $len = length($dr) if $len < length($dr); warn("$self status reports (server name: $name)\n"); warn(" sent(ctrl)". '.' x ($len + 3 - length($cs)) . " $cs octets\n"); warn(" recv(ctrl)". '.' x ($len + 3 - length($cr)) . " $cr octets\n"); warn(" sent(data)". '.' x ($len + 3 - length($ds)) . " $ds octets\n"); warn(" recv(data)". '.' x ($len + 3 - length($dr)) . " $dr octets\n"); 1; } ;# destroy an object. sub DESTROY ($) { my $self = shift; $self->quit; my $p = \%{$stats{$self->{ftp_server}}}; $p->{ctrl_sent} += $self->{tcp_sent_octets}; $p->{ctrl_recv} += $self->{tcp_recv_octets}; $self->stats if $self->{ftp_stats}; carp("FTP DESTROYING $self") if $LOG > 5; Fan::TCP::DESTROY($self); } ;# creat a new object. ;# bindaddress undef ;# server nont ;# port 21 ;# user anonymous ;# pass "$user\@$hostname" sub new ($%) { my $this = shift; my $class = ref($this) || $this; my %param = @_; # if we required verbose output, set tcp_debug. $param{tcp_debug} = 1 if $LOG > 5; # bless myself. my $self = bless Fan::TCP->new(%param), $class; ref($self) or return undef; # log message carp("FTP CREATING $self") if $LOG > 5; # some default values. $self->{ftp_retry} = 3 if !exists($self->{ftp_retry}); $self->{ftp_login_retry} = 0 if !exists($self->{ftp_login_retry}); $self->{ftp_login_delay} = 60 if !exists($self->{ftp_login_delay}); # result is myself. $self; } ;# sub passive ($;$) { my $self = shift; if (@_) { my $t = shift; if ($LOG > 6) { warn("FTP $self passive mode " .($t ? "enabled" : "disabled").".\n"); } $self->{ftp_passive} = $t; } $self->{ftp_passive}; } ;# send FTP request. sub putreq ($$) { my $self = shift; my $req = shift; my $i = $self->{ftp_retry}; $self->clearerror || return undef; $self->{lastcode} = 0; $self->{lastmesg} = ''; do { # if we have no connection, try login. unless ($self->handle || $self->login) { $self->{lastmesg} = "can't login to the server"; return undef; } # try send command. unless ($self->putln($req)) { $self->{lastmesg} = $self->error; return undef; } # check result, 421 means connection closed. unless ($self->getres == 421) { return $self->{lastcode}; # this is good! } # or 421 was returned. warn("FTP: server said code=421, try again.\n") if $LOG > 4; $self->quit; } while ($i-- > 0 && sleep(10)) ; warn("FTP: gave up \"$req\".\n") if $LOG > 4; $self->{lastmesg} = "putreq: too many retries"; undef; } ;# recieve FTP response. sub getres ($) { my $self = shift; my $buffer = ''; my $line; while (defined($line = $self->getln)) { $buffer .= "\n" if $buffer ne ''; $buffer .= $line; if ($line =~ /^(\d\d\d) /) { $self->{lastcode} = $1; ($self->{lastmesg} = $') =~ s/\s+$//; $self->{buffer} = $buffer; return $self->{lastcode}; } } $self->{lastcode} = 0; $self->{lastmesg} = "no response from server"; undef; # status may be changed in getln or end-of-file } ;# quit session. sub quit ($) { my $self = shift; $self->cleardataconn; if ($self->handle) { # has connection $self->putreq("QUIT") && $self->getres; # ignore result $self->close; } } ;# sub login ($) { my $self = shift; my $i = $self->{ftp_login_retry}; # log... warn("login: connecting to the server...\n") if $LOG > 4.5; # loop for retries... do { # try login if ($self->do_login) { # success to login warn("login: success.\n") if $LOG > 4.5; return 1; } # check result code if ($self->{lastcode} != 421) { $self->error($self->{lastmesg}); return undef; } } while ($i-- > 0 && sleep($self->{ftp_retry_delay})) ; # retry timed out warn("login: too many login failure, gave up.\n") if $LOG > 4.5; $self->error("too many login failure, ".$self->{lastmesg}); undef; } ;# connecting the server. sub do_login ($) { my $self = shift; # close existing connection first. $self->quit; # force to clear status. $self->clearerror(1); # clear some FTP flags delete($self->{no_size}); delete($self->{no_mdtm}); delete($self->{no_chmod}); delete($self->{no_umask}); delete($self->{no_idle}); # get values. my $user = $self->{ftp_user}; my $pass = $self->{ftp_pass}; my $server = $self->{ftp_server}; my $port = $self->{ftp_port} || $ftp_port; my $bindaddr = $self->{ftp_bindaddr}; my $group = $self->{ftp_group}; my $gpass = $self->{ftp_gpass}; my $passive = $self->{ftp_passive}; my $idle = $self->{ftp_idle}; my $dir = $self->{ftp_directory}; # check default values. if ($user eq '') { $user = 'anonymous'; } if ($pass eq '') { $pass = getpwuid($<).'@'.$hostname; } if ($server eq '') { carp("FTP server not defined"), return undef; } if ($self->{ftp_gateway} ne '') { $user .= '@'.$server; $server = $self->{ftp_gateway}; } # opening connection. $self->do_client( tcp_bindaddr => $bindaddr, tcp_host => $server, tcp_port => $port ) or carp("FTP opening connection failed"), return undef; # we want initial message if ($self->getres != 220) { carp("do_login: can't connect") if $LOG > 5; $self->quit; return undef; } # try to send USER command unless ($self->putln("USER $user")) { carp("do_login: putln ".$self->error) if $LOG > 5; $self->quit; return undef; } # check response for USER command if ($self->getres == 331) { unless ($self->putln("PASS $pass")) { carp("do_login: putln ".$self->error) if $LOG > 5; $self->quit; return undef; } $self->getres; # to catch response code. } # check response for USER or PASS command if ($self->{lastcode} != 230) { carp("do_login: PASS: ".$self->error); $self->quit; return undef; } # if we are required to setup group... unless ($group eq '' || $self->group($group, $gpass)) { carp("do_login: GROUP: ".$self->error); $self->quit; return undef; } # is passive mode prefered? if ($passive) { $self->passive(1); # no error should occur. } # shall we change idle timer? if ($idle > 0 || $idle == -1) { my($i, $maxi) = $self->idle; $i = $idle == -1 || $idle > $maxi ? $maxi : $idle; $self->idle($i); # ignore result. } # we can change initial directory. unless ($dir eq '' || $self->chdir($dir)) { carp("do_login CHDIR: ".$self->error); $self->quit; return undef; } # success code. 1; } ;# ;# open accept socket, and send PORT command to the server. ;# sub port ($) { my $self = shift; $self->clearerror || return undef; $self->cleardataconn; # always success my ($port, $addr, $family) = $self->sockname; my $acpt = Fan::TCP->new(); unless (defined($acpt)) { warn("Fan::TCP->new failed"); return undef; } unless ($acpt->do_server(tcp_family => $family, tcp_bindaddr => $addr)) { warn("Fan::TCP->do_server failed"); return undef; } ($port, $addr, $family) = $acpt->sockname; my $command; if ($family == AF_INET) { unless ($addr =~ tr/./,/ == 3) { warn("ADDRESS=$addr must have just 3 dots"); return undef; } $addr .= sprintf(",%d,%d", ($port >> 8) & 0xff, $port & 0xff); $command = 'PORT'; } else { $addr = "|2|$addr|$port|"; $command = 'EPRT'; } unless ($self->putreq("$command $addr") =~ /^2/) { warn("$command command failed\n") if $LOG > 5; $self->error($self->{lastmesg}); return undef; } $self->{ftp_acpt} = $acpt; # now, ready to accept 1; } ;# sub acpt ($) { my $self = shift; $self->clearerror || return undef; my $data = $self->{ftp_acpt}->new_client; delete($self->{ftp_acpt}); # this cause close. $self->{ftp_data} = $data; 1; } ;# ;# open passive socket ;# sub pasv ($) { my $self = shift; $self->clearerror && $self->cleardataconn || return undef; my $family = ($self->sockname)[2]; my ($a_regexp, $command); if ($family == AF_INET) { $a_regexp = '\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)'; $command = 'PASV'; } else { $a_regexp = '\([^\d\s]{3}(\d+)[^\d\s]\)'; $command = 'EPSV'; } if ($self->putreq($command) !~ /^2/) { $self->error($self->{lastmesg}); return undef; } if ($self->{lastmesg} !~ $a_regexp) { $self->error("pasv: no ADDR,PORT pair found"); return undef; } my $bindaddr = $self->{ftp_bindaddr}; my ($port, $addr); if ($family == AF_INET) { $port = $5 * 256 + $6; $addr = join('.', $1, $2, $3, $4); } else { $port = $1; $addr = ($self->peername)[1]; } my $data = Fan::TCP->new(); $data && $data->do_client( tcp_family => $family, tcp_bindaddr => $bindaddr, tcp_host => $addr, tcp_port => $port ) or $self->error("can't do_client"), return undef; $self->{ftp_data} = $data; 1; } ;# clear data connection. ;# accept socket will be also closed. sub cleardataconn ($) { my $self = shift; # count up if (ref($self->{ftp_data})) { my $x = $self->{ftp_data}; $x->close; my $p = \%{$stats{$self->{ftp_server}}}; $p->{data_sent} += $x->{tcp_sent_octets}; $p->{data_recv} += $x->{tcp_recv_octets}; } # clean up delete($self->{ftp_acpt}); delete($self->{ftp_data}); # always success 1; } ;# sub makedataconn ($$) { my $self = shift; my $command = shift; ($self->passive ? $self->pasv : $self->port) && $self->putreq($command) == 150 && ($self->passive || $self->acpt) ? $self->{ftp_data} : $self->status(0); } ;# sub list ($$) { my $self = shift; my $path = shift; # ASCII mode was required for listing directory. $self->ascii or return undef; my $data = $self->makedataconn("LIST $path"); ref($data) && $data->isa('Fan::TCP') or $self->status(0), return undef; my $line = $data->getln; my @list = $line =~ /^total\s/ ? () : ($line); while (defined($line = $data->getln)) { push(@list, $line); } undef $data; $self->cleardataconn; $self->getres =~ /^2/ or $self->status(0), return undef; join("\n", @list); } ;# sub get ($$;$) { my $self = shift; my ($rx, $lx) = @_; my ($temp, $length); $lx = $rx if $lx eq ''; ($temp = $lx) =~ s%[^/]+$%.in.$&%; local *FILE; unless (CORE::open(FILE, ">$temp")) { return $self->status(0); } my $data = $self->makedataconn("RETR $rx"); unless (ref($data) && $data->isa('Fan::TCP')) { CORE::close(FILE); unlink($temp); return $self->status(0); } local $_; while (defined($_ = $data->getdata(4096))) { (print FILE $_) || last; } undef $data; $self->cleardataconn; CORE::close(FILE); if ($self->getres !~ /^2/) { unlink($temp); return $self->status(0); } unless (CORE::rename($temp, $lx)) { my $e = $!.''; $self->error($e); return undef; } 1; } ;# sub put ($$;$) { my $self = shift; my ($lx, $rx) = @_; my $length; local ($_, *FILE); -f $lx && CORE::open(FILE, $lx) or return undef; $rx = $lx if $rx eq ''; my $data = $self->makedataconn("STOR $rx"); CORE::close(FILE), return $self->status(0) if ref($data) ne 'Fan::TCP'; while (($length = read(FILE, $_, 2048)) > 0) { $data->putdata($_) || last; } undef $data; $self->cleardataconn; CORE::close(FILE); if ($length || $self->getres !~ /^2/) { return $self->status(0); } 1; } ;# sub stat ($;$) { my $self = shift; my $command = "STAT"; $command .= ' '.shift if @_; if ($self->putreq($command) !~ /^2/) { $self->error($self->{lastmesg}); return undef; } local $_ = $self->{buffer}; s/\r//g; # ignore "\r". 1 while s/^\d\d\d-[^\n]*\n//; # skip prepended messages s/^total\s+\d+\n//; # ignore first "total ..." s/(^|\n)2\d\d ([^\n]*$)//; # skip result message # Some FTP servers or FTP gateways close connection by STAT # command. Check it now! my $tmp = $2; if ($tmp =~ /good\s?bye/i) { if ($LOG > 4) { warn("stat: server said \"$self->{lastmesg}\"\n"); warn("stat: connection might be closed.\n"); warn("stat: check your server.\n"); } $self->error("stat: connection might be closed"); return undef; } # or concatinated string will be returned. $_; } ;# CAUTION ;# CHDIR SHOULD SUPPORT "" IN DIRECTORY NAME ;# sub chdir ($$) { my $self = shift; my $dir = shift; if ($self->putreq("CWD $dir") !~ /^2/) { $self->error($self->{lastmesg}); return undef; } 1; } ;# sub cwd ($$) { my $self = shift; $self->chdir(shift); } ;# CAUTION ;# PWD SHOULD SUPPORT "" IN DIRECTORY NAME ;# sub pwd ($) { my $self = shift; if ($self->putreq("PWD") !~ /^2/) { $self->error($self->{lastmesg}); return undef; } if ($self->{lastmesg} !~ /^"(\S+)"/) { $self->error("pwd: no directory name found"); return undef; } $1; } ;# ;# sub type ($;$) { my $self = shift; if (@_) { my $type = shift; if ($self->putreq("TYPE $type") !~ /^2/) { $self->error("type: $self->{lastmesg}"); return undef; } $self->{ftp_type} = $type; } $self->{ftp_type}; } ;# sub image ($) { my $self = shift; $self->type eq 'I' || $self->type('I'); } ;# sub ascii ($) { my $self = shift; $self->type eq 'A' || $self->type('A'); } ;# sub size ($$) { my $self = shift; my $path = shift; if ($self->{no_size}) { $self->error("SIZE: not supported."); return undef; } if ($self->putreq("SIZE $path") !~ /^2/) { $self->{no_size}++ if $self->{lastcode} =~ /^5/; $self->error("size: $self->{lastmesg}"); return undef; } if ($self->{lastmesg} !~ /^\d+$/) { $self->error("size: no SIZE found"); return undef; } return $&; } ;# sub mtime ($$) { my $self = shift; my $path = shift; if ($self->{no_mdtm}){ $self->error("MDTM: not supported."); return undef; } if ($self->putreq("MDTM $path") !~ /^2/) { $self->{no_mdtm}++ if $self->{lastcode} =~ /^5/; $self->error("mdtm: $self->{lastmesg}"); return undef; } if ($self->{lastmesg} !~ /^\d+$/) { $self->error("mdtm: no MTIME found"); return undef; } return $&; } ;# sub unlink ($$) { my $self = shift; my $path = shift; if ($self->putreq("DELE $path") !~ /^2/) { $self->error("dele: $self->{lastmesg}"); return undef; } 1; } ;# sub mkdir ($$) { my $self = shift; my $path = shift; if ($self->putreq("MKD $path") !~ /^2/) { $self->error("MKD: $self->{lastmesg}"); return undef; } 1; } ;# sub rmdir ($$) { my $self = shift; my $path = shift; if ($self->putreq("RMD $path") !~ /^2/) { $self->error("RMD: $self->{lastmesg}"); return undef; } 1; } ;# sub rename ($$$) { my $self = shift; my $old = shift; my $new = shift; if ($self->putreq("RNFR $old") != 350) { $self->error("RNFR: $self->{lastmesg}"); return undef; } if ($self->putreq("RNTO $new") !~ /^2/) { $self->error("RNTO: $self->{lastmesg}"); return undef; } 1; } ;# sub chmod ($$$) { my $self = shift; my $mode = shift; # number, not a octal string my $file = shift; my $perm = sprintf("%o", $mode); if ($self->{no_chmod}) { $self->error("SITE CHMOD: not supported."); return undef; } if ($self->putreq("SITE CHMOD $perm $file") != 200) { $self->{no_chmod}++ if $self->{lastcode} =~ /^5/; $self->error("SITE CHMOD: $self->{lastmesg}"); return undef; } 1; } ;# sub umask ($$) { my $self = shift; my $umask = shift; if ($self->{no_umask}){ $self->error("SITE UMASK: not supported."); return undef; } if ($self->putreq("SITE UMASK $umask") !~ /^2/) { $self->{no_umask}++ if $self->{lastcode} =~ /^5/; $self->error("UMASK: $self->{lastmesg}"); return undef; } 1; } ;# sub group ($$$) { my $self = shift; my $group = shift; my $gpass = shift; if ($self->putreq("SITE GROUP $group") !~ /^2/) { $self->error("GROUP: $self->{lastmesg}"); return undef; } if ($self->putreq("SITE GPASS $gpass") !~ /^2/) { $self->error("GPASS: $self->{lastmesg}"); return undef; } 1; } ;# sub idle { my $self = shift; if ($self->{no_idle}) { $self->error("SITE IDLE: not supported."); return wantarray ? () : undef; } if (@_) { if ($self->putreq("SITE IDLE $_[$[]") !~ /^2/) { $self->{no_idle}++ if $self->{lastcode} =~ /^5/; $self->error("IDLE: $self->{lastmesg}"); return undef; } if ($self->{lastmesg} !~ /\d+/) { $self->error("IDLE: no IDLE timer found"); return undef; } return $&; } else { if ($self->putreq("SITE IDLE") !~ /^2/) { $self->{no_idle}++ if $self->{lastcode} =~ /^5/; $self->error("IDLE: $self->{lastmesg}"); return undef; } if ($self->{lastmesg} !~ /(\d+)\D+(\d+)/) { $self->error("IDLE: no IDLE,MAXIDLE timers found"); return undef; } return wantarray ? ($1, $2) : $1; } } ;# end of Fan::FTP module