File Coverage

lib/SMB/Client.pm
Criterion Covered Total %
statement 39 242 16.1
branch 0 122 0.0
condition 0 95 0.0
subroutine 13 27 48.1
pod 1 12 8.3
total 53 498 10.6


line stmt bran cond sub pod time code
1             # SMB Perl library, Copyright (C) 2014 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   1816 use strict;
  1         2  
  1         28  
19 1     1   6 use warnings;
  1         2  
  1         30  
20              
21 1     1   5 use parent 'SMB::Agent';
  1         2  
  1         8  
22              
23 1     1   48 use SMB::v2::Commands;
  1         1  
  1         25  
24 1     1   574 use SMB::v2::Command::Negotiate;
  1         2  
  1         28  
25 1     1   561 use SMB::v2::Command::SessionSetup;
  1         3  
  1         31  
26 1     1   599 use SMB::v2::Command::TreeConnect;
  1         2  
  1         30  
27 1     1   675 use SMB::v2::Command::Create;
  1         3  
  1         36  
28 1     1   650 use SMB::v2::Command::Close;
  1         3  
  1         24  
29 1     1   536 use SMB::v2::Command::QueryDirectory;
  1         5  
  1         36  
30 1     1   614 use SMB::v2::Command::Read;
  1         2  
  1         24  
31 1     1   508 use SMB::v2::Command::Write;
  1         2  
  1         28  
32 1     1   560 use SMB::Tree;
  1         2  
  1         2766  
