File Coverage

blib/lib/LWP/Protocol/sftp.pm
Criterion Covered Total %
statement 21 94 22.3
branch 0 48 0.0
condition 0 12 0.0
subroutine 7 9 77.7
pod 1 1 100.0
total 29 164 17.6


line stmt bran cond sub pod time code
1             package LWP::Protocol::sftp;
2              
3             our $VERSION = '0.05';
4              
5             # BEGIN { local $| =1; print "loading LWP::Protocol::sftp\n"; }
6              
7              
8 1     1   29948 use strict;
  1         3  
  1         35  
9 1     1   5 use warnings;
  1         3  
  1         31  
10              
11 1     1   5 use base qw(LWP::Protocol);
  1         6  
  1         953  
12             LWP::Protocol::implementor(sftp => __PACKAGE__);
13              
14             require LWP::MediaTypes;
15             require HTTP::Request;
16             require HTTP::Response;
17             require HTTP::Status;
18             require HTTP::Date;
19              
20             require URI::Escape;
21             require HTML::Entities;
22              
23 1     1   52780 use Net::SFTP::Foreign;
  1         80872  
  1         43  
24 1     1   10 use Net::SFTP::Foreign::Constants qw(:flags :status);
  1         2  
  1         223  
25 1     1   7 use Fcntl qw(S_ISDIR);
  1         2  
  1         58  
26              
27 1     1   7 use constant PUT_BLOCK_SIZE => 8192;
  1         2  
  1         1376  
