File Coverage

blib/lib/Net/SFTP/Server.pm
Criterion Covered Total %
statement 40 320 12.5
branch 1 152 0.6
condition 1 269 0.3
subroutine 12 44 27.2
pod 0 25 0.0
total 54 810 6.6


line stmt bran cond sub pod time code
1             package Net::SFTP::Server;
2              
3             $VERSION = '0.03';
4              
5 2     2   25088 use strict;
  2         5  
  2         87  
6 2     2   12 use warnings;
  2         3  
  2         71  
7 2     2   12 use Carp;
  2         4  
  2         183  
8              
9 2     2   12 use Fcntl qw(O_NONBLOCK F_SETFL F_GETFL);
  2         4  
  2         125  
10 2     2   2041 use Errno ();
  2         3061  
  2         51  
11 2     2   25 use Scalar::Util qw(dualvar);
  2         3  
  2         199  
12              
13 2     2   673 use Net::SFTP::Server::Constants qw(:all);
  2         5  
  2         678  
14 2     2   1269 use Net::SFTP::Server::Buffer;
  2         7  
  2         539  
15             our @CARP_NOT = qw(Net::SFTP::Server::Buffer);
16              
17             our $debug;
18              
19             sub _debug {
20 0     0   0 local $\;
21 0 0       0 print STDERR ((($debug & 256) ? "Server#$$#" : "#"), @_,"\n");
22             }
23              
24             sub _debugf {
25 0     0   0 my $fmt = shift;
26 0         0 _debug sprintf($fmt, @_);
27             }
28              
29             sub _hexdump {
30 2     2   11 no warnings qw(uninitialized);
  2         4  
  2         961  
31 0     0   0 my $data = shift;
32 0         0 while ($data =~ /(.{1,32})/smg) {
33 0         0 my $line=$1;
34 0         0 my @c= (( map { sprintf "%02x",$_ } unpack('C*', $line)),
  0         0  
35             ((" ") x 32))[0..31];
36 0 0       0 $line=~s/(.)/ my $c=$1; unpack("c",$c)>=32 ? $c : '.' /egms;
  0         0  
  0         0  
37 0         0 local $\;
38 0         0 print STDERR join(" ", @c, '|', $line), "\n";
39             }
40             }
41              
42             sub set_error {
43 0     0 0 0 my $self = shift;
44 0         0 my $error = shift;
45 0 0       0 if ($error) {
46 0 0       0 my $str = (@_ ? join('', @_) : "Unknown error $error");
47 0 0 0     0 $debug and $debug & 64 and _debug("error: $error, $str");
48 0         0 $self->{error} = dualvar($error, $str);
49             }
50             else {
51 0         0 $self->{error} = 0
52             }
53             }
54              
55 0     0 0 0 sub error { shift->{error} }
56              
57             sub set_exit {
58 0     0 0 0 my $self = shift;
59 0         0 my $exit = shift;
60 0         0 $self->{exit} = $exit;
61             }
62              
63             sub set_error_and_exit {
64 0     0 0 0 my $self = shift;
65 0         0 my $code = shift;
66 0         0 $self->set_exit(!!$code);
67 0         0 $self->set_error($code, @_);
68             }
69              
70             sub _prepare_fh {
71 0     0   0 my ($name, $fh) = @_;
72 0   0     0 $fh ||= do {
73 2     2   14 no strict 'refs';
  2         3  
  2         7047  
74 0         0 \*{uc "STD$name"};
  0         0  
75             };
76 0 0       0 fileno $fh < 0 and croak "${name}_fh is not a valid file handle";
77 0         0 my $flags = fcntl($fh, F_GETFL, 0);
78 0         0 fcntl($fh, F_SETFL, $flags | O_NONBLOCK);
79 0         0 $fh;
80             }
81              
82             sub new {
83 0 0   0 0 0 @_ & 1 or croak 'Usage: $class->new(%opts)';
84 0         0 my ($class, %opts) = @_;
85              
86 0         0 my $in_fh = _prepare_fh(in => delete $opts{in_fh});
87 0         0 my $out_fh = _prepare_fh(out => delete $opts{out_fh});
88              
89 0         0 my $timeout = delete $opts{timeout};
90              
91 0         0 my $self = { protocol_version => 0,
92             in_fh => $in_fh,
93             out_fh => $out_fh,
94             in_buffer => '',
95             out_buffer => '',
96             in_buffer_max_size => 65 * 1024,
97             max_packet_size => 64 * 1024,
98             packet_handler_cache => [],
99             command_handler_cache => [],
100             timeout => $timeout,
101             };
102              
103 0         0 bless $self, $class;
104              
105 0         0 $self->set_error_and_exit;
106              
107 0         0 return $self;
108             }
109              
110             sub set_protocol_version {
111 0     0 0 0 my ($self, $version) = @_;
112 0         0 $self->{packet_handler_cache} = [];
113 0         0 $self->{command_handler_cache} = [];
114 0         0 $self->{protocol_version} = $version;
115             }
116              
117             sub _do_io_unix {
118 0     0   0 my ($self, $wait_for_packet) = @_;
119              
120 0         0 my $out_b = \$self->{out_buffer};
121 0         0 my $out_fh = $self->{out_fh};
122 0         0 my $out_fn = fileno $out_fh;
123 0         0 my $in_b = \$self->{in_buffer};
124 0         0 my $in_fh = $self->{in_fh};
125 0         0 my $in_fn = fileno $in_fh;
126 0         0 my $in_buffer_max_size = $self->{in_buffer_max_size};
127 0         0 my $timeout = $self->{timeout};
128 0         0 my $packet_len;
129             my $in_fh_closed;
130              
131 0         0 local $SIG{PIPE} = 'IGNORE';
132              
133 0 0 0     0 $debug and $debug & 32 and
134             _debugf("_do_io_unix enter buffer_in: %d, buffer_out: %d",
135             length $$in_b, length $$out_b);
136              
137 0         0 while (1) {
138 0 0 0     0 if (!defined $packet_len and length $$in_b >= 4) {
139 0         0 $packet_len = unpack(N => $$in_b) + 4;
140 0 0 0     0 $debug and $debug & 32 and _debug "_do_io_unix packet_len: $packet_len";
141              
142 0 0       0 if ($packet_len > $in_buffer_max_size) {
143 0         0 $self->set_error_and_exit(1, "Packet of length $packet_len is too big");
144 0         0 return undef;
145             }
146             }
147              
148 0 0 0     0 if (defined $packet_len and $wait_for_packet) {
149 0   0     0 $wait_for_packet = ($packet_len > length $$in_b and
150             !$in_fh_closed);
151 0 0 0     0 $debug and $debug & 32 and _debug "wait_for_packet set to $wait_for_packet";
152             }
153              
154 0 0 0     0 $debug and $debug & 32 and
      0        
155             _debugf("_do_io_unix wait_for_packet: %d, packet_len: %s, in buffer: %d, out buffer: %d",
156             $wait_for_packet,
157             ($packet_len // 'undef'),
158             length($$in_b), length($$out_b));
159              
160 0 0 0     0 last unless ($wait_for_packet or length $$out_b);
161              
162 0         0 my $rb = '';
163 0 0 0     0 length $$in_b < $in_buffer_max_size
164             and !$in_fh_closed
165             and vec($rb, $in_fn, 1) = 1;
166              
167 0         0 my $wb = '';
168 0 0       0 vec($wb, $out_fn, 1) = 1 if length $$out_b;
169              
170 0 0 0     0 $rb eq '' and $wb eq '' and croak "Internal error: useless select";
171              
172 0         0 my $n = select($rb, $wb, undef, $timeout);
173 0 0 0     0 $debug and $debug & 32 and _debug "_do_io_unix select n: $n";
174 0 0       0 if ($n >= 0) {
175 0 0       0 if (vec($wb, $out_fn, 1)) {
176 0         0 my $bytes = syswrite($out_fh, $$out_b);
177 0 0 0     0 if ($debug and $debug & 32) {
178 0   0     0 _debugf("_do_io_unix write queue: %s, syswrite: %s",
179             length $$out_b,
180             ($bytes // 'undef'));
181 0 0 0     0 $debug & 2048 and $bytes and _hexdump(substr($$out_b, 0, $bytes));
182             }
183 0 0       0 if ($bytes) {
184 0         0 substr($$out_b, 0, $bytes, '');
185             }
186             else {
187 0         0 $self->set_error_and_exit(1, "Broken connection");
188 0         0 return undef;
189             }
190             }
191 0 0       0 if (vec($rb, $in_fn, 1)) {
192 0         0 my $bytes = sysread($in_fh, $$in_b, 16*1024, length $$in_b);
193 0 0 0     0 if ($debug and $debug & 32) {
194 0   0     0 _debugf("_do_io_unix sysread: %s, total read: %d",
195             ($bytes // 'undef'),
196             length $$in_b);
197 0 0 0     0 $debug & 1024 and $bytes and _hexdump(substr($$in_b, -$bytes));
198             }
199 0 0       0 unless ($bytes) {
200 0         0 $self->set_error_and_exit(1, "Connection closed by remote peer");
201 0         0 $in_fh_closed = 1;
202 0         0 undef $wait_for_packet;
203             }
204             }
205             }
206             else {
207 0 0 0     0 next if ($n < 0 and $! == Errno::EINTR());
208 0 0 0     0 $debug and $debug & 32
      0        
209             and _debugf("_do_io_unix failed, wait_for_packet: %d, packet_len: %s, in buffer: %d, out buffer: %d, n: %d, \$!: %s (%d)",
210             $wait_for_packet, ($packet_len // 'undef'), length($$in_b), length($$out_b), $n, $!, int $!);
211 0         0 return undef;
212             }
213             }
214 0 0 0     0 $debug and $debug & 32
      0        
215             and _debugf("_do_io_unix done, wait_for_packet: %d, packet_len: %s, in buffer: %d, out buffer: %d",
216             $wait_for_packet, ($packet_len // 'undef'), length($$in_b), length($$out_b));
217              
218 0         0 return !$in_fh_closed;
219             }
220              
221             *_do_io = \&_do_io_unix;
222              
223             sub get_packet {
224 0     0 0 0 my $self = shift;
225 0         0 my $in_b = \$self->{in_buffer};
226 0         0 my $in_b_len = length $$in_b;
227 0 0 0     0 $debug and $debug & 1 and
    0          
228             _debugf("shift packet, in buffer len: %d, peeked packet len: %s",
229             $in_b_len,
230             ($in_b_len >= 4 ? unpack N => $$in_b : '-'));
231              
232 0 0       0 $in_b_len >= 4 or return undef;
233 0         0 my $pkt_len = (unpack N => $$in_b);
234 0 0       0 $in_b_len >= 4 + $pkt_len or return undef;
235 0 0 0     0 $debug and $debug & 1 and _debug("got it!");
236 0         0 substr($$in_b, 0, 4, '');
237 0         0 substr($$in_b, 0, $pkt_len, '');
238             }
239              
240             my %packer = ( uint8 => \&buf_push_uint8,
241             uint32 => \&buf_push_uint32,
242             uint64 => sub { croak "uint64 packing unimplemented" },
243             str => \&buf_push_str,
244             utf8 => \&buf_push_utf8,
245             name => \&buf_push_name,
246             attrs => \&buf_push_attrs,
247             raw => \&buf_push_raw);
248              
249             sub push_packet {
250 0     0 0 0 my $self = shift;
251 0         0 my $out_b = \$self->{out_buffer};
252 0 0       0 if (length $$out_b) {
253 0         0 $self->set_error_and_exit(1,
254             "Internal error, packet already in output buffer");
255 0         0 return undef;
256             }
257              
258 0 0       0 if (@_ == 1) {
259 0         0 buf_push_str($$out_b, $_[0]);
260             }
261             else {
262 0 0       0 @_ & 1 and croak 'Usage: $sftp_server->push_packet(type => data, type => data, ...) or $sftp_server->push_packet($load)';
263 0         0 $$out_b = "\x00\x00\x00\x00";
264 0         0 while (@_) {
265 0         0 my $type = shift;
266 0         0 my $packer = $packer{$type};
267 0 0       0 if (defined $packer) {
268 0         0 $packer->($$out_b, $_[0]);
269 0         0 shift;
270             }
271             else {
272 0         0 $self->set_error_and_exit(1,
273             "Internal error, invalid packing type $type");
274 0         0 return;
275             }
276             }
277 0         0 substr $$out_b, 0, 4, pack(N => (length($$out_b) - 4));
278             }
279 0 0 0     0 if ($debug and $debug & 1) {
280 0         0 _debugf "push_packet packet len %d", length $$out_b;
281 0 0       0 $debug & 8 and _hexdump $$out_b;
282             }
283              
284 0         0 1;
285             }
286              
287             my %command_id = (init => 1,
288             open => 3,
289             close => 4,
290             read => 5,
291             write => 6,
292             lstat => 7,
293             fstat => 8,
294             setstat => 9,
295             fsetstat => 10,
296             opendir => 11,
297             readdir => 12,
298             remove => 13,
299             mkdir => 14,
300             rmdir => 15,
301             realpath => 16,
302             stat => 17,
303             rename => 18,
304             readlink => 19,
305             symlink => 20,
306             link => 21,
307             block => 22,
308             unblock => 23,
309             extended => 200);
310              
311             my %response_id = (version => 2,
312             status => 101,
313             handle => 102,
314             data => 103,
315             name => 104,
316             attrs => 105,
317             extended => 201);
318              
319             my @command_name;
320             while (my ($k, $v) = each %command_id) {
321             $command_name[$v] = $k;
322             }
323              
324 0     0 0 0 sub command_name { $command_name[$_[1]] }
325              
326 0     0 0 0 sub response_id { $response_id{$_[1]} }
327              
328             sub dispatch_packet {
329 0     0 0 0 my $self = shift;
330 0 0       0 my ($cmd) = buf_shift_uint8($_[0])
331             or return $self->bad_packet();
332 0 0       0 my ($id) = ($cmd == 1 ? undef : buf_shift_uint32 $_[0])
    0          
333             or return $self->bad_packet($cmd);
334              
335 0 0 0     0 $debug and $debug & 1
      0        
336             and _debugf("dispatch packet cmd %s, id: %s", $cmd, ($id // '-'));
337              
338 0   0     0 my $sub = $self->{_packet_handler_cache}[$cmd] ||= do {
339 0   0     0 my $name = $self->command_name($cmd) || 'unknown';
340 0 0 0     0 $self->can("handle_packet_${name}_v$self->{protocol_version}") ||
341             $self->can("handle_packet_${name}") ||
342             $self->can('unsupported_command');
343             };
344 0 0 0     0 $debug and $debug & 4096 and _debug "packet handler: $sub";
345 0         0 $sub->($self, $cmd, $id, $_[0]);
346             }
347              
348             my @status_messages = ( "ok",
349             "eof",
350             "no such file",
351             "permission denied",
352             "failure",
353             "bad message",
354             "no connection",
355             "connection lost",
356             "operation not supported" );
357              
358             sub push_status_response {
359 0     0 0 0 my ($self, $id, $status, $msg, $lang) = @_;
360 0   0     0 $msg //= ($status_messages[$status] // "failure");
      0        
361 0   0     0 $lang //= 'en';
362 0 0 0     0 $debug and $debug & 2 and _debug "push id: $id, status: $status, msg: $msg, lang: $lang";
363 0         0 $self->push_packet(uint8 => SSH_FXP_STATUS,
364             uint32 => $id, uint32 => $status,
365             utf8 => $msg, str => $lang);
366             }
367              
368             sub push_status_ok_response {
369 0     0 0 0 my ($self, $id) = @_;
370 0         0 $self->push_status_response($id, SSH_FX_OK)
371             }
372              
373             sub push_status_eof_response {
374 0     0 0 0 my ($self, $id) = @_;
375 0         0 $self->push_status_response($id, SSH_FX_EOF)
376             }
377              
378             sub push_handle_response {
379 0     0 0 0 my ($self, $id, $hid) = @_;
380 0 0 0     0 $debug and $debug & 2 and _debug "push handle hid: $hid";
381 0         0 $self->push_packet(uint8 => SSH_FXP_HANDLE, uint32 => $id, str => $hid);
382             }
383              
384             sub push_name_response {
385 0     0 0 0 my $self = shift;
386 0         0 my $id = shift;
387 0         0 my $count = @_;
388 0         0 $self->push_packet(uint8 => SSH_FXP_NAME,
389             uint32 => $id, uint32 => $count,
390 0         0 map { (name => $_) } @_);
391             }
392              
393             sub push_attrs_response {
394 0     0 0 0 my ($self, $id, $attrs) = @_;
395 0         0 $self->push_packet(uint8 => SSH_FXP_ATTRS,
396             uint32 => $id, attrs => $attrs);
397             }
398              
399             sub unsupported_command {
400 0     0 0 0 my ($self, $cmd, $id) = @_;
401 0   0     0 my $name = (uc $self->command_name($cmd) || $cmd);
402 0 0 0     0 $debug and $debug & 2
      0        
403             and _debugf("unsupported command %s [%d], id: %s",
404             $name, $cmd, ($id // '-'));
405 0         0 $self->push_status_response($id, SSH_FX_OP_UNSUPPORTED,
406             "command $name is not supported");
407             }
408              
409             sub run {
410 0     0 0 0 my $self = shift;
411 0         0 until ($self->{exit}) {
412 0 0       0 $self->_do_io(1) or next;
413 0         0 my $pkt = $self->get_packet;
414 0 0       0 $self->dispatch_packet($pkt) if defined $pkt;
415             }
416 0         0 $self->{exit};
417             }
418              
419             sub bad_packet {
420 0     0 0 0 my ($self, $cmd, $id) = @_;
421 0   0     0 $cmd //= 'undef';
422 0   0     0 $id //= 'id';
423 0         0 $self->set_error_and_exit(1, "Invalid packet cmd: $cmd, id: $id");
424             }
425              
426             sub bad_command {
427 0     0 0 0 my ($self, $cmd, $id, $msg) = @_;
428 0         0 my $str = "Bad message";
429 0 0       0 $str .= ": $msg" if defined $msg;
430 0         0 $self->push_status_response($id, SSH_FX_BAD_MESSAGE, $str);
431             }
432              
433             sub dispatch_command {
434 0     0 0 0 my $self = shift;
435 0         0 my $cmd = shift;
436              
437 0 0 0     0 $debug and $debug & 2
      0        
      0        
438             and _debugf("dispatch command cmd %d %s, id: %s",
439             $cmd,
440             ($self->command_name($cmd) // '-'),
441             ($_[0] // '-'));
442              
443 0   0     0 my $sub = $self->{_command_handler_cache}[$cmd] ||= do {
444 0   0     0 my $name = $self->command_name($cmd) || 'unknown';
445             $self->can("handle_command_${name}_v$self->{protocol_version}") ||
446             $self->can("handle_command_${name}") ||
447 0 0 0 0   0 sub { shift->unsupported_command($cmd, $_[0]) };
  0         0  
448             };
449 0         0 $sub->($self, @_);
450             }
451              
452             sub handle_packet_init_v0 {
453 0     0 0 0 my ($self, $cmd) = @_;
454 0   0     0 my $version = buf_shift_uint32($_[3]) // goto BAD_PACKET;
455 0         0 my @ext;
456 0         0 while (length $_[3]) {
457 0   0     0 push (@ext,
      0        
458             (buf_shift_str($_[3]) // goto BAD_PACKET),
459             (buf_shift_str($_[3]) // goto BAD_PACKET));
460             }
461 0         0 return $self->dispatch_command($cmd, undef, $version, @ext);
462              
463 0         0 BAD_PACKET:
464             return $self->bad_packet($cmd);
465             }
466              
467             sub handle_command_init_v0 {
468 0     0 0 0 my $self = shift;
469 0         0 shift; # $id
470 0         0 my $version = shift;
471 0 0       0 $version >= 3 or return $self->bad_packet(1);
472 0         0 $self->set_protocol_version(3);
473 0         0 $self->push_packet(uint8 => SSH_FXP_VERSION, uint32 => 3,
474 0         0 map { (str => $_) } $self->server_extensions);
475             }
476              
477             sub server_extensions {
478 0     0 0 0 return ('libnet-sftp-server@cpan.org' => 1);
479             }
480              
481             sub _make_packet_handler {
482 36     36   56 my $name = shift;
483 36         222 my @args = map "\n (buf_shift_$_(\$_[3]) // goto BAD_PACKET)", @_;
484 36         85 my $args = join(",", @args);
485 36         74 my $code = <
486             sub {
487             my (\$self, \$cmd, \$id) = \@_;
488             \$debug and \$debug & 2 and _debug "$name unpacker called";
489             return \$self->dispatch_command(\$cmd, \$id,$args);
490             BAD_PACKET:
491             \$self->bad_command(\$cmd, \$id, 'missing parameter')
492             }
493             EOC
494 36 50 33     97 $debug and $debug & 16384 and _debug "$name packet handler code:\n$code";
495 36         48 my $method = "handle_packet_$name";
496 2     2   22 no strict 'refs';
  2         5  
  2         657  
497 36 0 0 0   5834 *$method = eval $code;
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0 0 0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0   0        
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
498             }
499              
500             _make_packet_handler open_v3 => qw(utf8 uint32 attrs);
501             _make_packet_handler close_v3 => qw(str);
502             _make_packet_handler read_v3 => qw(str uint64 uint32);
503             _make_packet_handler write_v3 => qw(str uint64 str);
504             _make_packet_handler stat_v3 => qw(utf8);
505             _make_packet_handler lstat_v3 => qw(utf8);
506             _make_packet_handler fstat_v3 => qw(str);
507             _make_packet_handler setstat_v3 => qw(utf8 attrs);
508             _make_packet_handler fsetstat_v3 => qw(str attrs);
509             _make_packet_handler opendir_v3 => qw(utf8);
510             _make_packet_handler readdir_v3 => qw(str);
511             _make_packet_handler remove_v3 => qw(utf8);
512             _make_packet_handler mkdir_v3 => qw(utf8 attrs);
513             _make_packet_handler rmdir_v3 => qw(utf8);
514             _make_packet_handler realpath_v3 => qw(utf8);
515             _make_packet_handler rename_v3 => qw(utf8 utf8);
516             _make_packet_handler readlink_v3 => qw(utf8);
517             _make_packet_handler symlink_v3 => qw(utf8 utf8 utf8);
518              
519             1;
520             __END__