File Coverage

lib/SMB/Server.pm
Criterion Covered Total %
statement 24 139 17.2
branch 0 86 0.0
condition 0 44 0.0
subroutine 8 14 57.1
pod 1 6 16.6
total 33 289 11.4


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::Server;
17              
18 1     1   486 use strict;
  1         2  
  1         33  
19 1     1   5 use warnings;
  1         2  
  1         21  
20              
21 1     1   5 use parent 'SMB::Agent';
  1         3  
  1         5  
22              
23 1     1   61 use File::Basename qw(basename);
  1         1  
  1         78  
24 1     1   12 use SMB::Tree;
  1         2  
  1         24  
25 1     1   6 use SMB::v2::Command::Negotiate;
  1         2  
  1         30  
26 1     1   5 use SMB::v2::Command::Create;
  1         2  
  1         28  
27 1     1   4 use SMB::v2::Command::QueryDirectory;
  1         2  
  1         1573  
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          
    0          
63 0           $self->err("No share_roots specified and no shares/ autodetected");
64 0           $share_roots = {};
65             } 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             } 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 0           $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 0       if ($command->is('Negotiate') && $command->supports_protocol(2)) {
117 0           $command = SMB::v2::Command::Negotiate->new_from_v1($command);
118             }
119             }
120              
121 0 0         if ($command->is_smb2) {
122 0           my $error = 0;
123 0           my $fid = $command->{fid};
124 0           my $openfile = undef;
125              
126 0 0 0       if (($tid || exists $command->{fid}) && !$tree) {
    0 0        
127 0           $error = SMB::STATUS_SMB_BAD_TID;
128             }
129             elsif ($fid) {
130 0 0         $openfile = $connection->{openfiles}{@$fid}
131             or $error = SMB::STATUS_FILE_CLOSED;
132 0           $command->openfile($openfile);
133             }
134              
135 0 0         if ($error) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
136             # skip command processing
137             }
138             elsif ($command->is('Negotiate')) {
139 0           $command->security_buffer($connection->auth->generate_spnego(is_initial => 1));
140             }
141             elsif ($command->is('SessionSetup')) {
142 0           $connection->auth->process_spnego($command->security_buffer);
143 0           $command->security_buffer($connection->auth->generate_spnego);
144 0           $command->header->uid($connection->id);
145 0           my $auth_completed = $connection->auth->auth_completed;
146 0 0 0       $error = SMB::STATUS_LOGON_FAILURE
      0        
147             if !$command->security_buffer
148             || defined $auth_completed && !$auth_completed;
149             }
150             elsif ($command->is('TreeConnect')) {
151 0           my ($addr, $share) = $self->parse_share_uri($command->verify_uri);
152 0           my $tree_root = $self->share_roots->{$share};
153 0 0 0       if ($tree_root || $share eq 'IPC$') {
154 0           my $tid = $command->header->tid(@{$connection->{trees}} + 1);
  0            
155 0           push @{$connection->{trees}}, SMB::Tree->new($share, $tid, root => $tree_root);
  0            
156             } else {
157 0           $error = SMB::STATUS_BAD_NETWORK_NAME;
158             }
159             }
160             elsif ($command->is('Create')) {
161 0           my $file = SMB::File->new(
162             name => $command->file_name,
163             share_root => $tree->root,
164             is_ipc => $tree->is_ipc,
165             );
166 0           my $disposition = $command->disposition;
167 0 0 0       if ($file->exists && $disposition == SMB::File::DISPOSITION_OPEN) {
168 0 0 0       if ($command->requested_directory && !$file->is_directory) {
    0 0        
169 0           $error = SMB::STATUS_NOT_A_DIRECTORY;
170             } elsif ($command->requested_non_directory && $file->is_directory) {
171 0           $error = SMB::STATUS_FILE_IS_A_DIRECTORY;
172             }
173             }
174 0 0         unless ($error) {
175 0           $openfile = $file->open_by_disposition($disposition);
176 0 0         if ($openfile) {
177 0           $fid = [ ++$connection->{last_fid}, 0 ];
178 0           $connection->{openfiles}{@$fid} = $openfile;
179 0           $command->fid($fid);
180 0           $command->openfile($openfile);
181             } else {
182 0           $error = SMB::STATUS_NO_SUCH_FILE;
183             }
184             }
185             }
186             elsif ($command->is('Close')) {
187 0           $openfile->close;
188 0           delete $connection->{openfiles}{@$fid};
189             }
190             elsif ($command->is('Read')) {
191 0           $command->{buffer} = $openfile->file->read(
192             length => $command->{length},
193             offset => $command->{offset},
194             minlen => $command->{minimum_count},
195             remain => $command->{remaining_bytes},
196             );
197 0 0         $error = SMB::STATUS_END_OF_FILE unless defined $command->{buffer};
198             }
199             elsif ($command->is('QueryDirectory')) {
200 0 0         $command->file_index($openfile->last_index)
201             unless $command->flags & SMB::v2::Command::QueryDirectory::FLAGS_INDEX;
202 0           $command->{files} = $openfile->file->find_files(
203             pattern => $command->file_pattern,
204             start_idx => $command->file_index,
205             );
206 0 0         $error = SMB::STATUS_INVALID_PARAMETER unless defined $command->{files};
207             }
208 0           $command->prepare_response;
209 0 0         $command->set_status($error) if $error;
210 0           $connection->send_command($command);
211 0           return;
212             }
213              
214 0           $self->msg("Command $command ignored; missing functionality");
215             }
216              
217             sub run ($) {
218 0     0 0   my $self = shift;
219              
220 0           my $socket_pool = $self->socket_pool;
221 0           my $connections = $self->connections;
222              
223 0           while (my @ready_sockets = $socket_pool->can_read) {
224 0           foreach my $socket (@ready_sockets) {
225 0 0         if ($socket == $self->main_socket) {
226 0   0       my $client_socket = $socket->accept || next;
227 0           my $connection = $self->add_connection(
228             $client_socket, ++$self->{client_id},
229             trees => [],
230             last_fid => 0,
231             openfiles => {},
232             );
233 0 0         unless ($connection) {
234 0           $socket->close;
235 0           next;
236             }
237 0           $self->on_connect($connection);
238             }
239             else {
240 0 0         my $connection = $self->get_connection($socket)
241             or die "Unexpected data on unmanaged $socket";
242 0           my $command = $self->recv_command($connection);
243 0 0         if (!$command) {
244 0           $self->on_disconnect($connection);
245 0           $self->delete_connection($connection);
246 0           next;
247             }
248 0           $self->on_command($connection, $command);
249             }
250             }
251             }
252             }
253              
254             1;