28              
29             our %DEFAULTS = ( ls => [],
30             new => [] );
31              
32             my $dont_escape_in_paths = '^A-Za-z0-9\-\._~/';
33              
34             sub request
35             {
36 0     0 1   my($self, $request, $proxy, $arg, $size) = @_;
37              
38             # print __PACKAGE__."->request($self, $request, $proxy, $arg, $size)\n";
39              
40 0 0 0       $size = 4096 unless defined $size and $size > 0;
41              
42             # check proxy
43 0 0         defined $proxy and
44             return HTTP::Response->new(HTTP::Status::RC_BAD_REQUEST,
45             'You can not proxy through the sftp subsystem');
46              
47             # check method
48 0           my $method = $request->method;
49              
50             # check url
51 0           my $url = $request->url;
52              
53 0           my $scheme = $url->scheme;
54 0 0         if ($scheme ne 'sftp') {
55 0           return HTTP::Response->new(HTTP::Status::RC_INTERNAL_SERVER_ERROR,
56             "LWP::Protocol::sftp::request called for '$scheme'")
57             }
58              
59 0           my $host = $url->host;
60 0           my $port = $url->port;
61 0           my $user = $url->user;
62 0           my $password = $url->password;
63              
64 0           my $path = $url->path;
65 0 0 0       $path = '/' unless defined $path and length $path;
66              
67 0           my $sftp = Net::SFTP::Foreign->new(host => $host,
68             user => $user,
69             port => $port,
70             password => $password,
71 0           @{$DEFAULTS{new}});
72 0 0         if ($sftp->error) {
73 0           return HTTP::Response->new(HTTP::Status::RC_SERVICE_UNAVAILABLE,
74             "unable to establish SSH connection to remote machine (".$sftp->error.")")
75             }
76              
77             # handle GET and HEAD methods
78              
79 0           my $response = eval {
80              
81 0 0 0       if ($method eq 'GET' || $method eq 'HEAD') {
82              
83 0 0         my $stat = $sftp->stat($path) or die "remote file stat failed";
84              
85             # check if-modified-since
86 0           my $ims = $request->header('If-Modified-Since');
87 0 0         if (defined $ims) {
88 0           my $time = HTTP::Date::str2time($ims);
89 0 0 0       if (defined $time and $time >= $stat->mtime) {
90 0           return HTTP::Response->new(HTTP::Status::RC_NOT_MODIFIED,
91             "$method $path")
92             }
93             }
94              
95             # Ok, should be an OK response by now...
96 0           my $response = HTTP::Response->new(HTTP::Status::RC_OK);
97              
98             # fill in response headers
99 0           $response->header('Last-Modified', HTTP::Date::time2str($stat->mtime));
100              
101 0 0         if (S_ISDIR($stat->perm)) { # If the path is a directory, process it
102             # generate the HTML for directory
103 0           my $ls = $sftp->ls($path, ordered => 1,
104 0 0         @{$DEFAULTS{ls}}) or die "remote ls failed";
105              
106             # Make directory listing
107 0           my @lines = map {
108 0           my $fn = $_->{filename};
109 0 0         $fn .= '/' if S_ISDIR($_->{a}->perm);
110 0           my $furl = URI::Escape::uri_escape($fn, $dont_escape_in_paths);
111 0           my $desc = HTML::Entities::encode($fn);
112 0           qq{
  • $desc}
  • 113             } @$ls;
    114              
    115 0           $path =~ s|/?$|/|;
    116 0           my $ue_path = URI::Escape::uri_escape($path, $dont_escape_in_paths);
    117 0           my $ee_path = HTML::Entities::encode($path);
    118              
    119             # regenerate base url without password
    120 0           my $base = 'sftp://';
    121 0 0         $base .= URI::Escape::uri_escape($user) . '@' if defined $user;
    122 0           $base .= URI::Escape::uri_escape($host);
    123 0 0         $base .= ':' . URI::Escape::uri_escape($port) if defined $port;
    124 0           $base .= $ue_path;
    125              
    126 0           my $html = join("\n",
    127             "\n",
    128             "Directory $ee_path",
    129             "",
    130             "\n",
    131             "

    Directory listing of $ee_path

    ",
    132             "
      ", @lines, "
    ",
    133             "\n\n");
    134              
    135 0           $response->header('Content-Type', 'text/html');
    136 0           $response->header('Content-Length', length $html);
    137 0 0         $html = "" if $method eq "HEAD";
    138              
    139 0           return $self->collect_once($arg, $response, $html);
    140             }
    141              
    142             # path is a regular file
    143 0           my $file_size = $stat->size;
    144 0           $response->header('Content-Length', $file_size);
    145 0           LWP::MediaTypes::guess_media_type($path, $response);
    146              
    147             # read the file
    148 0 0         if ($method ne "HEAD") {
    149 0 0         my $fh = $sftp->open($path) or die "remote file open failed";
    150             $response = $self->collect($arg, $response, sub {
    151 0     0     my $content = $sftp->read($fh, $size);
    152 0 0         defined $content ? \$content : \"" });
      0            
    153 0 0         $sftp->close($fh) or die "remote file read failed";
    154             }
    155 0           return $response;
    156             }
    157              
    158             # handle PUT method
    159 0 0         if ($method eq 'PUT') {
    160 0 0         my $fh = $sftp->open($path, SSH2_FXF_WRITE | SSH2_FXF_CREAT | SSH2_FXF_TRUNC) or die "remote file open failed";
    161              
    162 0           my $content = $request->content;
    163 0           while (length $content) {
    164 0 0         my $bytes = $sftp->write($fh, $content) or die "remote file write failed";
    165 0           substr($content, 0, $bytes, '');
    166             }
    167              
    168 0 0         $sftp->close($fh) or die "remote file write failed";
    169              
    170 0           return HTTP::Response->new(HTTP::Status::RC_OK);
    171             }
    172              
    173             # unsupported method
    174 0           return HTTP::Response->new(HTTP::Status::RC_BAD_REQUEST,
    175             "Library does not allow method $method for 'sftp:' URLs");
    176             };
    177              
    178 0 0         if ($@) {
    179 0           my $error = $sftp->error;
    180 0           return HTTP::Response->new(HTTP::Status::RC_INTERNAL_SERVER_ERROR,
    181             "SFTP error: $@ - $error");
    182             }
    183 0           return $response;
    184             }
    185              
    186             1;
    187             __END__