File Coverage

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