File Coverage

lib/SMB/Client.pm
Criterion Covered Total %
statement 42 316 13.2
branch 0 164 0.0
condition 0 148 0.0
subroutine 14 32 43.7
pod 1 16 6.2
total 57 676 8.4


line stmt bran cond sub pod time code
1             # SMB Perl library, Copyright (C) 2014-2018 Mikhael Goikhman, migo@cpan.org
2             #
3             # This program is free software: you can redistribute it and/or modify
4             # it under the terms of the GNU General Public License as published by
5             # the Free Software Foundation, either version 3 of the License, or
6             # (at your option) any later version.
7             #
8             # This program is distributed in the hope that it will be useful,
9             # but WITHOUT ANY WARRANTY; without even the implied warranty of
10             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11             # GNU General Public License for more details.
12             #
13             # You should have received a copy of the GNU General Public License
14             # along with this program. If not, see .
15              
16             package SMB::Client;
17              
18 1     1   1413 use strict;
  1         2  
  1         25  
19 1     1   5 use warnings;
  1         1  
  1         25  
20              
21 1     1   4 use parent 'SMB::Agent';
  1         1  
  1         6  
22              
23 1     1   48 use SMB::v2::Commands;
  1         2  
  1         28  
24 1     1   368 use SMB::v2::Command::Negotiate;
  1         2  
  1         27  
25 1     1   353 use SMB::v2::Command::SessionSetup;
  1         2  
  1         26  
26 1     1   359 use SMB::v2::Command::TreeConnect;
  1         3  
  1         26  
27 1     1   401 use SMB::v2::Command::Create;
  1         5  
  1         45  
28 1     1   558 use SMB::v2::Command::Close;
  1         6  
  1         40  
29 1     1   555 use SMB::v2::Command::QueryDirectory;
  1         3  
  1         41  
30 1     1   511 use SMB::v2::Command::Read;
  1         4  
  1         39  
31 1     1   578 use SMB::v2::Command::Write;
  1         3  
  1         40  
32 1     1   507 use SMB::v2::Command::SetInfo;
  1         3  
  1         35  
33 1     1   468 use SMB::Tree;
  1         3  
  1         4442  