33              
34             sub new ($$%) {
35 0     0 1   my $class = shift;
36 0           my $share_uri = shift;
37 0           my %options = @_;
38              
39 0   0       my $verbose = delete $options{verbose} || 0;
40 0 0         $options{quiet} = 1 unless $verbose;
41              
42 0           my $self = $class->SUPER::new(
43             %options,
44             server_id => 0, # running index
45             curr_conn_key => undef, # key in connections hash
46             unique_conn_addr => 1,
47             );
48              
49 0 0         $self->connect($share_uri, %options)
50             if $share_uri;
51              
52 0           return $self;
53             }
54              
55             sub connect ($$%) {
56 0     0 0   my $self = shift;
57 0           my $share_uri = shift;
58 0           my %options = @_;
59              
60 0 0         my ($addr, $share) = $share_uri =~ m![/\\]!
61             ? $self->parse_share_uri($share_uri)
62             : ($share_uri);
63 0 0         die "Please specify share uri //server.name.or.ip[:port]/share or server.name.or.ip[:port]\n"
64             unless $addr;
65 0 0         $addr .= ':445' unless $addr =~ /:/;
66              
67 0 0         my $socket = IO::Socket::INET->new(PeerAddr => $addr, Proto => 'tcp')
68             or die "Can't open $addr: $!\n";
69              
70 0 0         if ($options{just_socket}) {
71 0           $self->{socket} = $socket;
72             } else {
73 0           my ($conn_key) = $self->add_connection(
74             $socket, --$self->{server_id},
75             addr => $addr,
76             share => $share,
77             username => $options{username},
78             password => $options{password},
79             tree => undef,
80             cwd => '',
81             openfiles => {},
82             dialect => undef,
83             session_id => 0,
84             message_id => 0,
85             sent_request => undef,
86             );
87              
88 0           $self->curr_conn_key($conn_key);
89             }
90              
91 0           return $self;
92             }
93              
94             sub get_curr_connection ($) {
95 0     0 0   my $self = shift;
96              
97 0           my $connections = $self->connections;
98 0 0         unless (%$connections) {
99 0           $self->err("Called get_curr_connection when no connections established");
100 0           return;
101             }
102              
103 0           my $connection = $connections->{$self->curr_conn_key};
104 0 0         unless ($connection) {
105 0           $self->err("Called get_curr_connection when curr_conn_key is invalid");
106 0           return;
107             }
108              
109 0           return $connection;
110             }
111              
112             sub find_connection_by_tree ($$) {
113 0     0 0   my $self = shift;
114 0   0       my $tree = shift // die;
115              
116 0           for (values %{$self->connections}) {
  0            
117 0 0         return $_ if $_->tree == $tree;
118             }
119              
120 0           $self->err("Can't find connection for $tree");
121 0           return;
122             }
123              
124             sub process_request ($$$%) {
125 0     0 0   my $self = shift;
126 0           my $connection = shift;
127 0           my $command_name = shift;
128 0           my %command_options = @_;
129              
130 0           my $command_class = "SMB::v2::Command::$command_name";
131 0           my $command_code = $SMB::v2::Commands::command_codes{$command_name};
132 0           my $no_warn = delete $command_options{_no_warn};
133              
134 0 0         my $request = $command_class->new(
135             SMB::v2::Header->new(
136             mid => $connection->{message_id}++,
137             uid => $connection->session_id,
138             tid => $connection->tree ? $connection->tree->id : 0,
139             code => $command_code,
140             ),
141             );
142 0           $request->set(%command_options);
143              
144 0           $connection->send_command($request);
145 0           $connection->sent_request($request);
146              
147 0           my $response = $self->wait_for_response($connection);
148              
149 0 0 0       warn "SMB Error on $command_name response: " . ($response ? sprintf "%x", $response->status : "internal") . "\n"
    0 0        
150             if !$no_warn && (!$response || $response->is_error);
151              
152 0           return $response;
153             }
154              
155             sub process_negotiate_if_needed ($$) {
156 0     0 0   my $self = shift;
157 0           my $connection = shift;
158              
159 0 0         return 1 if $connection->dialect;
160              
161 0           my $response = $self->process_request($connection, 'Negotiate');
162 0 0 0       if ($response && $response->is_success) {
163 0 0         unless ($connection->auth->process_spnego($response->security_buffer)) {
164 0           $self->err("Server does not support our negotiation mechanism, expect problems on SessionSetup");
165             }
166 0           $connection->dialect($response->dialect);
167 0           return 1;
168             }
169              
170 0           return 0;
171             }
172              
173             sub process_sessionsetup_if_needed ($$) {
174 0     0 0   my $self = shift;
175 0           my $connection = shift;
176              
177 0 0         return 1 if $connection->session_id;
178              
179 0           my $response = $self->process_request($connection, 'SessionSetup',
180             security_buffer => $connection->auth->generate_spnego,
181             _no_warn => 1,
182             );
183 0           my $more_processing = $response->status == SMB::STATUS_MORE_PROCESSING_REQUIRED;
184 0 0 0       return 0
      0        
      0        
185             unless $response && ($response->is_success || $more_processing)
186             && $connection->auth->process_spnego($response->security_buffer);
187 0           $connection->session_id($response->header->uid);
188 0 0         return 1 if $response->is_success;
189 0 0         return 0 unless $more_processing;
190              
191 0           $response = $self->process_request($connection, 'SessionSetup',
192             security_buffer => $connection->auth->generate_spnego(
193             username => $connection->username,
194             password => $connection->password,
195             ),
196             );
197 0 0 0       if ($response && $response->is_success && $connection->auth->process_spnego($response->security_buffer)) {
      0        
198 0 0         die "Got different session id on second SessionSetup"
199             unless $connection->session_id == $response->header->uid;
200 0           return 1;
201             }
202              
203 0           return 0;
204             }
205              
206             sub check_session ($$) {
207 0     0 0   my $self = shift;
208 0           my $connection = shift;
209              
210             return
211 0   0       $self->process_negotiate_if_needed($connection) &&
212             $self->process_sessionsetup_if_needed($connection);
213             }
214              
215             sub connect_tree ($%) {
216 0     0 0   my $self = shift;
217 0           my %options = @_;
218              
219 0   0       my $connection = $self->get_curr_connection || return;
220              
221 0 0 0       my ($addr, $share, $username, $password) =
222 0           map { $connection->{$_} || $options{$_} || die "No $_ to connect_tree\n" }
223             qw(addr share username password);
224              
225 0 0         return unless $self->check_session($connection);
226              
227 0           $addr =~ s/:\d+//;
228 0           my $response = $self->process_request($connection, 'TreeConnect', uri => "\\\\$addr\\$share");
229 0 0 0       if ($response && $response->is_success) {
230 0           my $tree = SMB::Tree->new(
231             $share, $response->header->tid,
232             addr => $addr,
233             client => $self,
234             cwd => '',
235             );
236 0           $connection->tree($tree);
237 0           return $tree;
238             }
239              
240 0           return;
241             }
242              
243             sub _normalize_path ($$;$) {
244 0   0 0     my $path = shift // '';
245 0   0       my $base = shift // '';
246 0   0       my $to_dos = shift || 0;
247              
248 0 0         $path = "$base/$path" if $path =~ m!^[^/]!;
249              
250 0           $path =~ s![/\\]+$!/!g; # to unix
251             # remove "./", "any/../", "../.." at the end
252 0           while ($path =~ s=(^|/)\.(?:/|$)=$1=g) {}
253 0           while ($path =~ s=(^|/)(?!\.\./)[^/]+/\.\.(?:/|$)=$1=g) {}
254 0           $path =~ s!(?:(?:^|/)\.\.)+/?$!!;
255 0           $path =~ s!/$!!;
256              
257 0 0         if ($to_dos) {
258 0           $path =~ s=^/==;
259 0           $path =~ s=/=\\=g;
260             }
261              
262 0           return $path;
263             }
264              
265             sub _basename ($;$) {
266 0   0 0     my $path = shift // '';
267 0   0       my $is_dos = shift || 0;
268              
269 0 0         my $delim = $is_dos ? '\\' : '/';
270              
271 0 0         return $path =~ /.*\Q$delim\E(.*)/ ? $1 : $path;
272             }
273              
274             sub perform_tree_command ($$$@) {
275 0     0 0   my $self = shift;
276 0           my $tree = shift;
277 0           my $command = shift;
278              
279 0   0       my $connection = $self->find_connection_by_tree($tree) || return;
280              
281 0 0         if ($command eq 'chdir') {
    0          
    0          
    0          
    0          
282 0   0       my $dir = shift // '';
283              
284 0           $tree->cwd(_normalize_path($dir, $tree->cwd));
285             } elsif ($command eq 'find') {
286 0   0       my $pattern = _normalize_path(shift || "*", $tree->cwd, 1);
287 0 0         my $dirname = $pattern =~ /^(.*)\\(.*)/ ? $1 : "";
288 0 0         $pattern = $2 if $2;
289              
290 0           my $response = $self->process_request($connection, 'Create',
291             file_name => $dirname,
292             file_attributes => SMB::File::ATTR_DIRECTORY,
293             );
294 0 0 0       return unless $response && $response->is_success;
295 0           my $fid = $response->fid;
296 0           $response = $self->process_request($connection, 'QueryDirectory',
297             file_pattern => $pattern,
298             fid => $fid,
299             );
300 0 0 0       my $files = $response && $response->is_success ? $response->files : undef;
301 0           $self->process_request($connection, 'Close',
302             fid => $fid,
303             );
304              
305 0 0         return wantarray ? @$files : $files;
306             } elsif ($command eq 'dnload') {
307 0   0       my $filename = shift // '';
308 0 0         return $self->err("No filename") if $filename eq '';
309 0           $filename = _normalize_path($filename, $tree->cwd, 1);
310 0   0       my $dst_filename = _normalize_path(shift || _basename($filename, 1), '.');
311              
312 0           my $response = $self->process_request($connection, 'Create',
313             file_name => $filename,
314             );
315 0 0 0       return unless $response && $response->is_success;
316 0           my $file = $response->openfile->file;
317 0           my $fid = $response->fid;
318 0           my $remaining = $file->end_of_file;
319 0           my $time = $file->mtime;
320 0           my $content = '';
321 0           my $offset = 0;
322 0           while ($remaining) {
323 0 0         my $length = $remaining >= 65536 ? 65536 : $remaining;
324 0           $remaining -= $length;
325 0           $response = $self->process_request($connection, 'Read',
326             fid => $fid,
327             offset => $offset,
328             length => $length,
329             remaining_bytes => $remaining,
330             );
331 0 0 0       return unless $response && $response->is_success;
332 0           my $read = $response->length;
333 0 0         return $self->err("Unexpected $read bytes read instead of $length at offset $offset")
334             if $read != $length;
335 0           $content .= $response->buffer;
336 0           $offset += $length;
337             }
338 0           $self->process_request($connection, 'Close',
339             fid => $fid,
340             );
341              
342 0 0         open DST, '>', $dst_filename
343             or return $self->err("Can't open $dst_filename for write: $!");
344 0 0         print DST $content
345             or return $self->err("Can't write content to $dst_filename: $!");
346 0 0         close DST
347             or return $self->err("Can't close $dst_filename after write: $!");
348              
349             # consider to set $time on file
350 0           return 1;
351             } elsif ($command eq 'upload') {
352 0   0       my $filename = shift // '';
353 0 0         return $self->err("No filename") if $filename eq '';
354 0           $filename = _normalize_path($filename, '.');
355 0   0       my $dst_filename = _normalize_path(shift || _basename($filename), $tree->cwd, 1);
356              
357 0           local $/ = undef;
358 0 0         open SRC, '<', $filename
359             or return $self->err("Can't open $filename for read: $!");
360 0   0       my $content =
361             // return $self->err("Can't read content from $filename: $!");
362 0 0         close SRC
363             or return $self->err("Can't close $filename after read: $!");
364              
365 0           my $response = $self->process_request($connection, 'Create',
366             file_name => $dst_filename,
367             options => SMB::v2::Command::Create::OPTIONS_NON_DIRECTORY_FILE,
368             access_mask => 0x12019f,
369             disposition => SMB::File::DISPOSITION_OVERWRITE_IF,
370             );
371 0 0 0       return unless $response && $response->is_success;
372 0           my $fid = $response->fid;
373 0           my $remaining = length($content);
374 0           my $offset = 0;
375 0           while ($remaining) {
376 0 0         my $length = $remaining >= 65536 ? 65536 : $remaining;
377 0           $remaining -= $length;
378 0           $response = $self->process_request($connection, 'Write',
379             fid => $fid,
380             offset => $offset,
381             remaining_bytes => $remaining,
382             buffer => substr($content, $offset, $length),
383             );
384 0 0 0       return unless $response && $response->is_success;
385 0           my $written = $response->length;
386 0 0         return $self->err("Unexpected $written bytes written instead of $length at offset $offset")
387             if $written != $length;
388 0           $offset += $length;
389             }
390 0           $self->process_request($connection, 'Close',
391             fid => $fid,
392             );
393              
394 0           return 1;
395             } elsif ($command eq 'remove') {
396 0   0       my $filename = shift // '';
397 0 0         return $self->err("No filename") if $filename eq '';
398 0           $filename = _normalize_path($filename, $tree->cwd, 1);
399              
400 0   0       my $is_dir = shift || 0;
401 0 0         my $options = ($is_dir
402             ? SMB::v2::Command::Create::OPTIONS_DIRECTORY_FILE
403             : SMB::v2::Command::Create::OPTIONS_NON_DIRECTORY_FILE
404             ) | SMB::v2::Command::Create::OPTIONS_DELETE_ON_CLOSE;
405 0           my $response = $self->process_request($connection, 'Create',
406             file_name => $filename,
407             options => $options,
408             access_mask => 0x10081,
409             );
410 0 0 0       return unless $response && $response->is_success;
411 0           my $fid = $response->fid;
412 0           $self->process_request($connection, 'Close',
413             fid => $fid,
414             );
415 0 0 0       return unless $response && $response->is_success;
416              
417 0           return 1;
418             }
419              
420 0           return;
421             }
422              
423             sub on_response ($$$$) {
424 0     0 0   my $self = shift;
425 0           my $connection = shift;
426 0           my $response = shift;
427 0           my $request = shift;
428              
429 0           return 0;
430             }
431              
432             sub wait_for_response ($$) {
433 0     0 0   my $self = shift;
434 0           my $connection = shift;
435 0           my $request = $connection->sent_request;
436              
437 0 0         return unless $request;
438              
439 0           my $response = $connection->recv_command;
440 0 0         if (!$response) {
441 0           $self->delete_connection($connection);
442 0           return;
443             }
444              
445 0 0         unless ($response->is_response_to($request)) {
446 0           $self->err("Unexpected: " . $response->dump);
447 0           return;
448             }
449              
450 0           return $response;
451             }
452              
453             1;