# -*- perl -*- # $Id: ftp.pm,v 1.11 2003/02/19 14:58:37 langhein Exp $ # derived from: ftp.pm,v 1.31 2001/10/26 20:13:20 gisle Exp # Implementation of the ftp protocol (RFC 959). We let the Net::FTP # package do all the dirty work. package LWP::Parallel::Protocol::ftp; use Carp (); use HTTP::Status (); use HTTP::Negotiate (); use HTTP::Response (); use LWP::MediaTypes (); use File::Listing (); require LWP::Parallel::Protocol; require LWP::Protocol::ftp; @ISA = qw(LWP::Parallel::Protocol LWP::Protocol::ftp); use strict; eval { package LWP::Parallel::Protocol::MyFTP; require Net::FTP; Net::FTP->require_version(2.00); use vars qw(@ISA); @ISA=qw(Net::FTP); sub new { my $class = shift; LWP::Debug::trace('()'); my $self = $class->SUPER::new(@_) || return undef; my $mess = $self->message; # welcome message LWP::Debug::debug($mess); $mess =~ s|\n.*||s; # only first line left $mess =~ s|\s*ready\.?$||; # Make the version number more HTTP like $mess =~ s|\s*\(Version\s*|/| and $mess =~ s|\)$||; ${*$self}{myftp_server} = $mess; #$response->header("Server", $mess); $self; } sub http_server { my $self = shift; ${*$self}{myftp_server}; } sub home { my $self = shift; my $old = ${*$self}{myftp_home}; if (@_) { ${*$self}{myftp_home} = shift; } $old; } sub go_home { LWP::Debug::trace(''); my $self = shift; $self->cwd(${*$self}{myftp_home}); } sub request_count { my $self = shift; ++${*$self}{myftp_reqcount}; } sub ping { LWP::Debug::trace(''); my $self = shift; return $self->go_home; } }; my $init_failed = $@; =item ($socket, $second_arg) = $prot->handle_connect ($req, $proxy, $timeout); This method connects with the server on the machine and port specified in the $req object. If a $proxy is given, it will return an error, since the FTP protocol does not allow proxying. (See below on how such an error is propagated to the caller). If successful, the first argument will contain the IO::Socket object that connects to the specified site. The second argument is empty (for ftp, that is. See LWP::Protocol::http for different usage). If the connection fails, $socket is set to 'undef', and the second argument contains a HTTP::Response object holding a textual representation of the error. (You can use its 'code' and 'message' methods to find out what went wrong) =cut sub handle_connect { my ($self, $request, $proxy, $timeout) = @_; # mostly directly copied from the original Protocol::ftp, changes # are marked with "# ML" comment (mostly return values) # check proxy if (defined $proxy) { return (undef, new HTTP::Response &HTTP::Status::RC_BAD_REQUEST, 'You can not proxy through the ftp'); # ML } my $url = $request->url; if ($url->scheme ne 'ftp') { my $scheme = $url->scheme; return (undef, new HTTP::Response &HTTP::Status::RC_INTERNAL_SERVER_ERROR, "LWP::Protocol::ftp::request called for '$scheme'"); # ML } # check method my $method = $request->method; unless ($method eq 'GET' || $method eq 'HEAD' || $method eq 'PUT') { return (undef, new HTTP::Response &HTTP::Status::RC_BAD_REQUEST, 'Library does not allow method ' . "$method for 'ftp:' URLs"); # ML } if ($init_failed) { return (undef, new HTTP::Response &HTTP::Status::RC_INTERNAL_SERVER_ERROR, $init_failed); # ML } my $host = $url->host; my $port = $url->port; my $user = $url->user; # taken out some additional variable declarations here, that are now # only needed in 'write_request' method. ################# # new in LWP 5.60 my $account = $request->header('Account'); # ML my $key; my $conn_cache = $self->{ua}{conn_cache}; if ($conn_cache) { $key = "$host:$port:$user"; $key .= ":$account" if defined($account); if (my $ftp = $conn_cache->withdraw("ftp", $key)) { if ($ftp->ping) { LWP::Debug::debug('Reusing old connection'); # save it again $conn_cache->deposit("ftp", $key, $ftp); # added $response object # ML my $response = HTTP::Response->new(&HTTP::Status::RC_OK, "Document follows"); return ($ftp, $response); } } } # try to make a connection my $ftp = LWP::Parallel::Protocol::MyFTP->new($host, Port => $port, Timeout => $timeout, ); # XXX Should be some what to pass on 'Passive' (header??) ################# my $response; unless ($ftp) { $@ =~ s/^Net::FTP: //; # new in LWP 5.60 $response = HTTP::Response->new(&HTTP::Status::RC_INTERNAL_SERVER_ERROR,$@); } else { # Create an initial response object $response = HTTP::Response->new(&HTTP::Status::RC_OK, "Document follows"); ################# # new in LWP 5.60 $response->header(Server => $ftp->http_server); $response->header('Client-Request-Num' => $ftp->request_count); ################# $response->request($request); } return ($ftp, $response); # ML } sub write_request { my ($self, $request, $ftp, $response, $arg, $timeout) = @_; # Some of the following variable declarations, directly copied from # the original Protocol::ftp module, appear both in 'handle_connect' # _and_ 'write_request' method. Although it introduces additional # overhead, we can't pass additional variables between those two # methods, but we need some of the values in both routines. We # allow the account to be specified in the "Account" header my $account = $request->header('Account'); my $url = $request->url; my $host = $url->host; my $port = $url->port; my $user = $url->user; my $password = $url->password; # If a basic autorization header is present than we prefer these over # the username/password specified in the URL. { my($u,$p) = $request->authorization_basic; if (defined $u) { $user = $u; $password = $p; } } my $method = $request->method; # from here on mostly directly clipped from the original # Protocol::ftp. Changes are marked with "# ML" comment # from here on it seems FTP will handle timeouts, right? # ML $ftp->timeout($timeout) if $timeout; LWP::Debug::debug("Logging in as $user (password $password)..."); unless ($ftp->login($user, $password, $account)) { # Unauthorized. Let's fake a RC_UNAUTHORIZED response my $mess = scalar($ftp->message); LWP::Debug::debug($mess); $mess =~ s/\n$//; my $res = HTTP::Response->new(&HTTP::Status::RC_UNAUTHORIZED, $mess); $res->header("Server", $ftp->http_server); $res->header("WWW-Authenticate", qq(Basic Realm="FTP login")); return (undef, $res); # ML } LWP::Debug::debug($ftp->message); ################# # new in LWP 5.60 my $home = $ftp->pwd; LWP::Debug::debug("home: '$home'"); $ftp->home($home); # ML my $key; $key = "$host:$port:$user"; $key .= ":$account" if defined($account); # my $conn_cache = $self->{ua}{conn_cache}; $conn_cache->deposit("ftp", $key, $ftp) if $conn_cache; ################# # Get & fix the path my @path = $url->path_segments; # removed in LWP 5.48 #shift(@path); # There will always be an empty first component #pop(@path) while @path && $path[-1] eq ''; # remove empty tailing comps my $remote_file = pop(@path); $remote_file = '' unless defined $remote_file; my $type; if (ref $remote_file) { my @params; ($remote_file, @params) = @$remote_file; for (@params) { $type = $_ if s/^type=//; } } if ($type && $type eq 'a') { $ftp->ascii; } else { $ftp->binary; } for (@path) { LWP::Debug::debug("CWD $_"); unless ($ftp->cwd($_)) { return (undef, new HTTP::Response &HTTP::Status::RC_NOT_FOUND, "Can't chdir to $_"); } } if ($method eq 'GET' || $method eq 'HEAD') { # new in ftp.pm,v 1.23 (fixed in ftp.pm,v 1.24) LWP::Debug::debug("MDTM"); if (my $mod_time = $ftp->mdtm($remote_file)) { $response->last_modified($mod_time); if (my $ims = $request->if_modified_since) { if ($mod_time <= $ims) { $response->code(&HTTP::Status::RC_NOT_MODIFIED); $response->message("Not modified"); return (undef, $response); } } } # end_of_new_stuff ################# # new in LWP 5.60 # We'll use this later to abort the transfer if necessary. # if $max_size is defined, we need to abort early. Otherwise, it's # a normal transfer my $max_size = undef; # Set resume location, if the client requested it if ($request->header('Range') && $ftp->supported('REST')) { my $range_info = $request->header('Range'); # Change bytes=2772992-6781209 to just 2772992 my ($start_byte,$end_byte) = $range_info =~ /.*=\s*(\d+)-(\d+)/; if (!defined $start_byte || !defined $end_byte || ($start_byte < 0) || ($start_byte > $end_byte) || ($end_byte < 0)) { return (undef, HTTP::Response->new(&HTTP::Status::RC_BAD_REQUEST, 'Incorrect syntax for Range request')); } $max_size = $end_byte-$start_byte; $ftp->restart($start_byte); } elsif ($request->header('Range') && !$ftp->supported('REST')) { return (undef,HTTP::Response->new(&HTTP::Status::RC_NOT_IMPLEMENTED, "Server does not support resume.")); } ################ my $data; # the data handle LWP::Debug::debug("retrieve file?"); if (length($remote_file) and $data = $ftp->retr($remote_file)) { # remove reading from socket into 'read_chunk' method. # just return our new $listen_socket here. my($type, @enc) = LWP::MediaTypes::guess_media_type($remote_file); $response->header('Content-Type', $type) if $type; for (@enc) { $response->push_header('Content-Encoding', $_); } my $mess = $ftp->message; LWP::Debug::debug($mess); if ($mess =~ /\((\d+)\s+bytes\)/) { $response->header('Content-Length', "$1"); } return ($data, $response); # ML } elsif (!length($remote_file) || $ftp->code == 550) { # no file, the remote file is actually a directory, so cdw into directory if (length($remote_file) && !$ftp->cwd($remote_file)) { LWP::Debug::debug("chdir before listing failed"); return (undef, new HTTP::Response &HTTP::Status::RC_NOT_FOUND, "File '$remote_file' not found"); # ML } # It should now be safe to try to list the directory LWP::Debug::debug("dir"); my @lsl = $ftp->dir; # Try to figure out if the user want us to convert the # directory listing to HTML. my @variants = ( ['html', 0.60, 'text/html' ], ['dir', 1.00, 'text/ftp-dir-listing' ] ); #$HTTP::Negotiate::DEBUG=1; my $prefer = HTTP::Negotiate::choose(\@variants, $request); my $content = ''; if (!defined($prefer)) { return (undef, new HTTP::Response &HTTP::Status::RC_NOT_ACCEPTABLE, "Neither HTML nor directory listing wanted"); # ML } elsif ($prefer eq 'html') { $response->header('Content-Type' => 'text/html'); $content = "