34              
35             sub new ($$%) {
36 0     0 1   my $class = shift;
37 0           my $share_uri = shift;
38 0           my %options = @_;
39              
40 0           my $self = $class->SUPER::new(
41             %options,
42             server_id => 0, # running index
43             curr_conn_key => undef, # key in connections hash
44             unique_conn_addr => 1,
45             );
46              
47 0 0         $self->connect($share_uri, %options)
48             if $share_uri;
49              
50 0           return $self;
51             }
52              
53             sub connect ($$%) {
54 0     0 0   my $self = shift;
55 0           my $share_uri = shift;
56 0           my %options = @_;
57              
58 0 0         my ($addr, $share) = $share_uri =~ m![/\\]!
59             ? $self->parse_share_uri($share_uri)
60             : ($share_uri);
61 0 0         die "Please specify share uri //server.name.or.ip[:port]/share or server.name.or.ip[:port]\n"
62             unless $addr;
63 0 0         $addr .= ':445' unless $addr =~ /:/;
64              
65 0 0         my $socket = IO::Socket::INET->new(PeerAddr => $addr, Proto => 'tcp')
66             or die "Can't open $addr: $!\n";
67              
68 0 0         if ($options{just_socket}) {
69 0           $self->{socket} = $socket;
70             } else {
71             my ($conn_key) = $self->add_connection(
72             $socket, --$self->{server_id},
73             addr => $addr,
74             share => $share,
75             username => $options{username},
76             password => $options{password},
77 0           tree => undef,
78             cwd => '',
79             openfiles => {},
80             dialect => undef,
81             session_id => 0,
82             message_id => 0,
83             sent_request => undef,
84             );
85              
86 0           $self->curr_conn_key($conn_key);
87             }
88              
89 0           return $self;
90             }
91              
92             sub get_curr_connection ($) {
93 0     0 0   my $self = shift;
94              
95 0           my $connections = $self->connections;
96 0 0         unless (%$connections) {
97 0           $self->err("Called get_curr_connection without established connections");
98 0           return;
99             }
100              
101 0           my $connection = $connections->{$self->curr_conn_key};
102 0 0         unless ($connection) {
103 0           $self->err("Called get_curr_connection when curr_conn_key is invalid");
104 0           return;
105             }
106              
107 0           return $connection;
108             }
109              
110             sub find_connection_by_tree ($$) {
111 0     0 0   my $self = shift;
112 0   0       my $tree = shift // die;
113              
114 0           for (values %{$self->connections}) {
  0            
115 0 0         return $_ if $_->tree == $tree;
116             }
117              
118 0           $self->err("Can't find connection for $tree");
119 0           return;
120             }
121              
122             sub process_request ($$$%) {
123 0     0 0   my $self = shift;
124 0           my $connection = shift;
125 0           my $command_name = shift;
126 0           my %command_options = @_;
127              
128 0           my $command_class = "SMB::v2::Command::$command_name";
129 0           my $command_code = $SMB::v2::Commands::command_codes{$command_name};
130 0           my $no_warn = delete $command_options{_no_warn};
131              
132             my $request = $command_class->new(
133             SMB::v2::Header->new(
134 0 0         mid => $connection->{message_id}++,
135             uid => $connection->session_id,
136             tid => $connection->tree ? $connection->tree->id : 0,
137             code => $command_code,
138             ),
139             );
140 0           $request->set(%command_options);
141              
142 0           $connection->send_command($request);
143 0           $connection->sent_request($request);
144              
145 0           my $response = $self->wait_for_response($connection);
146              
147 0 0 0       warn "SMB Error on $command_name response: " . ($response ? $response->status_name : "internal") . "\n"
    0 0        
148             if !$no_warn && (!$response || $response->is_error);
149              
150 0           return $response;
151             }
152              
153             sub process_negotiate_if_needed ($$) {
154 0     0 0   my $self = shift;
155 0           my $connection = shift;
156              
157 0 0         return 1 if $connection->dialect;
158              
159 0           my $response = $self->process_request($connection, 'Negotiate');
160 0 0 0       if ($response && $response->is_success) {
161 0 0         unless ($connection->auth->process_spnego($response->security_buffer)) {
162 0           $self->err("Server does not support our negotiation mechanism, expect problems on SessionSetup");
163             }
164 0           $connection->dialect($response->dialect);
165 0           return 1;
166             }
167              
168 0           return 0;
169             }
170              
171             sub process_sessionsetup_if_needed ($$) {
172 0     0 0   my $self = shift;
173 0           my $connection = shift;
174              
175 0 0         return 1 if $connection->session_id;
176              
177 0           my $response = $self->process_request($connection, 'SessionSetup',
178             security_buffer => $connection->auth->generate_spnego,
179             _no_warn => 1,
180             );
181 0           my $more_processing = $response->status == SMB::STATUS_MORE_PROCESSING_REQUIRED;
182 0 0 0       return 0
      0        
      0        
183             unless $response && ($response->is_success || $more_processing)
184             && $connection->auth->process_spnego($response->security_buffer);
185 0           $connection->session_id($response->header->uid);
186 0 0         return 1 if $response->is_success;
187 0 0         return 0 unless $more_processing;
188              
189 0           $response = $self->process_request($connection, 'SessionSetup',
190             security_buffer => $connection->auth->generate_spnego(
191             username => $connection->username,
192             password => $connection->password,
193             ),
194             );
195 0 0 0       if ($response && $response->is_success && $connection->auth->process_spnego($response->security_buffer)) {
      0        
196 0 0         die "Got different session id on second SessionSetup"
197             unless $connection->session_id == $response->header->uid;
198 0           return 1;
199             }
200              
201 0           return 0;
202             }
203              
204             sub check_session ($$) {
205 0     0 0   my $self = shift;
206 0           my $connection = shift;
207              
208             return
209 0   0       $self->process_negotiate_if_needed($connection) &&
210             $self->process_sessionsetup_if_needed($connection);
211             }
212              
213             sub connect_tree ($%) {
214 0     0 0   my $self = shift;
215 0           my %options = @_;
216              
217 0   0       my $connection = $self->get_curr_connection || return;
218              
219             my ($addr, $share, $username, $password) =
220 0 0 0       map { $options{$_} || $connection->{$_} || die "No $_ to connect_tree\n" }
  0            
221             qw(addr share username password);
222              
223 0 0         return unless $self->check_session($connection);
224              
225 0           $addr =~ s/:\d+//;
226 0           my $response = $self->process_request($connection, 'TreeConnect', uri => "\\\\$addr\\$share");
227 0 0 0       if ($response && $response->is_success) {
228 0           my $tree = SMB::Tree->new(
229             $share, $response->header->tid,
230             addr => $addr,
231             client => $self,
232             cwd => '',
233             );
234 0           $connection->tree($tree);
235 0           return $tree;
236             }
237              
238 0           return;
239             }
240              
241             sub _normalize_path ($$;$) {
242 0   0 0     my $path = shift // '';
243 0   0       my $base = shift // '';
244 0   0       my $to_dos = shift || 0;
245              
246 0 0         $path = "$base/$path" if $path =~ m!^[^/]!;
247              
248 0           $path =~ s![/\\]+$!/!g; # to unix
249             # remove "./", "any/../", "../.." at the end
250 0           while ($path =~ s=(^|/)\.(?:/|$)=$1=g) {}
251 0           while ($path =~ s=(^|/)(?!\.\./)[^/]+/\.\.(?:/|$)=$1=g) {}
252 0           $path =~ s!(?:(?:^|/)\.\.)+/?$!!;
253 0           $path =~ s!/$!!;
254              
255 0 0         if ($to_dos) {
256 0           $path =~ s=^/==;
257 0           $path =~ s=/=\\=g;
258             }
259              
260 0           return $path;
261             }
262              
263             sub _basename ($;$) {
264 0   0 0     my $path = shift // '';
265 0   0       my $is_dos = shift || 0;
266              
267 0 0         my $delim = $is_dos ? '\\' : '/';
268              
269 0 0         return $path =~ /.*\Q$delim\E(.*)/ ? $1 : $path;
270             }
271              
272             sub dnload_file ($$$$) {
273 0     0 0   my $self = shift;
274 0           my $connection = shift;
275 0   0       my $filename = shift // return $self->err("No remote file name to download");
276 0   0       my $dst_filename = shift // return $self->err("No local file name to save");
277              
278 0           my $response = $self->process_request($connection, 'Create',
279             file_name => $filename,
280             );
281 0 0 0       return unless $response && $response->is_success;
282              
283 0           my $file = $response->openfile->file;
284 0           my $fid = $response->fid;
285 0           my $remaining = $file->end_of_file;
286 0           my $time = $file->mtime;
287 0           my $content = '';
288 0           my $offset = 0;
289 0           while ($remaining) {
290 0 0         my $length = $remaining >= 65536 ? 65536 : $remaining;
291 0           $remaining -= $length;
292 0           $response = $self->process_request($connection, 'Read',
293             fid => $fid,
294             offset => $offset,
295             length => $length,
296             remaining_bytes => $remaining,
297             );
298 0 0 0       return unless $response && $response->is_success;
299 0           my $read = $response->length;
300 0 0         return $self->err("Unexpected $read bytes read instead of $length at offset $offset")
301             if $read != $length;
302 0           $content .= $response->buffer;
303 0           $offset += $length;
304             }
305 0           $self->process_request($connection, 'Close',
306             fid => $fid,
307             );
308              
309 0 0         open DST, '>', $dst_filename
310             or return $self->err("Can't open $dst_filename for write: $!");
311 0 0         print DST $content
312             or return $self->err("Can't write content to $dst_filename: $!");
313 0 0         close DST
314             or return $self->err("Can't close $dst_filename after write: $!");
315              
316             # consider to set $time on file
317 0           return 1;
318             }
319              
320             sub upload_file ($$$$) {
321 0     0 0   my $self = shift;
322 0           my $connection = shift;
323 0   0       my $filename = shift // return $self->err("No local file name to load");
324 0   0       my $dst_filename = shift // return $self->err("No remote file name to upload");
325              
326 0           local $/ = undef;
327 0 0         open SRC, '<', $filename
328             or return $self->err("Can't open $filename for read: $!");
329 0   0       my $content =
330             // return $self->err("Can't read content from $filename: $!");
331 0 0         close SRC
332             or return $self->err("Can't close $filename after read: $!");
333              
334 0           my $response = $self->process_request($connection, 'Create',
335             file_name => $dst_filename,
336             options => SMB::v2::Command::Create::OPTIONS_NON_DIRECTORY_FILE,
337             access_mask => 0x12019f,
338             disposition => SMB::File::DISPOSITION_OVERWRITE_IF,
339             );
340 0 0 0       return unless $response && $response->is_success;
341 0           my $fid = $response->fid;
342 0           my $remaining = length($content);
343 0           my $offset = 0;
344 0           while ($remaining) {
345 0 0         my $length = $remaining >= 65536 ? 65536 : $remaining;
346 0           $remaining -= $length;
347 0           $response = $self->process_request($connection, 'Write',
348             fid => $fid,
349             offset => $offset,
350             remaining_bytes => $remaining,
351             buffer => substr($content, $offset, $length),
352             );
353 0 0 0       return unless $response && $response->is_success;
354 0           my $written = $response->length;
355 0 0         return $self->err("Unexpected $written bytes written instead of $length at offset $offset")
356             if $written != $length;
357 0           $offset += $length;
358             }
359 0           $self->process_request($connection, 'Close',
360             fid => $fid,
361             );
362              
363 0           return 1;
364             }
365              
366             sub remove_file ($$$$) {
367 0     0 0   my $self = shift;
368 0           my $connection = shift;
369 0   0       my $file = shift // return $self->err("No file to remove");
370 0           my $recursive = shift;
371              
372 0           my $remove_using_setinfo = $ENV{SMB_CLIENT_REMOVE_FILE_USING_SETINFO};
373              
374 0 0         my $options = $file->is_directory
375             ? SMB::v2::Command::Create::OPTIONS_DIRECTORY_FILE
376             : SMB::v2::Command::Create::OPTIONS_NON_DIRECTORY_FILE;
377 0 0         $options |= SMB::v2::Command::Create::OPTIONS_DELETE_ON_CLOSE
378             unless $remove_using_setinfo;
379 0           my $response = $self->process_request($connection, 'Create',
380             file_name => $file->name,
381             options => $options,
382             access_mask => 0x10081,
383             );
384 0 0 0       return unless $response && $response->is_success;
385 0           my $fid = $response->fid;
386              
387 0 0         if ($remove_using_setinfo) {
388 0           $response = $self->process_request($connection, 'SetInfo',
389             fid => $fid,
390             type => SMB::v2::Command::SetInfo::TYPE_FILE,
391             level => SMB::v2::Command::SetInfo::FILE_LEVEL_DISPOSITION,
392             buffer => chr(SMB::v2::Command::SetInfo::FILE_DISPOSITION_DELETE_ON_CLOSE),
393             );
394 0 0 0       return unless $response && $response->is_success;
395             }
396              
397 0 0 0       if ($recursive && $file->is_directory) {
398 0           my @files = ();
399 0           while (1) {
400 0           $response = $self->process_request($connection, 'QueryDirectory',
401             file_pattern => "*",
402             fid => $fid,
403             );
404 0 0 0       last if $response && $response->status == SMB::STATUS_NO_MORE_FILES;
405 0 0 0       return $self->err("Failed to get file list in " . $file->name)
406             unless $response && $response->is_success;
407 0           push @files, @{$response->files};
  0            
408             }
409 0           my $dirname = $file->name;
410 0           for my $file (@files) {
411             # TODO: consider to have full file name already on parse-response
412 0 0         $file->name("$dirname\\" . $file->name) if $dirname;
413 0 0         next if $file->name =~ m/(^|\\)\.\.?$/;
414 0 0         return $self->err("Failed to remove inner ". $file->name)
415             unless $self->remove_file($connection, $file, 1);
416             }
417             }
418              
419 0           $self->process_request($connection, 'Close',
420             fid => $fid,
421             );
422 0 0 0       return unless $response && $response->is_success;
423              
424 0           return 1;
425             }
426              
427             sub rename_file ($$$$;$) {
428 0     0 0   my $self = shift;
429 0           my $connection = shift;
430 0   0       my $filename1 = shift // return $self->err("No old filename to rename");
431 0   0       my $filename2 = shift // return $self->err("No new filename to rename");
432 0   0       my $force = shift || 0;
433              
434 0           my $response = $self->process_request($connection, 'Create',
435             file_name => $filename1,
436             options => 0,
437             access_mask => 0x10081,
438             );
439 0 0 0       return unless $response && $response->is_success;
440 0           my $fid = $response->fid;
441              
442 0 0         my $rename_struct = SMB::Packer->new
443             ->uint8($force ? 1 : 0)
444             ->zero(7) # reserved
445             ->zero(8) # root dir handle
446             ->uint16(length($filename2) * 2)
447             ->uint16(0) # reserved
448             ->str($filename2);
449              
450 0           $response = $self->process_request($connection, 'SetInfo',
451             fid => $fid,
452             type => SMB::v2::Command::SetInfo::TYPE_FILE,
453             level => SMB::v2::Command::SetInfo::FILE_LEVEL_RENAME,
454             buffer => $rename_struct->data,
455             );
456 0 0 0       return unless $response && $response->is_success;
457              
458 0           $self->process_request($connection, 'Close',
459             fid => $fid,
460             );
461 0 0 0       return unless $response && $response->is_success;
462              
463 0           return 1;
464             }
465              
466             sub perform_tree_command ($$$@) {
467 0     0 0   my $self = shift;
468 0           my $tree = shift;
469 0           my $command = shift;
470              
471 0   0       my $connection = $self->find_connection_by_tree($tree) || return;
472 0 0 0       my %options = @_ && ref($_[0]) eq 'HASH' ? %{shift()} : ();
  0            
473              
474 0 0         if ($command eq 'chdir') {
    0          
    0          
    0          
    0          
    0          
    0          
475 0   0       my $dir = shift // '';
476              
477 0           $tree->cwd(_normalize_path($dir, $tree->cwd));
478             }
479             elsif ($command eq 'find') {
480 0   0       my $pattern = _normalize_path(shift || "*", $tree->cwd, 1);
481 0 0         my $dirname = $pattern =~ /^(.*)\\(.*)/ ? $1 : "";
482 0 0         $pattern = $2 if $2;
483              
484 0           my $response = $self->process_request($connection, 'Create',
485             file_name => $dirname,
486             file_attributes => SMB::File::ATTR_DIRECTORY,
487             );
488 0 0 0       return unless $response && $response->is_success;
489 0           my $fid = $response->fid;
490 0           $response = $self->process_request($connection, 'QueryDirectory',
491             file_pattern => $pattern,
492             fid => $fid,
493             );
494 0 0 0       my $files = $response && $response->is_success ? $response->files : undef;
495 0           $self->process_request($connection, 'Close',
496             fid => $fid,
497             );
498              
499 0 0         return unless $files;
500 0 0         return wantarray ? @$files : $files;
501             }
502             elsif ($command eq 'dnload') {
503 0   0       my $filename = shift // '';
504 0 0         return $self->err("No filename") if $filename eq '';
505 0           $filename = _normalize_path($filename, $tree->cwd, 1);
506 0   0       my $dst_filename = _normalize_path(shift || _basename($filename, 1), '.');
507              
508 0           return $self->dnload_file($connection, $filename, $dst_filename);
509             }
510             elsif ($command eq 'upload') {
511 0   0       my $filename = shift // '';
512 0 0         return $self->err("No filename") if $filename eq '';
513 0           $filename = _normalize_path($filename, '.');
514 0   0       my $dst_filename = _normalize_path(shift || _basename($filename), $tree->cwd, 1);
515              
516 0           return $self->upload_file($connection, $filename, $dst_filename);
517             }
518             elsif ($command eq 'remove') {
519 0   0       my $filename = shift // '';
520 0 0         return $self->err("No filename") if $filename eq '';
521 0           $filename = _normalize_path($filename, $tree->cwd, 1);
522              
523 0           my $recursive = $options{recursive};
524 0   0       my $is_dir = shift // $recursive;
525 0           my $file = SMB::File->new(name => $filename, is_directory => $is_dir);
526              
527 0           return $self->remove_file($connection, $file, $recursive);
528             }
529             elsif ($command eq 'rename') {
530 0   0       my $filename1 = shift // '';
531 0 0         return $self->err("No filename1") if $filename1 eq '';
532 0           $filename1 = _normalize_path($filename1, $tree->cwd, 1);
533 0   0       my $filename2 = shift // '';
534 0 0         return $self->err("No filename2") if $filename2 eq '';
535 0           $filename2 = _normalize_path($filename2, $tree->cwd, 1);
536 0           my $force = $options{force};
537              
538 0           return $self->rename_file($connection, $filename1, $filename2, $force);
539             }
540             elsif ($command eq 'copy') {
541 0   0       my $filename1 = shift // '';
542 0 0         return $self->err("No filename1") if $filename1 eq '';
543 0           $filename1 = _normalize_path($filename1, $tree->cwd, 1);
544 0   0       my $filename2 = shift // '';
545 0 0         return $self->err("No filename2") if $filename2 eq '';
546 0           $filename2 = _normalize_path($filename2, $tree->cwd, 1);
547              
548 0           my $tmp_filename = "/var/tmp/copy-$$";
549 0   0       my $success =
550             $self->dnload_file($connection, $filename1, $tmp_filename) &&
551             $self->upload_file($connection, $tmp_filename, $filename2);
552 0           unlink $tmp_filename;
553              
554 0           return $success;
555             }
556              
557 0           return;
558             }
559              
560             sub on_response ($$$$) {
561 0     0 0   my $self = shift;
562 0           my $connection = shift;
563 0           my $response = shift;
564 0           my $request = shift;
565              
566 0           return 0;
567             }
568              
569             sub wait_for_response ($$) {
570 0     0 0   my $self = shift;
571 0           my $connection = shift;
572 0           my $request = $connection->sent_request;
573              
574 0 0         return unless $request;
575              
576 0           my ($response) = $connection->recv_command;
577 0 0         if (!$response) {
578 0           $self->delete_connection($connection);
579 0           return;
580             }
581              
582 0 0 0       if ($response->is_response_to($request) && $response->status == SMB::STATUS_PENDING) {
583 0           $self->dbg("Ignoring STATUS_PENDING response");
584 0           ($response) = $connection->recv_command;
585             }
586              
587 0 0         unless ($response->is_response_to($request)) {
588 0           $self->err("Unexpected: " . $response->to_string);
589 0           return;
590             }
591              
592 0           return $response;
593             }
594              
595             1;