From eb3608133b41b9cb7b4dd55cb15d77691d2300c6 Mon Sep 17 00:00:00 2001 From: Chris Johns Date: Wed, 26 Aug 2020 13:38:54 +1000 Subject: tester: Change to a simpler TFTP server - Add a simpler TFTP to allow parallel test hardware - Remove the imported tftpy server Closes #4063 --- tester/rt/tftp.py | 41 +- tester/rt/tftpserver.py | 699 +++++++++++++++++++++++++++++++++++ tester/rt/tftpy/COPYING | 21 -- tester/rt/tftpy/README | 115 ------ tester/rt/tftpy/TftpClient.py | 107 ------ tester/rt/tftpy/TftpContexts.py | 429 --------------------- tester/rt/tftpy/TftpPacketFactory.py | 47 --- tester/rt/tftpy/TftpPacketTypes.py | 494 ------------------------- tester/rt/tftpy/TftpServer.py | 271 -------------- tester/rt/tftpy/TftpShared.py | 52 --- tester/rt/tftpy/TftpStates.py | 611 ------------------------------ tester/rt/tftpy/__init__.py | 27 -- tester/rtems-tftp-server | 45 +++ 13 files changed, 766 insertions(+), 2193 deletions(-) create mode 100644 tester/rt/tftpserver.py delete mode 100644 tester/rt/tftpy/COPYING delete mode 100644 tester/rt/tftpy/README delete mode 100644 tester/rt/tftpy/TftpClient.py delete mode 100644 tester/rt/tftpy/TftpContexts.py delete mode 100644 tester/rt/tftpy/TftpPacketFactory.py delete mode 100644 tester/rt/tftpy/TftpPacketTypes.py delete mode 100644 tester/rt/tftpy/TftpServer.py delete mode 100644 tester/rt/tftpy/TftpShared.py delete mode 100644 tester/rt/tftpy/TftpStates.py delete mode 100644 tester/rt/tftpy/__init__.py create mode 100755 tester/rtems-tftp-server diff --git a/tester/rt/tftp.py b/tester/rt/tftp.py index d518036..7af3a62 100644 --- a/tester/rt/tftp.py +++ b/tester/rt/tftp.py @@ -43,7 +43,7 @@ import sys from rtemstoolkit import error from rtemstoolkit import reraise -import tftpy +import tftpserver class tftp(object): '''RTEMS Testing TFTP base.''' @@ -88,7 +88,8 @@ class tftp(object): def _stop(self): try: if self.server: - self.server.stop(now = True) + self.server.stop() + self.finished = True except: pass @@ -101,6 +102,10 @@ class tftp(object): def _timeout(self): self._stop() + while self.running or not self.finished: + self._unlock('_timeout') + time.sleep(0.1) + self._lock('_timeout') if self.timeout is not None: self.timeout() @@ -119,22 +124,21 @@ class tftp(object): return None def _listener(self): - tftpy_log = logging.getLogger('tftpy') - tftpy_log.setLevel(100) + self._lock('_listener') + exe = self.exe + self.exe = None + self._unlock('_listener') + self.server = tftpserver.tftp_server(host = 'all', + port = self.port, + timeout = 1, + forced_file = exe, + sessions = 1) try: - self.server = tftpy.TftpServer(tftproot = '.', - dyn_file_func = self._exe_handle) - except tftpy.TftpException as te: - raise error.general('tftp: %s' % (str(te))) - if self.server is not None: - try: - self.server.listen('0.0.0.0', self.port, 0.5) - except tftpy.TftpException as te: - raise error.general('tftp: %s' % (str(te))) - except IOError as ie: - if ie.errno == errno.EACCES: - raise error.general('tftp: permissions error: check tftp server port') - raise error.general('tftp: io error: %s' % (str(ie))) + self.server.start() + self.server.run() + except: + self.server.stop() + raise def _runner(self): self._lock('_runner') @@ -146,9 +150,7 @@ class tftp(object): except: caught = sys.exc_info() self._lock('_runner') - self._init() self.running = False - self.finished = True self.caught = caught self._unlock('_runner') @@ -187,6 +189,7 @@ class tftp(object): self._timeout() caught = self.caught self.caught = None + self._init() self._unlock('_open') if caught is not None: reraise.reraise(*caught) diff --git a/tester/rt/tftpserver.py b/tester/rt/tftpserver.py new file mode 100644 index 0000000..012aae4 --- /dev/null +++ b/tester/rt/tftpserver.py @@ -0,0 +1,699 @@ +# SPDX-License-Identifier: BSD-2-Clause +'''The TFTP Server handles a read only TFTP session.''' + +# Copyright (C) 2020 Chris Johns (chrisj@rtems.org) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import print_function + +import argparse +import os +import socket +import sys +import time +import threading + +try: + import socketserver +except ImportError: + import SocketServer as socketserver + +from rtemstoolkit import error +from rtemstoolkit import log +from rtemstoolkit import version + + +class tftp_session(object): + '''Handle the TFTP session packets initiated on the TFTP port (69). + ''' + # pylint: disable=useless-object-inheritance + # pylint: disable=too-many-instance-attributes + + opcodes = ['nul', 'RRQ', 'WRQ', 'DATA', 'ACK', 'ERROR', 'OACK'] + + OP_RRQ = 1 + OP_WRQ = 2 + OP_DATA = 3 + OP_ACK = 4 + OP_ERROR = 5 + OP_OACK = 6 + + E_NOT_DEFINED = 0 + E_FILE_NOT_FOUND = 1 + E_ACCESS_VIOLATION = 2 + E_DISK_FULL = 3 + E_ILLEGAL_TFTP_OP = 4 + E_UKNOWN_TID = 5 + E_FILE_ALREADY_EXISTS = 6 + E_NO_SUCH_USER = 7 + E_NO_ERROR = 10 + + def __init__(self, host, port, base, forced_file, reader=None): + # pylint: disable=too-many-arguments + self.host = host + self.port = port + self.base = base + self.forced_file = forced_file + if reader is None: + self.data_reader = self._file_reader + else: + self.data_reader = reader + self.filein = None + self.resends_limit = 5 + # These are here to shut pylint up + self.block = 0 + self.block_size = 512 + self.timeout = 0 + self.resends = 0 + self.finished = False + self.filename = None + self._reinit() + + def _reinit(self): + '''Reinitialise all the class variables used by the protocol.''' + if self.filein is not None: + self.filein.close() + self.filein = None + self.block = 0 + self.block_size = 512 + self.timeout = 0 + self.resends = 0 + self.finished = False + self.filename = None + + def _file_reader(self, command, **kwargs): + '''The default file reader if the user does not provide one. + + The call returns a two element tuple where the first element + is an error code, and the second element is data if the error + code is 0 else it is an error message. + ''' + # pylint: disable=too-many-return-statements + if command == 'open': + if 'filename' not in kwargs: + raise error.general('tftp-reader: invalid open: no filename') + filename = kwargs['filename'] + try: + self.filein = open(filename, 'rb') + filesize = os.stat(filename).st_size + except FileNotFoundError: + return self.E_FILE_NOT_FOUND, 'file not found (%s)' % ( + filename) + except PermissionError: + return self.E_ACCESS_VIOLATION, 'access violation' + except IOError as ioe: + return self.E_NOT_DEFINED, str(ioe) + return self.E_NO_ERROR, str(filesize) + if command == 'read': + if self.filein is None: + raise error.general('tftp-reader: read when not open') + if 'blksize' not in kwargs: + raise error.general('tftp-reader: invalid read: no blksize') + # pylint: disable=bare-except + try: + return self.E_NO_ERROR, self.filein.read(kwargs['blksize']) + except IOError as ioe: + return self.E_NOT_DEFINED, str(ioe) + except: + return self.E_NOT_DEFINED, 'unknown error' + if command == 'close': + if self.filein is not None: + self.filein.close() + self.filein = None + return self.E_NO_ERROR, "closed" + return self.E_NOT_DEFINED, 'invalid reader state' + + @staticmethod + def _pack_bytes(data=None): + bdata = bytearray() + if data is not None: + if not isinstance(data, list): + data = [data] + for item in data: + if isinstance(item, int): + bdata.append(item >> 8) + bdata.append(item & 0xff) + elif isinstance(item, str): + bdata.extend(item.encode()) + bdata.append(0) + else: + bdata.extend(item) + return bdata + + def _response(self, opcode, data): + code = self.opcodes.index(opcode) + if code == 0 or code >= len(self.opcodes): + raise error.general('invalid opcode: ' + opcode) + bdata = self._pack_bytes([code, data]) + #print(''.join(format(x, '02x') for x in bdata)) + return bytes(bdata) + + def _error_response(self, code, message): + if log.tracing: + log.trace('tftp: error: %s:%d: %d: %s' % + (self.host, self.port, code, message)) + self.finished = True + return self._response('ERROR', self._pack_bytes([code, message, 0])) + + def _data_response(self, block, data): + if len(data) < self.block_size: + self.finished = True + return self._response('DATA', self._pack_bytes([block, data])) + + def _oack_response(self, data): + self.resends += 1 + if self.resends >= self.resends_limit: + return self._error_response(self.E_NOT_DEFINED, + 'resend limit reached') + return self._response('OACK', self._pack_bytes(data)) + + def _next_block(self, block): + # has the current block been acknowledged? + if block == self.block: + self.resends = 0 + self.block += 1 + err, data = self.data_reader('read', blksize=self.block_size) + if err != self.E_NO_ERROR: + return self._error_response(err, data) + # close if the length of data is less than the block size + if len(data) < self.block_size: + self.data_reader('close') + else: + self.resends += 1 + if self.resends >= self.resends_limit: + return self._error_response(self.E_NOT_DEFINED, + 'resend limit reached') + return self._data_response(self.block, data) + + def _read_req(self, data): + # if the last block is not 0 something has gone wrong and + # TID match. Restart the session. It could be the client + # is a simple implementation that does not move the send + # port on each retry. + if self.block != 0: + self.data_reader('close') + self._reinit() + # Get the filename, mode and options + self.filename = self.get_option('filename', data) + if self.filename is None: + return self._error_response(self.E_NOT_DEFINED, + 'filename not found in request') + if self.forced_file is not None: + self.filename = self.forced_file + # open the reader + err, message = self.data_reader('open', filename=self.filename) + if err != self.E_NO_ERROR: + return self._error_response(err, message) + # the no error on open message is the file size + try: + tsize = int(message) + except ValueError: + tsize = 0 + mode = self.get_option('mode', data) + if mode is None: + return self._error_response(self.E_NOT_DEFINED, + 'mode not found in request') + oack_data = self._pack_bytes() + value = self.get_option('timeout', data) + if value is not None: + oack_data += self._pack_bytes(['timeout', value]) + self.timeout = int(value) + value = self.get_option('blksize', data) + if value is not None: + oack_data += self._pack_bytes(['blksize', value]) + self.block_size = int(value) + else: + self.block_size = 512 + value = self.get_option('tsize', data) + if value is not None and tsize > 0: + oack_data += self._pack_bytes(['tsize', str(tsize)]) + # Send the options ack + return self._oack_response(oack_data) + + def _write_req(self): + # WRQ is not supported + return self._error_response(self.E_ILLEGAL_TFTP_OP, + "writes not supported") + + def _op_ack(self, data): + # send the next block of data + block = (data[2] << 8) | data[3] + return self._next_block(block) + + def process(self, host, port, data): + '''Process the incoming client data sending a response. If the session + has finished return None. + ''' + if host != self.host and port != self.port: + return self._error_response(self.E_UKNOWN_TID, + 'unkown transfer ID') + if self.finished: + return None + opcode = (data[0] << 8) | data[1] + if opcode == self.OP_RRQ: + return self._read_req(data) + if opcode in [self.OP_WRQ, self.OP_DATA]: + return self._write_req() + if opcode == self.OP_ACK: + return self._op_ack(data) + return self._error_response(self.E_ILLEGAL_TFTP_OP, + "unknown or unsupported opcode") + + def decode(self, host, port, data): + '''Decode the packet for diagnostic purposes. + ''' + # pylint: disable=too-many-branches + out = '' + dlen = len(data) + if dlen > 2: + opcode = (data[0] << 8) | data[1] + if 0 < opcode < len(self.opcodes): + if opcode in [self.OP_RRQ, self.OP_WRQ]: + out += ' ' + self.opcodes[opcode] + ', ' + i = 2 + while data[i] != 0: + out += chr(data[i]) + i += 1 + while i < dlen - 1: + out += ', ' + i += 1 + while data[i] != 0: + out += chr(data[i]) + i += 1 + elif opcode == self.OP_DATA: + block = (data[2] << 8) | data[3] + out += ' ' + self.opcodes[opcode] + ', ' + out += '#' + str(block) + ', ' + if dlen > 4: + out += '%02x%02x..%02x%02x' % (data[4], data[5], + data[-2], data[-1]) + else: + out += '%02x%02x%02x%02x' % (data[4], data[5], data[6], + data[6]) + out += ',' + str(dlen - 4) + elif opcode == self.OP_ACK: + block = (data[2] << 8) | data[3] + out += ' ' + self.opcodes[opcode] + ' ' + str(block) + elif opcode == self.OP_ERROR: + out += 'E ' + self.opcodes[opcode] + ', ' + out += str((data[2] << 8) | (data[3])) + out += ': ' + str(data[4:].decode()) + i = 2 + while data[i] != 0: + out += chr(data[i]) + i += 1 + elif opcode == self.OP_OACK: + out += ' ' + self.opcodes[opcode] + i = 1 + while i < dlen - 1: + out += ', ' + i += 1 + while data[i] != 0: + out += chr(data[i]) + i += 1 + else: + out += 'E INV(%d)' % (opcode) + else: + out += 'E INVALID LENGTH' + return out[:2] + '[%s:%d] (%d) ' % (host, port, len(data)) + out[2:] + + @staticmethod + def get_option(option, data): + '''Get the option from the TFTP packet.''' + dlen = len(data) - 1 + opcode = (data[0] << 8) | data[1] + next_option = False + if opcode in [1, 2]: + count = 0 + i = 2 + while i < dlen: + value = '' + while data[i] != 0: + value += chr(data[i]) + i += 1 + i += 1 + if option == 'filename' and count == 0: + return value + if option == 'mode' and count == 1: + return value + if value == option and (count % 1) == 0: + next_option = True + elif next_option: + return value + count += 1 + return None + + def get_timeout(self, default_timeout, timeout_guard): + '''Get the timeout. The timeout can be an option.''' + if self.timeout == 0: + return self.timeout + timeout_guard + return default_timeout + + def get_block_size(self): + '''Get the block size. The block size can be an option.''' + return self.block_size + + +class udp_handler(socketserver.BaseRequestHandler): + '''TFTP UDP handler for a TFTP session.''' + def _notice(self, text): + if self.server.tftp.notices: + log.notice(text) + else: + log.trace(text) + + def handle_session(self, index): + '''Handle the TFTP session data.''' + # pylint: disable=too-many-locals + # pylint: disable=broad-except + # pylint: disable=too-many-branches + client_ip = self.client_address[0] + client_port = self.client_address[1] + client = '%s:%i' % (client_ip, client_port) + self._notice('] tftp: %d: start: %s' % (index, client)) + try: + session = tftp_session(client_ip, client_port, + self.server.tftp.base, + self.server.tftp.forced_file, + self.server.tftp.reader) + response = session.process(client_ip, client_port, self.request[0]) + if response is not None: + if log.tracing and self.server.tftp.packet_trace: + log.trace(' > ' + session.decode(client_ip, client_port, + self.request[0])) + timeout = session.get_timeout(self.server.tftp.timeout, 1) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', 0)) + sock.settimeout(timeout) + while response is not None: + if log.tracing and self.server.tftp.packet_trace: + log.trace( + ' < ' + + session.decode(client_ip, client_port, response)) + sock.sendto(response, (client_ip, client_port)) + if session.finished: + break + try: + data, address = sock.recvfrom(2 + 2 + + session.get_block_size()) + if log.tracing and self.server.tftp.packet_trace: + log.trace( + ' > ' + + session.decode(address[0], address[1], data)) + except socket.error as serr: + if log.tracing: + log.trace('] tftp: %d: receive: %s: error: %s' \ + % (index, client, serr)) + return + except socket.gaierror as serr: + if log.tracing: + log.trace('] tftp: %d: receive: %s: error: %s' \ + % (index, client, serr)) + return + response = session.process(address[0], address[1], data) + except error.general as gerr: + self._notice('] tftp: %dd: error: %s' % (index, gerr)) + except error.internal as ierr: + self._notice('] tftp: %d: error: %s' % (index, ierr)) + except error.exit: + pass + except KeyboardInterrupt: + pass + except Exception as exp: + self._notice('] tftp: %d: error: %s: %s' % (index, type(exp), exp)) + self._notice('] tftp: %d: end: %s' % (index, client)) + + def handle(self): + '''The UDP server handle method.''' + if self.server.tftp.sessions is None \ + or self.server.tftp.session < self.server.tftp.sessions: + self.handle_session(self.server.tftp.next_session()) + + +class udp_server(socketserver.ThreadingMixIn, socketserver.UDPServer): + '''UDP server. Default behaviour.''' + + +class tftp_server(object): + '''TFTP server runs a UDP server to handle TFTP sessions.''' + + # pylint: disable=useless-object-inheritance + # pylint: disable=too-many-instance-attributes + + def __init__(self, + host, + port, + timeout=10, + base=None, + forced_file=None, + sessions=None, + reader=None): + # pylint: disable=too-many-arguments + self.lock = threading.Lock() + self.notices = False + self.packet_trace = False + self.timeout = timeout + self.host = host + self.port = port + self.server = None + self.server_thread = None + if base is None: + base = os.getcwd() + self.base = base + self.forced_file = forced_file + if sessions is not None and not isinstance(sessions, int): + raise error.general('tftp session count is not a number') + self.sessions = sessions + self.session = 0 + self.reader = reader + + def __del__(self): + self.stop() + + def _lock(self): + self.lock.acquire() + + def _unlock(self): + self.lock.release() + + def start(self): + '''Start the TFTP server. Returns once started.''' + # pylint: disable=attribute-defined-outside-init + if log.tracing: + log.trace('] tftp: server: %s:%i' % (self.host, self.port)) + if self.host == 'all': + host = '' + else: + host = self.host + try: + self.server = udp_server((host, self.port), udp_handler) + except Exception as exp: + raise error.general('tftp server create: %s' % (exp)) + # We cannot set tftp in __init__ because the object is created + # in a separate package. + self.server.tftp = self + self.server_thread = threading.Thread(target=self.server.serve_forever) + self.server_thread.daemon = True + self.server_thread.start() + + def stop(self): + '''Stop the TFTP server and close the server port.''' + self._lock() + try: + if self.server is not None: + self.server.shutdown() + self.server.server_close() + self.server = None + finally: + self._unlock() + + def run(self): + '''Run the TFTP server for the specified number of sessions.''' + running = True + while running: + period = 1 + self._lock() + if self.server is None: + running = False + period = 0 + elif self.sessions is not None: + if self.sessions == 0: + running = False + period = 0 + else: + period = 0.25 + self._unlock() + if period > 0: + time.sleep(period) + self.stop() + + def get_session(self): + '''Return the session count.''' + count = 0 + self._lock() + try: + count = self.session + finally: + self._unlock() + return count + + def next_session(self): + '''Return the next session number.''' + count = 0 + self._lock() + try: + self.session += 1 + count = self.session + finally: + self._unlock() + return count + + def enable_notices(self): + '''Call to enable notices. The server is quiet without this call.''' + self._lock() + self.notices = True + self._unlock() + + def trace_packets(self): + '''Call to enable packet tracing as a diagnostic.''' + self._lock() + self.packet_trace = True + self._unlock() + + +def load_log(logfile): + '''Set the log file.''' + if logfile is None: + log.default = log.log(streams=['stdout']) + else: + log.default = log.log(streams=[logfile]) + + +def run(args=sys.argv, command_path=None): + '''Run a TFTP server session.''' + # pylint: disable=dangerous-default-value + # pylint: disable=unused-argument + # pylint: disable=too-many-statements + ecode = 0 + notice = None + server = None + # pylint: disable=bare-except + try: + description = 'A TFTP Server that supports a read only TFTP session.' + + nice_cwd = os.path.relpath(os.getcwd()) + if len(nice_cwd) > len(os.path.abspath(nice_cwd)): + nice_cwd = os.path.abspath(nice_cwd) + + argsp = argparse.ArgumentParser(prog='rtems-tftp-server', + description=description) + argsp.add_argument('-l', + '--log', + help='log file.', + type=str, + default=None) + argsp.add_argument('-v', + '--trace', + help='enable trace logging for debugging.', + action='store_true', + default=False) + argsp.add_argument('--trace-packets', + help='enable trace logging of packets.', + action='store_true', + default=False) + argsp.add_argument( + '-B', + '--bind', + help='address to bind the server too (default: %(default)s).', + type=str, + default='all') + argsp.add_argument( + '-P', + '--port', + help='port to bind the server too (default: %(default)s).', + type=int, + default='69') + argsp.add_argument('-t', '--timeout', + help = 'timeout in seconds, client can override ' \ + '(default: %(default)s).', + type = int, default = '10') + argsp.add_argument( + '-b', + '--base', + help='base path, not checked (default: %(default)s).', + type=str, + default=nice_cwd) + argsp.add_argument( + '-F', + '--force-file', + help='force the file to be downloaded overriding the client.', + type=str, + default=None) + argsp.add_argument('-s', '--sessions', + help = 'number of TFTP sessions to run before exiting ' \ + '(default: forever.', + type = int, default = None) + + argopts = argsp.parse_args(args[1:]) + + load_log(argopts.log) + log.notice('RTEMS Tools - TFTP Server, %s' % (version.string())) + log.output(log.info(args)) + log.tracing = argopts.trace + + server = tftp_server(argopts.bind, argopts.port, argopts.timeout, + argopts.base, argopts.force_file, + argopts.sessions) + server.enable_notices() + + try: + server.start() + server.run() + finally: + server.stop() + + except error.general as gerr: + notice = str(gerr) + ecode = 1 + except error.internal as ierr: + notice = str(ierr) + ecode = 1 + except error.exit: + pass + except KeyboardInterrupt: + notice = 'abort: user terminated' + ecode = 1 + except SystemExit: + pass + except: + notice = 'abort: unknown error' + ecode = 1 + if server is not None: + del server + if notice is not None: + log.stderr(notice) + sys.exit(ecode) + + +if __name__ == "__main__": + run() diff --git a/tester/rt/tftpy/COPYING b/tester/rt/tftpy/COPYING deleted file mode 100644 index c9f2c9c..0000000 --- a/tester/rt/tftpy/COPYING +++ /dev/null @@ -1,21 +0,0 @@ -The 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. diff --git a/tester/rt/tftpy/README b/tester/rt/tftpy/README deleted file mode 100644 index ad7a871..0000000 --- a/tester/rt/tftpy/README +++ /dev/null @@ -1,115 +0,0 @@ -Copyright, Michael P. Soulier, 2010. - -About Release 0.6.2: -==================== -Maintenance release to fix a couple of reported issues. - -About Release 0.6.1: -==================== -Maintenance release to fix several reported problems, including a rollover -at 2^16 blocks, and some contributed work on dynamic file objects. - -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 diff --git a/tester/rt/tftpy/TftpClient.py b/tester/rt/tftpy/TftpClient.py deleted file mode 100644 index eb82c05..0000000 --- a/tester/rt/tftpy/TftpClient.py +++ /dev/null @@ -1,107 +0,0 @@ -# vim: ts=4 sw=4 et ai: -# -*- coding: utf8 -*- -"""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 -import logging -from .TftpShared import * -from .TftpPacketTypes import * -from .TftpContexts import TftpContextClientDownload, TftpContextClientUpload - -log = logging.getLogger('tftpy.TftpClient') - -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=69, options={}, localip = ""): - TftpSession.__init__(self) - self.context = None - self.host = host - self.iport = port - self.filename = None - self.options = options - self.localip = localip - if 'blksize' in self.options: - size = self.options['blksize'] - tftpassert(int == 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 writes the file to output, - which can be a file-like object or a path to a local file. 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, stdout is used.""" - # We're downloading. - log.debug("Creating download context with the following params:") - log.debug("host = %s, port = %s, filename = %s" % (self.host, self.iport, filename)) - 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, - localip = self.localip) - 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. It reads the file from input, which - can be a file-like object or a path to a local file. 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. - - Note: If input is a hyphen, stdin is used.""" - self.context = TftpContextClientUpload(self.host, - self.iport, - filename, - input, - self.options, - packethook, - timeout, - localip = self.localip) - 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) diff --git a/tester/rt/tftpy/TftpContexts.py b/tester/rt/tftpy/TftpContexts.py deleted file mode 100644 index da85886..0000000 --- a/tester/rt/tftpy/TftpContexts.py +++ /dev/null @@ -1,429 +0,0 @@ -# vim: ts=4 sw=4 et ai: -# -*- coding: utf8 -*- -"""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 -import time -import sys -import os -import logging - -log = logging.getLogger('tftpy.TftpContext') - -############################################################################### -# 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 s in self.dups: - 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, localip = ""): - """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) - if localip != "": - self.sock.bind((localip, 0)) - 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 - # 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, close_fileobj=True): - """Perform session cleanup, since the end method should always be - called explicitely by the calling code, this works better than the - destructor. - Set close_fileobj to False so fileobj can be returned open.""" - log.debug("in TftpContext.end - closing socket") - self.sock.close() - if close_fileobj and 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.warning("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.warning("Received traffic from %s, expected host %s. Discarding" - % (raddress, self.host)) - - if self.tidport and self.tidport != rport: - log.warning("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, - upload_open=None): - TftpContext.__init__(self, - host, - port, - timeout, - ) - # 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 - self.upload_open = upload_open - - 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, - localip = ""): - TftpContext.__init__(self, - host, - port, - timeout, - localip) - self.file_to_transfer = filename - self.options = options - self.packethook = packethook - # If the input object has a read() function, - # assume it is file-like. - if hasattr(input, 'read'): - self.fileobj = input - elif 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 as 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.warning("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, - localip = ""): - TftpContext.__init__(self, - host, - port, - timeout, - localip) - # FIXME: should we refactor setting of these params? - self.file_to_transfer = filename - self.options = options - self.packethook = packethook - self.filelike_fileobj = False - # If the output object has a write() function, - # assume it is file-like. - if hasattr(output, 'write'): - self.fileobj = output - self.filelike_fileobj = True - # If the output filename is -, then use stdout - elif output == '-': - self.fileobj = sys.stdout - self.filelike_fileobj = True - 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 as 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.warning("resending last packet") - self.state.resendLast() - except TftpFileNotFoundError as err: - # If we received file not found, then we should not save the open - # output file or we'll be left with a size zero file. Delete it, - # if it exists. - log.error("Received File not found error") - if self.fileobj is not None and not self.filelike_fileobj: - if os.path.exists(self.fileobj.name): - log.debug("unlinking output file of %s", self.fileobj.name) - os.unlink(self.fileobj.name) - - raise - - def end(self): - """Finish up the context.""" - TftpContext.end(self, not self.filelike_fileobj) - self.metrics.end_time = time.time() - log.debug("Set metrics.end_time to %s" % self.metrics.end_time) - self.metrics.compute() diff --git a/tester/rt/tftpy/TftpPacketFactory.py b/tester/rt/tftpy/TftpPacketFactory.py deleted file mode 100644 index 41f39a9..0000000 --- a/tester/rt/tftpy/TftpPacketFactory.py +++ /dev/null @@ -1,47 +0,0 @@ -# vim: ts=4 sw=4 et ai: -# -*- coding: utf8 -*- -"""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 * -import logging - -log = logging.getLogger('tftpy.TftpPacketFactory') - -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(str("!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(opcode in self.classes, - "Unsupported opcode: %d" % opcode) - - packet = self.classes[opcode]() - - return packet diff --git a/tester/rt/tftpy/TftpPacketTypes.py b/tester/rt/tftpy/TftpPacketTypes.py deleted file mode 100644 index 3d3bdf8..0000000 --- a/tester/rt/tftpy/TftpPacketTypes.py +++ /dev/null @@ -1,494 +0,0 @@ -# vim: ts=4 sw=4 et ai: -# -*- coding: utf8 -*- -"""This module implements the packet types of TFTP itself, and the -corresponding encode and decode methods for them.""" - - -import struct -import sys -import logging -from .TftpShared import * - -log = logging.getLogger('tftpy.TftpPacketTypes') - -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 = {} - - # Always use unicode strings, except at the encode/decode barrier. - # Simpler to keep things clear. - def setoptions(self, options): - log.debug("in TftpPacketWithOptions.setoptions") - log.debug("options: %s", options) - myoptions = {} - for key in options: - newkey = key - if isinstance(key, bytes): - newkey = newkey.decode('ascii') - newval = options[key] - if isinstance(newval, bytes): - newval = newval.decode('ascii') - myoptions[newkey] = newval - log.debug("populated myoptions with %s = %s", newkey, myoptions[newkey]) - - log.debug("setting options hash to: %s", 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.""" - fmt = b"!" - options = {} - - log.debug("decode_options: buffer is: %s", 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 i in range(len(buffer)): - if ord(buffer[i:i+1]) == 0: - log.debug("found a null at length %d", length) - if length > 0: - fmt += b"%dsx" % length - length = -1 - else: - raise TftpException("Invalid options in buffer") - length += 1 - - log.debug("about to unpack, fmt is: %s", fmt) - mystruct = struct.unpack(fmt, buffer) - - tftpassert(len(mystruct) % 2 == 0, - "packet with odd number of option/value pairs") - - for i in range(0, len(mystruct), 2): - key = mystruct[i].decode('ascii') - val = mystruct[i+1].decode('ascii') - log.debug("setting option %s to %s", key, val) - log.debug("types are %s and %s", type(key), type(val)) - options[key] = val - - 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") - # Make sure filename and mode are bytestrings. - filename = self.filename - mode = self.mode - if not isinstance(filename, bytes): - filename = filename.encode('ascii') - if not isinstance(self.mode, bytes): - mode = mode.encode('ascii') - - ptype = None - if self.opcode == 1: ptype = "RRQ" - else: ptype = "WRQ" - log.debug("Encoding %s packet, filename = %s, mode = %s", - ptype, filename, mode) - for key in self.options: - log.debug(" Option %s = %s", key, self.options[key]) - - fmt = b"!H" - fmt += b"%dsx" % len(filename) - if mode == b"octet": - fmt += b"5sx" - else: - raise AssertionError("Unsupported mode: %s" % mode) - # Add options. Note that the options list must be bytes. - options_list = [] - if len(list(self.options.keys())) > 0: - log.debug("there are options to encode") - for key in self.options: - # Populate the option name - name = key - if not isinstance(name, bytes): - name = name.encode('ascii') - options_list.append(name) - fmt += b"%dsx" % len(name) - # Populate the option value - value = self.options[key] - # Work with all strings. - if isinstance(value, int): - value = str(value) - if not isinstance(value, bytes): - value = value.encode('ascii') - options_list.append(value) - fmt += b"%dsx" % len(value) - - log.debug("fmt is %s", fmt) - log.debug("options_list is %s", options_list) - log.debug("size of struct is %d", struct.calcsize(fmt)) - - self.buffer = struct.pack(fmt, - self.opcode, - filename, - mode, - *options_list) - - log.debug("buffer is %s", 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 - fmt = b"" - nulls = length = tlength = 0 - log.debug("in decode: about to iterate buffer counting nulls") - subbuf = self.buffer[2:] - for i in range(len(subbuf)): - if ord(subbuf[i:i+1]) == 0: - nulls += 1 - log.debug("found a null at length %d, now have %d", length, nulls) - fmt += b"%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 fmt: %s", fmt) - log.debug("unpacking buffer: %s", repr(shortbuf)) - mystruct = struct.unpack(fmt, shortbuf) - - tftpassert(len(mystruct) == 2, "malformed packet") - self.filename = mystruct[0].decode('ascii') - self.mode = mystruct[1].decode('ascii').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:]) - log.debug("options dict is now %s", self.options) - 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") - data = self.data - if not isinstance(self.data, bytes): - data = self.data.encode('ascii') - fmt = b"!HH%ds" % len(data) - self.buffer = struct.pack(fmt, - self.opcode, - self.blocknumber, - 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(str("!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(str("!HH"), self.opcode, self.blocknumber) - return self - - def decode(self): - if len(self.buffer) > 4: - log.debug("detected TFTP ACK but request is too large, will truncate") - log.debug("buffer was: %s", repr(self.buffer)) - self.buffer = self.buffer[0:4] - self.opcode, self.blocknumber = struct.unpack(str("!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: b"File not found", - 2: b"Access violation", - 3: b"Disk full or allocation exceeded", - 4: b"Illegal TFTP operation", - 5: b"Unknown transfer ID", - 6: b"File already exists", - 7: b"No such user", - 8: b"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.""" - fmt = b"!HH%dsx" % len(self.errmsgs[self.errorcode]) - log.debug("encoding ERR packet with fmt %s", fmt) - self.buffer = struct.pack(fmt, - 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") - fmt = b"!HH" - log.debug("Decoding ERR packet with fmt: %s", fmt) - self.opcode, self.errorcode = struct.unpack(fmt, - self.buffer) - else: - log.debug("Good ERR packet > 4 bytes") - fmt = b"!HH%dsx" % (len(self.buffer) - 5) - log.debug("Decoding ERR packet with fmt: %s", fmt) - self.opcode, self.errorcode, self.errmsg = struct.unpack(fmt, - 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): - fmt = b"!H" # opcode - options_list = [] - log.debug("in TftpPacketOACK.encode") - for key in self.options: - value = self.options[key] - if isinstance(value, int): - value = str(value) - if not isinstance(key, bytes): - key = key.encode('ascii') - if not isinstance(value, bytes): - value = value.encode('ascii') - log.debug("looping on option key %s", key) - log.debug("value is %s", value) - fmt += b"%dsx" % len(key) - fmt += b"%dsx" % len(value) - options_list.append(key) - options_list.append(value) - self.buffer = struct.pack(fmt, 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 name in options: - if name == 'blksize': - # We can accept anything between the min and max values. - size = int(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("blksize %s option outside allowed range" % size) - elif name == 'tsize': - size = int(self.options[name]) - if size < 0: - raise TftpException("Negative file sizes not supported") - else: - raise TftpException("Unsupported option: %s" % name) - return True diff --git a/tester/rt/tftpy/TftpServer.py b/tester/rt/tftpy/TftpServer.py deleted file mode 100644 index 2789322..0000000 --- a/tester/rt/tftpy/TftpServer.py +++ /dev/null @@ -1,271 +0,0 @@ -# vim: ts=4 sw=4 et ai: -# -*- coding: utf8 -*- -"""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 -import threading -import logging -from errno import EINTR -from .TftpShared import * -from .TftpPacketTypes import * -from .TftpPacketFactory import TftpPacketFactory -from .TftpContexts import TftpContextServer - -log = logging.getLogger('tftpy.TftpServer') - -class TftpServer(TftpSession): - """This class implements a tftp server object. Run the listen() method to - listen for client requests. - - tftproot is the path to the tftproot directory to serve files from and/or - write them to. - - dyn_file_func is a callable that takes a requested download - path that is not present on the file system and must return either a - file-like object to read from or None if the path should appear as not - found. This permits the serving of dynamic content. - - upload_open is a callable that is triggered on every upload with the - requested destination path and server context. It must either return a - file-like object ready for writing or None if the path is invalid.""" - - def __init__(self, - tftproot='/tftpboot', - dyn_file_func=None, - upload_open=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 - self.upload_open = upload_open - # A dict of sessions, where each session is keyed by a string like - # ip:tid for the remote end. - self.sessions = {} - # A threading event to help threads synchronize with the server - # is_running state. - self.is_running = threading.Event() - - self.shutdown_gracefully = False - self.shutdown_immediately = False - - for name in 'dyn_file_func', 'upload_open': - attr = getattr(self, name) - if attr and not callable(attr): - raise TftpException("{} supplied, but it is not callable.".format(name)) - 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 __del__(self): - if self.sock is not None: - try: - self.sock.close() - except: - pass - - 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.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.sock.bind((listenip, listenport)) - _, self.listenport = self.sock.getsockname() - except socket.error as err: - # Reraise it for now. - raise err - - self.is_running.set() - - log.info("Starting receive loop...") - while True: - log.debug("shutdown_immediately is %s" % self.shutdown_immediately) - log.debug("shutdown_gracefully is %s" % self.shutdown_gracefully) - if self.shutdown_immediately: - log.warning("Shutting down now. Session count: %d" % - len(self.sessions)) - self.sock.close() - for key in self.sessions: - self.sessions[key].end() - self.sessions = [] - break - - elif self.shutdown_gracefully: - if not self.sessions: - log.warning("In graceful shutdown mode and all " - "sessions complete.") - self.sock.close() - break - - # 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) - try: - readyinput, readyoutput, readyspecial = \ - select.select(inputlist, [], [], SOCK_TIMEOUT) - except select.error as err: - if err[0] == EINTR: - # Interrupted system call - log.debug("Interrupted syscall, retrying") - continue - else: - raise - - 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)) - - if self.shutdown_gracefully: - log.warning("Discarding data on main port, " - "in graceful shutdown mode") - continue - - # 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 key in self.sessions: - log.debug("Creating new server context for " - "session key = %s" % key) - try: - self.sessions[key] = TftpContextServer(raddress, - rport, - timeout, - self.root, - self.dyn_file_func, - self.upload_open) - self.sessions[key].start(buffer) - except TftpException as err: - deletion_list.append(key) - log.error("Fatal exception thrown from " - "session %s: %s" % (key, str(err))) - except KeyboardInterrupt: - pass - except: - deletion_list.append(key) - log.error("Fatal exception thrown from " - "session %s: %s" % (key, str(err))) - else: - log.warning("received traffic on main socket for " - "existing session??") - log.info("Currently handling these sessions:") - for session_key, session in list(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.debug("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 as 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 as 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 key in self.sessions: - 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.warning( - "Strange, session %s is not on the deletion list" % key) - - self.is_running.clear() - - log.debug("server returning from while loop") - self.shutdown_gracefully = self.shutdown_immediately = False - - def stop(self, now=False): - """Stop the server gracefully. Do not take any new transfers, - but complete the existing ones. If force is True, drop everything - and stop. Note, immediately will not interrupt the select loop, it - will happen when the server returns on ready data, or a timeout. - ie. SOCK_TIMEOUT""" - if now: - self.shutdown_immediately = True - else: - self.shutdown_gracefully = True diff --git a/tester/rt/tftpy/TftpShared.py b/tester/rt/tftpy/TftpShared.py deleted file mode 100644 index 88530c3..0000000 --- a/tester/rt/tftpy/TftpShared.py +++ /dev/null @@ -1,52 +0,0 @@ -# vim: ts=4 sw=4 et ai: -# -*- coding: utf8 -*- -"""This module holds all objects shared by all other modules in tftpy.""" - - - -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 - -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) - -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 - -class TftpFileNotFoundError(TftpException): - """This class represents an error condition where we received a file - not found error.""" - pass diff --git a/tester/rt/tftpy/TftpStates.py b/tester/rt/tftpy/TftpStates.py deleted file mode 100644 index 42bac1d..0000000 --- a/tester/rt/tftpy/TftpStates.py +++ /dev/null @@ -1,611 +0,0 @@ -# vim: ts=4 sw=4 et ai: -# -*- coding: utf8 -*- -"""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 -import logging - -log = logging.getLogger('tftpy.TftpStates') - -############################################################################### -# 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 len(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'] = 0 - else: - log.info("Dropping unsupported option '%s'" % option) - log.debug("Returning these accepted options: %s", accepted_options) - return accepted_options - - 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) - 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 - if self.context.tidport == None: - log.debug("Error packet received outside session. Discarding") - else: - 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.warning("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) - sendto_port = self.context.tidport - if not sendto_port: - # If the tidport wasn't set, then the remote end hasn't even - # started talking to us yet. That's not good. Maybe it's not - # there. - sendto_port = self.context.port - self.context.sock.sendto(self.context.last_pkt.encode().buffer, - (self.context.host, sendto_port)) - 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.warning("There is no block zero!") - self.sendError(TftpErrors.IllegalTftpOp) - raise TftpException("There is no block zero!") - log.warning("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 TftpServerState(TftpState): - """The base class for server states.""" - - def __init__(self, context): - TftpState.__init__(self, context) - - # This variable is used to store the absolute path to the file being - # managed. - self.full_path = None - - 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.") - log.warning("Received non-octet mode request. I'll reply with binary data.") - - # 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) - - # Build the filename on this server and ensure it is contained - # in the specified root directory. - # - # Filenames that begin with server root are accepted. It's - # assumed the client and server are tightly connected and this - # provides backwards compatibility. - # - # Filenames otherwise are relative to the server root. If they - # begin with a '/' strip it off as otherwise os.path.join will - # treat it as absolute (regardless of whether it is ntpath or - # posixpath module - if pkt.filename.startswith(self.context.root): - full_path = pkt.filename - else: - full_path = os.path.join(self.context.root, pkt.filename.lstrip('/')) - - # Use abspath to eliminate any remaining relative elements - # (e.g. '..') and ensure that is still within the server's - # root directory - self.full_path = os.path.abspath(full_path) - log.debug("full_path is %s", full_path) - if self.full_path.startswith(self.context.root): - log.info("requested file is in the server root - good") - else: - log.warning("requested file is not within the server root - bad") - self.sendError(TftpErrors.IllegalTftpOp) - raise TftpException("bad file path") - - self.context.file_to_transfer = pkt.filename - - return sendoack - - -class TftpStateServerRecvRRQ(TftpServerState): - """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.full_path - 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, raddress=raddress, rport=rport) - - 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: - log.warn("File not found: %s", path) - self.sendError(TftpErrors.FileNotFound) - raise TftpException("File not found: {}".format(path)) - - # Options negotiation. - if sendoack and 'tsize' in self.context.options: - # getting the file size for the tsize option. As we handle - # file-like objects and not only real files, we use this seeking - # method instead of asking the OS - self.context.fileobj.seek(0, os.SEEK_END) - tsize = str(self.context.fileobj.tell()) - self.context.fileobj.seek(0, 0) - self.context.options['tsize'] = tsize - - 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(TftpServerState): - """This class represents the state of the TFTP server when it has just - received a WRQ packet.""" - def make_subdirs(self): - """The purpose of this method is to, if necessary, create all of the - subdirectories leading up to the file to the written.""" - # Pull off everything below the root. - subpath = self.full_path[len(self.context.root):] - log.debug("make_subdirs: subpath is %s", subpath) - # Split on directory separators, but drop the last one, as it should - # be the filename. - dirs = subpath.split(os.sep)[:-1] - log.debug("dirs is %s", dirs) - current = self.context.root - for dir in dirs: - if dir: - current = os.path.join(current, dir) - if os.path.isdir(current): - log.debug("%s is already an existing directory", current) - else: - os.mkdir(current, 0o700) - - 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.full_path - if self.context.upload_open: - f = self.context.upload_open(path, self.context) - if f is None: - self.sendError(TftpErrors.AccessViolation) - raise TftpException("Dynamic path %s not permitted" % path) - else: - self.context.fileobj = f - else: - log.info("Opening file %s for writing" % path) - if os.path.exists(path): - # FIXME: correct behavior? - log.warning("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.make_subdirs() - 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.debug("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.warning("Received duplicate ACK for block %d" - % pkt.blocknumber) - self.context.metrics.add_dup(pkt) - - else: - log.warning("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.warning("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.warning("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: %s" % 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: %s" % 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 as 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) - log.debug("Received ERR packet: %s", pkt) - if pkt.errorcode == TftpErrors.FileNotFound: - raise TftpFileNotFoundError("File not found") - else: - raise TftpException("Received ERR from server: {}".format(pkt)) - - else: - self.sendError(TftpErrors.IllegalTftpOp) - raise TftpException("Received unknown packet type from server: %s" % pkt) - - # By default, no state change. - return self diff --git a/tester/rt/tftpy/__init__.py b/tester/rt/tftpy/__init__.py deleted file mode 100644 index 71b8e3d..0000000 --- a/tester/rt/tftpy/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# vim: ts=4 sw=4 et ai: -# -*- coding: utf8 -*- -""" -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.7 -required_version = (2, 7) -if sys.version_info < required_version: - raise ImportError("Requires at least Python 2.7") - -from .TftpShared import * -from . import TftpPacketTypes -from . import TftpPacketFactory -from .TftpClient import TftpClient -from .TftpServer import TftpServer -from . import TftpContexts -from . import TftpStates diff --git a/tester/rtems-tftp-server b/tester/rtems-tftp-server new file mode 100755 index 0000000..361809a --- /dev/null +++ b/tester/rtems-tftp-server @@ -0,0 +1,45 @@ +#! /usr/bin/env python +# SPDX-License-Identifier: BSD-2-Clause +'''A command line standalone TFTP Server. This is useful when testing +and setting up a TFTP target.''' + +# Copyright (C) 2020 Chris Johns (chrisj@rtems.org) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# pylint: disable=invalid-name + +from __future__ import print_function + +import os +import sys + +base = os.path.dirname(os.path.abspath(sys.argv[0])) +rtems = os.path.dirname(base) +sys.path = [rtems] + sys.path + +try: + import rt.tftpserver + rt.tftpserver.run(sys.argv) +except ImportError: + print("Incorrect RTEMS Tools installation", file=sys.stderr) + sys.exit(1) -- cgit v1.2.3