LWP::Conn::FTP
package LWP::Conn::FTP;
# $Id: FTP.pm,v 1.17 1998/07/05 22:20:51 aas Exp $
# Copyright 1997-1998 Gisle Aas.
#
# This library is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
use IO::Socket ();
use LWP::MainLoop qw(mainloop);
use strict;
use vars qw($DEBUG @ISA);
@ISA=qw(IO::Socket::INET);
sub new
{
my($class, %cnf) = @_;
my $mgr = delete $cnf{ManagedBy} ||
Carp::croak("'ManagedBy' is mandatory");
my $host = delete $cnf{Host} || delete $cnf{PeerAddr} ||
Carp::croak("'Host' is mandatory for FTP");
my $port;
$port = $1 if $host =~ s/:(\d+)//;
$port = delete $cnf{Port} || delete $cnf{PeerPort} || $port || 21;
my $timeout = delete $cnf{Timeout} || 5*60;
my $idle_timeout = delete $cnf{IdleTimeout} || $timeout;
my $conn_timeout = delete $cnf{ConnTimeout} || $timeout;
my $req_limit = delete $cnf{ReqLimit} || 4;
if (%cnf && $^W) {
for (keys %cnf) {
warn "Unknown LWP::Conn::FTP->new attribute '$_' ignored\n";
}
}
return LWP::Conn::_Connect->new($host, $port, $conn_timeout,
"LWP::Conn::FTP::Start",
[$mgr, $req_limit, $timeout, $idle_timeout]
);
}
sub state
{
my($self, $state) = @_;
print "STATE: $state\n" if $DEBUG && $DEBUG > 1;
my $class = "LWP::Conn::FTP::$state";
bless $self, $class;
}
sub inactive
{
my $self = shift;
$self->_error("Timeout");
}
sub error
{
my($self, $msg) = @_;
$self->_error("$msg: " . $self->message);
}
sub _error
{
my($self, $msg) = @_;
chomp($msg);
print STDERR "ERROR: $msg\n";
mainloop->forget($self);
$self->close;
if (my $data = delete *$self->{'lwp_data'}) {
$data->close;
}
*$self->{'lwp_mgr'}->connection_closed($self);
if (my $req = delete *$self->{'lwp_req'}) {
$req->give_response(590, $msg);
}
}
sub readable
{
my $self = shift;
my $buf = \ *$self->{'lwp_rbuf'};
my $n = sysread($self, $$buf, 2048, length($$buf));
if (!defined($n)) {
$self->_error("Bad read: $!");
} elsif ($n == 0) {
$self->_error("EOF");
} else {
$self->check_rbuf;
}
}
sub check_rbuf
{
my $self = shift;
my $buf = \ *$self->{'lwp_rbuf'};
if (length $$buf) {
my @lines = split(/\015?\012/, $$buf);
if (substr($$buf, -1, 1) ne "\012") {
# the last line was not complete
*$self->{'lwp_rbuf'} = pop @lines;
} else {
*$self->{'lwp_rbuf'} = "";
}
push(@{*$self->{'lwp_lines'}}, @lines);
}
$self->parse_response;
}
sub parse_response
{
my $self = shift;
my($code, $more, @res);
while (@{*$self->{'lwp_lines'}}) {
my $line = shift @{*$self->{'lwp_lines'}};
if ($line =~ /^(\d\d\d)([\-\s])/) {
$more = $2 eq "-";
if ($code) {
$more++ if $code ne $1;
} else {
$code = $1;
}
} elsif (!$code) {
push(@res, $line);
return $self->reponse_error(join("\n", @res));
}
push(@res, $line);
last unless $more;
}
if ($more) {
unshift(@{*$self->{'lwp_lines'}}, @res);
} elsif ($code) {
*$self->{'lwp_response_code'} = $code;
*$self->{'lwp_response_mess'} = \@res;
print STDERR " <===\t", join("\n\t", @res), "\n" if $DEBUG;
$self->response(substr($code, 0, 1), $code);
$self->parse_response;
}
}
sub response_error
{
my($self, $bad_response) = @_;
print STDERR "FTP: Bad server response '$bad_response' ignored\n";
}
sub code
{
my $self = shift;
*$self->{'lwp_response_code'} || "000";
}
sub message
{
my $self = shift;
wantarray ? @{*$self->{'lwp_response_mess'}}
: join("\n", @{*$self->{'lwp_response_mess'}}, "");
}
sub response
{
my($self, $r, $code, $mess) = @_;
print STDERR "Response $code ignored\n";
}
sub send_cmd
{
my($self, $cmd, $next_state) = @_;
if ($DEBUG) {
my $out = $cmd;
$out =~ s/^(PASS\s+)(.+)/$1 . "*" x length($2)/e;
print STDERR "===>\t$out\n";
}
$cmd .= "\015\012";
# XXX should really wait for the socket to become writable, but
# it is very unlikely that it should not be that.
my $n = $self->syswrite($cmd, length($cmd));
$self->_error("Can't syswrite ($n)") if !$n || $n != length($cmd);
$self->state($next_state) if $next_state;
}
sub activate
{
}
sub stop
{
my $self = shift;
$self->_error("STOP");
}
sub login_info
{
my($self, $req) = @_;
my $url = $req->url;
my($user,$pass) = $req->authorization_basic;
$user ||= $url->user || "anonymous";
$pass ||= $url->password || "nobody@";
my $acct = $req->header("Account") || "home";
($user, $pass, $acct);
}
sub give_response
{
my($self, $code, $mess, $more) = @_;
my $req = delete *$self->{'lwp_req'};
if (ref($more) || !defined($more)) {
$more->{Server} = *$self->{'lwp_server_product'};
}
$req->give_response($code, $mess, $more);
$self->activate;
}
package LWP::Conn::FTP::Start;
use base 'LWP::Conn::FTP';
use LWP::MainLoop qw(mainloop);
sub connected
{
my($self, $param) = @_;
@{*$self}{'lwp_mgr', 'lwp_rlim',
'lwp_timeout', 'lwp_idle_timeout'} = @$param;
*$self->{'lwp_type'} = "";
*$self->{'lwp_rbuf'} = "";
mainloop->readable($self);
mainloop->timeout($self, *$self->{'lwp_idle_timeout'});
$self->activate;
}
sub connect_failed
{
my($self, $msg, $param) = @_;
my $mgr = shift @$param;
while (my $req = $mgr->get_request($self)) {
$req->give_response(590, $msg);
}
$mgr->connection_closed($self);
}
sub response
{
my($self, $r) = @_;
$self->error("Bad welcome") unless $r eq "2";
my $mess = $self->message;
*$self->{'lwp_greeting'} = $mess;
# Try to make it into a HTTP product token
$mess =~ s/^\d+\s+//;
$mess =~ s/^[\w\.]+\s+//; # host name
$mess =~ s/\s+ready\.?\s+$//;
$mess =~ s/\s+\(Version\s+/\// && $mess =~ s/\)$//;
*$self->{'lwp_server_product'} = $mess;
$self->send_cmd("SYST" => "Syst");
}
package LWP::Conn::FTP::Syst;
use base 'LWP::Conn::FTP';
sub response
{
my($self, $r) = @_;
if ($r eq "2") {
chomp(my $mess = $self->message);
*$self->{'lwp_syst'} = $mess;
$mess =~ s/^\d+\s+//;
*$self->{'lwp_unix'}++ if $mess =~ /\bUNIX\b/i;
*$self->{'lwp_server_product'} .= " ($mess)";
}
*$self->{'lwp_idle'}++;
$self->state("Outlogged");
$self->activate;
}
package LWP::Conn::FTP::Outlogged;
use base 'LWP::Conn::FTP';
sub activate
{
my $self = shift;
my $req = *$self->{'lwp_mgr'}->get_request;
if (!$req) {
*$self->{'lwp_idle'}++;
*$self->{'lwp_mgr'}->connection_idle($self);
return;
} elsif (*$self->{'lwp_idle'}) {
*$self->{'lwp_idle'} = 0;
*$self->{'lwp_mgr'}->connection_active($self);
}
*$self->{'lwp_req'} = $req;
(*$self->{'lwp_user'}, *$self->{'lwp_pass'}, *$self->{'lwp_acct'})
= $self->login_info($req);
$self->send_cmd("USER " . *$self->{'lwp_user'} => "User");
}
package LWP::Conn::FTP::User;
use base 'LWP::Conn::FTP';
sub response
{
my($self, $r) = @_;
if ($r eq "3") {
my $pass = *$self->{'lwp_pass'};
$self->send_cmd("PASS $pass" => "Pass");
} elsif ($r eq "2") {
$self->login_complete;
} else {
$self->cant_login;
}
}
sub login_complete
{
my $self = shift;
$self->state("Ready");
$self->activate;
}
sub cant_login
{
my $self = shift;
my $mess = $self->message;
$mess =~ s/^\d+\s+//;
chomp($mess);
$self->state("Outlogged");
$self->give_response(401, $mess,
{"WWW-Authenticate" => 'Basic realm="FTP"',
});
$self->activate;
}
package LWP::Conn::FTP::Pass;
use base 'LWP::Conn::FTP::User';
sub response
{
my($self, $r) = @_;
if ($r eq "3") {
my $acct = *$self->{'lwp_acct'};
$self->send_cmd("ACCT $acct" => "Acct");
} elsif ($r eq "2") {
$self->login_complete;
} else {
$self->cant_login;
}
}
package LWP::Conn::FTP::Acct;
use base 'LWP::Conn::FTP::User';
sub response
{
my($self, $r) = @_;
if ($r eq "2") {
$self->login_complete;
} else {
$self->cant_login;
}
}
package LWP::Conn::FTP::Rein;
use base 'LWP::Conn::FTP';
sub response
{
my($self, $r) = @_;
if ($r eq "2") {
$self->send_cmd("USER " . *$self->{'lwp_user'} => "User");
} else {
if (my $req = delete *$self->{'lwp_req'}) {
*$self->{'lwp_mgr'}->pushback_request($self, $req);
}
$self->error("Can't reinitialize");
}
}
package LWP::Conn::FTP::Type;
use base 'LWP::Conn::FTP';
sub response
{
my($self, $r) = @_;
if ($r eq "2") {
$self->state("Ready");
$self->activate;
} else {
$self->error("Can't set TYPE");
}
}
package LWP::Conn::FTP::Ready;
use base 'LWP::Conn::FTP';
use LWP::MainLoop qw(mainloop);
sub type
{
my($self, $type) = @_;
return 1 if *$self->{'lwp_type'} eq $type;
*$self->{'lwp_type'} = $type;
$self->send_cmd("TYPE $type" => "Type");
0;
}
sub activate
{
my $self = shift;
my $req = *$self->{'lwp_req'};
unless ($req) {
$req = *$self->{'lwp_mgr'}->get_request;
if (!$req) {
*$self->{'lwp_idle'}++;
*$self->{'lwp_mgr'}->connection_idle($self);
return;
}
elsif (*$self->{'lwp_idle'}) {
*$self->{'lwp_idle'} = 0;
*$self->{'lwp_mgr'}->connection_active($self);
}
*$self->{'lwp_req'} = $req;
my($user, $pass, $acct) = $self->login_info($req);
if ($user ne *$self->{'lwp_user'}) {
(*$self->{'lwp_user'}, *$self->{'lwp_pass'}, *$self->{'lwp_acct'})
= ($user, $pass, $acct);
$self->send_cmd("REIN" => "Rein");
return;
}
}
# We now have a request to perform and is logged in as the correct
# user.
my $method = uc($req->method);
my $file = $req->url->path;
if ($method =~ /^(GET|HEAD|PUT)$/) {
# It would be nice to also support APPEND, PUT-UNIQUE
return unless $self->type("I"); # we always use binary transfer mode
$self->file_trans($method, $file);
return;
my @cwd = qw();
if (@cwd) {
@{*$self->{'lwp_cwd'}} = @cwd;
$self->state("Cwd");
$self->cwd;
return;
} else {
$self->cwd_done;
}
} elsif ($method eq "DELETE") {
$self->send_cmd("DELE $file" => "Dele");
} elsif ($method eq "RENAME") {
$self->give_response(501, "RENAME not implemented yet");
} elsif ($method eq "TRACE") {
my $req = delete *$self->{'lwp_req'};
my $res = $req->new_response(200, "OK");
$res->date(time);
$res->server(*$self->{'lwp_server_product'});
$res->content_type("message/http");
$res->content($req->as_string);
$req->response_done($res);
$self->activate;
} else {
$self->give_response(501, "Method not implemented");
}
}
sub cwd_done
{
# now we want to actually try to fetch the file
# we could start by running SIZE, MDTM and such to get header
# information and also to check if the file is there.
my $self = shift;
}
sub file_trans
{
my($self, $method, $file) = @_;
*$self->{'lwp_meth'} = $method;
*$self->{'lwp_file'} = $file;
my $res = *$self->{'lwp_req'}->new_response(200, "OK");
$res->date(time);
$res->server(*$self->{'lwp_server_product'});
# XXX we should guess content_type and such here
*$self->{'lwp_res'} = $res;
if ($method eq "PUT") {
$self->port("W");
} else {
unless (*$self->{'lwp_noSIZE'}) {
$self->send_cmd("SIZE $file" => "Size");
return;
}
unless (*$self->{'lwp_noMDTM'}) {
$self->send_cmd("MDTM $file" => "Mdtm");
return;
}
$self->port(0);
}
}
sub port
{
my($self, $write) = @_;
my $data = IO::Socket::INET->new(Listen => 1,
LocalAddr => $self->sockhost,
);
*$self->{'lwp_done'} = 0;
if ($data) {
my $port = $data->sockport;
$port = ($port >> 8) . "," . ($port & 0xFF);
$port = join(",", split(/\./, $data->sockhost)) . ",$port";
$self->send_cmd("PORT $port" => "Port");
bless $data, "LWP::Conn::FTP::Data::Listen"; # 4 level name - whow!!
mainloop->readable($data);
*$data->{'lwp_write'} = *$self->{'lwp_req'}->content_ref if $write;
# A little circular reference makes life more interesting...
*$data->{'lwp_ftp'} = $self;
*$self->{'lwp_data'} = $data;
} else {
$self->_error("Can't create passive data socket");
}
}
use Socket qw(MSG_OOB);
sub abort
{
my $self = shift;
send($self, "\377\364", 0); # TELNET: IAC, IP
send($self, "\377\362", MSG_OOB); # TELNET: IAC, DM
$self->send_cmd("ABOR");
if (my $data = delete *$self->{'lwp_data'}) {
$data->close;
}
}
package LWP::Conn::FTP::Size;
use base 'LWP::Conn::FTP';
sub response
{
my($self, $r, $code) = @_;
my $skip_mdtm = *$self->{'lwp_noMDTM'};
if ($r eq "2") {
if ($self->message =~ /^\d+\s+(\d+)$/) {
*$self->{'lwp_res'}->content_length($1);
}
} elsif ($code eq "550") {
# Unluckily, we get the same answer for a file that does not
# exists and a file that happens to be a directory, so we must
# continue (but we can skip MDTM)
$skip_mdtm++
} else {
*$self->{'lwp_noSIZE'}++;
}
if ($skip_mdtm) {
$self->state("Ready");
$self->port();
} else {
my $file = *$self->{'lwp_file'};
$self->send_cmd("MDTM $file" => "Mdtm");
}
}
package LWP::Conn::FTP::Mdtm;
use base 'LWP::Conn::FTP';
use HTTP::Date qw(str2time);
sub response
{
my($self, $r, $code) = @_;
if ($r eq "2") {
if ($self->message =~ /^\d+\s+(\d{8})(\d{6})?$/) {
my $t = str2time($2 ? "$1T$2" : $1);
*$self->{'lwp_res'}->last_modified($t);
# XXX This is also the place to implement If-Modified-Since
}
} elsif ($code ne "550") {
*$self->{'lwp_noMDTM'}++;
}
$self->state("Ready");
$self->port();
}
package LWP::Conn::FTP::Dele;
use base 'LWP::Conn::FTP';
sub response
{
my($self, $r, $code) = @_;
$self->state("Ready");
my $mess = $self->message;
$mess =~ s/^\d+\s+//;
chomp($mess);
if ($r eq "2") {
$self->give_response(204, $mess);
} elsif ($code eq "550") {
$self->give_response(404, $mess);
} else {
$self->give_response(400, $mess);
}
}
package LWP::Conn::FTP::Port;
use base 'LWP::Conn::FTP';
sub response
{
my($self, $r) = @_;
if ($r eq "2") {
my $cmd = *$self->{'lwp_meth'} eq "PUT" ? "STOR" : "RETR";
my $file = *$self->{'lwp_file'};
$self->send_cmd("$cmd $file" => "Trans");
} else {
$self->_error("PORT failed");
}
}
package LWP::Conn::FTP::Trans;
use base 'LWP::Conn::FTP::Ready';
sub activate
{
# ignore
}
sub response
{
my($self, $r, $code) = @_;
if ($r eq "1") {
# info message only, we know that the response will succeed
# and if method is "HEAD" we might want to send a ABRT at
# this time...
my $res = *$self->{'lwp_res'};
if ($self->message =~ /\((\d+)\s+bytes\)/) {
# If it is already set, should we compare it with the
# previous value??
$res->content_length($1);
}
*$self->{'lwp_req'}->response_data("", $res);
# XXX catch except
$self->abort if *$self->{'lwp_meth'} eq "HEAD";
} elsif ($r eq "2") {
# we are done. Must sync with closing of data connection
$self->data_done($code);
} elsif ($code eq "426") { # transfer aborted
*$self->{'lwp_res'}->header("Abort" => $self->message);
$self->data_done($code);
} elsif ($code eq "550") { # no such file
if (lc($self->message) =~ /or directory/) {
delete(*$self->{'lwp_data'})->close;
$self->state("Ready");
$self->give_response(404);
} else {
# It might still be a directory, try to list it
my $file = *$self->{'lwp_file'};
$self->send_cmd("LIST $file" => "List");
my $res = *$self->{'lwp_res'};
$res->content_type("text/ftp-dir-listing");
$res->remove_header("Content-Encoding");
}
} else {
$self->error("Trans");
}
}
sub data
{
my $self = shift;
#return if *$self->{'lwp_meth'} eq "HEAD";
eval {
*$self->{'lwp_req'}->response_data($_[0], *$self->{'lwp_res'});
};
if ($@) {
# Initiate ABRT
$self->abort
}
}
sub data_really_done
{
my $self = shift;
my $req = delete *$self->{'lwp_req'};
my $res = delete *$self->{'lwp_res'};
$req->response_done($res);
# Start with next request
$self->state("Ready");
$self->activate;
}
sub data_done
{
my($self, $code) = @_;
if ($code && $code eq "426") {
$self->data_really_done;
} else {
$self->state("TransWait");
}
}
package LWP::Conn::FTP::TransWait;
use base 'LWP::Conn::FTP::Trans';
sub activate {}
sub data_done
{
my $self = shift;
$self->data_really_done;
}
sub response
{
my $self = shift;
my($r, $code) = @_;
if ($code eq "225" || $code eq "226") {
# ABOR command successful ignored
$self->data_really_done;
return;
}
$self->SUPER::response(@_);
}
package LWP::Conn::FTP::List;
use base 'LWP::Conn::FTP::Trans';
sub response
{
my($self, $r, $code) = @_;
if ($r eq "1") {
# info message, ignore
*$self->{'lwp_req'}->response_data("", *$self->{'lwp_res'});
# XXX catch except
} elsif ($r eq "2") {
# we are done. Must sync with data_done callback
$self->data_done($self->message);
} elsif ($code eq "550") {
delete(*$self->{'lwp_data'})->close;
$self->state("Ready");
$self->give_response(404);
} else {
$self->error("LIST");
}
}
package LWP::Conn::FTP::Cwd;
use base 'LWP::Conn::FTP';
sub cwd
{
my $self = shift;
my $dir = shift @{*$self->{'lwp_cwd'}};
if ($dir) {
if ($dir eq "..") {
$self->send_cmd("CDUP");
} else {
$self->send_cmd("CWD $dir");
}
} else {
$self->state("Ready");
$self->cwd_done;
}
}
sub response
{
my($self, $r) = @_;
if ($r eq "2") {
$self->cwd;
} else {
$self->error("Can't CWD");
}
}
package LWP::Conn::FTP::Data::Listen;
use base 'IO::Socket::INET';
use LWP::MainLoop qw(mainloop);
sub readable
{
my $self = shift;
if (my $data = $self->accept) {
print "FTP DATA ACCEPT\n" if $LWP::Conn::FTP::DEBUG &&
$LWP::Conn::FTP::DEBUG > 2;
mainloop->readable($data);
bless $data, "LWP::Conn::FTP::Data";
if (my $w = *$self->{'lwp_write'}) {
*$data->{'lwp_write'} = $w;
*$data->{'lwp_wbuf'} = '';
mainloop->writable($data);
}
my $ftp = *$self->{'lwp_ftp'};
*$data->{'lwp_ftp'} = $ftp;
*$ftp->{'lwp_data'} = $data;
} else {
*$self->{'lwp_ftp'}->_error("Can't accept");
}
mainloop->forget($self);
$self->close;
}
sub close
{
my $self = shift;
mainloop->forget($self);
$self->SUPER::close;
}
package LWP::Conn::FTP::Data;
use base 'LWP::Conn::FTP::Data::Listen';
use LWP::MainLoop qw(mainloop);
sub readable
{
my $self = shift;
my $buf = "";
mainloop->activity(*$self->{'lwp_ftp'});
my $n = sysread($self, $buf, 2048);
if ($n) {
print "FTP DATA READ $n bytes\n" if $LWP::Conn::FTP::DEBUG &&
$LWP::Conn::FTP::DEBUG > 2;
*$self->{'lwp_ftp'}->data($buf);
} else {
if (defined $n) {
*$self->{'lwp_ftp'}->data_done();
} else {
*$self->{'lwp_ftp'}->_error("Data connection error: $!");
}
$self->close;
}
}
sub writable
{
my $self = shift;
#print "Writeable\n";
mainloop->activity(*$self->{'lwp_ftp'});
my $buf = \*$self->{'lwp_wbuf'};
unless (defined $$buf and length $$buf) {
my $w = *$self->{'lwp_write'};
unless ($w) {
*$self->{'lwp_ftp'}->data_done();
$self->close;
return;
}
$w = $$w if ref($$w);
if (ref($w) eq "CODE") {
$$buf = &$w();
unless (defined $$buf and length $$buf) {
delete *$self->{'lwp_write'};
return;
}
} else {
$$buf = $$w;
delete *$self->{'lwp_write'};
}
return unless length $$buf;
}
my $len = length($$buf);
$len = 2048 if $len > 2048;
my $n = syswrite($self, $$buf, $len);
if ($n) {
print "FTP DATA WRITE $n bytes\n" if $LWP::Conn::FTP::DEBUG &&
$LWP::Conn::FTP::DEBUG > 2;
substr($$buf, 0, $n) = '';
} else {
*$self->{'lwp_ftp'}->_error("Data connection error: $!");
$self->close;
}
}
1;