summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README3
-rw-r--r--bin/tftpy_client.py16
-rwxr-xr-xlib/tftpy.py176
3 files changed, 180 insertions, 15 deletions
diff --git a/README b/README
index 6b8a7ec..38ad26a 100644
--- a/README
+++ b/README
@@ -45,7 +45,8 @@ Limitations:
------------
- Server is not yet implemented
- Only 'octet' mode is supported
-- Uploading is not yet tested
+- Uploading in the client is not yet implemented
+- The only option supported is blksize
Author:
=======
diff --git a/bin/tftpy_client.py b/bin/tftpy_client.py
index e5c17b7..7b5fbc5 100644
--- a/bin/tftpy_client.py
+++ b/bin/tftpy_client.py
@@ -41,8 +41,22 @@ def main():
dest='debug',
default=False,
help='upgrade logging from info to debug')
+ parser.add_option('-q',
+ '--quiet',
+ action='store_true',
+ dest='quiet',
+ default=False,
+ help="downgrade logging from info to warning")
options, args = parser.parse_args()
if not options.host or not options.filename:
+ sys.stderr.write("Both the --host and --filename options "
+ "are required.\n")
+ parser.print_help()
+ sys.exit(1)
+
+ if options.debug and options.quiet:
+ sys.stderr.write("The --debug and --quiet options are "
+ "mutually exclusive.\n")
parser.print_help()
sys.exit(1)
@@ -59,6 +73,8 @@ def main():
if options.debug:
tftpy.setLogLevel(logging.DEBUG)
+ elif options.quiet:
+ tftpy.setLogLevel(logging.WARNING)
else:
tftpy.setLogLevel(logging.INFO)
diff --git a/lib/tftpy.py b/lib/tftpy.py
index 08b130b..df6c967 100755
--- a/lib/tftpy.py
+++ b/lib/tftpy.py
@@ -450,7 +450,9 @@ class TftpPacketFactory(object):
def create(self, opcode):
tftpassert(self.classes.has_key(opcode),
"Unsupported opcode: %d" % opcode)
+
packet = self.classes[opcode]()
+
logger.debug("packet is %s" % packet)
return packet
@@ -467,14 +469,18 @@ class TftpPacketFactory(object):
class TftpState(object):
"""This class represents a particular state for a TFTP Session. It encapsulates a
state, kind of like an enum. The states mean the following:
- nil - Session not yet established
- rrq - Just sent RRQ in a download, waiting for response
- wrq - Just sent WRQ in an upload, waiting for response
- dat - Transferring data
- oack - Received oack, negotiating options
- ack - Acknowledged oack, awaiting response
- err - Fatal problems, giving up
- fin - Transfer completed
+ nil - Client/Server - Session not yet established
+ rrq - Client - Just sent RRQ in a download, waiting for response
+ Server - Just received an RRQ
+ wrq - Client - Just sent WRQ in an upload, waiting for response
+ Server - Just received a WRQ
+ dat - Client/Server - Transferring data
+ oack - Client - Just received oack
+ Server - Just sent OACK
+ ack - Client - Acknowledged oack, awaiting response
+ Server - Just received ACK to OACK
+ err - Client/Server - Fatal problems, giving up
+ fin - Client/Server - Transfer completed
"""
states = ['nil',
'rrq',
@@ -583,7 +589,8 @@ class TftpServer(TftpSession):
logger.debug("Performing select on this inputlist: %s" % inputlist)
readyinput, readyoutput, readyspecial = select.select(inputlist,
[],
- [])
+ [],
+ SOCK_TIMEOUT)
#(buffer, (raddress, rport)) = self.sock.recvfrom(MAX_BLKSIZE)
#recvpkt = tftp_factory.parse(buffer)
@@ -655,6 +662,11 @@ class TftpServer(TftpSession):
else:
raise TftpException, "Can't find the owner for this traffic!"
+ logger.debug("Looping on all handlers to check for timeouts")
+ now = time.time()
+ for key in self.handlers:
+ self.handlers[key].check_timeout(now)
+
class TftpServerHandler(TftpSession):
"""This class implements a handler for a given server session, handling
the work for one download."""
@@ -663,6 +675,7 @@ class TftpServerHandler(TftpSession):
TftpSession.__init__(self)
logger.info("Starting new handler. Key %s." % key)
self.key = key
+ self.host, self.port = self.key.split(':')
self.listenip = listenip
# Note, correct state here is important as it tells the handler whether it's
# handling a download or an upload.
@@ -671,6 +684,12 @@ class TftpServerHandler(TftpSession):
self.mode = None
self.filename = None
self.sock = False
+ self.options = []
+ self.blocknumber = 0
+ self.buffer = None
+ self.fileobj = None
+ self.timesent = 0
+ self.timeouts = 0
count = 0
while not self.sock:
self.sock = self.gensock(listenip)
@@ -678,6 +697,31 @@ class TftpServerHandler(TftpSession):
if count > 10:
raise TftpException, "Failed to bind this handler to any port"
+ def check_timeout(self, now):
+ """This method checks to see if we've timed-out waiting for traffic
+ from the client."""
+ if self.timesent:
+ if now - self.timesent > SOCK_TIMEOUT:
+ self.timeout()
+
+ def timeout(self):
+ """This method handles a timeout condition."""
+ logger.debug("Handling timeout for handler %s" % self.key)
+ self.timeouts += 1
+ if self.timeouts > TIMEOUT_RETRIES:
+ raise TftpException, "Hit max retries, giving up."
+
+ if self.state.state == 'dat':
+ logger.debug("Timing out on DAT. Need to resend.")
+ self.send_dat(resend=True)
+ elif self.state.state == 'oack':
+ logger.debug("Timing out on OACK. Need to resend.")
+ self.send_oack()
+ else:
+ tftpassert(False,
+ "Timing out in unsupported state %s" %
+ self.state.state)
+
def gensock(self, listenip):
"""This method generates a new UDP socket, whose listening port must
be randomly generated, and not conflict with any already in use. For
@@ -697,8 +741,8 @@ class TftpServerHandler(TftpSession):
raise
def handle(self, pkttuple=None):
- """This method informs a handler instance that it has data waiting on its socket that
- it must read and process."""
+ """This method informs a handler instance that it has data waiting on
+ its socket that it must read and process."""
recvpkt = raddress = rport = None
if pkttuple:
logger.debug("Handed pkt %s for handler %s" % (recvpkt, self.key))
@@ -709,6 +753,7 @@ class TftpServerHandler(TftpSession):
logger.debug("Read %d bytes" % len(buffer))
recvpkt = tftp_factory.parse(buffer)
+ # FIXME - refactor into another method, this is too big
if isinstance(recvpkt, TftpPacketRRQ):
logger.debug("Handler %s received RRQ packet" % self.key)
logger.debug("Requested file is %s, mode is %s" % (recvpkt.filename,
@@ -745,7 +790,39 @@ class TftpServerHandler(TftpSession):
if os.path.exists(self.filename):
logger.debug("File %s exists." % self.filename)
- # FIXME - Check options, start upload.
+ # Check options. Currently we only support the blksize
+ # option.
+ if recvpkt.options.has_key('blksize'):
+ logger.debug("RRQ includes a blksize option")
+ blksize = recvpkt.options['blksize']
+ if blksize >= MIN_BLKSIZE and blksize <= MAX_BLKSIZE:
+ logger.debug("Client requested blksize = %d"
+ % blksize)
+ self.options['blksize'] = blksize
+ else:
+ logger.warning("Client %s requested invalid "
+ "blocksize %d, responding with default"
+ % (self.key, blksize))
+ self.options['blksize'] = DEF_BLKSIZE
+
+ logger.debug("Composing and sending OACK packet")
+ self.send_oack()
+
+ elif len(recvpkt.options.keys()) > 0:
+ logger.warning("Client %s requested unsupported options: %s"
+ % (self.key, recvpkt.options))
+ logger.warning("Responding with negotiation error")
+ self.senderror(self.sock,
+ TftpErrors.FailedNegotiation,
+ self.host,
+ self.port)
+ raise TftpException, "Failed option negotiation"
+
+ else:
+ logger.debug("Client %s requested no options."
+ % self.key)
+ self.start_download()
+
else:
logger.error("Requested file %s does not exist." %
self.filename)
@@ -761,10 +838,81 @@ class TftpServerHandler(TftpSession):
"but we're in state %s" % (self.key, self.state))
self.errors += 1
+ # Next packet type
+ elif isinstance(recvpkt, TftpPacketACK):
+ logger.debug("Received an ACK from the client.")
+ if recvpkt.blocknumber == 0 and self.state.state == 'oack':
+ logger.debug("Received ACK with 0 blocknumber, starting download")
+ self.start_download()
+ else:
+ if self.state.state == 'dat':
+ if self.blocknumber == recvpkt.blocknumber:
+ logger.debug("Received ACK for block %d, "
+ "sending next DAT" % recvpkt.blocknumber)
+ self.send_dat()
+ elif recvpkt.blocknumber < self.blocknumber:
+ logger.warn("Received old ACK for block number %d"
+ % recvpkt.blocknumber)
+ else:
+ logger.warn("Received ACK for block number "
+ "%d, apparently from the future"
+ % recvpkt.blocknumber)
+ else:
+ logger.error("Received ACK with block number %d "
+ "while in state %s"
+ % (recvpkt.blocknumber,
+ self.state.state))
+
+ elif isinstance(recvpkt, TftpPacketERR):
+ logger.error("Received error packet from client: %s" % recvpkt)
+ self.state.state = 'err'
+ raise TftpException, "Received error from client"
+
# Handle other packet types.
- # FIXME
+ else:
+ logger.error("Received packet %s while handling a download"
+ % recvpkt)
+ self.senderror(self.sock,
+ TftpErrors.IllegalTftpOp,
+ self.host,
+ self.port)
+ raise TftpException, "Invalid packet received during download"
+
+ def start_download(self):
+ """This method opens self.filename, stores the resulting file object
+ in self.fileobj, and calls send_dat()."""
+ self.state.state = 'dat'
+ self.fileobj = open(self.filename, "r")
+ self.send_dat()
+
+ def send_dat(self, resend=False):
+ """This method reads sends a DAT packet based on what is in self.buffer."""
+ if not resend:
+ self.buffer = self.fileobj.read(int(self.options['blksize']))
+ if not self.buffer:
+ logger.info("Reached EOF on file %s" % self.filename)
+ self.blocknumber += 1
+ if self.blocknumber > 65535:
+ self.blocknumber = 0
+ else:
+ logger.warn("Resending block number %d" % self.blocknumber)
+ dat = TftpPacketDAT()
+ dat.buffer = self.buffer
+ dat.blocknumber = self.blocknumber
+ logger.debug("Sending DAT packet %d" % self.blocknumber)
+ self.sock.sendto(dat.encode().buffer, (self.host, self.port))
+ self.timesent = time.time()
+
+ def send_oack(self):
+ """This method sends an OACK packet based on current params."""
+ logger.debug("Composing and sending OACK packet")
+ oack = TftpPacketOACK()
+ oack.options = self.options
+ self.sock.sendto(oack.encode().buffer,
+ (self.host, self.port))
+ self.timesent = time.time()
+ self.state.state = 'oack'
-
class TftpClient(TftpSession):
"""This class is an implementation of a tftp client."""
def __init__(self, host, port, options={}):