tftpy-0.6.0/0000755000175000017500000000000011613123423013137 5ustar msouliermsouliertftpy-0.6.0/tftpy/0000755000175000017500000000000011613123423014305 5ustar msouliermsouliertftpy-0.6.0/tftpy/TftpStates.py0000644000175000017500000005555511613107347017006 0ustar msouliermsoulier"""This module implements all state handling during uploads and downloads, the main interface to which being the TftpState base class. The concept is simple. Each context object represents a single upload or download, and the state object in the context object represents the current state of that transfer. The state object has a handle() method that expects the next packet in the transfer, and returns a state object until the transfer is complete, at which point it returns None. That is, unless there is a fatal error, in which case a TftpException is returned instead.""" from TftpShared import * from TftpPacketTypes import * import os ############################################################################### # State classes ############################################################################### class TftpState(object): """The base class for the states.""" def __init__(self, context): """Constructor for setting up common instance variables. The involved file object is required, since in tftp there's always a file involved.""" self.context = context def handle(self, pkt, raddress, rport): """An abstract method for handling a packet. It is expected to return a TftpState object, either itself or a new state.""" raise NotImplementedError, "Abstract method" def handleOACK(self, pkt): """This method handles an OACK from the server, syncing any accepted options.""" if pkt.options.keys() > 0: if pkt.match_options(self.context.options): log.info("Successful negotiation of options") # Set options to OACK options self.context.options = pkt.options for key in self.context.options: log.info(" %s = %s" % (key, self.context.options[key])) else: log.error("Failed to negotiate options") raise TftpException, "Failed to negotiate options" else: raise TftpException, "No options found in OACK" def returnSupportedOptions(self, options): """This method takes a requested options list from a client, and returns the ones that are supported.""" # We support the options blksize and tsize right now. # FIXME - put this somewhere else? accepted_options = {} for option in options: if option == 'blksize': # Make sure it's valid. if int(options[option]) > MAX_BLKSIZE: log.info("Client requested blksize greater than %d " "setting to maximum" % MAX_BLKSIZE) accepted_options[option] = MAX_BLKSIZE elif int(options[option]) < MIN_BLKSIZE: log.info("Client requested blksize less than %d " "setting to minimum" % MIN_BLKSIZE) accepted_options[option] = MIN_BLKSIZE else: accepted_options[option] = options[option] elif option == 'tsize': log.debug("tsize option is set") accepted_options['tsize'] = 1 else: log.info("Dropping unsupported option '%s'" % option) log.debug("Returning these accepted options: %s" % accepted_options) return accepted_options def serverInitial(self, pkt, raddress, rport): """This method performs initial setup for a server context transfer, put here to refactor code out of the TftpStateServerRecvRRQ and TftpStateServerRecvWRQ classes, since their initial setup is identical. The method returns a boolean, sendoack, to indicate whether it is required to send an OACK to the client.""" options = pkt.options sendoack = False if not self.context.tidport: self.context.tidport = rport log.info("Setting tidport to %s" % rport) log.debug("Setting default options, blksize") self.context.options = { 'blksize': DEF_BLKSIZE } if options: log.debug("Options requested: %s" % options) supported_options = self.returnSupportedOptions(options) self.context.options.update(supported_options) sendoack = True # FIXME - only octet mode is supported at this time. if pkt.mode != 'octet': self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, \ "Only octet transfers are supported at this time." # test host/port of client end if self.context.host != raddress or self.context.port != rport: self.sendError(TftpErrors.UnknownTID) log.error("Expected traffic from %s:%s but received it " "from %s:%s instead." % (self.context.host, self.context.port, raddress, rport)) # FIXME: increment an error count? # Return same state, we're still waiting for valid traffic. return self log.debug("Requested filename is %s" % pkt.filename) # There are no os.sep's allowed in the filename. # FIXME: Should we allow subdirectories? if pkt.filename.find(os.sep) >= 0: self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "%s found in filename, not permitted" % os.sep self.context.file_to_transfer = pkt.filename return sendoack def sendDAT(self): """This method sends the next DAT packet based on the data in the context. It returns a boolean indicating whether the transfer is finished.""" finished = False blocknumber = self.context.next_block # Test hook if DELAY_BLOCK and DELAY_BLOCK == blocknumber: import time log.debug("Deliberately delaying 10 seconds...") time.sleep(10) tftpassert( blocknumber > 0, "There is no block zero!" ) dat = None blksize = self.context.getBlocksize() buffer = self.context.fileobj.read(blksize) log.debug("Read %d bytes into buffer" % len(buffer)) if len(buffer) < blksize: log.info("Reached EOF on file %s" % self.context.file_to_transfer) finished = True dat = TftpPacketDAT() dat.data = buffer dat.blocknumber = blocknumber self.context.metrics.bytes += len(dat.data) log.debug("Sending DAT packet %d" % dat.blocknumber) self.context.sock.sendto(dat.encode().buffer, (self.context.host, self.context.tidport)) if self.context.packethook: self.context.packethook(dat) self.context.last_pkt = dat return finished def sendACK(self, blocknumber=None): """This method sends an ack packet to the block number specified. If none is specified, it defaults to the next_block property in the parent context.""" log.debug("In sendACK, passed blocknumber is %s" % blocknumber) if blocknumber is None: blocknumber = self.context.next_block log.info("Sending ack to block %d" % blocknumber) ackpkt = TftpPacketACK() ackpkt.blocknumber = blocknumber self.context.sock.sendto(ackpkt.encode().buffer, (self.context.host, self.context.tidport)) self.context.last_pkt = ackpkt def sendError(self, errorcode): """This method uses the socket passed, and uses the errorcode to compose and send an error packet.""" log.debug("In sendError, being asked to send error %d" % errorcode) errpkt = TftpPacketERR() errpkt.errorcode = errorcode self.context.sock.sendto(errpkt.encode().buffer, (self.context.host, self.context.tidport)) self.context.last_pkt = errpkt def sendOACK(self): """This method sends an OACK packet with the options from the current context.""" log.debug("In sendOACK with options %s" % self.context.options) pkt = TftpPacketOACK() pkt.options = self.context.options self.context.sock.sendto(pkt.encode().buffer, (self.context.host, self.context.tidport)) self.context.last_pkt = pkt def resendLast(self): "Resend the last sent packet due to a timeout." log.warn("Resending packet %s on sessions %s" % (self.context.last_pkt, self)) self.context.metrics.resent_bytes += len(self.context.last_pkt.buffer) self.context.metrics.add_dup(self.context.last_pkt) self.context.sock.sendto(self.context.last_pkt.encode().buffer, (self.context.host, self.context.tidport)) if self.context.packethook: self.context.packethook(self.context.last_pkt) def handleDat(self, pkt): """This method handles a DAT packet during a client download, or a server upload.""" log.info("Handling DAT packet - block %d" % pkt.blocknumber) log.debug("Expecting block %s" % self.context.next_block) if pkt.blocknumber == self.context.next_block: log.debug("Good, received block %d in sequence" % pkt.blocknumber) self.sendACK() self.context.next_block += 1 log.debug("Writing %d bytes to output file" % len(pkt.data)) self.context.fileobj.write(pkt.data) self.context.metrics.bytes += len(pkt.data) # Check for end-of-file, any less than full data packet. if len(pkt.data) < self.context.getBlocksize(): log.info("End of file detected") return None elif pkt.blocknumber < self.context.next_block: if pkt.blocknumber == 0: log.warn("There is no block zero!") self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "There is no block zero!" log.warn("Dropping duplicate block %d" % pkt.blocknumber) self.context.metrics.add_dup(pkt) log.debug("ACKing block %d again, just in case" % pkt.blocknumber) self.sendACK(pkt.blocknumber) else: # FIXME: should we be more tolerant and just discard instead? msg = "Whoa! Received future block %d but expected %d" \ % (pkt.blocknumber, self.context.next_block) log.error(msg) raise TftpException, msg # Default is to ack return TftpStateExpectDAT(self.context) class TftpStateServerRecvRRQ(TftpState): """This class represents the state of the TFTP server when it has just received an RRQ packet.""" def handle(self, pkt, raddress, rport): "Handle an initial RRQ packet as a server." log.debug("In TftpStateServerRecvRRQ.handle") sendoack = self.serverInitial(pkt, raddress, rport) path = self.context.root + os.sep + self.context.file_to_transfer log.info("Opening file %s for reading" % path) if os.path.exists(path): # Note: Open in binary mode for win32 portability, since win32 # blows. self.context.fileobj = open(path, "rb") elif self.context.dyn_file_func: log.debug("No such file %s but using dyn_file_func" % path) self.context.fileobj = \ self.context.dyn_file_func(self.context.file_to_transfer) if self.context.fileobj is None: log.debug("dyn_file_func returned 'None', treating as " "FileNotFound") self.sendError(TftpErrors.FileNotFound) raise TftpException, "File not found: %s" % path else: self.sendError(TftpErrors.FileNotFound) raise TftpException, "File not found: %s" % path # Options negotiation. if sendoack: # Note, next_block is 0 here since that's the proper # acknowledgement to an OACK. # FIXME: perhaps we do need a TftpStateExpectOACK class... self.sendOACK() # Note, self.context.next_block is already 0. else: self.context.next_block = 1 log.debug("No requested options, starting send...") self.context.pending_complete = self.sendDAT() # Note, we expect an ack regardless of whether we sent a DAT or an # OACK. return TftpStateExpectACK(self.context) # Note, we don't have to check any other states in this method, that's # up to the caller. class TftpStateServerRecvWRQ(TftpState): """This class represents the state of the TFTP server when it has just received a WRQ packet.""" def handle(self, pkt, raddress, rport): "Handle an initial WRQ packet as a server." log.debug("In TftpStateServerRecvWRQ.handle") sendoack = self.serverInitial(pkt, raddress, rport) path = self.context.root + os.sep + self.context.file_to_transfer log.info("Opening file %s for writing" % path) if os.path.exists(path): # FIXME: correct behavior? log.warn("File %s exists already, overwriting..." % self.context.file_to_transfer) # FIXME: I think we should upload to a temp file and not overwrite the # existing file until the file is successfully uploaded. self.context.fileobj = open(path, "wb") # Options negotiation. if sendoack: log.debug("Sending OACK to client") self.sendOACK() else: log.debug("No requested options, expecting transfer to begin...") self.sendACK() # Whether we're sending an oack or not, we're expecting a DAT for # block 1 self.context.next_block = 1 # We may have sent an OACK, but we're expecting a DAT as the response # to either the OACK or an ACK, so lets unconditionally use the # TftpStateExpectDAT state. return TftpStateExpectDAT(self.context) # Note, we don't have to check any other states in this method, that's # up to the caller. class TftpStateServerStart(TftpState): """The start state for the server. This is a transitory state since at this point we don't know if we're handling an upload or a download. We will commit to one of them once we interpret the initial packet.""" def handle(self, pkt, raddress, rport): """Handle a packet we just received.""" log.debug("In TftpStateServerStart.handle") if isinstance(pkt, TftpPacketRRQ): log.debug("Handling an RRQ packet") return TftpStateServerRecvRRQ(self.context).handle(pkt, raddress, rport) elif isinstance(pkt, TftpPacketWRQ): log.debug("Handling a WRQ packet") return TftpStateServerRecvWRQ(self.context).handle(pkt, raddress, rport) else: self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, \ "Invalid packet to begin up/download: %s" % pkt class TftpStateExpectACK(TftpState): """This class represents the state of the transfer when a DAT was just sent, and we are waiting for an ACK from the server. This class is the same one used by the client during the upload, and the server during the download.""" def handle(self, pkt, raddress, rport): "Handle a packet, hopefully an ACK since we just sent a DAT." if isinstance(pkt, TftpPacketACK): log.info("Received ACK for packet %d" % pkt.blocknumber) # Is this an ack to the one we just sent? if self.context.next_block == pkt.blocknumber: if self.context.pending_complete: log.info("Received ACK to final DAT, we're done.") return None else: log.debug("Good ACK, sending next DAT") self.context.next_block += 1 log.debug("Incremented next_block to %d" % (self.context.next_block)) self.context.pending_complete = self.sendDAT() elif pkt.blocknumber < self.context.next_block: log.debug("Received duplicate ACK for block %d" % pkt.blocknumber) self.context.metrics.add_dup(pkt) else: log.warn("Oooh, time warp. Received ACK to packet we " "didn't send yet. Discarding.") self.context.metrics.errors += 1 return self elif isinstance(pkt, TftpPacketERR): log.error("Received ERR packet from peer: %s" % str(pkt)) raise TftpException, \ "Received ERR packet from peer: %s" % str(pkt) else: log.warn("Discarding unsupported packet: %s" % str(pkt)) return self class TftpStateExpectDAT(TftpState): """Just sent an ACK packet. Waiting for DAT.""" def handle(self, pkt, raddress, rport): """Handle the packet in response to an ACK, which should be a DAT.""" if isinstance(pkt, TftpPacketDAT): return self.handleDat(pkt) # Every other packet type is a problem. elif isinstance(pkt, TftpPacketACK): # Umm, we ACK, you don't. self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received ACK from peer when expecting DAT" elif isinstance(pkt, TftpPacketWRQ): self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received WRQ from peer when expecting DAT" elif isinstance(pkt, TftpPacketERR): self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received ERR from peer: " + str(pkt) else: self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received unknown packet type from peer: " + str(pkt) class TftpStateSentWRQ(TftpState): """Just sent an WRQ packet for an upload.""" def handle(self, pkt, raddress, rport): """Handle a packet we just received.""" if not self.context.tidport: self.context.tidport = rport log.debug("Set remote port for session to %s" % rport) # If we're going to successfully transfer the file, then we should see # either an OACK for accepted options, or an ACK to ignore options. if isinstance(pkt, TftpPacketOACK): log.info("Received OACK from server") try: self.handleOACK(pkt) except TftpException: log.error("Failed to negotiate options") self.sendError(TftpErrors.FailedNegotiation) raise else: log.debug("Sending first DAT packet") self.context.pending_complete = self.sendDAT() log.debug("Changing state to TftpStateExpectACK") return TftpStateExpectACK(self.context) elif isinstance(pkt, TftpPacketACK): log.info("Received ACK from server") log.debug("Apparently the server ignored our options") # The block number should be zero. if pkt.blocknumber == 0: log.debug("Ack blocknumber is zero as expected") log.debug("Sending first DAT packet") self.context.pending_complete = self.sendDAT() log.debug("Changing state to TftpStateExpectACK") return TftpStateExpectACK(self.context) else: log.warn("Discarding ACK to block %s" % pkt.blocknumber) log.debug("Still waiting for valid response from server") return self elif isinstance(pkt, TftpPacketERR): self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received ERR from server: " + str(pkt) elif isinstance(pkt, TftpPacketRRQ): self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received RRQ from server while in upload" elif isinstance(pkt, TftpPacketDAT): self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received DAT from server while in upload" else: self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received unknown packet type from server: " + str(pkt) # By default, no state change. return self class TftpStateSentRRQ(TftpState): """Just sent an RRQ packet.""" def handle(self, pkt, raddress, rport): """Handle the packet in response to an RRQ to the server.""" if not self.context.tidport: self.context.tidport = rport log.info("Set remote port for session to %s" % rport) # Now check the packet type and dispatch it properly. if isinstance(pkt, TftpPacketOACK): log.info("Received OACK from server") try: self.handleOACK(pkt) except TftpException, err: log.error("Failed to negotiate options: %s" % str(err)) self.sendError(TftpErrors.FailedNegotiation) raise else: log.debug("Sending ACK to OACK") self.sendACK(blocknumber=0) log.debug("Changing state to TftpStateExpectDAT") return TftpStateExpectDAT(self.context) elif isinstance(pkt, TftpPacketDAT): # If there are any options set, then the server didn't honour any # of them. log.info("Received DAT from server") if self.context.options: log.info("Server ignored options, falling back to defaults") self.context.options = { 'blksize': DEF_BLKSIZE } return self.handleDat(pkt) # Every other packet type is a problem. elif isinstance(pkt, TftpPacketACK): # Umm, we ACK, the server doesn't. self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received ACK from server while in download" elif isinstance(pkt, TftpPacketWRQ): self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received WRQ from server while in download" elif isinstance(pkt, TftpPacketERR): self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received ERR from server: " + str(pkt) else: self.sendError(TftpErrors.IllegalTftpOp) raise TftpException, "Received unknown packet type from server: " + str(pkt) # By default, no state change. return self tftpy-0.6.0/tftpy/TftpPacketFactory.py0000644000175000017500000000265611533447440020276 0ustar msouliermsoulier"""This module implements the TftpPacketFactory class, which can take a binary buffer, and return the appropriate TftpPacket object to represent it, via the parse() method.""" from TftpShared import * from TftpPacketTypes import * class TftpPacketFactory(object): """This class generates TftpPacket objects. It is responsible for parsing raw buffers off of the wire and returning objects representing them, via the parse() method.""" def __init__(self): self.classes = { 1: TftpPacketRRQ, 2: TftpPacketWRQ, 3: TftpPacketDAT, 4: TftpPacketACK, 5: TftpPacketERR, 6: TftpPacketOACK } def parse(self, buffer): """This method is used to parse an existing datagram into its corresponding TftpPacket object. The buffer is the raw bytes off of the network.""" log.debug("parsing a %d byte packet" % len(buffer)) (opcode,) = struct.unpack("!H", buffer[:2]) log.debug("opcode is %d" % opcode) packet = self.__create(opcode) packet.buffer = buffer return packet.decode() def __create(self, opcode): """This method returns the appropriate class object corresponding to the passed opcode.""" tftpassert(self.classes.has_key(opcode), "Unsupported opcode: %d" % opcode) packet = self.classes[opcode]() return packet tftpy-0.6.0/tftpy/TftpContexts.py0000644000175000017500000003412311613103403017323 0ustar msouliermsoulier"""This module implements all contexts for state handling during uploads and downloads, the main interface to which being the TftpContext base class. The concept is simple. Each context object represents a single upload or download, and the state object in the context object represents the current state of that transfer. The state object has a handle() method that expects the next packet in the transfer, and returns a state object until the transfer is complete, at which point it returns None. That is, unless there is a fatal error, in which case a TftpException is returned instead.""" from TftpShared import * from TftpPacketTypes import * from TftpPacketFactory import TftpPacketFactory from TftpStates import * import socket, time, sys ############################################################################### # Utility classes ############################################################################### class TftpMetrics(object): """A class representing metrics of the transfer.""" def __init__(self): # Bytes transferred self.bytes = 0 # Bytes re-sent self.resent_bytes = 0 # Duplicate packets received self.dups = {} self.dupcount = 0 # Times self.start_time = 0 self.end_time = 0 self.duration = 0 # Rates self.bps = 0 self.kbps = 0 # Generic errors self.errors = 0 def compute(self): # Compute transfer time self.duration = self.end_time - self.start_time if self.duration == 0: self.duration = 1 log.debug("TftpMetrics.compute: duration is %s" % self.duration) self.bps = (self.bytes * 8.0) / self.duration self.kbps = self.bps / 1024.0 log.debug("TftpMetrics.compute: kbps is %s" % self.kbps) for key in self.dups: self.dupcount += self.dups[key] def add_dup(self, pkt): """This method adds a dup for a packet to the metrics.""" log.debug("Recording a dup of %s" % pkt) s = str(pkt) if self.dups.has_key(s): self.dups[s] += 1 else: self.dups[s] = 1 tftpassert(self.dups[s] < MAX_DUPS, "Max duplicates reached") ############################################################################### # Context classes ############################################################################### class TftpContext(object): """The base class of the contexts.""" def __init__(self, host, port, timeout, dyn_file_func=None): """Constructor for the base context, setting shared instance variables.""" self.file_to_transfer = None self.fileobj = None self.options = None self.packethook = None self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.settimeout(timeout) self.timeout = timeout self.state = None self.next_block = 0 self.factory = TftpPacketFactory() # Note, setting the host will also set self.address, as it's a property. self.host = host self.port = port # The port associated with the TID self.tidport = None # Metrics self.metrics = TftpMetrics() # Fluag when the transfer is pending completion. self.pending_complete = False # Time when this context last received any traffic. # FIXME: does this belong in metrics? self.last_update = 0 # The last packet we sent, if applicable, to make resending easy. self.last_pkt = None self.dyn_file_func = dyn_file_func # Count the number of retry attempts. self.retry_count = 0 def getBlocksize(self): """Fetch the current blocksize for this session.""" return int(self.options.get('blksize', 512)) def __del__(self): """Simple destructor to try to call housekeeping in the end method if not called explicitely. Leaking file descriptors is not a good thing.""" self.end() def checkTimeout(self, now): """Compare current time with last_update time, and raise an exception if we're over the timeout time.""" log.debug("checking for timeout on session %s" % self) if now - self.last_update > self.timeout: raise TftpTimeout, "Timeout waiting for traffic" def start(self): raise NotImplementedError, "Abstract method" def end(self): """Perform session cleanup, since the end method should always be called explicitely by the calling code, this works better than the destructor.""" log.debug("in TftpContext.end") if self.fileobj is not None and not self.fileobj.closed: log.debug("self.fileobj is open - closing") self.fileobj.close() def gethost(self): "Simple getter method for use in a property." return self.__host def sethost(self, host): """Setter method that also sets the address property as a result of the host that is set.""" self.__host = host self.address = socket.gethostbyname(host) host = property(gethost, sethost) def setNextBlock(self, block): if block >= 2 ** 16: log.debug("Block number rollover to 0 again") block = 0 self.__eblock = block def getNextBlock(self): return self.__eblock next_block = property(getNextBlock, setNextBlock) def cycle(self): """Here we wait for a response from the server after sending it something, and dispatch appropriate action to that response.""" try: (buffer, (raddress, rport)) = self.sock.recvfrom(MAX_BLKSIZE) except socket.timeout: log.warn("Timeout waiting for traffic, retrying...") raise TftpTimeout, "Timed-out waiting for traffic" # Ok, we've received a packet. Log it. log.debug("Received %d bytes from %s:%s" % (len(buffer), raddress, rport)) # And update our last updated time. self.last_update = time.time() # Decode it. recvpkt = self.factory.parse(buffer) # Check for known "connection". if raddress != self.address: log.warn("Received traffic from %s, expected host %s. Discarding" % (raddress, self.host)) if self.tidport and self.tidport != rport: log.warn("Received traffic from %s:%s but we're " "connected to %s:%s. Discarding." % (raddress, rport, self.host, self.tidport)) # If there is a packethook defined, call it. We unconditionally # pass all packets, it's up to the client to screen out different # kinds of packets. This way, the client is privy to things like # negotiated options. if self.packethook: self.packethook(recvpkt) # And handle it, possibly changing state. self.state = self.state.handle(recvpkt, raddress, rport) # If we didn't throw any exceptions here, reset the retry_count to # zero. self.retry_count = 0 class TftpContextServer(TftpContext): """The context for the server.""" def __init__(self, host, port, timeout, root, dyn_file_func=None): TftpContext.__init__(self, host, port, timeout, dyn_file_func ) # At this point we have no idea if this is a download or an upload. We # need to let the start state determine that. self.state = TftpStateServerStart(self) self.root = root self.dyn_file_func = dyn_file_func def __str__(self): return "%s:%s %s" % (self.host, self.port, self.state) def start(self, buffer): """Start the state cycle. Note that the server context receives an initial packet in its start method. Also note that the server does not loop on cycle(), as it expects the TftpServer object to manage that.""" log.debug("In TftpContextServer.start") self.metrics.start_time = time.time() log.debug("Set metrics.start_time to %s" % self.metrics.start_time) # And update our last updated time. self.last_update = time.time() pkt = self.factory.parse(buffer) log.debug("TftpContextServer.start() - factory returned a %s" % pkt) # Call handle once with the initial packet. This should put us into # the download or the upload state. self.state = self.state.handle(pkt, self.host, self.port) def end(self): """Finish up the context.""" TftpContext.end(self) self.metrics.end_time = time.time() log.debug("Set metrics.end_time to %s" % self.metrics.end_time) self.metrics.compute() class TftpContextClientUpload(TftpContext): """The upload context for the client during an upload. Note: If input is a hyphen, then we will use stdin.""" def __init__(self, host, port, filename, input, options, packethook, timeout): TftpContext.__init__(self, host, port, timeout) self.file_to_transfer = filename self.options = options self.packethook = packethook if input == '-': self.fileobj = sys.stdin else: self.fileobj = open(input, "rb") log.debug("TftpContextClientUpload.__init__()") log.debug("file_to_transfer = %s, options = %s" % (self.file_to_transfer, self.options)) def __str__(self): return "%s:%s %s" % (self.host, self.port, self.state) def start(self): log.info("Sending tftp upload request to %s" % self.host) log.info(" filename -> %s" % self.file_to_transfer) log.info(" options -> %s" % self.options) self.metrics.start_time = time.time() log.debug("Set metrics.start_time to %s" % self.metrics.start_time) # FIXME: put this in a sendWRQ method? pkt = TftpPacketWRQ() pkt.filename = self.file_to_transfer pkt.mode = "octet" # FIXME - shouldn't hardcode this pkt.options = self.options self.sock.sendto(pkt.encode().buffer, (self.host, self.port)) self.next_block = 1 self.last_pkt = pkt # FIXME: should we centralize sendto operations so we can refactor all # saving of the packet to the last_pkt field? self.state = TftpStateSentWRQ(self) while self.state: try: log.debug("State is %s" % self.state) self.cycle() except TftpTimeout, err: log.error(str(err)) self.retry_count += 1 if self.retry_count >= TIMEOUT_RETRIES: log.debug("hit max retries, giving up") raise else: log.warn("resending last packet") self.state.resendLast() def end(self): """Finish up the context.""" TftpContext.end(self) self.metrics.end_time = time.time() log.debug("Set metrics.end_time to %s" % self.metrics.end_time) self.metrics.compute() class TftpContextClientDownload(TftpContext): """The download context for the client during a download. Note: If output is a hyphen, then the output will be sent to stdout.""" def __init__(self, host, port, filename, output, options, packethook, timeout): TftpContext.__init__(self, host, port, timeout) # FIXME: should we refactor setting of these params? self.file_to_transfer = filename self.options = options self.packethook = packethook # FIXME - need to support alternate return formats than files? # File-like objects would be ideal, ala duck-typing. # If the filename is -, then use stdout if output == '-': self.fileobj = sys.stdout else: self.fileobj = open(output, "wb") log.debug("TftpContextClientDownload.__init__()") log.debug("file_to_transfer = %s, options = %s" % (self.file_to_transfer, self.options)) def __str__(self): return "%s:%s %s" % (self.host, self.port, self.state) def start(self): """Initiate the download.""" log.info("Sending tftp download request to %s" % self.host) log.info(" filename -> %s" % self.file_to_transfer) log.info(" options -> %s" % self.options) self.metrics.start_time = time.time() log.debug("Set metrics.start_time to %s" % self.metrics.start_time) # FIXME: put this in a sendRRQ method? pkt = TftpPacketRRQ() pkt.filename = self.file_to_transfer pkt.mode = "octet" # FIXME - shouldn't hardcode this pkt.options = self.options self.sock.sendto(pkt.encode().buffer, (self.host, self.port)) self.next_block = 1 self.last_pkt = pkt self.state = TftpStateSentRRQ(self) while self.state: try: log.debug("State is %s" % self.state) self.cycle() except TftpTimeout, err: log.error(str(err)) self.retry_count += 1 if self.retry_count >= TIMEOUT_RETRIES: log.debug("hit max retries, giving up") raise else: log.warn("resending last packet") self.state.resendLast() def end(self): """Finish up the context.""" TftpContext.end(self) self.metrics.end_time = time.time() log.debug("Set metrics.end_time to %s" % self.metrics.end_time) self.metrics.compute() tftpy-0.6.0/tftpy/TftpServer.py0000644000175000017500000002121611612710565016775 0ustar msouliermsoulier"""This module implements the TFTP Server functionality. Instantiate an instance of the server, and then run the listen() method to listen for client requests. Logging is performed via a standard logging object set in TftpShared.""" import socket, os, time import select from TftpShared import * from TftpPacketTypes import * from TftpPacketFactory import TftpPacketFactory from TftpContexts import TftpContextServer class TftpServer(TftpSession): """This class implements a tftp server object. Run the listen() method to listen for client requests. It takes two optional arguments. tftproot is the path to the tftproot directory to serve files from and/or write them to. dyn_file_func is a callable that must return a file-like object to read from during downloads. This permits the serving of dynamic content.""" def __init__(self, tftproot='/tftpboot', dyn_file_func=None): self.listenip = None self.listenport = None self.sock = None # FIXME: What about multiple roots? self.root = os.path.abspath(tftproot) self.dyn_file_func = dyn_file_func # A dict of sessions, where each session is keyed by a string like # ip:tid for the remote end. self.sessions = {} if os.path.exists(self.root): log.debug("tftproot %s does exist" % self.root) if not os.path.isdir(self.root): raise TftpException, "The tftproot must be a directory." else: log.debug("tftproot %s is a directory" % self.root) if os.access(self.root, os.R_OK): log.debug("tftproot %s is readable" % self.root) else: raise TftpException, "The tftproot must be readable" if os.access(self.root, os.W_OK): log.debug("tftproot %s is writable" % self.root) else: log.warning("The tftproot %s is not writable" % self.root) else: raise TftpException, "The tftproot does not exist." def listen(self, listenip="", listenport=DEF_TFTP_PORT, timeout=SOCK_TIMEOUT): """Start a server listening on the supplied interface and port. This defaults to INADDR_ANY (all interfaces) and UDP port 69. You can also supply a different socket timeout value, if desired.""" tftp_factory = TftpPacketFactory() # Don't use new 2.5 ternary operator yet # listenip = listenip if listenip else '0.0.0.0' if not listenip: listenip = '0.0.0.0' log.info("Server requested on ip %s, port %s" % (listenip, listenport)) try: # FIXME - sockets should be non-blocking self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.bind((listenip, listenport)) except socket.error, err: # Reraise it for now. raise log.info("Starting receive loop...") while True: # Build the inputlist array of sockets to select() on. inputlist = [] inputlist.append(self.sock) for key in self.sessions: inputlist.append(self.sessions[key].sock) # Block until some socket has input on it. log.debug("Performing select on this inputlist: %s" % inputlist) readyinput, readyoutput, readyspecial = select.select(inputlist, [], [], SOCK_TIMEOUT) deletion_list = [] # Handle the available data, if any. Maybe we timed-out. for readysock in readyinput: # Is the traffic on the main server socket? ie. new session? if readysock == self.sock: log.debug("Data ready on our main socket") buffer, (raddress, rport) = self.sock.recvfrom(MAX_BLKSIZE) log.debug("Read %d bytes" % len(buffer)) # Forge a session key based on the client's IP and port, # which should safely work through NAT. key = "%s:%s" % (raddress, rport) if not self.sessions.has_key(key): log.debug("Creating new server context for " "session key = %s" % key) self.sessions[key] = TftpContextServer(raddress, rport, timeout, self.root, self.dyn_file_func) try: self.sessions[key].start(buffer) except TftpException, err: deletion_list.append(key) log.error("Fatal exception thrown from " "session %s: %s" % (key, str(err))) else: log.warn("received traffic on main socket for " "existing session??") log.info("Currently handling these sessions:") for session_key, session in self.sessions.items(): log.info(" %s" % session) else: # Must find the owner of this traffic. for key in self.sessions: if readysock == self.sessions[key].sock: log.info("Matched input to session key %s" % key) try: self.sessions[key].cycle() if self.sessions[key].state == None: log.info("Successful transfer.") deletion_list.append(key) except TftpException, err: deletion_list.append(key) log.error("Fatal exception thrown from " "session %s: %s" % (key, str(err))) # Break out of for loop since we found the correct # session. break else: log.error("Can't find the owner for this packet. " "Discarding.") log.debug("Looping on all sessions to check for timeouts") now = time.time() for key in self.sessions: try: self.sessions[key].checkTimeout(now) except TftpTimeout, err: log.error(str(err)) self.sessions[key].retry_count += 1 if self.sessions[key].retry_count >= TIMEOUT_RETRIES: log.debug("hit max retries on %s, giving up" % self.sessions[key]) deletion_list.append(key) else: log.debug("resending on session %s" % self.sessions[key]) self.sessions[key].state.resendLast() log.debug("Iterating deletion list.") for key in deletion_list: log.info('') log.info("Session %s complete" % key) if self.sessions.has_key(key): log.debug("Gathering up metrics from session before deleting") self.sessions[key].end() metrics = self.sessions[key].metrics if metrics.duration == 0: log.info("Duration too short, rate undetermined") else: log.info("Transferred %d bytes in %.2f seconds" % (metrics.bytes, metrics.duration)) log.info("Average rate: %.2f kbps" % metrics.kbps) log.info("%.2f bytes in resent data" % metrics.resent_bytes) log.info("%d duplicate packets" % metrics.dupcount) log.debug("Deleting session %s" % key) del self.sessions[key] log.debug("Session list is now %s" % self.sessions) else: log.warn("Strange, session %s is not on the deletion list" % key) tftpy-0.6.0/tftpy/TftpClient.py0000644000175000017500000001146511613072754016754 0ustar msouliermsoulier"""This module implements the TFTP Client functionality. Instantiate an instance of the client, and then use its upload or download method. Logging is performed via a standard logging object set in TftpShared.""" import types from TftpShared import * from TftpPacketTypes import * from TftpContexts import TftpContextClientDownload, TftpContextClientUpload class TftpClient(TftpSession): """This class is an implementation of a tftp client. Once instantiated, a download can be initiated via the download() method, or an upload via the upload() method.""" def __init__(self, host, port, options={}): TftpSession.__init__(self) self.context = None self.host = host self.iport = port self.filename = None self.options = options if self.options.has_key('blksize'): size = self.options['blksize'] tftpassert(types.IntType == type(size), "blksize must be an int") if size < MIN_BLKSIZE or size > MAX_BLKSIZE: raise TftpException, "Invalid blksize: %d" % size def download(self, filename, output, packethook=None, timeout=SOCK_TIMEOUT): """This method initiates a tftp download from the configured remote host, requesting the filename passed. It saves the file to a local file specified in the output parameter. If a packethook is provided, it must be a function that takes a single parameter, which will be a copy of each DAT packet received in the form of a TftpPacketDAT object. The timeout parameter may be used to override the default SOCK_TIMEOUT setting, which is the amount of time that the client will wait for a receive packet to arrive. Note: If output is a hyphen then stdout is used.""" # We're downloading. log.debug("Creating download context with the following params:") log.debug("host = %s, port = %s, filename = %s, output = %s" % (self.host, self.iport, filename, output)) log.debug("options = %s, packethook = %s, timeout = %s" % (self.options, packethook, timeout)) self.context = TftpContextClientDownload(self.host, self.iport, filename, output, self.options, packethook, timeout) self.context.start() # Download happens here self.context.end() metrics = self.context.metrics log.info('') log.info("Download complete.") if metrics.duration == 0: log.info("Duration too short, rate undetermined") else: log.info("Downloaded %.2f bytes in %.2f seconds" % (metrics.bytes, metrics.duration)) log.info("Average rate: %.2f kbps" % metrics.kbps) log.info("%.2f bytes in resent data" % metrics.resent_bytes) log.info("Received %d duplicate packets" % metrics.dupcount) def upload(self, filename, input, packethook=None, timeout=SOCK_TIMEOUT): """This method initiates a tftp upload to the configured remote host, uploading the filename passed. If a packethook is provided, it must be a function that takes a single parameter, which will be a copy of each DAT packet sent in the form of a TftpPacketDAT object. The timeout parameter may be used to override the default SOCK_TIMEOUT setting, which is the amount of time that the client will wait for a DAT packet to be ACKd by the server. The input option is the full path to the file to upload, which can optionally be '-' to read from stdin. Note: If output is a hyphen then stdout is used.""" self.context = TftpContextClientUpload(self.host, self.iport, filename, input, self.options, packethook, timeout) self.context.start() # Upload happens here self.context.end() metrics = self.context.metrics log.info('') log.info("Upload complete.") if metrics.duration == 0: log.info("Duration too short, rate undetermined") else: log.info("Uploaded %d bytes in %.2f seconds" % (metrics.bytes, metrics.duration)) log.info("Average rate: %.2f kbps" % metrics.kbps) log.info("%.2f bytes in resent data" % metrics.resent_bytes) log.info("Resent %d packets" % metrics.dupcount) tftpy-0.6.0/tftpy/TftpPacketTypes.py0000644000175000017500000004001511612710565017761 0ustar msouliermsoulier"""This module implements the packet types of TFTP itself, and the corresponding encode and decode methods for them.""" import struct from TftpShared import * class TftpSession(object): """This class is the base class for the tftp client and server. Any shared code should be in this class.""" # FIXME: do we need this anymore? pass class TftpPacketWithOptions(object): """This class exists to permit some TftpPacket subclasses to share code regarding options handling. It does not inherit from TftpPacket, as the goal is just to share code here, and not cause diamond inheritance.""" def __init__(self): self.options = {} def setoptions(self, options): log.debug("in TftpPacketWithOptions.setoptions") log.debug("options: " + str(options)) myoptions = {} for key in options: newkey = str(key) myoptions[newkey] = str(options[key]) log.debug("populated myoptions with %s = %s" % (newkey, myoptions[newkey])) log.debug("setting options hash to: " + str(myoptions)) self._options = myoptions def getoptions(self): log.debug("in TftpPacketWithOptions.getoptions") return self._options # Set up getter and setter on options to ensure that they are the proper # type. They should always be strings, but we don't need to force the # client to necessarily enter strings if we can avoid it. options = property(getoptions, setoptions) def decode_options(self, buffer): """This method decodes the section of the buffer that contains an unknown number of options. It returns a dictionary of option names and values.""" format = "!" options = {} log.debug("decode_options: buffer is: " + repr(buffer)) log.debug("size of buffer is %d bytes" % len(buffer)) if len(buffer) == 0: log.debug("size of buffer is zero, returning empty hash") return {} # Count the nulls in the buffer. Each one terminates a string. log.debug("about to iterate options buffer counting nulls") length = 0 for c in buffer: #log.debug("iterating this byte: " + repr(c)) if ord(c) == 0: log.debug("found a null at length %d" % length) if length > 0: format += "%dsx" % length length = -1 else: raise TftpException, "Invalid options in buffer" length += 1 log.debug("about to unpack, format is: %s" % format) mystruct = struct.unpack(format, buffer) tftpassert(len(mystruct) % 2 == 0, "packet with odd number of option/value pairs") for i in range(0, len(mystruct), 2): log.debug("setting option %s to %s" % (mystruct[i], mystruct[i+1])) options[mystruct[i]] = mystruct[i+1] return options class TftpPacket(object): """This class is the parent class of all tftp packet classes. It is an abstract class, providing an interface, and should not be instantiated directly.""" def __init__(self): self.opcode = 0 self.buffer = None def encode(self): """The encode method of a TftpPacket takes keyword arguments specific to the type of packet, and packs an appropriate buffer in network-byte order suitable for sending over the wire. This is an abstract method.""" raise NotImplementedError, "Abstract method" def decode(self): """The decode method of a TftpPacket takes a buffer off of the wire in network-byte order, and decodes it, populating internal properties as appropriate. This can only be done once the first 2-byte opcode has already been decoded, but the data section does include the entire datagram. This is an abstract method.""" raise NotImplementedError, "Abstract method" class TftpPacketInitial(TftpPacket, TftpPacketWithOptions): """This class is a common parent class for the RRQ and WRQ packets, as they share quite a bit of code.""" def __init__(self): TftpPacket.__init__(self) TftpPacketWithOptions.__init__(self) self.filename = None self.mode = None def encode(self): """Encode the packet's buffer from the instance variables.""" tftpassert(self.filename, "filename required in initial packet") tftpassert(self.mode, "mode required in initial packet") ptype = None if self.opcode == 1: ptype = "RRQ" else: ptype = "WRQ" log.debug("Encoding %s packet, filename = %s, mode = %s" % (ptype, self.filename, self.mode)) for key in self.options: log.debug(" Option %s = %s" % (key, self.options[key])) format = "!H" format += "%dsx" % len(self.filename) if self.mode == "octet": format += "5sx" else: raise AssertionError, "Unsupported mode: %s" % mode # Add options. options_list = [] if self.options.keys() > 0: log.debug("there are options to encode") for key in self.options: # Populate the option name format += "%dsx" % len(key) options_list.append(key) # Populate the option value format += "%dsx" % len(str(self.options[key])) options_list.append(str(self.options[key])) log.debug("format is %s" % format) log.debug("options_list is %s" % options_list) log.debug("size of struct is %d" % struct.calcsize(format)) self.buffer = struct.pack(format, self.opcode, self.filename, self.mode, *options_list) log.debug("buffer is " + repr(self.buffer)) return self def decode(self): tftpassert(self.buffer, "Can't decode, buffer is empty") # FIXME - this shares a lot of code with decode_options nulls = 0 format = "" nulls = length = tlength = 0 log.debug("in decode: about to iterate buffer counting nulls") subbuf = self.buffer[2:] for c in subbuf: log.debug("iterating this byte: " + repr(c)) if ord(c) == 0: nulls += 1 log.debug("found a null at length %d, now have %d" % (length, nulls)) format += "%dsx" % length length = -1 # At 2 nulls, we want to mark that position for decoding. if nulls == 2: break length += 1 tlength += 1 log.debug("hopefully found end of mode at length %d" % tlength) # length should now be the end of the mode. tftpassert(nulls == 2, "malformed packet") shortbuf = subbuf[:tlength+1] log.debug("about to unpack buffer with format: %s" % format) log.debug("unpacking buffer: " + repr(shortbuf)) mystruct = struct.unpack(format, shortbuf) tftpassert(len(mystruct) == 2, "malformed packet") self.filename = mystruct[0] self.mode = mystruct[1].lower() # force lc - bug 17 log.debug("set filename to %s" % self.filename) log.debug("set mode to %s" % self.mode) self.options = self.decode_options(subbuf[tlength+1:]) return self class TftpPacketRRQ(TftpPacketInitial): """ :: 2 bytes string 1 byte string 1 byte ----------------------------------------------- RRQ/ | 01/02 | Filename | 0 | Mode | 0 | WRQ ----------------------------------------------- """ def __init__(self): TftpPacketInitial.__init__(self) self.opcode = 1 def __str__(self): s = 'RRQ packet: filename = %s' % self.filename s += ' mode = %s' % self.mode if self.options: s += '\n options = %s' % self.options return s class TftpPacketWRQ(TftpPacketInitial): """ :: 2 bytes string 1 byte string 1 byte ----------------------------------------------- RRQ/ | 01/02 | Filename | 0 | Mode | 0 | WRQ ----------------------------------------------- """ def __init__(self): TftpPacketInitial.__init__(self) self.opcode = 2 def __str__(self): s = 'WRQ packet: filename = %s' % self.filename s += ' mode = %s' % self.mode if self.options: s += '\n options = %s' % self.options return s class TftpPacketDAT(TftpPacket): """ :: 2 bytes 2 bytes n bytes --------------------------------- DATA | 03 | Block # | Data | --------------------------------- """ def __init__(self): TftpPacket.__init__(self) self.opcode = 3 self.blocknumber = 0 self.data = None def __str__(self): s = 'DAT packet: block %s' % self.blocknumber if self.data: s += '\n data: %d bytes' % len(self.data) return s def encode(self): """Encode the DAT packet. This method populates self.buffer, and returns self for easy method chaining.""" if len(self.data) == 0: log.debug("Encoding an empty DAT packet") format = "!HH%ds" % len(self.data) self.buffer = struct.pack(format, self.opcode, self.blocknumber, self.data) return self def decode(self): """Decode self.buffer into instance variables. It returns self for easy method chaining.""" # We know the first 2 bytes are the opcode. The second two are the # block number. (self.blocknumber,) = struct.unpack("!H", self.buffer[2:4]) log.debug("decoding DAT packet, block number %d" % self.blocknumber) log.debug("should be %d bytes in the packet total" % len(self.buffer)) # Everything else is data. self.data = self.buffer[4:] log.debug("found %d bytes of data" % len(self.data)) return self class TftpPacketACK(TftpPacket): """ :: 2 bytes 2 bytes ------------------- ACK | 04 | Block # | -------------------- """ def __init__(self): TftpPacket.__init__(self) self.opcode = 4 self.blocknumber = 0 def __str__(self): return 'ACK packet: block %d' % self.blocknumber def encode(self): log.debug("encoding ACK: opcode = %d, block = %d" % (self.opcode, self.blocknumber)) self.buffer = struct.pack("!HH", self.opcode, self.blocknumber) return self def decode(self): self.opcode, self.blocknumber = struct.unpack("!HH", self.buffer) log.debug("decoded ACK packet: opcode = %d, block = %d" % (self.opcode, self.blocknumber)) return self class TftpPacketERR(TftpPacket): """ :: 2 bytes 2 bytes string 1 byte ---------------------------------------- ERROR | 05 | ErrorCode | ErrMsg | 0 | ---------------------------------------- Error Codes Value Meaning 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 Failed to negotiate options """ def __init__(self): TftpPacket.__init__(self) self.opcode = 5 self.errorcode = 0 # FIXME: We don't encode the errmsg... self.errmsg = None # FIXME - integrate in TftpErrors references? self.errmsgs = { 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: "Failed to negotiate options" } def __str__(self): s = 'ERR packet: errorcode = %d' % self.errorcode s += '\n msg = %s' % self.errmsgs.get(self.errorcode, '') return s def encode(self): """Encode the DAT packet based on instance variables, populating self.buffer, returning self.""" format = "!HH%dsx" % len(self.errmsgs[self.errorcode]) log.debug("encoding ERR packet with format %s" % format) self.buffer = struct.pack(format, self.opcode, self.errorcode, self.errmsgs[self.errorcode]) return self def decode(self): "Decode self.buffer, populating instance variables and return self." buflen = len(self.buffer) tftpassert(buflen >= 4, "malformed ERR packet, too short") log.debug("Decoding ERR packet, length %s bytes" % buflen) if buflen == 4: log.debug("Allowing this affront to the RFC of a 4-byte packet") format = "!HH" log.debug("Decoding ERR packet with format: %s" % format) self.opcode, self.errorcode = struct.unpack(format, self.buffer) else: log.debug("Good ERR packet > 4 bytes") format = "!HH%dsx" % (len(self.buffer) - 5) log.debug("Decoding ERR packet with format: %s" % format) self.opcode, self.errorcode, self.errmsg = struct.unpack(format, self.buffer) log.error("ERR packet - errorcode: %d, message: %s" % (self.errorcode, self.errmsg)) return self class TftpPacketOACK(TftpPacket, TftpPacketWithOptions): """ :: +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+ | opc | opt1 | 0 | value1 | 0 | optN | 0 | valueN | 0 | +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+ """ def __init__(self): TftpPacket.__init__(self) TftpPacketWithOptions.__init__(self) self.opcode = 6 def __str__(self): return 'OACK packet:\n options = %s' % self.options def encode(self): format = "!H" # opcode options_list = [] log.debug("in TftpPacketOACK.encode") for key in self.options: log.debug("looping on option key %s" % key) log.debug("value is %s" % self.options[key]) format += "%dsx" % len(key) format += "%dsx" % len(self.options[key]) options_list.append(key) options_list.append(self.options[key]) self.buffer = struct.pack(format, self.opcode, *options_list) return self def decode(self): self.options = self.decode_options(self.buffer[2:]) return self def match_options(self, options): """This method takes a set of options, and tries to match them with its own. It can accept some changes in those options from the server as part of a negotiation. Changed or unchanged, it will return a dict of the options so that the session can update itself to the negotiated options.""" for name in self.options: if options.has_key(name): if name == 'blksize': # We can accept anything between the min and max values. size = self.options[name] if size >= MIN_BLKSIZE and size <= MAX_BLKSIZE: log.debug("negotiated blksize of %d bytes" % size) options[blksize] = size else: raise TftpException, "Unsupported option: %s" % name return True tftpy-0.6.0/tftpy/TftpShared.py0000644000175000017500000000330611613106551016730 0ustar msouliermsoulier"""This module holds all objects shared by all other modules in tftpy.""" import logging LOG_LEVEL = logging.NOTSET MIN_BLKSIZE = 8 DEF_BLKSIZE = 512 MAX_BLKSIZE = 65536 SOCK_TIMEOUT = 5 MAX_DUPS = 20 TIMEOUT_RETRIES = 5 DEF_TFTP_PORT = 69 # A hook for deliberately introducing delay in testing. DELAY_BLOCK = 0 # Initialize the logger. logging.basicConfig() # The logger used by this library. Feel free to clobber it with your own, if you like, as # long as it conforms to Python's logging. log = logging.getLogger('tftpy') def tftpassert(condition, msg): """This function is a simple utility that will check the condition passed for a false state. If it finds one, it throws a TftpException with the message passed. This just makes the code throughout cleaner by refactoring.""" if not condition: raise TftpException, msg def setLogLevel(level): """This function is a utility function for setting the internal log level. The log level defaults to logging.NOTSET, so unwanted output to stdout is not created.""" global log log.setLevel(level) class TftpErrors(object): """This class is a convenience for defining the common tftp error codes, and making them more readable in the code.""" NotDefined = 0 FileNotFound = 1 AccessViolation = 2 DiskFull = 3 IllegalTftpOp = 4 UnknownTID = 5 FileAlreadyExists = 6 NoSuchUser = 7 FailedNegotiation = 8 class TftpException(Exception): """This class is the parent class of all exceptions regarding the handling of the TFTP protocol.""" pass class TftpTimeout(TftpException): """This class represents a timeout error waiting for a response from the other end.""" pass tftpy-0.6.0/tftpy/__init__.py0000644000175000017500000000136311612707707016435 0ustar msouliermsoulier""" This library implements the tftp protocol, based on rfc 1350. http://www.faqs.org/rfcs/rfc1350.html At the moment it implements only a client class, but will include a server, with support for variable block sizes. As a client of tftpy, this is the only module that you should need to import directly. The TftpClient and TftpServer classes can be reached through it. """ import sys # Make sure that this is at least Python 2.3 verlist = sys.version_info if not verlist[0] >= 2 or not verlist[1] >= 3: raise AssertionError, "Requires at least Python 2.3" from TftpShared import * from TftpPacketTypes import * from TftpPacketFactory import * from TftpClient import * from TftpServer import * from TftpContexts import * from TftpStates import * tftpy-0.6.0/README0000644000175000017500000000616011613123106014020 0ustar msouliermsoulierCopyright, Michael P. Soulier, 2010. About Release 0.6.0: ==================== Maintenance update to fix several reported issues, including proper retransmits on timeouts, and further expansion of unit tests. About Release 0.5.1: ==================== Maintenance update to fix a bug in the server, overhaul the documentation for the website, fix a typo in the unit tests, fix a failure to set default blocksize, and a divide by zero error in speed calculations for very short transfers. Also, this release adds support for input/output in client as stdin/stdout About Release 0.5.0: ==================== Complete rewrite of the state machine. Now fully implements downloading and uploading. About Release 0.4.6: ==================== Feature release to add the tsize option. Thanks to Kuba Kończyk for the patch. About Release 0.4.5: ==================== Bugfix release for compatability issues on Win32, among other small issues. About Release 0.4.4: ==================== Bugfix release for poor tolerance of unsupported options in the server. About Release 0.4.3: ==================== Bugfix release for an issue with the server's detection of the end of the file during a download. About Release 0.4.2: ==================== Bugfix release for some small installation issues with earlier Python releases. About Release 0.4.1: ==================== Bugfix release to fix the installation path, with some restructuring into a tftpy package from the single module used previously. About Release 0.4: ================== This release adds a TftpServer class with a sample implementation in bin. The server uses a single thread with multiple handlers and a select() loop to handle multiple clients simultaneously. Only downloads are supported at this time. About Release 0.3: ================== This release fixes a major RFC 1350 compliance problem with the remote TID. About Release 0.2: ================== This release adds variable block sizes, and general option support, implementing RFCs 2347 and 2348. This is accessible in the TftpClient class via the options dict, or in the sample client via the --blocksize option. About Release 0.1: ================== This is an initial release in the spirit of "release early, release often". Currently the sample client works, supporting RFC 1350. The server is not yet implemented, and RFC 2347 and 2348 support (variable block sizes) is underway, planned for 0.2. About Tftpy: ============ Purpose: -------- Tftpy is a TFTP library for the Python programming language. It includes client and server classes, with sample implementations. Hooks are included for easy inclusion in a UI for populating progress indicators. It supports RFCs 1350, 2347, 2348 and the tsize option from RFC 2349. Dependencies: ------------- Python 2.3+, hopefully. Let me know if it fails to work. Trifles: -------- Home page: http://tftpy.sf.net/ Project page: http://sourceforge.net/projects/tftpy/ License is the MIT License See COPYING in this distribution. Limitations: ------------ - Only 'octet' mode is supported. - The only options supported are blksize and tsize. Author: ======= Michael P. Soulier tftpy-0.6.0/ChangeLog0000644000175000017500000002252511613122656014726 0ustar msouliermsoulier 2011-07-24 Michael P. Soulier 04aaa2e: Fixing issue #3, expanding unit tests. 2011-07-23 Michael P. Soulier 40977c6: Fixing some pyflakes complaints add4440: Fixes issue #23, breaking up TftpStates into TftpStates and TftpContexts. 949c998: Fixing issue #9, removing blksize option from client if not supplied. a43773e: Fixing issue #16 on github, server failing to use timeout time in checkTimeout() method. 1e74abf: Adding retries on timeouts, still have to exhaustively test. Should close issue #21 on github. 2011-06-02 Michael P. Soulier 6fd9391: Fixing a file descriptor leak. Closes issue 22. f6442eb: Adding a server download state test to the unit tests. 2010-10-18 Kenny Millington a6cff4f: Fix exceptions propagating out of TftpServer.listen() 71d827d: Allow dyn_file_func to trigger a FileNotFound error. 2010-10-13 Michael P. Soulier 4396124: Forcing decode mode to lower case, fixes bug 17. 2010-07-20 Michael P. Soulier 45185ed: Fixing setNextBlock to roll over at 2**16 - 1 instead of 2**16, which was causing problems when uploading large files. 2010-07-14 Michael P. Soulier e1b1be2: Updating README for 0.5.1 4f61f7f: Updated changelog for 0.5.1. e35cd2d: Added simple doc examples and install info. 2010-07-12 Michael P. Soulier 74f6756: Playing with sphinx formatting 2010-07-11 Michael P. Soulier 1caa220: Latest doc updates ad94976: Replacing epydoc output on website. 402b2ae: Adding initial Sphinx docs 2010-05-25 Michael P. Soulier 0b54068: Fixing typo in unit test 58623df: Adding support for input/output as stdin/stdout 2010-05-24 Michael P. Soulier f4a3ff6: Fixing failure to set default blocksize if options were provided but blksize was not one of them. 2010-05-12 Patrick Oppenlander 1a2b556: fix incorrectly assigned state transition 360b0b9: fix divide by zero in speed calculation for short transfers 2010-05-10 Michael P. Soulier 3c40546: Updated site html formatting 9ed42a8: Website update 5f0e405: Updating notes d4c15e1: Fixing the license in the setup.py 2f0c0db: Updated website becb299: Updating metadata for 0.5.0 release faebd44: Fixing buffering issue in upload. Uploads work now. a071549: Updated README 2bb8326: First working upload with new state machine. Not usable yet, upload fails to always send all data for some reason. 4a4f53a: Fixed an obvious error introduced with the dyn_file_func merge 2010-04-24 Michael P. Soulier 8a56d94: Merge commit 'angry-elf/master' into merge 2010-02-18 Michael P. Soulier 8343ccf: Taking patch from "Mike C. Fletcher" , fixing a bad reference to dyn_func_file from a state object. 2010-02-18 Alexey Loshkarev 72c4769: Fix dyn_file_func (was broken?) Fix error message (filename was not displayed) 2009-10-24 Michael P. Soulier badf18f: Updated epydoc output for website. 2009-09-24 Michael P. Soulier a80639c: Changed licenses to the MIT License ce7fc32: Fixing some log messages and bad variable references. 2009-08-18 Michael P. Soulier 781072b: Updated resent_data in metrics. 3ae3b31: Fixed server metrics summary. 2009-08-16 Michael P. Soulier a6a18c1: First successful download with both client and server. 2009-08-15 Michael P. Soulier 62b22fb: Did some rework for the state machine in a server context. Removed the handler framework in favour of a TftpContextServer used as the session. 2009-06-20 Michael P. Soulier 03e4e74: Fixing up some of the upload code. 2009-07-21 Michael P. Soulier 5ee5f63: Adding patch for dynamic content from Alex ? 2009-04-10 Michael P. Soulier c61ca17: Fixing a merge error in rebase 410e14c: Fixed bug in tidport handling, and lack of OACK response. 874fef5: Fixing OACK handling with new state machine. 5072f6d: Fixed TftpClient with new state machine. 2009-04-08 Michael P. Soulier e7a63bb: Started overhaul of state machine. 2009-04-10 Michael P. Soulier 41bf3a2: Improving sample client output on error and fixing default blocksize when server ignores options. bd2e195: Merged upload patch. 2009-04-09 Michael P. Soulier 449f10a: Updating version in setup 2009-04-08 Michael P. Soulier 40185e5: Website update 2009-04-07 Michael P. Soulier bc55a17: Fixing bogus warnings in options handling. 74c68b1: Merge branch 'master' of git@github.com:msoulier/tftpy d058642: Fixing tftproot configured for server as a relative path. 2009-03-15 Michael P. Soulier 23b32d0: Updated site with stylesheet 0cfcea2: Website update 2009-03-14 Michael P. Soulier abf0f1f: Adding website 2008-10-08 Michael P. Soulier ca7a06a: Fixed the use of the tsize option in RRQ packets. 2008-10-05 Michael P. Soulier 0a5df33: Rolling 0.4.6 2008-10-04 Michael P. Soulier 07416bf: Rebased tsize branch and added a --tsize option to the client. Now sending all packets to the progresshook, not just DAT packets, so that the client can see the OACK. Not yet making use of the returned tsize. Need to test this on a server that supports tsize. 2008-07-30 Michael P. Soulier 8a0162b: Adding transfer size option patch from Kuba Kończyk. Patch 2018609 in SF tracker. 2008-10-03 Michael P. Soulier c408389: Merged from SVN trunk after register to PyPi 2008-10-04 msoulier 65ef2d9: Updated for PyPi 2008-07-30 Michael P. Soulier 6730280: Adding upload patch from Lorenz Schori - patch 1897344 in SF tracker 2008-05-28 msoulier 33b1353: Tagging 0.4.5. 936e4df: Updated for v0.4.5 release. caff30d: Fix for bug 1967647, referencing self.sock instead of sock. 2008-05-20 msoulier 70f22b1: Fix for [ 1932310 ] security check always fail for windows. 596af40: Fixed division by zero error in rate calculations in download function of client. Thanks to Stefaan Vanheesbeke for the report. 3b1bae3: Fix for bug [ 1932330 ] binary downloads fail in Windows. 2008-01-31 msoulier 648564c: Updated README. 792df2d: Updated ChangeLog 941f5bf: Updating version to 0.4.4 2007-12-16 msoulier f8af287: Fixing 1851544 - server not tolerant of unsupported options Thanks to Landon Jurgens for the report. 2007-07-17 msoulier 89a8382: Updated for 0.4.3 release. 2007-07-16 msoulier 2a98d72: Removed redundant comparison. 955ced3: Fixing string/integer comparison. Thanks to Simon P. Ditner, bug #1755146. 2007-06-05 msoulier 493dcac: Updated for 0.4.2 bb47795: Fixed unit test for factory 2007-03-31 msoulier d9665e1: Updating docs for epydoc. b68ceca: Updated build process. d8730c7: Adding epydoc target. 2007-03-15 msoulier 0b41ffb: Updated ChangeLog 2007-02-23 msoulier 8f5595c: Simplifying use of optparse. Thanks to Steven Bethard for the suggestions. 2007-02-17 msoulier 5c52975: Removed mention of sorceror's apprentice problem. c8df0fd: Rearranged packaging a bit to fix an importing problem. c7d86d3: Supplying a default blksize options in the server. Fix for 1633625. 2007-02-10 msoulier 07906cd: Added a check for rogue packets in the server. 2007-02-09 msoulier f53e68b: Making the lib backwards-compatible to Python 2.3. 2006-12-17 msoulier efd248f: Rolling to version 0.4.1. 95b6a72: Restructuring single lib into a package. a1ad552: Restructuring single lib into a package. c43a24c: Restructuring single lib into a package. 5e6d8fe: Restructuring single lib into a package. 6eb1501: Fixing install location of library. 2006-12-16 msoulier 15023eb: Added server to package. ac2faa3: Updated ChangeLog, and rolled version to 0.4 2006-12-15 msoulier f79a1e9: Making server exit gracefully. 16ebbf2: Tweak to EOF handling in server. 7723705: First working server tests with two clients. 5cfbae3: Added lots in the server to support a download, with timeouts. Not yet tested with a client, but the damn thing runs. d5b7276: Fixed a bug in handling block number rollovers. 2006-12-14 msoulier 7441f0a: Got handling of file not found working in server. 3b4d177: Starting on sample server. 94ef067: Successful test on basic select loop 2006-12-11 msoulier 6f186f2: Added some security checks around the tftproot. Further fleshed-out the handler. Still not actually starting the transfer. 2006-12-10 msoulier b5a96ec: Fleshing out server handler implementation. fc2a587: Started on the server 2006-12-09 msoulier aece5aa: Added --debug option to sample client. 204cce4: Adding license 4fc510b: Adding ChangeLog 07e2976: Bumped the version. 104dfe0: Changed the port variables to something more intelligent. 15c5a0f: Fixing poor TID implementation. 2006-10-25 msoulier 8e6cd77: Added testcase for TftpPacketFactory. 2006-10-13 msoulier 7486502: Implemented retries on download timeouts. 0528b1b: Added some info statements regarding option negotiation. 4c73041: Updated testcases, fixed one error in decode_options 2006-10-11 msoulier f2b7d5d: Updated testcases 837344c: Updated makefile 08af50a: Adding makefile 2006-10-10 msoulier 99b3bbd: Moved LICENSE to COPYING 2006-10-09 msoulier 2e42f99: Added test for WRQ packet 6ebd6fc: Fixed broken decode, and adjusted the client options. 2006-10-08 msoulier 6db1b2c: Starting on unit tests 2006-10-05 msoulier e771f67: Fixed handling of port cb75a4b: Updating for production 19e8f0f: Freezing 0.2 0a13eb5: Fixed poor EOF detection ed15161: Got variable blocksizes working. 2006-10-04 msoulier c24bba2: Added confirmation of incoming traffic to known remote host. c11ac3a: Restructured in preparation for tftp options 2827cf1: Updated README c6094b0: Updated README 09de253: Added seconds to logs 82821e5: Upping version to 0.2 88c387b: Added OACK packet, and factored-out client code. tftpy-0.6.0/PKG-INFO0000644000175000017500000000103711613123423014235 0ustar msouliermsoulierMetadata-Version: 1.0 Name: tftpy Version: 0.6.0 Summary: Python TFTP library Home-page: http://tftpy.sourceforge.net Author: Michael P. Soulier Author-email: msoulier@digitaltorque.ca License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Topic :: Internet tftpy-0.6.0/COPYING0000644000175000017500000000206711533446475014217 0ustar msouliermsoulierThe MIT License Copyright (c) 2009 Michael P. Soulier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tftpy-0.6.0/bin/0000755000175000017500000000000011613123423013707 5ustar msouliermsouliertftpy-0.6.0/bin/tftpy_server.py0000755000175000017500000000265111612710565017034 0ustar msouliermsoulier#!/usr/bin/env python import sys, logging from optparse import OptionParser import tftpy def main(): usage="" parser = OptionParser(usage=usage) parser.add_option('-i', '--ip', type='string', help='ip address to bind to (default: INADDR_ANY)', default="") parser.add_option('-p', '--port', type='int', help='local port to use (default: 69)', default=69) parser.add_option('-r', '--root', type='string', help='path to serve from', default=None) parser.add_option('-d', '--debug', action='store_true', default=False, help='upgrade logging from info to debug') options, args = parser.parse_args() if options.debug: tftpy.setLogLevel(logging.DEBUG) else: tftpy.setLogLevel(logging.INFO) if not options.root: parser.print_help() sys.exit(1) server = tftpy.TftpServer(options.root) try: server.listen(options.ip, options.port) except tftpy.TftpException, err: sys.stderr.write("%s\n" % str(err)) sys.exit(1) except KeyboardInterrupt: pass if __name__ == '__main__': main() tftpy-0.6.0/bin/tftpy_client.py0000755000175000017500000001005511612662116016777 0ustar msouliermsoulier#!/usr/bin/env python import sys, logging, os from optparse import OptionParser import tftpy def main(): usage="" parser = OptionParser(usage=usage) parser.add_option('-H', '--host', help='remote host or ip address') parser.add_option('-p', '--port', help='remote port to use (default: 69)', default=69) parser.add_option('-f', '--filename', help='filename to fetch (deprecated, use download)') parser.add_option('-D', '--download', help='filename to download') parser.add_option('-u', '--upload', help='filename to upload') parser.add_option('-b', '--blksize', help='udp packet size to use (default: 512)') parser.add_option('-o', '--output', help='output file, - for stdout (default: same as download)') parser.add_option('-i', '--input', help='input file, - for stdin (default: same as upload)') parser.add_option('-d', '--debug', action='store_true', default=False, help='upgrade logging from info to debug') parser.add_option('-q', '--quiet', action='store_true', default=False, help="downgrade logging from info to warning") parser.add_option('-t', '--tsize', action='store_true', default=False, help="ask client to send tsize option in download") options, args = parser.parse_args() # Handle legacy --filename argument. if options.filename: options.download = options.filename if not options.host or (not options.download and not options.upload): 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) class Progress(object): def __init__(self, out): self.progress = 0 self.out = out def progresshook(self, pkt): if isinstance(pkt, tftpy.TftpPacketDAT): self.progress += len(pkt.data) self.out("Transferred %d bytes" % self.progress) elif isinstance(pkt, tftpy.TftpPacketOACK): self.out("Received OACK, options are: %s" % pkt.options) if options.debug: tftpy.setLogLevel(logging.DEBUG) elif options.quiet: tftpy.setLogLevel(logging.WARNING) else: tftpy.setLogLevel(logging.INFO) progresshook = Progress(tftpy.log.info).progresshook tftp_options = {} if options.blksize: tftp_options['blksize'] = int(options.blksize) if options.tsize: tftp_options['tsize'] = 0 tclient = tftpy.TftpClient(options.host, int(options.port), tftp_options) try: if options.download: if not options.output: options.output = os.path.basename(options.download) tclient.download(options.download, options.output, progresshook) elif options.upload: if not options.input: options.input = os.path.basename(options.upload) tclient.upload(options.upload, options.input, progresshook) except tftpy.TftpException, err: sys.stderr.write("%s\n" % str(err)) sys.exit(1) except KeyboardInterrupt: pass if __name__ == '__main__': main() tftpy-0.6.0/setup.py0000644000175000017500000000125611613122737014664 0ustar msouliermsoulier#!/usr/bin/env python from distutils.core import setup setup(name='tftpy', version='0.6.0', description='Python TFTP library', author='Michael P. Soulier', author_email='msoulier@digitaltorque.ca', url='http://tftpy.sourceforge.net', packages=['tftpy'], scripts=['bin/tftpy_client.py','bin/tftpy_server.py'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Topic :: Internet', ] )