#! /usr/bin/env python # Tigger's tftpd for booting the Compaq T1500 # This version released 2004.01.17 # You are free to use, modify or derive from this AT YOUR OWN RISK. # Rationale: # RFC1350 specifies that TFTP transfers should occur in lockstep, # i.e. each packet of data must be acknowledged before the next is # sent. It would appear that the T1500 does not always acknowledge # received packets correctly. This server therefore violates the # RFC by sending a few packets ahead and taking acknowledgement of # one packet as implicit acknowledgement of earlier packets. This # should reduce the incidence of boot failures. import sys import os import pwd import datetime from socket import * import socket as sckt from getopt import * from select import select from struct import * from errno import EPERM, EACCES, ENOENT # -- You may wish to change these -- TIMEOUT = 3 # Minimum number of seconds until retransmission ATTEMPTS = 5 # Give up after this many retransmissions WINDOW = 6 # Send this many packets ahead # -- There should be no need to change this -- TFTP_PORT = 69 # -- Packet types (opcodes) -- OP_RRQ = '\x01' OP_WRQ = '\x02' OP_DATA = '\x03' OP_ACK = '\x04' OP_ERROR = '\x05' # -- Error codes -- E_ERROR = '\x00\x00' # Not defined, see error message (if any) E_NOTFOUND = '\x00\x01' # File not found E_ACCESS = '\x00\x02' # Access violation E_FULL = '\x00\x03' # Disk full or allocation exceeded E_ILLEGAL = '\x00\x04' # Illegal TFTP operation E_TID = '\x00\x05' # Unknown transfer ID E_EXISTS = '\x00\x06' # File already exists E_USER = '\x00\x07' # No such user # -- Connection states -- CS_STOP = 0 CS_INIT = 1 CS_TX = 2 # -- Connection class -- class Connection: def __init__(self, addr): self.addr = addr # Otherwise known as "transfer ID" self.__state = CS_INIT self.socket = socket(AF_INET, SOCK_DGRAM) self.socket.bind(('', 0)) self.__retries = 0 self.__lastack = 0 self.__finalblock = None self.__file = None def handle(self, packet, tid): state = self.__state op = packet[1:2] if tid != self.addr: self.error(E_TID, 'Unknown transfer ID') self.__state = state elif state == CS_INIT: if op == OP_RRQ: spl = packet[2:].split('\x00') if len(spl) < 2: self.error(E_ERROR, 'Broken request') else: filename = spl[0] mode = spl[1].lower() print 'tftpd:', `self.addr`, 'Request for', `filename` if mode == 'octet': try: # Using open() instead of file() for compatibility f = open(filename, 'rb') # File exists and is readable - but is it really a file? if not os.path.isfile(filename): f.close() raise IOError(EPERM, 'Not a regular file') # If a file is unseekable we won't be able to serve it. # This should raise an exception in such cases. f.seek(0) self.__file = f self.__state = CS_TX self.sendnext() except IOError, (errno, strerror): if errno == ENOENT: self.error(E_NOTFOUND, strerror) elif errno == EPERM or errno == EACCES: self.error(E_ACCESS, strerror) else: self.error(E_ERROR, strerror) else: # Only support octet (binary) transfers self.error(E_ERROR, mode+': Transfer mode not implemented') elif op == OP_WRQ: # Writing files is not implemented because it introduces too many # potential security nightmares and is not required for the T1500 self.error(E_ACCESS, 'Permission denied') elif op == OP_ERROR: self.__state = CS_STOP else: # There's a bug in the client self.error(E_ILLEGAL, 'Illegal operation') elif state == CS_TX: if op == OP_RRQ: pass # Assume duplicate RRQ is a network anomaly elif op == OP_ACK: blockno = unpack('!h',packet[2:4])[0] if blockno > self.__lastack: self.__lastack = blockno self.__retries = 0 self.sendnext() else: self.error(E_ILLEGAL, 'Illegal operation') return (self.__state == CS_STOP) def error(self, enum, msg): self.__state = CS_STOP print 'tftpd:', `self.addr`, 'Error:', `msg` data = '\x00' + OP_ERROR + E_ERROR + msg + '\x00' try: self.socket.sendto(data, self.addr) except IOError: pass # Ignore IO errors when sending error message! def timeout(self): if self.__retries < ATTEMPTS: self.__retries += 1 self.sendnext() else: self.__state = CS_STOP return (self.__state == CS_STOP) def sendnext(self): if self.__lastack == self.__finalblock: self.__state = CS_STOP else: pktfrom = self.__lastack + 1 pktto = pktfrom + WINDOW for blockno in range(pktfrom, pktto): offset = (blockno-1)*512 try: self.__file.seek(offset) block = self.__file.read(512) data = '\x00' + OP_DATA + pack('!h', blockno) + block try: self.socket.sendto(data, self.addr) except IOError: self.__state = CS_STOP except IOError: self.error(E_ERROR, 'Internal server error.') if len(block)<512: self.__finalblock = blockno break # -- usage() -- def usage(): print """Usage: tftpd [-u user] [-r root] -u, --user= Drop privileges and run as this user (recommended) -r, --root= Use this root directory (recommended) -h, --help Display this help and exit""" # -- main() -- def main(): timenow = datetime.datetime.utcnow() connections = {} useroot = False runasuser = False # Parse command line options try: opts, args = getopt(sys.argv[1:], 'hu:r:', \ ['help', 'user=', 'root=']) except GetoptError: usage() sys.exit(2) for o, a in opts: if o in ('-h', '--help'): usage() sys.exit() elif o in ('-u', '--user'): runasuser = a elif o in ('-r', '--root'): useroot = a # Announce startup print 'tftpd: Started at', str(timenow) # Bind socket for incoming requests try: master = socket(AF_INET, SOCK_DGRAM) master.bind(('', TFTP_PORT)) except sckt.error, (errno, strerror): print 'tftpd:', strerror, '(are you root?)' sys.exit(errno) slist = [master] # Get user id if runasuser: try: uid = pwd.getpwnam(runasuser)[2] except KeyError: print 'tftpd: No such user:', runasuser sys.exit(1) # Change root if useroot: try: os.chroot(useroot) except OSError, oserr: print 'tftpd:', oserr.strerror+':', oserr.filename sys.exit(oserr.errno) # Drop privileges if runasuser: os.setuid(uid) # Informative (?) messages if runasuser: print 'tftpd: Running as', runasuser else: print 'tftpd: WARNING: Running with full privileges' if useroot: print 'tftpd: Root directory is', useroot else: print 'tftpd: WARNING: Running with unrestricted filesystem access' # Main loop while 1: ready = select(slist, [], [], TIMEOUT)[0] if ready == []: sockets = connections.keys() for s in sockets: c = connections[s] if c.timeout(): print 'tftpd:', `addr`, 'Timed out' slist.remove(s) del connections[s] else: for s in ready: packet, addr = s.recvfrom(1024) if s is master: timenow = datetime.datetime.utcnow() print 'tftpd:', `addr`, 'Connected at', str(timenow) c = Connection(addr) connections[c.socket] = c slist.append(c.socket) s = c.socket if connections[s].handle(packet, addr): print 'tftpd:', `addr`, 'Connection closed' slist.remove(s) del connections[s] # ----- if __name__ == '__main__': sys.stdout = sys.stderr try: main() except KeyboardInterrupt: print 'tftpd: Interrupted' except: print 'tftpd: Unhandled exception (traceback follows)' raise