File Coverage

blib/lib/Mojo/TFTPd.pm
Criterion Covered Total %
statement 92 126 73.0
branch 36 56 64.2
condition 24 30 80.0
subroutine 17 21 80.9
pod 1 2 50.0
total 170 235 72.3


line stmt bran cond sub pod time code
1             package Mojo::TFTPd;
2              
3             =head1 NAME
4              
5             Mojo::TFTPd - Trivial File Transfer Protocol daemon
6              
7             =head1 VERSION
8              
9             0.03
10              
11             =head1 SYNOPSIS
12              
13             use Mojo::TFTPd;
14             my $tftpd = Mojo::TFTPd->new;
15              
16             $tftpd->on(error => sub {
17             warn "TFTPd: $_[1]\n";
18             });
19              
20             $tftpd->on(rrq => sub {
21             my($tftpd, $c) = @_;
22             open my $FH, '<', $c->file;
23             $c->filehandle($FH);
24             $c->filesize(-s $c->file);
25             });
26              
27             $tftpd->on(wrq => sub {
28             my($tftpd, $c) = @_;
29             open my $FH, '>', '/dev/null';
30             $c->filehandle($FH);
31             });
32              
33             $tftpd->on(finish => sub {
34             my($tftpd, $c, $error) = @_;
35             warn "Connection: $error\n" if $error;
36             });
37              
38             $tftpd->start;
39             $tftpd->ioloop->start unless $tftpd->ioloop->is_running;
40              
41             =head1 DESCRIPTION
42              
43             This module implements a server for the
44             L.
45              
46             From Wikipedia:
47              
48             Trivial File Transfer Protocol (TFTP) is a file transfer protocol notable
49             for its simplicity. It is generally used for automated transfer of
50             configuration or boot files between machines in a local environment.
51              
52             The connection ($c) which is referred to in this document is an instance of
53             L.
54              
55             =cut
56              
57 3     3   128836 use Mojo::Base 'Mojo::EventEmitter';
  3         89716  
  3         27  
58 3     3   26588 use Mojo::IOLoop;
  3         1092591  
  3         23  
59 3     3   2343 use Mojo::TFTPd::Connection;
  3         12  
  3         44  
60 3     3   181 use constant OPCODE_RRQ => 1;
  3         6  
  3         193  
61 3     3   17 use constant OPCODE_WRQ => 2;
  3         4  
  3         138  
62 3     3   16 use constant OPCODE_DATA => 3;
  3         6  
  3         134  
63 3     3   16 use constant OPCODE_ACK => 4;
  3         6  
  3         145  
64 3     3   18 use constant OPCODE_ERROR => 5;
  3         6  
  3         198  
65 3     3   16 use constant OPCODE_OACK => 6;
  3         7  
  3         165  
66 3   50 3   14 use constant CHECK_INACTIVE_INTERVAL => $ENV{MOJO_TFTPD_CHECK_INACTIVE_INTERVAL} || 3;
  3         6  
  3         171  
67 3     3   18 use constant MIN_BLOCK_SIZE => 8;
  3         11  
  3         139  
68 3     3   15 use constant MAX_BLOCK_SIZE => 65464; # From RFC 2348
  3         6  
  3         188  
69 3 50   3   15 use constant DEBUG => $ENV{MOJO_TFTPD_DEBUG} ? 1 : 0;
  3         6  
  3         7600  
