File Coverage

blib/lib/Net/SFTP/Server/FS.pm
Criterion Covered Total %
statement 28 207 13.5
branch 0 134 0.0
condition 0 38 0.0
subroutine 9 44 20.4
pod 0 33 0.0
total 37 456 8.1


line stmt bran cond sub pod time code
1             package Net::SFTP::Server::FS;
2              
3 1     1   24516 use strict;
  1         2  
  1         33  
4 1     1   5 use warnings;
  1         1  
  1         26  
5             # use Carp;
6              
7 1     1   7 use Fcntl;
  1         2  
  1         364  
8 1     1   5 use File::Spec;
  1         2  
  1         21  
9 1     1   784 use File::Strmode;
  1         1009  
  1         44  
10 1     1   6 use Cwd qw(realpath);
  1         2  
  1         40  
11              
12 1     1   553 use Net::SFTP::Server::Constants qw(:all);
  1         3  
  1         373  
13              
14 1     1   809 use Net::SFTP::Server;
  1         4  
  1         113  
15             our @ISA = qw(Net::SFTP::Server);
16              
17             BEGIN {
18 1     1   4 *_debug = \&Net::SFTP::Server::_debug;
19 1         2 *_debugf = \&Net::SFTP::Server::_debugf;
20 1         2 *_hexdump = \&Net::SFTP::Server::_hexdump;
21 1         3823 *debug = \$Net::SFTP::Server::debug;
22             }
23              
24             our $debug;
25              
26             sub new {
27 0     0 0   my $class = shift;
28 0           my $self = $class->SUPER::new(@_);
29 0           $self->{next_handler_id} = 'A';
30 0           $self->{handlers} = {};
31 0           $self;
32             }
33              
34             sub save_handler {
35 0     0 0   my $self = shift;
36 0           my $id = $self->{next_handler_id}++;
37 0           $self->{handlers}{$id} = [@_];
38 0           $id;
39             }
40              
41 0     0 0   sub save_file_handler { shift->save_handler(file => @_) }
42 0     0 0   sub save_dir_handler { shift->save_handler(dir => @_) }
43              
44             sub get_handler {
45 0     0 0   my ($self, $id) = @_;
46 0 0         my $h = $self->{handlers}{$id}
47             or return;
48 0 0         wantarray ? @$h : $h->[1];
49             }
50              
51             sub get_file_handler {
52 0 0   0 0   my @h = shift->get_handler(@_) or return;
53 0 0         shift @h eq 'file' or return;
54 0 0         wantarray ? @h : $h[0];
55             }
56              
57             sub get_dir_handler {
58 0 0   0 0   my @h = shift->get_handler(@_) or return;
59 0 0         shift @h eq 'dir' or return;
60 0 0         wantarray ? @h : $h[0];
61             }
62              
63             sub remove_handler {
64 0     0 0   my ($self, $id) = @_;
65 0           my $h = delete $self->{handlers}{$id};
66 0 0         wantarray ? (defined $h ? @$h : ()) : $h;
    0          
67             }
68              
69             my @errno2status;
70             $errno2status[Errno::ENOENT] = SSH_FX_NO_SUCH_FILE;
71             $errno2status[Errno::EBADF] = SSH_FX_NO_SUCH_FILE;
72             $errno2status[Errno::ELOOP] = SSH_FX_NO_SUCH_FILE;
73             $errno2status[Errno::EPERM] = SSH_FX_PERMISSION_DENIED;
74             $errno2status[Errno::EACCES] = SSH_FX_PERMISSION_DENIED;
75             $errno2status[Errno::EFAULT] = SSH_FX_PERMISSION_DENIED;
76             $errno2status[Errno::ENAMETOOLONG] = SSH_FX_BAD_MESSAGE;
77             $errno2status[Errno::EINVAL] = SSH_FX_BAD_MESSAGE;
78             $errno2status[Errno::ENOSYS] = SSH_FX_OP_UNSUPPORTED;
79              
80             sub errno_to_status {
81 0     0 0   my ($self, $errno) = @_;
82 0   0       $errno2status[$errno] // SSH_FX_FAILURE;
83             }
84              
85             sub push_status_errno_response {
86 0     0 0   my ($self, $id) = @_;
87 0           $self->push_status_response($id, $self->errno_to_status($!), $!);
88             }
89              
90             sub sftp_open_flags_to_sysopen {
91 0     0 0   my ($self, $flags) = @_;
92 0           my $posix = 0;
93 0 0         if ($flags & SSH_FXF_READ) {
    0          
94 0 0         if ($flags & SSH_FXF_WRITE) {
95 0           $posix = O_RDWR;
96             }
97             else {
98 0           $posix = O_RDONLY;
99             }
100             }
101             elsif ($flags & SSH_FXF_WRITE) {
102 0           $posix = O_WRONLY;
103             }
104 0 0         if ($flags & SSH_FXF_CREAT) {
105 0           $posix |= O_CREAT;
106             }
107 0 0         if ($flags & SSH_FXF_TRUNC) {
108 0           $posix |= O_TRUNC;
109             }
110 0 0         if ($flags & SSH_FXF_EXCL) {
111 0           $posix |= O_EXCL;
112             }
113 0 0 0       $debug and $debug & 128 and _debug "flags $flags to posix $posix";
114 0           $posix;
115             }
116              
117             sub _set_attrs {
118 0     0     my ($obj, $attrs) = @_;
119 0           local $@;
120 0           local $SIG{__DIE__};
121 0           eval {
122 0 0         if ($attrs) {
123 0 0         if (defined $attrs->{size}) {
124 0 0         truncate $obj, $attrs->{size} or return;
125             }
126 0 0         if (defined $attrs->{permissions}) {
127 0 0         chmod $attrs->{permissions}, $obj or return;
128             }
129 0 0         if (defined $attrs->{gid}) {
130 0 0         chown $attrs->{uid}, $attrs->{gid}, $obj or return;
131             }
132 0 0         if (defined $attrs->{atime}) {
133 0 0         utime $attrs->{atime}, $attrs->{mtime}, $obj or return;
134             }
135             }
136 0           1;
137             };
138             }
139              
140             sub handle_command_open_v3 {
141 0     0 0   my ($self, $id, $path, $flags, $attrs) = @_;
142 0           my $writable = $flags & SSH_FXF_WRITE;
143 0           my $pflags = $self->sftp_open_flags_to_sysopen($flags);
144 0           my $perms = $attrs->{mode};
145 0           my $old_umask;
146 0 0         if (defined $perms) {
147 0           $old_umask = umask $perms;
148             }
149             else {
150 0           $perms = 0666;
151             }
152 0           my $fh;
153 0 0         unless (sysopen $fh, $path, $pflags, $perms) {
154 0           $self->push_status_errno_response($id);
155 0 0         umask $old_umask if defined $old_umask;
156 0           return;
157             }
158 0 0         umask $old_umask if defined $old_umask;
159 0 0         if ($writable) {
160 0 0         _set_attrs($path, $attrs)
161             or $self->send_status_errno_response($id);
162             }
163 0           my $hid = $self->save_file_handler($fh, $flags, $perms);
164 0 0 0       $debug and $debug & 2 and _debug "file $path open as $hid (pkt id: $id)";
165 0           $self->push_handle_response($id, $hid);
166             }
167              
168             sub handle_command_read_v3 {
169 0     0 0   my ($self, $id, $hid, $off, $len) = @_;
170 0   0       my $fh = $self->get_file_handler($hid) //
171             return $self->push_status_response($id, SSH_FX_FAILURE,
172             "Bad handler");
173 0 0         $len = 65536 if $len > 65536;
174              
175 0   0       sysseek($fh, $off, 0) // return $self->push_status_errno_response($id);
176 0   0       my $bytes = sysread($fh, my($data), $len) //
177             return $self->push_status_errno_response($id);
178 0 0         $bytes == 0 and
179             return $self->push_status_response($id, SSH_FX_EOF);
180             # TODO: build packet on buffer_out to reduce data copying
181 0           $self->push_packet(uint8 => SSH_FXP_DATA,
182             uint32 => $id,
183             str => $data);
184             }
185              
186             sub handle_command_write_v3 {
187 0     0 0   my ($self, $id, $hid, $off) = @_;
188 0   0       my $fh = $self->get_file_handler($hid) //
189             return $self->push_status_response($id, SSH_FX_FAILURE,
190             "Bad handler");
191 0   0       sysseek($fh, $off, 0) // return $self->push_status_errno_response($id);
192 0           my $len = length $_[4];
193 0           while ($len) {
194 0 0         my $bytes = syswrite($fh, $_[4], $len, -$len)
195             or return $self->push_status_errno_response($id);
196 0           $len -= $bytes;
197             }
198 0           $self->push_status_ok_response($id);
199             }
200              
201             sub handle_command_close_v3 {
202 0     0 0   my ($self, $id, $hid) = @_;
203 0 0         my ($type, $fh) = $self->remove_handler($hid)
204             or return $self->push_status_response($id, SSH_FX_FAILURE, "Bad file handler");
205 0 0         if ($type eq 'dir') {
    0          
206 0 0 0       $debug and $debug & 2 and _debug "closing dir handle $hid (id: $id)";
207 0 0         closedir($fh) or return $self->push_status_errno_response($id);
208             }
209             elsif ($type eq 'file') {
210 0 0 0       $debug and $debug & 2 and _debug "closing file handle $hid (id: $id)";
211 0 0         close($fh) or return $self->push_status_errno_response($id);
212             }
213             else {
214 0           die "Internal error: unknown handler type $type";
215             }
216 0           $self->push_status_ok_response($id);
217             }
218              
219             sub handle_command_opendir_v3 {
220 0     0 0   my ($self, $id, $path) = @_;
221 0 0         opendir my $dh, $path or return $self->push_status_errno_response($id);
222 0           my $hid = $self->save_dir_handler($dh, $path);
223 0 0 0       $debug and $debug & 2 and _debug "dir $path open as $hid (pkt id: $id)";
224 0           $self->push_handle_response($id, $hid);
225             }
226              
227             our @month2name = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
228              
229             sub resolve_uid {
230 0     0 0   my ($self, $uid) = @_;
231 0           my $name = getpwuid $uid;
232 0 0         defined $name ? $name : $uid;
233             }
234              
235             sub resolve_gid {
236 0     0 0   my ($self, $gid) = @_;
237 0           my $name = getgrgid $gid;
238 0 0         defined $name ? $name : $gid;
239             }
240              
241             sub readdir_name {
242 0     0 0   my ($self, $dir, $entry, $lstat) = @_;
243 0           my $fn = File::Spec->catfile($dir, $entry);
244 0 0         my (undef, undef, $mode, $nlink, $uid, $gid, undef, $size, $atime, $mtime) =
    0          
245             ($lstat ? lstat $fn : stat $fn) or return { filename => $entry };
246 0           my (undef, $min, $hour, $mday, $mon, $year) = localtime $mtime;
247 0           my $current_year = (localtime)[5];
248 0 0         my $longname = sprintf("%10s %3d %-9s %-9s % 8d %-3s %2d % 5s %s",
249             strmode($mode),
250             $nlink,
251             $self->resolve_uid($uid),
252             $self->resolve_gid($gid),
253             $size,
254             $month2name[$mon],
255             $mday,
256             ($year == $current_year
257             ? sprintf("%02d:%02d", $hour, $min)
258             : $year + 1900),
259             $entry);
260              
261 0 0 0       $debug and $debug & 2 and _debug "longname: $longname (entry: $entry)";
262              
263             return {
264 0           filename => $entry,
265             longname => $longname,
266             attrs => {
267             size => $size,
268             uid => $uid,
269             gid => $gid,
270             permissions => $mode,
271             atime => $atime,
272             mtime => $mtime
273             }
274             }
275             }
276              
277             sub handle_command_readdir_v3 {
278 0     0 0   my ($self, $id, $hid) = @_;
279 0 0         my ($dh, $path) = $self->get_dir_handler($hid)
280             or $self->push_status_response($id, SSH_FX_FAILURE, "Bad directory handler");
281 0           my @entry;
282 0           while (defined (my $entry = readdir $dh)) {
283 0           push @entry, $entry;
284 0 0         last if @entry > 200;
285             }
286 0 0         @entry or return $self->push_status_eof_response($id);
287 0           $self->push_name_response($id, map $self->readdir_name($path, $_), @entry);
288             }
289              
290             sub stat_to_attrs {
291 0     0 0   my ($self, undef, undef, $mode, undef, $uid, $gid, undef, $size, $atime, $mtime) = @_;
292             return {
293 0           size => $size,
294             uid => $uid,
295             gid => $gid,
296             permissions => $mode,
297             atime => $atime,
298             mtime => $mtime
299             };
300             }
301              
302             sub handle_command_lstat_v3 {
303 0     0 0   my ($self, $id, $path) = @_;
304 0 0         my @stat = lstat $path
305             or return $self->push_status_errno_response($id);
306 0           $self->push_attrs_response($id, $self->stat_to_attrs(@stat));
307             }
308              
309             sub handle_command_stat_v3 {
310 0     0 0   my ($self, $id, $path) = @_;
311 0 0         my @stat = stat $path
312             or return $self->push_status_errno_response($id);
313 0           $self->push_attrs_response($id, $self->stat_to_attrs(@stat));
314             }
315              
316             sub handle_command_fstat_v3 {
317 0     0 0   my ($self, $id, $hid) = @_;
318 0   0       my $fh = $self->get_handler($hid)
319             // return $self->push_status_response($id, SSH_FX_FAILURE,
320             "Bad file handler");
321 0 0         my @stat = stat $fh
322             or return $self->push_status_errno_response($id);
323 0           $self->push_attrs_response($id, $self->stat_to_attrs(@stat));
324             }
325              
326             sub _set_attrs_and_push_status_response {
327 0     0     my ($self, $id, $obj, $attrs) = @_;
328 0 0         _set_attrs($obj, $attrs)
329             ? $self->push_status_ok_response($id)
330             : $self->push_status_errno_response($id);
331             }
332              
333             sub handle_command_setstat_v3 {
334 0     0 0   _set_attrs_and_push_status_response(@_)
335             }
336              
337             sub handle_command_fsetstat_v3 {
338 0     0 0   my ($self, $id, $hid, $attrs) = @_;
339 0   0       my $fh = $self->get_file_handler($hid)
340             // return $self->push_status_response($id, SSH_FX_FAILURE,
341             "Bad file handler");
342 0           _set_attrs_and_push_status_response($self, $id, $fh, $attrs);
343             }
344              
345             sub handle_command_remove_v3 {
346 0     0 0   my ($self, $id, $path) = @_;
347 0 0         unlink $path
348             or return $self->push_status_errno_response($id);
349 0           $self->push_status_ok_response($id);
350             }
351              
352             sub handle_command_mkdir_v3 {
353 0     0 0   my ($self, $id, $path, $attrs) = @_;
354 0           my $old_umask;
355              
356 0 0         $old_umask = umask $attrs->{permissions}
357             if defined $attrs->{permissions};
358              
359 0 0         unless (mkdir $path) {
360 0           $self->send_status_errno_response($id);
361 0 0         umask $old_umask if defined $old_umask;
362 0           return;
363             }
364 0 0         umask $old_umask if defined $old_umask;
365 0           _set_attrs_and_push_status_response($self, $id, $path, $attrs);
366             }
367              
368             sub handle_command_rmdir_v3 {
369 0     0 0   my ($self, $id, $path) = @_;
370 0 0         rmdir $path
371             or return $self->push_status_errno_response($id);
372 0           $self->push_status_ok_response($id);
373             }
374              
375             sub handle_command_realpath_v3 {
376 0     0 0   my ($self, $id, $path) = @_;
377 0           local $@;
378 0           local $SIG{__DIE__};
379 0   0       my $realpath = eval { realpath($path) }
  0            
380             // return $self->push_status_errno_response($id);
381 0           $self->push_name_response($id, { filename => $realpath });
382             }
383              
384             sub handle_command_rename_v3 {
385 0     0 0   my ($self, $id, $old, $new) = @_;
386 0 0         -e $new and
387             return $self->push_status_response($id, SSH_FX_FAILURE, "File exists");
388 0 0         rename $old, $new or
389             return $self->push_status_errno_response($id);
390 0           $self->push_status_ok_response($id);
391             }
392              
393             sub handle_command_readlink_v3 {
394 0     0 0   my ($self, $id, $path) = @_;
395 0           local $@;
396 0           local $SIG{__DIE__};
397 0   0       my $readlink = eval { readlink($path) }
  0            
398             // return $self->push_status_errno_response($id);
399              
400 0           $self->push_name_response($id, { filename => $readlink });
401             }
402              
403             sub handle_command_symlink_v3 {
404 0     0 0   my ($self, $id, $target, $link) = @_;
405 0 0         eval { symlink $target, $link }
  0            
406             or $self->push_status_errno_message($id);
407 0           $self->push_status_ok_message($id);
408             }
409              
410             1;
411              
412             __END__