--- /dev/null
+package Net::TFTPd;
+
+use 5.006;
+use Carp;
+use strict;
+use warnings;
+use IO::Socket;
+
+require Exporter;
+
+# modified for supporting small block sizes, O.Z. 15.08.2007
+use constant TFTP_MIN_BLKSIZE => 8;
+use constant TFTP_DEFAULT_BLKSIZE => 512;
+use constant TFTP_MAX_BLKSIZE => 65464;
+use constant TFTP_MIN_TIMEOUT => 1;
+use constant TFTP_MAX_TIMEOUT => 60;
+use constant TFTP_DEFAULT_PORT => 69;
+
+use constant TFTP_OPCODE_RRQ => 1;
+use constant TFTP_OPCODE_WRQ => 2;
+use constant TFTP_OPCODE_DATA => 3;
+use constant TFTP_OPCODE_ACK => 4;
+use constant TFTP_OPCODE_ERROR => 5;
+use constant TFTP_OPCODE_OACK => 6;
+
+# Type Op # Format without header
+#
+# 2 bytes string 1 byte string 1 byte
+# -------------------------------------------------
+# RRQ/ | 01/02 | Filename | 0 | Mode | 0 |
+# WRQ -------------------------------------------------
+# 2 bytes 2 bytes n bytes
+# -----------------------------------
+# DATA | 03 | Block # | Data |
+# -----------------------------------
+# 2 bytes 2 bytes
+# ----------------------
+# ACK | 04 | Block # |
+# ----------------------
+# 2 bytes 2 bytes string 1 byte
+# ------------------------------------------
+# ERROR | 05 | ErrorCode | ErrMsg | 0 |
+# ------------------------------------------
+
+our %OPCODES = (
+ 1 => 'RRQ',
+ 2 => 'WRQ',
+ 3 => 'DATA',
+ 4 => 'ACK',
+ 5 => 'ERROR',
+ 6 => 'OACK',
+ 'RRQ' => TFTP_OPCODE_RRQ,
+ 'WRQ' => TFTP_OPCODE_WRQ,
+ 'DATA' => TFTP_OPCODE_DATA,
+ 'ACK' => TFTP_OPCODE_ACK,
+ 'ERROR' => TFTP_OPCODE_ERROR,
+ 'OACK' => TFTP_OPCODE_OACK
+);
+
+my %ERRORS = (
+ 0 => 'Not defined, see error message (if any)',
+ 1 => 'File not found',
+ 2 => 'Access violation',
+ 3 => 'Disk full or allocation exceeded',
+ 4 => 'Illegal TFTP operation',
+ 5 => 'Unknown transfer ID',
+ 6 => 'File already exists',
+ 7 => 'No such user',
+ 8 => 'Option negotiation'
+);
+
+our @ISA = qw(Exporter);
+
+# Items to export into callers namespace by default. Note: do not export
+# names by default without a very good reason. Use EXPORT_OK instead.
+# Do not simply export all your public functions/methods/constants.
+
+# This allows declaration use Net::TFTPd ':all';
+# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
+# will save memory.
+our %EXPORT_TAGS = (
+ 'all' => [ qw( %OPCODES ) ]
+);
+
+our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
+
+our @EXPORT = qw( );
+
+our $VERSION = '0.04';
+
+our $LASTERROR;
+
+my $debug;
+
+#
+# Usage: $tftpdOBJ = Net::TFTPd->new( ['RootDir' => 'path/to/files' | 'FileName' => 'path/to/file'] );
+# return the tftpdOBJ object if success or undef if error
+#
+sub new
+{
+ # create the future TFTPd object
+ my $self = shift;
+ my $class = ref($self) || $self;
+
+ # read parameters
+ my %cfg = @_;
+
+ # setting defaults
+ $cfg{'FileName'} or $cfg{'RootDir'} or croak "Usage: \$tftpdOBJ = Net::TFTPd->new(['RootDir' => 'path/to/files' | 'FileName' => 'path/to/file'] [, [ LocalPort => portnum ] [, ...]] );";
+
+ if($cfg{'RootDir'} and not -d($cfg{'RootDir'}) )
+ {
+ $LASTERROR = sprintf 'RootDir \'%s\' not found or is not a valid directory name\n', $cfg{'RootDir'};
+ return(undef);
+ }
+
+ if($cfg{'FileName'} and not -e($cfg{'FileName'}) )
+ {
+ $LASTERROR = sprintf 'FileName \'%s\' not found or is not a valid filename\n', $cfg{'FileName'};
+ return(undef);
+ }
+
+ my %params = (
+ 'Proto' => 'udp',
+ 'LocalPort' => $cfg{'LocalPort'} || TFTP_DEFAULT_PORT,
+ );
+
+ # bind only to specified address
+ if($cfg{'LocalAddr'})
+ {
+ $params{'LocalAddr'} = $cfg{'LocalAddr'};
+ }
+
+ if(my $udpserver = IO::Socket::INET->new(%params))
+ {
+#removed for using this module with IO v. 1.2301 under SUSE 10.1, O.Z. 15.08.2007
+# $udpserver->setsockopt(SOL_SOCKET, SO_RCVBUF, 0);
+# $udpserver->setsockopt(SOL_SOCKET, SO_SNDBUF, 0);
+
+ return bless {
+ 'LocalPort' => TFTP_DEFAULT_PORT,
+ 'Timeout' => 10,
+ 'ACKtimeout' => 4,
+ 'ACKretries' => 4,
+ 'Readable' => 1,
+ 'Writable' => 0,
+ 'CallBack' => undef,
+ 'BlkSize' => TFTP_DEFAULT_BLKSIZE,
+ 'Debug' => 0,
+ %cfg, # merge user parameters
+ '_UDPSERVER_' => $udpserver
+ }, $class;
+ }
+ else
+ {
+ $LASTERROR = "Error opening socket for listener: $@\n";
+ return(undef);
+ }
+}
+
+#
+# Usage: $tftpdOBJ->waitRQ($timeout);
+# return requestOBJ if success, 0 if $timeout elapsed, undef if error
+#
+sub waitRQ
+{
+ # the tftpd object
+# my $tftpd = shift;
+
+ my $self = shift;
+ my $class = ref($self) || $self;
+# return bless {}, $class;
+
+ # clone the object
+ my $request;
+ foreach my $key (keys(%{$self}))
+ {
+ # everything but '_xxx_'
+ $key =~ /^\_.+\_$/ and next;
+ $request->{$key} = $self->{$key};
+ }
+
+ # use $timeout or default from $tftpdOBJ
+ my $Timeout = shift || $request->{'Timeout'};
+
+ my $udpserver = $self->{'_UDPSERVER_'};
+
+ my ($datagram, $opcode, $datain);
+
+ # vars for IO select
+ my ($rin, $rout, $ein, $eout) = ('', '', '', '');
+ vec($rin, fileno($udpserver), 1) = 1;
+
+ # check if a message is waiting
+ if (select($rout=$rin, undef, $eout=$ein, $Timeout))
+ {
+ # read the message
+ if($udpserver->recv($datagram, TFTP_MAX_BLKSIZE + 4))
+ {
+ # decode the message
+ ($opcode, $datain) = unpack("na*", $datagram);
+
+ $request->{'_REQUEST_'}{'OPCODE'} = $opcode;
+
+ # get peer port and address
+ my($peerport, $peeraddr) = sockaddr_in($udpserver->peername);
+ $request->{'_REQUEST_'}{'PeerPort'} = $peerport;
+ $request->{'_REQUEST_'}{'PeerAddr'} = inet_ntoa($peeraddr);
+
+ # get filename and transfer mode
+ my @datain = split("\0", $datain);
+
+ $request->{'_REQUEST_'}{'FileName'} = shift(@datain);
+ $request->{'_REQUEST_'}{'Mode'} = uc(shift(@datain));
+ $request->{'_REQUEST_'}{'BlkSize'} = TFTP_DEFAULT_BLKSIZE;
+ $request->{'_REQUEST_'}{'LASTACK'} = 0;
+ $request->{'_REQUEST_'}{'PREVACK'} = -1;
+ # counter for transferred bytes
+ $request->{'_REQUEST_'}{'TotalBytes'} = 0;
+
+ if(scalar(@datain) >= 2)
+ {
+ $request->{'_REQUEST_'}{'RFC2347'} = { @datain };
+ }
+
+ return bless $request, $class;
+ }
+ else
+ {
+ $! = $udpserver->sockopt(SO_ERROR);
+ $LASTERROR = sprintf "Socket RECV error: %s\n", $!;
+ return(undef);
+ }
+ }
+ else
+ {
+ $LASTERROR = "Timed out waiting for RRQ/WRQ";
+ return(0);
+ }
+}
+
+#
+# Usage: $requestOBJ->processRQ();
+# return 1 if success, undef if error
+#
+sub processRQ
+{
+ # the request object
+ my $self = shift;
+
+ if(defined($self->newSOCK()))
+ {
+ # modified for supporting NETASCII transfers on 25/05/2009
+ if(($self->{'_REQUEST_'}{'Mode'} ne 'OCTET') && ($self->{'_REQUEST_'}{'Mode'} ne 'NETASCII'))
+ {
+ #request is not OCTET
+ $LASTERROR = sprintf "%s transfer mode is not supported\n", $self->{'_REQUEST_'}{'Mode'};
+ $self->sendERR(0, $LASTERROR);
+ return(undef);
+ }
+
+ # new socket opened successfully
+ if($self->{'_REQUEST_'}{'OPCODE'} eq TFTP_OPCODE_RRQ)
+ {
+ #################
+ # opcode is RRQ #
+ #################
+ if($self->{'Readable'})
+ {
+ # read is permitted
+ if($self->{'_REQUEST_'}{'FileName'} =~ /\.\.[\\\/]/)
+ {
+ # requested file contains '..\' or '../'
+ $LASTERROR = sprintf 'Access to \'%s\' is not permitted to %s', $self->{'_REQUEST_'}{'FileName'}, $self->{'_REQUEST_'}{'PeerAddr'};
+ $self->sendERR(2);
+ return(undef);
+ }
+
+ if(defined($self->checkFILE()))
+ {
+ # file is present
+ if(defined($self->negotiateOPTS()))
+ {
+ # RFC 2347 options negotiated
+ if(defined($self->openFILE()))
+ {
+ # file opened for read, start the transfer
+ if(defined($self->sendFILE()))
+ {
+ # file sent successfully
+ return(1);
+ }
+ else
+ {
+ # error sending file
+ return(undef);
+ }
+ }
+ else
+ {
+ # error opening file
+ return(undef);
+ }
+ }
+ else
+ {
+ # error negotiating options
+ $LASTERROR = "TFTP error 8: Option negotiation\n";
+ $self->sendERR(8);
+ return(undef);
+ }
+ }
+ else
+ {
+ # file not found
+ $LASTERROR = sprintf 'File \'%s\' not found', $self->{'_REQUEST_'}{'FileName'};
+ $self->sendERR(1);
+ return(undef);
+ }
+ }
+ else
+ {
+ # if server is not readable
+ $LASTERROR = "TFTP Error: Access violation";
+ $self->sendERR(2);
+ return(undef);
+ }
+ }
+ elsif($self->{'_REQUEST_'}{'OPCODE'} eq TFTP_OPCODE_WRQ)
+ {
+ #################
+ # opcode is WRQ #
+ #################
+ if($self->{'Writable'})
+ {
+ # write is permitted
+ if($self->{'_REQUEST_'}{'FileName'} =~ /\.\.[\\\/]/)
+ {
+ # requested file contains '..\' or '../'
+ $LASTERROR = sprintf 'Access to \'%s\' is not permitted to %s', $self->{'_REQUEST_'}{'FileName'}, $self->{'_REQUEST_'}{'PeerAddr'};
+ $self->sendERR(2);
+ return(undef);
+ }
+
+ if(!defined($self->checkFILE()))
+ {
+ # RFC 2347 options negotiated
+ if(defined($self->openFILE()))
+ {
+ # file is not present
+ if(defined($self->negotiateOPTS()))
+ {
+ # file opened for write, start the transfer
+ if(defined($self->recvFILE()))
+ {
+ # file received successfully
+ return(1);
+ }
+ else
+ {
+ # error receiving file
+ return(undef);
+ }
+ }
+ else
+ {
+ # error negotiating options
+ $LASTERROR = "TFTP error 8: Option negotiation\n";
+ $self->sendERR(8);
+ return(undef);
+ }
+ }
+ else
+ {
+ # error opening file
+ $self->sendERR(3);
+ return(undef);
+ }
+ }
+ else
+ {
+ # file not found
+ $LASTERROR = sprintf 'File \'%s\' already exists', $self->{'_REQUEST_'}{'FileName'};
+ $self->sendERR(6);
+ return(undef);
+ }
+ }
+ else
+ {
+ # if server is not writable
+ $LASTERROR = "TFTP Error: Access violation";
+ $self->sendERR(2);
+ return(undef);
+ }
+ }
+ else
+ {
+ #################
+ # other opcodes #
+ #################
+ $LASTERROR = sprintf "Opcode %d not supported as request", $self->{'_REQUEST_'}{'OPCODE'};
+ $self->sendERR(4);
+ return(undef);
+ }
+ }
+ else
+ {
+ return(undef);
+ }
+}
+
+#
+# Usage: $requestOBJ->getTotalBytes();
+# returns the number of bytes transferred by the request
+#
+sub getTotalBytes
+{
+ # the request object
+ my $self = shift;
+
+ return $self->{'_REQUEST_'}{'TotalBytes'};
+}
+
+#
+# Usage: $requestOBJ->getFileName();
+# returns the requested file name
+#
+sub getFileName
+{
+ # the request object
+ my $self = shift;
+
+ return $self->{'_REQUEST_'}{'FileName'};
+}
+
+#
+# Usage: $requestOBJ->getMode();
+# returns the transfer mode for the request
+#
+sub getMode
+{
+ # the request object
+ my $self = shift;
+
+ return $self->{'_REQUEST_'}{'Mode'};
+}
+
+#
+# Usage: $requestOBJ->getPeerAddr();
+# returns the address of the requesting client
+#
+sub getPeerAddr
+{
+ # the request object
+ my $self = shift;
+
+ return $self->{'_REQUEST_'}{'PeerAddr'};
+}
+
+#
+# Usage: $requestOBJ->getPeerPort();
+# returns the port of the requesting client
+#
+sub getPeerPort
+{
+ # the request object
+ my $self = shift;
+
+ return $self->{'_REQUEST_'}{'PeerPort'};
+}
+
+#
+# Usage: $requestOBJ->getBlkSize();
+# returns the block size used for the transfer
+#
+sub getBlkSize
+{
+ # the request object
+ my $self = shift;
+
+ return $self->{'_REQUEST_'}{'BlkSize'};
+}
+
+#
+# Usage: $requestOBJ->newSOCK();
+# return 1 if success or undef if error
+#
+sub newSOCK
+{
+ # the request object
+ my $self = shift;
+
+ # set parameters for the new socket
+ my %params = (
+ 'Proto' => 'udp',
+ 'PeerPort' => $self->{'_REQUEST_'}{'PeerPort'},
+ 'PeerAddr' => $self->{'_REQUEST_'}{'PeerAddr'}
+ );
+
+ # bind only to specified address
+ if($self->{'Address'})
+ {
+ $params{'LocalAddr'} = $self->{'Address'};
+ }
+
+ # open socket
+ if(my $udpserver = IO::Socket::INET->new(%params))
+ {
+#removed for using this module with IO v. 1.2301 under SUSE 10.1, O.Z. 15.08.2007
+# $udpserver->setsockopt(SOL_SOCKET, SO_RCVBUF, 0);
+# $udpserver->setsockopt(SOL_SOCKET, SO_SNDBUF, 0);
+
+ $self->{'_UDPSERVER_'} = $udpserver;
+ return(1);
+ }
+ else
+ {
+ $LASTERROR = "Error opening socket for reply: $@\n";
+ return(undef);
+ }
+}
+
+
+#
+# Usage: $requestOBJ->negotiateOPTS();
+# return 1 if success or undef if error
+#
+sub negotiateOPTS
+{
+ # the request object
+ my $self = shift;
+
+ if($self->{'_REQUEST_'}{'RFC2347'})
+ {
+ # parse RFC 2347 options if present
+ foreach my $option (keys(%{ $self->{'_REQUEST_'}{'RFC2347'} }))
+ {
+ if(uc($option) eq 'BLKSIZE')
+ {
+ # Negotiate the blocksize
+ if($self->{'_REQUEST_'}{'RFC2347'}{$option} > TFTP_MAX_BLKSIZE or $self->{'_REQUEST_'}{'RFC2347'}{$option} < TFTP_MIN_BLKSIZE)
+ {
+ $self->{'_REQUEST_'}{'RFC2347'}{$option} = $self->{'BlkSize'};
+ }
+ else
+ {
+ $self->{'_RESPONSE_'}{'RFC2347'}{$option} = $self->{'_REQUEST_'}{'RFC2347'}{$option};
+ $self->{'BlkSize'} = $self->{'_RESPONSE_'}{'RFC2347'}{$option};
+ }
+ }
+ elsif(uc($option) eq 'TSIZE')
+ {
+ # Negotiate the transfer size
+ if($self->{'_REQUEST_'}{'OPCODE'} eq TFTP_OPCODE_RRQ)
+ {
+ $self->{'_RESPONSE_'}{'RFC2347'}{$option} = $self->{'FileSize'};
+ }
+ else
+ {
+ $self->{'FileSize'} = $self->{'_REQUEST_'}{'RFC2347'}{$option};
+ }
+ }
+ elsif(uc($option) eq 'TIMEOUT')
+ {
+ # Negotiate the transfer timeout
+ if($self->{'_REQUEST_'}{'RFC2347'}{$option} > TFTP_MAX_TIMEOUT or $self->{'_REQUEST_'}{'RFC2347'}{$option} < TFTP_MIN_TIMEOUT)
+ {
+ $self->{'_RESPONSE_'}{'RFC2347'}{$option} = $self->{'ACKtimeout'};
+ }
+ else
+ {
+ $self->{'_RESPONSE_'}{'RFC2347'}{$option} = $self->{'_REQUEST_'}{'RFC2347'}{$option};
+ $self->{'ACKtimeout'} = $self->{'_REQUEST_'}{'RFC2347'}{$option};
+ }
+ }
+ else
+ {
+ # Negotiate other options...
+ }
+ }
+
+ # post processing
+ if($self->{'_REQUEST_'}{'OPCODE'} eq TFTP_OPCODE_WRQ)
+ {
+ if($self->{'FileSize'} and $self->{'BlkSize'})
+ {
+ $self->{'_REQUEST_'}{'LASTACK'} = int($self->{'FileSize'} / $self->{'BlkSize'}) + 1;
+ }
+ }
+
+ # send OACK for RFC 2347 options
+ return($self->sendOACK());
+ }
+ else
+ {
+ if($self->{'_REQUEST_'}{'OPCODE'} eq TFTP_OPCODE_WRQ)
+ {
+ # opcode is WRQ: send ACK for datablock 0
+ if($self->{'_UDPSERVER_'}->send(pack("nn", TFTP_OPCODE_ACK, 0)))
+ {
+ return(1);
+ }
+ else
+ {
+ $! = $self->{'_UDPSERVER_'}->sockopt(SO_ERROR);
+ $LASTERROR = sprintf "Socket SEND error: %s\n", $!;
+ return(undef);
+ }
+ }
+ else
+ {
+ return(1);
+ }
+ }
+}
+
+
+#
+# Usage: $requestOBJ->readFILE(\$data);
+# return number of bytes read from file if success or undef if error
+#
+sub readFILE
+{
+ my $self = shift;
+ my $datablk = shift;
+
+ if($self->{'_REQUEST_'}{'PREVACK'} < $self->{'_REQUEST_'}{'LASTACK'})
+ {
+ # if requested block is next block, read next block and return bytes read
+ my $fh = $self->{'_REQUEST_'}{'_FH_'};
+ # modified for supporting NETASCII transfers on 25/05/2009
+ # my $bytes = read($fh, $$datablk, $self->{'BlkSize'});
+ my $bytes = sysread($fh, $$datablk, $self->{'BlkSize'});
+ if(defined($bytes))
+ {
+ return($bytes);
+ }
+ else
+ {
+ $LASTERROR = sprintf "Error $! reading file '%s'", $self->{'_REQUEST_'}{'FileName'};
+ return(undef);
+ }
+ }
+ else
+ {
+ # if requested block is last block, return length of last block
+ return(length($$datablk));
+ }
+}
+
+
+#
+# Usage: $requestOBJ->writeFILE(\$data);
+# return number of bytes written to file if success or undef if error
+#
+sub writeFILE
+{
+ my $self = shift;
+ my $datablk = shift;
+
+ if($self->{'_REQUEST_'}{'PREVBLK'} > $self->{'_REQUEST_'}{'LASTBLK'})
+ {
+ # if last block is < than previous block, return length of last block
+ return(length($$datablk));
+ }
+ elsif($self->{'_REQUEST_'}{'LASTBLK'} eq ($self->{'_REQUEST_'}{'PREVBLK'} + 1))
+ {
+ # if block is next block, write next block and return bytes written
+ my $fh = $self->{'_REQUEST_'}{'_FH_'};
+ my $bytes = syswrite($fh, $$datablk);
+ return($bytes);
+ }
+ else
+ {
+ $LASTERROR = sprintf "TFTP Error DATA block %d is out of sequence, expected block was %d", $self->{'_REQUEST_'}{'LASTBLK'}, $self->{'_REQUEST_'}{'PREVBLK'} + 1;
+ $self->sendERR(5);
+ return(undef);
+ }
+}
+
+
+#
+# Usage: $requestOBJ->sendFILE();
+# return 1 if success or undef if error
+#
+sub sendFILE
+{
+ my $self = shift;
+
+ while(1)
+ {
+ if($self->{'_REQUEST_'}{'LASTACK'} < $self->{'_REQUEST_'}{'LASTBLK'})
+ {
+ my $datablk = 0;
+ if(defined($self->readFILE(\$datablk)))
+ {
+ # read from file successful
+ # increment the transferred bytes counter
+ $self->{'_REQUEST_'}{'TotalBytes'} += length($datablk);
+ if($self->sendDATA(\$datablk))
+ {
+ # send to socket successful
+ if($self->{'CallBack'})
+ {
+ &{$self->{'CallBack'}}($self);
+ }
+ }
+ else
+ {
+ # error sending to socket
+ return(undef);
+ }
+ }
+ else
+ {
+ # error reading from file
+ return(undef);
+ }
+ }
+ else
+ {
+ # transfer completed
+ return(1);
+ }
+ }
+}
+
+
+#
+# Usage: $requestOBJ->recvFILE();
+# return 1 if success or undef if error
+#
+sub recvFILE
+{
+ my $self = shift;
+
+ $self->{'_REQUEST_'}{'LASTBLK'} = 0;
+ $self->{'_REQUEST_'}{'PREVBLK'} = 0;
+
+ while(1)
+ {
+ my $datablk = 0;
+ if($self->recvDATA(\$datablk))
+ {
+ # DATA received
+ if(defined($self->writeFILE(\$datablk)))
+ {
+ # DATA written to file
+ my $udpserver = $self->{'_UDPSERVER_'};
+
+ if(defined($udpserver->send(pack("nn", TFTP_OPCODE_ACK, $self->{'_REQUEST_'}{'LASTBLK'}))))
+ {
+ # sent ACK
+ # increment the transferred bytes counter
+ $self->{'_REQUEST_'}{'TotalBytes'} += length($datablk);
+ if(length($datablk) < $self->{'BlkSize'})
+ {
+ return(1);
+ }
+ else
+ {
+ next;
+ }
+ }
+ else
+ {
+ $! = $udpserver->sockopt(SO_ERROR);
+ $LASTERROR = sprintf "Socket SEND error: %s\n", $!;
+ return(undef);
+ }
+ }
+ else
+ {
+ # error writing data
+ return(undef);
+ }
+ }
+ else
+ {
+ # timeout waiting for data
+ return(undef);
+ }
+ }
+}
+
+#
+# Usage: $requestOBJ->recvDATA(\$data);
+# return 1 if success or undef if error
+#
+sub recvDATA
+{
+ my $self = shift;
+ my $datablk = shift;
+
+ my ($datagram, $opcode, $datain);
+
+ my $udpserver = $self->{'_UDPSERVER_'};
+
+ # vars for IO select
+ my ($rin, $rout, $ein, $eout) = ('', '', '', '');
+ vec($rin, fileno($udpserver), 1) = 1;
+
+ # wait for data
+ if(select($rout=$rin, undef, $eout=$ein, $self->{'ACKtimeout'}))
+ {
+ # read the message
+ if($udpserver->recv($datagram, $self->{'BlkSize'} + 4))
+ {
+ # decode the message
+ ($opcode, $datain) = unpack("na*", $datagram);
+ if($opcode eq TFTP_OPCODE_DATA)
+ {
+ # message is DATA
+ $self->{'_REQUEST_'}{'PREVBLK'} = $self->{'_REQUEST_'}{'LASTBLK'};
+ ($self->{'_REQUEST_'}{'LASTBLK'}, $$datablk) = unpack("na*", $datain);
+
+ if($self->{'CallBack'})
+ {
+ &{$self->{'CallBack'}}($self);
+ }
+
+ return(1);
+ }
+ elsif($opcode eq TFTP_OPCODE_ERROR)
+ {
+ # message is ERR
+ $LASTERROR = sprintf "TFTP error message: %s", $datain;
+ return(undef);
+ }
+ else
+ {
+ # other messages...
+ $LASTERROR = sprintf "Opcode %d not supported waiting for DATA\n", $opcode;
+ return(undef);
+ }
+ }
+ else
+ {
+ $! = $udpserver->sockopt(SO_ERROR);
+ $LASTERROR = sprintf "Socket RECV error: %s\n", $!;
+ return(undef);
+ }
+ }
+ else
+ {
+ $LASTERROR = sprintf "Timeout occurred on DATA packet %d\n", $self->{'_REQUEST_'}{'LASTBLK'} + 1;
+ return(undef);
+ }
+}
+
+
+#
+# Usage: $requestOBJ->sendDATA(\$data);
+# return 1 if success or undef if error
+#
+sub sendDATA
+{
+ my $self = shift;
+ my $datablk = shift;
+
+ my $udpserver = $self->{'_UDPSERVER_'};
+ my $retry = 0;
+
+ my ($datagram, $opcode, $datain);
+
+ while($retry < $self->{'ACKretries'})
+ {
+ if($udpserver->send(pack("nna*", TFTP_OPCODE_DATA, $self->{'_REQUEST_'}{'LASTACK'} + 1, $$datablk)))
+ {
+ # vars for IO select
+ my ($rin, $rout, $ein, $eout) = ('', '', '', '');
+ vec($rin, fileno($udpserver), 1) = 1;
+
+ # wait for acknowledge
+ if(select($rout=$rin, undef, $eout=$ein, $self->{'ACKtimeout'}))
+ {
+ # read the message
+ if($udpserver->recv($datagram, TFTP_MAX_BLKSIZE + 4))
+ {
+ # decode the message
+ ($opcode, $datain) = unpack("na*", $datagram);
+ if($opcode eq TFTP_OPCODE_ACK)
+ {
+ # message is ACK
+ # modified for supporting more blocks count than 65535, O.Z. 15.08.2007
+ $self->{'_REQUEST_'}{'PREVACK'} = $self->{'_REQUEST_'}{'LASTACK'};
+ if(int(($self->{'_REQUEST_'}{'LASTACK'}+1) % 65536) == unpack("n", $datain)){
+ $self->{'_REQUEST_'}{'LASTACK'}++;
+ };
+ return(1);
+ }
+ elsif($opcode eq TFTP_OPCODE_ERROR)
+ {
+ # message is ERR
+ $LASTERROR = sprintf "TFTP error message: %s", $datain;
+ return(undef);
+ }
+ else
+ {
+ # other messages...
+ $LASTERROR = sprintf "Opcode %d not supported as a reply to DATA\n", $opcode;
+ return(undef);
+ }
+ }
+ else
+ {
+ $! = $udpserver->sockopt(SO_ERROR);
+ $LASTERROR = sprintf "Socket RECV error: %s\n", $!;
+ return(undef);
+ }
+ }
+ else
+ {
+ $LASTERROR = sprintf "Retry %d - timeout occurred on ACK packet %d\n", $retry, $self->{'_REQUEST_'}{'LASTACK'} + 1;
+ $debug and carp($LASTERROR);
+ $retry++;
+ }
+ }
+ else
+ {
+ $! = $udpserver->sockopt(SO_ERROR);
+ $LASTERROR = sprintf "Socket SEND error: %s\n", $!;
+ return(undef);
+ }
+ }
+}
+
+#
+# Usage: $requestOBJ->openFILE()
+# returns 1 if file is opened, undef if error
+#
+sub openFILE
+{
+ # the request object
+ my $self = shift;
+
+ if($self->{'_REQUEST_'}{'OPCODE'} eq TFTP_OPCODE_RRQ)
+ {
+ ########################################
+ # opcode is RRQ, open file for reading #
+ ########################################
+ if(open(RFH, "<".$self->{'_REQUEST_'}{'FileName'}))
+ {
+ # if OCTET mode, set FileHandle to binary mode...
+ if($self->{'_REQUEST_'}{'Mode'} eq 'OCTET')
+ {
+ binmode(RFH);
+ }
+
+ my $size = -s($self->{'_REQUEST_'}{'FileName'});
+ $self->{'_REQUEST_'}{'LASTBLK'} = 1 + int($size / $self->{'BlkSize'});
+
+ # save the filehandle reference...
+ $self->{'_REQUEST_'}{'_FH_'} = *RFH;
+
+ return(1);
+ }
+ else
+ {
+ $LASTERROR = sprintf "Error opening file \'%s\' for reading\n", $self->{'_REQUEST_'}{'FileName'};
+ return(undef);
+ }
+ }
+ elsif($self->{'_REQUEST_'}{'OPCODE'} eq TFTP_OPCODE_WRQ)
+ {
+ ########################################
+ # opcode is WRQ, open file for writing #
+ ########################################
+ if(open(WFH, ">".$self->{'_REQUEST_'}{'FileName'}))
+ {
+ # if OCTET mode, set FileHandle to binary mode...
+ if($self->{'_REQUEST_'}{'Mode'} eq 'OCTET')
+ {
+ binmode(WFH);
+ }
+
+ # save the filehandle reference...
+ $self->{'_REQUEST_'}{'_FH_'} = *WFH;
+
+ return(1);
+ }
+ else
+ {
+ $LASTERROR = sprintf "Error opening file \'%s\' for writing\n", $self->{'_REQUEST_'}{'FileName'};
+ return(undef);
+ }
+ }
+ else
+ {
+ ############################
+ # other opcodes are errors #
+ ############################
+ $LASTERROR = sprintf "OPCODE %d is not supported\n", $self->{'_REQUEST_'}{'OPCODE'};
+ return(undef);
+ }
+}
+
+#
+# Usage: $requestOBJ->closeFILE()
+# returns 1 if file is success, undef if error
+#
+sub closeFILE
+{
+ my $self = shift;
+
+ if($self->{'_REQUEST_'}{'_FH_'})
+ {
+ if(close($self->{'_REQUEST_'}{'_FH_'}))
+ {
+ return(1);
+ }
+ else
+ {
+ $LASTERROR = "Error closing filehandle\n";
+ return(undef);
+ }
+ }
+ else
+ {
+ return(1);
+ }
+}
+
+#
+# Usage: $requestOBJ->checkFILE()
+# returns 1 if file is found, undef if file is not found
+#
+sub checkFILE
+{
+ # the request object
+ my $self = shift;
+
+ # requested file
+ my $reqfile = $self->{'_REQUEST_'}{'FileName'};
+
+ if($self->{'FileName'})
+ {
+ # filename is fixed
+ $self->{'_REQUEST_'}{'FileName'} = $self->{'FileName'};
+
+ if(($self->{'FileName'} =~ /$reqfile/) and -e($self->{'FileName'}))
+ {
+ # fixed name contains requested file and file exists
+ $self->{'FileSize'} = -s($self->{'FileName'});
+ return(1);
+ }
+ }
+ elsif($self->{'RootDir'})
+ {
+ # rootdir is fixed
+ $reqfile = $self->{'RootDir'}.'/'.$reqfile;
+ $self->{'_REQUEST_'}{'FileName'} = $reqfile;
+
+ if(-e($reqfile))
+ {
+ # file exists in rootdir
+ $self->{'FileSize'} = -s($reqfile);
+ return(1);
+ }
+ }
+
+ return(undef);
+}
+
+#
+# Usage: $requestOBJ->sendOACK();
+# return 1 for success and undef for error (see $Net::TFTPd::LASTERROR for cause)
+#
+sub sendOACK
+{
+ # the request object
+ my $self = shift;
+ my $udpserver = $self->{'_UDPSERVER_'};
+ my $retry = 0;
+
+ my ($datagram, $opcode, $datain);
+
+ while($retry < $self->{'ACKretries'})
+ {
+ # send oack
+ my $data = join("\0", %{ $self->{'_RESPONSE_'}{'RFC2347'} })."\0";
+ if($udpserver->send(pack("na*", TFTP_OPCODE_OACK, $data)))
+ {
+ # opcode is RRQ
+ if($self->{'_REQUEST_'}{'OPCODE'} eq TFTP_OPCODE_RRQ)
+ {
+ # vars for IO select
+ my ($rin, $rout, $ein, $eout) = ('', '', '', '');
+ vec($rin, fileno($udpserver), 1) = 1;
+
+ # wait for acknowledge
+ if(select($rout=$rin, undef, $eout=$ein, $self->{'ACKtimeout'}))
+ {
+ # read the message
+ if($udpserver->recv($datagram, TFTP_MAX_BLKSIZE + 4))
+ {
+ # decode the message
+ ($opcode, $datain) = unpack("na*", $datagram);
+ if($opcode == TFTP_OPCODE_ACK)
+ {
+ # message is ACK
+ my $lastack = unpack("n", $datain);
+ if($lastack)
+ {
+ # ack is not for block 0... ERROR
+ $LASTERROR = sprintf "Received ACK for block %d instead of 0", $lastack;
+ return(undef);
+ }
+ return 1;
+ }
+ elsif($opcode == TFTP_OPCODE_ERROR)
+ {
+ # message is ERR
+ $LASTERROR = sprintf "TFTP error message: %s", $datain;
+ return(undef);
+ }
+ else
+ {
+ # other messages...
+ $LASTERROR = sprintf "Opcode %d not supported as a reply to OACK\n", $opcode;
+ return(undef);
+ }
+ }
+ else
+ {
+ $! = $udpserver->sockopt(SO_ERROR);
+ $LASTERROR = sprintf "Socket RECV error: %s\n", $!;
+ return (undef);
+ }
+ }
+ else
+ {
+ $LASTERROR = sprintf "Retry %d - timeout occurred waiting reply for OACK packet\n", $retry;
+ $debug and carp($LASTERROR);
+ $retry++;
+ }
+ }
+ elsif($self->{'_REQUEST_'}{'OPCODE'} eq TFTP_OPCODE_WRQ)
+ {
+ # opcode is WRQ
+ return(1);
+ }
+ }
+ else
+ {
+ $! = $udpserver->sockopt(SO_ERROR);
+ $LASTERROR = sprintf "Socket SEND error: %s\n", $!;
+ return(undef);
+ }
+ }
+}
+
+#
+# Usage: $requestOBJ->sendERR($code, $message);
+# returns 1 if success, undef if error
+#
+sub sendERR
+{
+ my $self = shift;
+ my($errcode, $errmsg) = @_;
+ # modified for supporting NETASCII transfers on 25/05/2009
+ #$errmsg or $errmsg = '';
+ $errmsg or $errmsg = $ERRORS{$errcode};
+
+ my $udpserver = $self->{'_UDPSERVER_'};
+
+ if($udpserver->send(pack("nnZ*", 5, $errcode, $errmsg)))
+ {
+ return(1);
+ }
+ else
+ {
+ $! = $udpserver->sockopt(SO_ERROR);
+ $LASTERROR = sprintf "Socket SEND error: %s\n", $!;
+ return(undef);
+ }
+}
+
+sub error
+{
+ return($LASTERROR);
+}
+
+# Preloaded methods go here.
+
+1;
+__END__
+
+# Below is stub documentation for your module. You better edit it!
+
+=head1 NAME
+
+Net::TFTPd - Perl extension for Trivial File Transfer Protocol Server
+
+=head1 SYNOPSIS
+
+ use strict;
+ use Net::TFTPd;
+
+ my $tftpdOBJ = Net::TFTPd->new('RootDir' => 'path/to/files')
+ or die "Error creating TFTPd listener: %s", Net::TFTPd->error;
+
+ my $tftpRQ = $tftpdOBJ->waitRQ(10)
+ or die "Error waiting for TFTP request: %s", Net::TFTPd->error;
+
+ $tftpRQ->processRQ()
+ or die "Error processing TFTP request: %s", Net::TFTPd->error;
+
+ printf "%u bytes has been transferred", $tftpRQ->getTotalBytes() || 0;
+
+=head1 DESCRIPTION
+
+C<Net::TFTPd> is a class implementing a simple I<Trivial File Transfer Protocol> server in Perl as described in RFC1350.
+
+C<Net::TFTPd> also supports the TFTP Option Extension (as described in RFC2347), with the following options:
+
+ RFC2348 TFTP Blocksize Option
+ RFC2349 TFTP Timeout Interval and Transfer Size Options
+
+=head1 EXPORT
+
+None by default.
+
+=head2 %OPCODES
+
+The %OPCODES tag exports the I<%OPCODES> hash:
+
+ %OPCODES = (
+ 1 => 'RRQ',
+ 2 => 'WRQ',
+ 3 => 'DATA',
+ 4 => 'ACK',
+ 5 => 'ERROR',
+ 6 => 'OACK',
+ 'RRQ' => 1,
+ 'WRQ' => 2,
+ 'DATA' => 3,
+ 'ACK' => 4,
+ 'ERROR' => 5,
+ 'OACK' => 6
+ );
+
+=head1 Listener constructor
+
+=head2 new()
+
+ $listener = new Net::TFTPd( ['RootDir' => 'path/to/files' | 'FileName' => 'path/to/file'] [, OPTIONS ] );
+
+or
+
+ $listener = Net::TFTPd->new( ['RootDir' => 'path/to/files' | 'FileName' => 'path/to/file'] [, OPTIONS ] );
+
+Create a new Net::TFTPd object where 'path/to/files' is the default path to file repository
+or 'path/to/file' is the single file allowed for download, and OPTIONS are the default server
+options.
+
+Valid options are:
+
+ Option Description Default
+ ------ ----------- -------
+ LocalAddr Interface to bind to (for multi-homed server) any
+ LocalPort Port to bind server to 69
+ Timeout Timeout in seconds to wait for a request 10
+ ACKtimeout Timeout in seconds to wait for an ACK packet 4
+ ACKretries Maximum number of retries waiting for ACK 4
+ Readable Clients are allowed to read files 1
+ Writable Clients are allowed to write files 0
+ BlkSize Minimum blocksize to negotiate for transfers 512
+ CallBack Reference to code executed for each transferred block -
+ Debug Activates debug mode (verbose) 0
+
+=head2 CallBack
+
+The CallBack code is called by processRQ method for each tranferred block.
+
+The code receives (into @_ array) a reference to internal I<$request> object.
+
+Example:
+
+ sub callback
+ {
+ my $req = shift;
+ printf "block: %u\/%u\n", $req->{'_REQUEST_'}{'LASTACK'}, $req->{'_REQUEST_'}{'LASTBLK'};
+ }
+
+ my $tftpdOBJ = Net::TFTPd->new('RootDir' => 'c:/temp', 'Timeout' => 60, 'CallBack' => \&callback) or die Net::TFTPd->error;
+
+=head1 Listener methods
+
+=head2 waitRQ()
+
+ $request = $listener->waitRQ([Timeout]);
+
+Waits for a client request (RRQ or WRQ) and returns a I<$request> object or I<undef> if timed out.
+
+If I<Timeout> is missing, the timeout defined for I<$listener> object is used instead.
+
+When the method returns, the program should fork() and process the request invoking processRQ() while the parent process should re-start waiting for another request.
+
+=head1 Request methods
+
+=head2 processRQ()
+
+ $ret = $request->processRQ();
+
+Processes a request and returns 1 if success, undef if error.
+
+=head2 getFileName()
+
+ $ret = $request->getFileName();
+
+Returns the requested file name.
+
+=head2 getMode()
+
+ $ret = $request->getMode();
+
+Returns the transfer mode for the request.
+
+=head2 getBlkSize()
+
+ $ret = $request->getBlkSize();
+
+Returns the block size used for the transfer.
+
+=head2 getPeerAddr()
+
+ $ret = $request->getPeerAddr();
+
+Returns the address of the requesting client.
+
+=head2 getPeerPort()
+
+ $ret = $request->getPeerMode();
+
+Returns the port of the requesting client.
+
+=head2 getTotalBytes()
+
+ $ret = $request->getTotalBytes();
+
+Returns the number of bytes transferred for the request.
+
+=head1 CREDITS
+
+Thanks to E<lt>VinceE<gt> for the NETASCII support and transferred bytes patch.
+
+=head1 AUTHOR
+
+Luigino Masarati, E<lt>lmasarati@hotmail.comE<gt>
+
+=head1 SEE ALSO
+
+L<Net::TFTP>.
+
+=cut
+