70              
71             our $VERSION = '0.03';
72              
73             =head1 EVENTS
74              
75             =head2 error
76              
77             $self->on(error => sub {
78             my($self, $str) = @_;
79             });
80              
81             This event is emitted when something goes wrong: Fail to L to socket,
82             read from socket or other internal errors.
83              
84             =head2 finish
85              
86             $self->on(finish => sub {
87             my($self, $c, $error) = @_;
88             });
89              
90             This event is emitted when the client finish, either successfully or due to an
91             error. C<$error> will be an empty string on success.
92              
93             =head2 rrq
94              
95             $self->on(rrq => sub {
96             my($self, $c) = @_;
97             });
98              
99             This event is emitted when a new read request arrives from a client. The
100             callback should set L or the connection
101             will be dropped.
102              
103             =head2 wrq
104              
105             $self->on(wrq => sub {
106             my($self, $c) = @_;
107             });
108              
109             This event is emitted when a new write request arrives from a client. The
110             callback should set L or the connection
111             will be dropped.
112              
113             =head1 ATTRIBUTES
114              
115             =head2 ioloop
116              
117             Holds an instance of L.
118              
119             =cut
120              
121             has ioloop => sub { Mojo::IOLoop->singleton };
122              
123             =head2 listen
124              
125             $str = $self->server;
126             $self->server("127.0.0.1:69");
127             $self->server("tftp://*:69"); # any interface
128              
129             The bind address for this server.
130              
131             =cut
132              
133             has listen => 'tftp://*:69';
134              
135             =head2 max_connections
136              
137             How many concurrent connections this server can handle. Default to 1000.
138              
139             =cut
140              
141             has max_connections => 1000;
142              
143             =head2 retries
144              
145             How many times the server should try to send ACK or DATA to the client before
146             dropping the L.
147              
148             =cut
149              
150             has retries => 1;
151              
152             =head2 inactive_timeout
153              
154             How long a L can stay idle before
155             being dropped.
156              
157             =cut
158              
159             has inactive_timeout => 15;
160              
161             =head1 METHODS
162              
163             =head2 start
164              
165             Starts listening to the address and port set in L. The L
166             event will be emitted if the server fail to start.
167              
168             =cut
169              
170             sub start {
171 0     0 1 0 my $self = shift;
172 0         0 my $reactor = $self->ioloop->reactor;
173 0         0 my $socket;
174              
175 0 0       0 $self->{connections} and return $self;
176 0         0 $self->{connections} = {};
177              
178             # split $self->listen into host and port
179 0         0 my ($host, $port) = $self->_parse_listen;
180              
181 0         0 warn "[Mojo::TFTPd] Listen to $host:$port\n" if DEBUG;
182              
183 0         0 $socket = IO::Socket::INET->new(
184             LocalAddr => $host,
185             LocalPort => $port,
186             Proto => 'udp',
187             );
188              
189 0 0       0 if(!$socket) {
190 0         0 delete $self->{connections};
191 0         0 return $self->emit(error => "Can't create listen socket: $!");
192             };
193              
194 0         0 Scalar::Util::weaken($self);
195              
196 0         0 $socket->blocking(0);
197 0     0   0 $reactor->io($socket, sub { $self->_incoming });
  0         0  
198 0         0 $reactor->watch($socket, 1, 0); # watch read events
199 0         0 $self->{socket} = $socket;
200             $self->{checker}
201             = $self->ioloop->recurring(CHECK_INACTIVE_INTERVAL || 3, sub {
202 0     0   0 my $time = time;
203 0         0 for my $c (values %{ $self->{connections} }) {
  0         0  
204 0 0       0 next if $time - $c->timeout < $c->{timestamp};
205 0         0 $c->error('Inactive timeout');
206 0         0 $self->_delete_connection($c);
207             }
208 0         0 });
209              
210 0         0 return $self;
211             }
212              
213             sub _incoming {
214 27     27   47546 my $self = shift;
215 27         53 my $socket = $self->{socket};
216 27         86 my $read = $socket->recv(my $datagram, MAX_BLOCK_SIZE + 4); # Add 4 Bytes of Opcode + Block#
217 27         107 my($opcode, $connection);
218              
219 27 100       77 if(!defined $read) {
220 1         29 return $self->emit(error => "Read: $!");
221             }
222              
223 26         108 $opcode = unpack 'n', substr $datagram, 0, 2, '';
224              
225             # new connection
226 26 100       101 if($opcode eq OPCODE_RRQ) {
    100          
227 10         38 return $self->_new_request(rrq => $datagram);
228             }
229             elsif($opcode eq OPCODE_WRQ) {
230 4         12 return $self->_new_request(wrq => $datagram);
231             }
232              
233             # existing connection
234 12         44 $connection = $self->{connections}{$socket->peername};
235              
236 12 100       91 if(!$connection) {
    100          
    50          
    0          
237 1         3 return $self->emit(error => "@{[$socket->peerhost]} has no connection");
  1         4  
238             }
239             elsif($opcode == OPCODE_ACK) {
240 6 100 100     23 return if $connection->receive_ack($datagram) and $connection->send_data;
241             }
242             elsif($opcode == OPCODE_DATA) {
243 5 100 100     26 return if $connection->receive_data($datagram) and $connection->send_ack;
244             }
245             elsif($opcode == OPCODE_ERROR) {
246 0         0 my($code, $msg) = unpack 'nZ*', $datagram;
247 0         0 $connection->error("($code) $msg");
248             }
249             else {
250 0         0 $connection->error("Unknown opcode");
251             }
252              
253             # if something goes wrong or finish with connection
254 6         38 $self->_delete_connection($connection);
255             }
256              
257             sub _new_request {
258 14     14   29 my($self, $type, $datagram) = @_;
259 14         64 my($file, $mode, @rfc) = split "\0", $datagram;
260 14         29 my $socket = $self->{socket};
261 14         15 my $connection;
262              
263 14         14 warn "[Mojo::TFTPd] <<< @{[$socket->peerhost]} $type $file $mode @rfc\n" if DEBUG;
264              
265 14 100       73 if(!$self->has_subscribers($type)) {
266 3         39 $self->emit(error => "Cannot handle $type requests");
267 3         44 return;
268             }
269 11 100       448 if($self->max_connections <= keys %{ $self->{connections} }) {
  11         115  
270 1         6 $self->emit(error => "Max connections ($self->{max_connections}) reached");
271 1         15 return;
272             }
273              
274 10         37 my %rfc = @rfc;
275 10         40 $connection = Mojo::TFTPd::Connection->new(
276             type => $type,
277             file => $file,
278             mode => $mode,
279             peerhost => $socket->peerhost,
280             peername => $socket->peername,
281             retries => $self->retries,
282             timeout => $self->inactive_timeout,
283             rfc => \%rfc,
284             socket => $socket,
285             );
286              
287 10 100       828 if ($rfc{blksize}) {
288 2 50       9 $rfc{blksize} = MIN_BLOCK_SIZE if $rfc{blksize} < MIN_BLOCK_SIZE;
289 2 50       6 $rfc{blksize} = MAX_BLOCK_SIZE if $rfc{blksize} > MAX_BLOCK_SIZE;
290 2         43 $connection->blocksize($rfc{blksize});
291             }
292 10 50 66     57 if ($rfc{timeout} and $rfc{timeout} >= 0 and $rfc{timeout} <= 255) {
      66        
293 1         22 $connection->timeout($rfc{timeout});
294             }
295 10 100 100     54 if ($type eq 'wrq' and $rfc{tsize}) {
296 1         23 $connection->filesize($rfc{tsize});
297             }
298              
299 10         42 $self->emit($type => $connection);
300              
301 10 100 66     2015 if (!$connection->filehandle) {
    100 66        
    50          
302 1   50     31 $connection->send_error(file_not_found => $connection->error // 'No filehandle');
303 1         21 $self->{connections}{$connection->peername} = $connection;
304             }
305             elsif ((%rfc and $connection->send_oack)
306             or $type eq 'rrq' ? $connection->send_data : $connection->send_ack) {
307 9         205 $self->{connections}{$connection->peername} = $connection;
308             }
309             else {
310 0         0 $self->emit(finish => $connection, $connection->error);
311             }
312             }
313              
314             sub _parse_listen {
315 7     7   7258 my $self = shift;
316              
317 7         165 my ($scheme, $host, $port) = $self->listen =~ m!
318             (?: ([^:/]+) :// )? # part before ://
319             ([^:]*) # everyting until a :
320             (?: : (\d+) )? # any digits after the :
321             !xms;
322              
323             # if scheme is set but no port, use scheme
324 7 100 100     809 $port = getservbyname($scheme, '') if $scheme && !defined $port;
325              
326             # use port 69 as fallback
327 7   100     32 $port //= 69;
328              
329             # if host == '*', replace it with '0.0.0.0'
330 7 100       17 $host = '0.0.0.0' if $host eq '*';
331              
332 7         39 return ($host, $port);
333             }
334              
335             sub _delete_connection {
336 6     6   10 my($self, $connection) = @_;
337 6         143 delete $self->{connections}{$connection->peername};
338 6         174 $self->emit(finish => $connection, $connection->error);
339             }
340              
341             sub DEMOLISH {
342 0     0 0   my $self = shift;
343 0 0         my $reactor = eval { $self->ioloop->reactor } or return; # may be undef during global destruction
  0            
344              
345 0 0         $reactor->remove($self->{checker}) if $self->{checker};
346 0 0         $reactor->remove($self->{socket}) if $self->{socket};
347             }
348              
349             =head1 AUTHOR
350              
351             Svetoslav Naydenov
352              
353             Jan Henning Thorsen - C
354              
355             =cut
356              
357             1;