File Coverage

lib/SMB/Server.pm
Criterion Covered Total %
statement 33 192 17.1
branch 0 138 0.0
condition 0 54 0.0
subroutine 11 17 64.7
pod 1 6 16.6
total 45 407 11.0


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::Server;
17              
18 1     1   527 use strict;
  1         3  
  1         33  
19 1     1   7 use warnings;
  1         3  
  1         36  
20              
21 1     1   7 use parent 'SMB::Agent';
  1         4  
  1         17  
22              
23 1     1   103 use File::Basename qw(basename);
  1         3  
  1         73  
24 1     1   9 use SMB::Tree;
  1         3  
  1         974  
25 1     1   9 use SMB::DCERPC;
  1         4  
  1         25  
26 1     1   7 use SMB::v2::Command::Negotiate;
  1         2  
  1         29  
27 1     1   6 use SMB::v2::Command::Create;
  1         2  
  1         25  
28 1     1   495 use SMB::v2::Command::QueryInfo;
  1         4  
  1         41  
29 1     1   9 use SMB::v2::Command::QueryDirectory;
  1         2  
  1         32  
30 1     1   500 use SMB::v2::Command::Ioctl;
  1         3  
  1         2636  
31              
32             sub new ($%) {
33 0     0 1   my $class = shift;
34 0           my %options = @_;
35              
36 0           my $share_roots = delete $options{share_roots};
37 0           my $port = delete $options{port};
38 0           my $fifo_filename = delete $options{fifo_filename};
39              
40 0 0 0       die "Neither port nor fifo-filename is specified for $class\n"
41             unless $port || $fifo_filename;
42              
43 0 0         my $main_socket = $fifo_filename
44             ? IO::Socket::UNIX->new(Listen => 1, Local => $fifo_filename)
45             : IO::Socket::INET->new(Listen => 1, LocalPort => $port, Reuse => 1);
46              
47 0 0         my $listen_label = $fifo_filename ? "fifo $fifo_filename" : "port $port";
48 0 0         die "Can't open $listen_label: $!\n" unless $main_socket;
49              
50 0   0       $options{passwd_filename} //= "$FindBin::Bin/../conf/passwd.txt";
51              
52 0           my $self = $class->SUPER::new(
53             %options,
54             main_socket => $main_socket,
55             client_id => 0, # running index
56             );
57              
58 0           $self->socket_pool->add($main_socket);
59              
60 0 0 0       if (!$share_roots && $FindBin::Bin) {
61 0           my $shares_dir = "$FindBin::Bin/../shares";
62 0 0 0       $share_roots = { map { basename($_) => $_ } grep { -d $_ && -x _ && -r _ } glob("$shares_dir/*") }
  0 0          
  0            
63             if -d $shares_dir;
64             }
65 0 0 0       unless ($share_roots) {
    0          
    0          
66 0           $self->err("No share_roots specified and no shares/ autodetected");
67 0           $share_roots = {};
68 0 0         } elsif (ref($share_roots) eq '' && $share_roots eq '-') {
69             # special syntax to request a share-less server, don't complain
70 0           $share_roots = {};
71 0           } elsif (ref($share_roots) ne 'HASH') {
72 0           $self->err("Invalid share_roots ($share_roots) specified");
73 0           $share_roots = {};
74             } elsif (!%$share_roots) {
75             $self->err("No shares to manage, specify non-empty share_roots hash");
76             }
77 0           $self->{share_roots} = $share_roots;
78              
79 0           $self->msg("$class started, listening on $listen_label");
80              
81 0           return $self;
82             }
83              
84             sub on_connect ($$) {
85 0     0 0   my $self = shift;
86 0           my $connection = shift;
87              
88             # intended to be overriden in sub-classes
89              
90 0           my $auth = $connection->auth;
91 0 0         $auth->load_user_passwords($self->passwd_filename)
92             or $auth->user_passwords({ test => '12345' });
93             }
94              
95             sub on_disconnect ($$) {
96 0     0 0   my $self = shift;
97 0           my $connection = shift;
98              
99             # intended to be overriden in sub-classes
100             }
101              
102             sub recv_command ($$) {
103 0     0 0   my $self = shift;
104 0           my $connection = shift;
105              
106 0           return $connection->recv_command;
107             }
108              
109             sub on_command ($$$) {
110 0     0 0   my $self = shift;
111 0           my $connection = shift;
112 0           my $command = shift;
113              
114 0           my $tid = $command->header->tid;
115 0 0         my $tree = $tid ? (grep { $_->id == $tid } @{$connection->{trees}})[0] : undef;
  0            
  0            
116 0 0         $command->{tree} = $tree if $tree;
117              
118 0 0         if ($command->is_smb1) {
119 0 0         if ($command->is('Negotiate')) {
120 0 0         if ($command->supports_smb_dialect(0x0202)) {
121 0           $command = SMB::v2::Command::Negotiate->new_from_v1($command);
122 0           $tid = 0;
123             } else {
124 0           $self->err("Client does not support SMB2, and we do not support SMB1, stopping");
125             }
126             }
127             }
128              
129 0 0         if ($command->is_smb2) {
130 0           my $error = 0;
131 0           my $fid = $command->{fid};
132 0           my $openfile = undef;
133              
134 0 0 0       if (($tid || exists $command->{fid}) && !$tree) {
    0 0        
    0 0        
    0 0        
135 0           $error = SMB::STATUS_SMB_BAD_TID;
136             }
137             elsif ($command->is('Ioctl') && $command->function == SMB::v2::Command::Ioctl::FSCTL_DFS_GET_REFERRALS) {
138 0           $error = SMB::STATUS_NOT_FOUND;
139             }
140             elsif ($command->is('Ioctl') && $command->function == SMB::v2::Command::Ioctl::FSCTL_PIPE_WAIT) {
141 0           $error = SMB::STATUS_OBJECT_NAME_NOT_FOUND;
142             }
143             elsif ($fid) {
144 0 0         if ($command->header->is_chained) {
145 0 0 0       $fid = $connection->chain_fid if $connection->chain_fid && $command->is_fid_unset($fid);
146             } else {
147 0           $connection->chain_fid(undef);
148             }
149 0 0         $openfile = $connection->{openfiles}{$fid->[0], $fid->[1]}
150             or $error = SMB::STATUS_FILE_CLOSED;
151 0           $command->openfile($openfile);
152             }
153              
154 0 0         if ($error) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
155             # skip command processing
156             }
157             elsif ($command->is('Negotiate')) {
158 0           $command->security_buffer($connection->auth->generate_spnego(is_initial => 1));
159             }
160             elsif ($command->is('SessionSetup')) {
161 0           $connection->auth->process_spnego($command->security_buffer);
162 0           $command->security_buffer($connection->auth->generate_spnego);
163 0           $command->header->uid($connection->id);
164 0           my $auth_completed = $connection->auth->auth_completed;
165 0 0 0       $error = SMB::STATUS_LOGON_FAILURE
      0        
166             if !$command->security_buffer
167             || defined $auth_completed && !$auth_completed;
168             }
169             elsif ($command->is('TreeConnect')) {
170 0           my ($addr, $share) = $self->parse_share_uri($command->verify_uri);
171 0           my $tree_root = $self->share_roots->{$share};
172 0 0 0       if ($tree_root || $share eq 'IPC$') {
173 0           my $tid = $command->header->tid(@{$connection->{trees}} + 1);
  0            
174 0           $tree = SMB::Tree->new($share, $tid, root => $tree_root);
175 0           push @{$connection->{trees}}, $tree;
  0            
176 0           $command->tree($tree);
177             } else {
178 0           $error = SMB::STATUS_BAD_NETWORK_NAME;
179             }
180             }
181             elsif ($command->is('Create')) {
182 0           my $file = SMB::File->new(
183             name => $command->file_name,
184             share_root => $tree->root,
185             is_ipc => $tree->is_ipc,
186             is_directory => $command->requested_directory,
187             );
188 0           my $disposition = $command->disposition;
189 0 0 0       if ($file->exists && $disposition == SMB::File::DISPOSITION_OPEN) {
190 0 0 0       if ($command->requested_directory && !$file->is_directory) {
    0 0        
191 0           $error = SMB::STATUS_NOT_A_DIRECTORY;
192             } elsif ($command->requested_non_directory && $file->is_directory) {
193 0           $error = SMB::STATUS_FILE_IS_A_DIRECTORY;
194             }
195             }
196 0 0         unless ($error) {
197 0           $openfile = $file->open_by_disposition($disposition);
198 0 0         if ($openfile) {
199 0 0         $openfile->{dcerpc} = SMB::DCERPC->new(name => $file->name)
200             if $file->is_svc;
201 0           $fid = [ ++$connection->{last_fid}, 0 ];
202 0 0         $connection->chain_fid($fid) if $command->has_next_in_chain;
203 0           $connection->{openfiles}{$fid->[0], $fid->[1]} = $openfile;
204 0           $command->fid($fid);
205 0           $command->openfile($openfile);
206 0 0         $openfile->delete_on_close(1) if $command->requested_delete_on_close;
207             } else {
208 0           $error = SMB::STATUS_NO_SUCH_FILE;
209             }
210             }
211             }
212             elsif ($command->is('Close')) {
213 0 0         if ($openfile->delete_on_close) {
214 0           my $filename = $openfile->file->filename;
215 0           $self->msg("Removing $filename");
216 0 0         $openfile->file->remove()
217             or $self->err("Failed to remove $filename: $!");
218             }
219 0           $openfile->close;
220 0           delete $connection->{openfiles}{$fid->[0], $fid->[1]};
221             }
222             elsif ($command->is('QueryInfo')) {
223 0           $command->prepare_info(quiet => $self->quiet);
224             }
225             elsif ($command->is('SetInfo')) {
226 0           my $rename_info = $command->requested_rename_info;
227 0 0         if ($rename_info) {
228 0           my $filename1 = $openfile->file->filename;
229 0   0       my $filename2 = $rename_info->{new_name} // die;
230 0   0       my $replace = $rename_info->{replace} // die;
231 0           $self->msg("Renaming $filename1 to $filename2");
232 0           ($error, my $message) = $openfile->file->rename($filename2);
233 0 0         $self->err("Failed to rename $filename1 to $filename2: $message")
234             if $error;
235             }
236 0 0         $openfile->delete_on_close(1) if $command->requested_delete_on_close;
237             }
238             elsif ($command->is('Read')) {
239 0 0         if ($openfile->{dcerpc}) {
240 0           ($command->{buffer}, $error) = $openfile->dcerpc->generate_packet;
241             }
242             else {
243             $command->{buffer} = $openfile->read(
244             length => $command->{length},
245             offset => $command->{offset},
246             minlen => $command->{minimum_count},
247             remain => $command->{remaining_bytes},
248 0           quiet => $self->quiet,
249             );
250 0 0         $error = SMB::STATUS_END_OF_FILE unless defined $command->{buffer};
251             }
252             }
253             elsif ($command->is('Write')) {
254 0 0         if ($openfile->{dcerpc}) {
255 0           $error = $openfile->dcerpc->process_packet($command->buffer);
256             }
257             else {
258 0           $error = SMB::STATUS_NOT_IMPLEMENTED;
259             }
260             }
261             elsif ($command->is('Ioctl')) {
262 0 0         if ($openfile->{dcerpc}) {
263 0           $error = $openfile->dcerpc->process_rpc_request($command->buffer);
264 0 0         unless ($error) {
265 0           (my $payload, $error) = $openfile->dcerpc->generate_rpc_response;
266 0 0         $command->buffer($payload) unless $error;
267             }
268             }
269             else {
270 0           $error = SMB::STATUS_NOT_IMPLEMENTED;
271             }
272             }
273             elsif ($command->is('QueryDirectory')) {
274 0 0         my $start_idx = $command->is_reopen ? 0 : $command->is_index_specified
    0          
275             ? $command->file_index : $openfile->last_index;
276 0           $command->{files} = $openfile->file->find_files(
277             pattern => $command->file_pattern,
278             start_idx => $start_idx,
279             reopen => $command->is_reopen,
280             quiet => $self->quiet,
281             );
282 0 0         $error = SMB::STATUS_INVALID_PARAMETER unless defined $command->{files};
283             }
284             elsif ($command->is('ChangeNotify')) {
285 0           $command->header->aid(++$connection->{last_aid});
286 0           $error = SMB::STATUS_PENDING;
287             }
288             elsif ($command->is('Cancel')) {
289 0           $command->header->code($SMB::v2::Commands::command_codes{'ChangeNotify'});
290 0           $error = SMB::STATUS_CANCELLED;
291             }
292 0           $command->prepare_response;
293 0 0         $command->set_status($error) if $error;
294 0           $connection->send_command($command);
295 0           return;
296             }
297              
298 0           $self->msg("Command $command ignored; missing functionality");
299             }
300              
301             sub run ($) {
302 0     0 0   my $self = shift;
303              
304 0           my $socket_pool = $self->socket_pool;
305 0           my $connections = $self->connections;
306              
307 0           while (my @ready_sockets = $socket_pool->can_read) {
308 0           foreach my $socket (@ready_sockets) {
309 0 0         if ($socket == $self->main_socket) {
310 0   0       my $client_socket = $socket->accept || next;
311             my $connection = $self->add_connection(
312             $client_socket, ++$self->{client_id},
313 0           trees => [],
314             last_aid => 0,
315             last_fid => 0,
316             chain_fid => undef,
317             openfiles => {},
318             );
319 0 0         unless ($connection) {
320 0           $socket->close;
321 0           next;
322             }
323 0           $self->on_connect($connection);
324             }
325             else {
326 0 0         my $connection = $self->get_connection($socket)
327             or die "Unexpected data on unmanaged $socket";
328 0 0         my @commands = $socket->eof
329             ? $connection->msg("Connection reset by peer")
330             : $self->recv_command($connection);
331 0 0         if (!@commands) {
332 0           $self->on_disconnect($connection);
333 0           $self->delete_connection($connection);
334 0           next;
335             }
336 0           for my $command (@commands) {
337 0           $self->on_command($connection, $command);
338             }
339             }
340             }
341             }
342             }
343              
344             1;