ptftpd
Advanced tools
+5
-5
@@ -1,4 +0,4 @@ | ||
| Metadata-Version: 2.0 | ||
| Metadata-Version: 2.1 | ||
| Name: ptftpd | ||
| Version: 1.2 | ||
| Version: 1.3 | ||
| Summary: pTFTPd, a pure-Python TFTP tool suite that works | ||
@@ -26,6 +26,6 @@ Home-page: https://github.com/mpetazzoni/ptftpd | ||
| - ``dhcpd``: a simple, stripped-down DHCP server. | ||
| - ``ptftpd``: the TFTP server (RFC1350, 2347, 2348 and 2349 compliant) | ||
| - ``ptftpd``: the TFTP server (RFC1350, 2347, 2348, 2349 and 7440 compliant) | ||
| - ``pxed``: a one-call PXE server using dhcpd and ptftpd. | ||
| - ``ptftp``: a simple TFTP client (RFC1350, 2347, 2348 and 2349 compliant and | ||
| capable) | ||
| - ``ptftp``: a simple TFTP client (RFC1350, 2347, 2348, 2349 and 7440 | ||
| compliant and capable) | ||
@@ -32,0 +32,0 @@ They all support the ``--help`` option to present the usage summary to |
| #!/usr/bin/env python | ||
| # coding=utf-8 | ||
@@ -356,5 +357,5 @@ # Authors: David Anderson | ||
| server.serve_forever() | ||
| except socket.error, e: | ||
| except socket.error as e: | ||
| sys.stderr.write('Socket error (%s): %s!\n' % | ||
| (errno.errorcode[e[0]], e[1])) | ||
| (errno.errorcode[e.args[0]], e.args[1])) | ||
| return 1 | ||
@@ -361,0 +362,0 @@ |
| #!/usr/bin/env python | ||
| # coding=utf-8 | ||
@@ -276,3 +277,3 @@ # Author: David Anderson | ||
| current = time.time() | ||
| old = [ip for ip, to in self.ips_allocated.iteritems() | ||
| old = [ip for ip, to in self.ips_allocated.items() | ||
| if to <= current] | ||
@@ -400,3 +401,3 @@ for ip in old: | ||
| loglevel=options.loglevel, | ||
| format='%(levelname)s(%(name)s): %(message)s') | ||
| fmt='%(levelname)s(%(name)s): %(message)s') | ||
@@ -407,5 +408,5 @@ try: | ||
| server.serve_forever() | ||
| except socket.error, e: | ||
| except socket.error as e: | ||
| sys.stderr.write('Socket error (%s): %s!\n' % | ||
| (errno.errorcode[e[0]], e[1])) | ||
| (errno.errorcode[e.args[0]], e.args[1])) | ||
| return 1 | ||
@@ -412,0 +413,0 @@ |
+16
-13
@@ -0,1 +1,2 @@ | ||
| # coding=utf-8 | ||
| # Author: Maxime Petazzoni | ||
@@ -61,3 +62,3 @@ # maxime.petazzoni@bulix.org | ||
| def __init__(self, stream, loglevel, format): | ||
| def __init__(self, stream, loglevel, fmt): | ||
| """Creates a new notification engine that simply logs to the given | ||
@@ -70,3 +71,3 @@ stream. | ||
| outputted. | ||
| format (string format): default format string to apply | ||
| fmt (string format): default format string to apply | ||
| on log messages. | ||
@@ -77,3 +78,3 @@ """ | ||
| self.setFormatter(logging.Formatter(format)) | ||
| self.setFormatter(logging.Formatter(fmt)) | ||
| self.setLevel(loglevel) | ||
@@ -83,4 +84,4 @@ | ||
| def install(logger, stream=sys.stderr, loglevel=logging.WARNING, | ||
| format='%(message)s'): | ||
| handler = StreamEngine(stream, loglevel, format) | ||
| fmt='%(message)s'): | ||
| handler = StreamEngine(stream, loglevel, fmt) | ||
| logger.addHandler(handler) | ||
@@ -90,3 +91,3 @@ | ||
| class DetailFilter(logging.Filter): | ||
| """This log filter filters log records that don't posess the extra | ||
| """This log filter filters log records that don't possess the extra | ||
| information we require for advanced notifications (host, port, file name | ||
@@ -97,3 +98,3 @@ and transfer state).""" | ||
| r = record.__dict__ | ||
| return ('host' in r and 'port' in r and 'file' in r and 'state' in r) | ||
| return all(['host' in r, 'port' in r, 'file' in r, 'state' in r]) | ||
@@ -103,3 +104,3 @@ | ||
| """The DetailledStreamEngine is a extension of the StreamEngine define | ||
| above designed to log detailled notifications. Pertinent log records are | ||
| above designed to log detailed notifications. Pertinent log records are | ||
| filtered using the DetailFilter, as set up by the install() method.""" | ||
@@ -121,4 +122,4 @@ | ||
| def install(logger, stream=sys.stderr, loglevel=logging.INFO, | ||
| format='%(message)s (%(host)s:%(port)d#%(file)s %(state)s)'): | ||
| handler = DetailledStreamEngine(stream, loglevel, format) | ||
| fmt='%(message)s (%(host)s:%(port)d#%(file)s %(state)s)'): | ||
| handler = DetailledStreamEngine(stream, loglevel, fmt) | ||
| handler.addFilter(DetailFilter()) | ||
@@ -153,4 +154,4 @@ logger.addHandler(handler) | ||
| callable = self.callbacks.get(record.state, self._nop) | ||
| callable(host=record.host, | ||
| callback = self.callbacks.get(record.state, self._nop) | ||
| callback(host=record.host, | ||
| port=record.port, | ||
@@ -161,3 +162,5 @@ file=record.file, | ||
| @staticmethod | ||
| def install(logger, callbacks={}): | ||
| def install(logger, callbacks=None): | ||
| if callbacks is None: | ||
| callbacks = {} | ||
| handler = CallbackEngine(callbacks) | ||
@@ -164,0 +167,0 @@ handler.addFilter(DetailFilter()) |
+99
-57
@@ -0,1 +1,2 @@ | ||
| # coding=utf-8 | ||
| # Author: Maxime Petazzoni | ||
@@ -38,2 +39,3 @@ # maxime.petazzoni@bulix.org | ||
| # - RFC2349 - TFTP Timeout interval and Transfer size options | ||
| # - RFC7440 - TFTP Windowsize Option | ||
@@ -47,2 +49,8 @@ # TFTP data packet size. A data packet with a length less than this | ||
| # TFTP default window size (equivalent to the option not being used). | ||
| TFTP_DEFAULT_WINDOW_SIZE = 1 | ||
| # Enhanced window size for LAN networks | ||
| TFTP_LAN_WINDOW_SIZE = 8 | ||
| # Maximum packet number (2^16). When reached, we may want to wraparound and | ||
@@ -100,12 +108,18 @@ # reset to TFTP_PACKETNUM_RESET to continue transfer (some clients may not | ||
| # TFTP transfer modes (mail is deprecated as of RFC1350) | ||
| TFTP_MODES = ['netascii', 'octet'] | ||
| TFTP_MODES = set(['netascii', 'octet']) | ||
| NETASCII_TO_OCTET = re.compile('\r\n') | ||
| OCTET_TO_NETASCII = re.compile('\r?\n') | ||
| # TFTP option names, as defined in RFC2348 and RFC2349 | ||
| # TFTP option names, as defined in RFC2348, RFC2349 and RFC7440 | ||
| TFTP_OPTION_BLKSIZE = 'blksize' | ||
| TFTP_OPTION_TIMEOUT = 'timeout' | ||
| TFTP_OPTION_TSIZE = 'tsize' | ||
| TFTP_OPTION_WINDOWSIZE = 'windowsize' | ||
| TFTP_OPTIONS = [TFTP_OPTION_BLKSIZE, TFTP_OPTION_TIMEOUT, TFTP_OPTION_TSIZE] | ||
| TFTP_OPTIONS = set([ | ||
| TFTP_OPTION_BLKSIZE, | ||
| TFTP_OPTION_TIMEOUT, | ||
| TFTP_OPTION_TSIZE, | ||
| TFTP_OPTION_WINDOWSIZE | ||
| ]) | ||
@@ -118,4 +132,8 @@ TFTP_BLKSIZE_MIN = 8 | ||
| TFTP_WINDOWSIZE_MIN = 1 | ||
| TFTP_WINDOWSIZE_MAX = 65535 | ||
| class TFTPHelper: | ||
| # noinspection PyPep8Naming | ||
| class TFTPHelper(object): | ||
| """ | ||
@@ -141,7 +159,10 @@ Static helper methods for the TFTP protocol. | ||
| packet = struct.pack('!H%dsc%dsc' % (len(filename), len(mode)), | ||
| OP_RRQ, filename, '\0', mode, '\0') | ||
| OP_RRQ, | ||
| filename.encode('ascii'), b'\0', | ||
| mode.encode('ascii'), b'\0') | ||
| for opt, val in opts.iteritems(): | ||
| for opt, val in opts.items(): | ||
| packet += struct.pack('!%dsc%dsc' % (len(opt), len(str(val))), | ||
| opt, '\0', str(val), '\0') | ||
| opt.encode('ascii'), b'\0', | ||
| str(val).encode('ascii'), b'\0') | ||
@@ -166,7 +187,10 @@ return packet | ||
| packet = struct.pack('!H%dsc%dsc' % (len(filename), len(mode)), | ||
| OP_WRQ, filename, '\0', mode, '\0') | ||
| OP_WRQ, | ||
| filename.encode('ascii'), b'\0', | ||
| mode.encode('ascii'), b'\0') | ||
| for opt, val in opts.iteritems(): | ||
| for opt, val in opts.items(): | ||
| packet += struct.pack('!%dsc%dsc' % (len(opt), len(str(val))), | ||
| opt, '\0', str(val), '\0') | ||
| opt.encode('ascii'), b'\0', | ||
| str(val).encode('ascii'), b'\0') | ||
@@ -211,3 +235,4 @@ return packet | ||
| return struct.pack('!HH%dsc' % len(error), | ||
| OP_ERROR, errno, error, '\0') | ||
| OP_ERROR, errno, | ||
| error.encode('ascii'), b'\0') | ||
@@ -220,3 +245,3 @@ def createDATA(num, data): | ||
| num: the data packet number (int). | ||
| data: the data to be sent (string). | ||
| data: the data to be sent (bytes). | ||
| Returns: | ||
@@ -243,6 +268,7 @@ The data packet as a string. | ||
| opts_str = "" | ||
| for opt, val in opts.iteritems(): | ||
| for opt, val in opts.items(): | ||
| opts_str += "%s%c%s%c" % (opt, '\0', val, '\0') | ||
| return struct.pack('!H%ds' % len(opts_str), OP_OACK, opts_str) | ||
| return struct.pack('!H%ds' % len(opts_str), | ||
| OP_OACK, opts_str.encode('ascii')) | ||
@@ -261,3 +287,3 @@ def parseRRQ(request): | ||
| packet = request.split('\0')[:-1] | ||
| packet = request.split(b'\0')[:-1] | ||
@@ -267,11 +293,16 @@ # If the length of the parsed list is not even, the packet is | ||
| if len(packet) % 2 != 0: | ||
| raise SyntaxError | ||
| raise SyntaxError('invalid request packet') | ||
| filename = packet[0] | ||
| mode = packet[1].lower() | ||
| filename = packet[0].decode('ascii') | ||
| if not filename: | ||
| raise SyntaxError('invalid filename') | ||
| mode = packet[1].decode('ascii').lower() | ||
| if mode not in TFTP_MODES: | ||
| raise SyntaxError('unknown mode %s' % mode) | ||
| opts = {} | ||
| for i in xrange(2, len(packet)-1, 2): | ||
| opt = packet[i].lower() | ||
| val = packet[i+1] | ||
| for i in range(2, len(packet)-1, 2): | ||
| opt = packet[i].decode('ascii').lower() | ||
| val = packet[i+1].decode('ascii') | ||
@@ -281,10 +312,5 @@ if opt in TFTP_OPTIONS: | ||
| try: | ||
| TFTP_MODES.index(mode) | ||
| if filename != '': | ||
| l.debug(" < %s: %s (mode: %s, opts: %s)" % | ||
| (TFTP_OPS[OP_RRQ], filename, mode, opts)) | ||
| return filename, mode, opts | ||
| except ValueError: | ||
| raise SyntaxError() | ||
| l.debug(" < %s: %s (mode: %s, opts: %s)" % | ||
| (TFTP_OPS[OP_RRQ], filename, mode, opts)) | ||
| return filename, mode, opts | ||
@@ -303,3 +329,3 @@ def parseWRQ(request): | ||
| packet = request.split('\0')[:-1] | ||
| packet = request.split(b'\0')[:-1] | ||
@@ -309,11 +335,16 @@ # If the length of the parsed list is not even, the packet is | ||
| if len(packet) % 2 != 0: | ||
| raise SyntaxError() | ||
| raise SyntaxError('invalid request packet') | ||
| filename = packet[0] | ||
| mode = packet[1].lower() | ||
| filename = packet[0].decode('ascii') | ||
| if not filename: | ||
| raise SyntaxError('invalid filename') | ||
| mode = packet[1].decode('ascii').lower() | ||
| if mode not in TFTP_MODES: | ||
| raise SyntaxError('unknown mode %s' % mode) | ||
| opts = {} | ||
| for i in xrange(2, len(packet)-1, 2): | ||
| opt = packet[i].lower() | ||
| val = packet[i+1] | ||
| for i in range(2, len(packet)-1, 2): | ||
| opt = packet[i].decode('ascii').lower() | ||
| val = packet[i+1].decode('ascii') | ||
@@ -323,10 +354,5 @@ if opt in TFTP_OPTIONS: | ||
| try: | ||
| TFTP_MODES.index(mode) | ||
| if filename != '': | ||
| l.debug(" < %s: %s (mode: %s, opts: %s)" % | ||
| (TFTP_OPS[OP_WRQ], filename, mode, opts)) | ||
| return filename, mode, opts | ||
| except ValueError: | ||
| raise SyntaxError() | ||
| l.debug(" < %s: %s (mode: %s, opts: %s)" % | ||
| (TFTP_OPS[OP_WRQ], filename, mode, opts)) | ||
| return filename, mode, opts | ||
@@ -356,3 +382,3 @@ def parseACK(request): | ||
| except struct.error: | ||
| raise SyntaxError() | ||
| raise SyntaxError('invalid acknowledgment packet') | ||
@@ -381,3 +407,3 @@ def parseDATA(request): | ||
| except struct.error: | ||
| raise SyntaxError() | ||
| raise SyntaxError('invalid data packet') | ||
@@ -400,3 +426,3 @@ def parseERROR(request): | ||
| errno = packet[0] | ||
| errmsg = request[2:].split('\0')[0] | ||
| errmsg = request[2:].split(b'\0')[0].decode('ascii') | ||
@@ -406,3 +432,3 @@ l.debug(" < %s: %s" % (TFTP_OPS[OP_ERROR], errmsg)) | ||
| except (struct.error, IndexError): | ||
| raise SyntaxError() | ||
| raise SyntaxError('invalid error packet') | ||
@@ -419,3 +445,3 @@ def parseOACK(request): | ||
| packet = request.split('\0')[:-1] | ||
| packet = request.split(b'\0')[:-1] | ||
@@ -425,7 +451,9 @@ # If the length of the parsed list is not even, the packet is | ||
| if len(packet) % 2 != 0: | ||
| raise SyntaxError() | ||
| raise SyntaxError('invalid request packet') | ||
| opts = {} | ||
| for i in xrange(0, len(packet)-1, 2): | ||
| opts[packet[i]] = packet[i+1] | ||
| for i in range(0, len(packet)-1, 2): | ||
| opt = packet[i].decode('ascii').lower() | ||
| val = packet[i+1].decode('ascii') | ||
| opts[opt] = val | ||
@@ -436,3 +464,3 @@ l.debug(" < %s: %s" % (TFTP_OPS[OP_OACK], opts)) | ||
| def getOP(data): | ||
| def get_opcode(data): | ||
| if data: | ||
@@ -442,3 +470,3 @@ try: | ||
| except (struct.error, KeyError): | ||
| raise SyntaxError() | ||
| raise SyntaxError('invalid packet') | ||
@@ -461,3 +489,3 @@ return None | ||
| blksize = int(opts[TFTP_OPTION_BLKSIZE]) | ||
| if blksize >= TFTP_BLKSIZE_MIN and blksize <= TFTP_BLKSIZE_MAX: | ||
| if TFTP_BLKSIZE_MIN <= blksize <= TFTP_BLKSIZE_MAX: | ||
| used[TFTP_OPTION_BLKSIZE] = blksize | ||
@@ -471,3 +499,3 @@ else: | ||
| timeout = int(opts[TFTP_OPTION_TIMEOUT]) | ||
| if timeout >= TFTP_TIMEOUT_MIN and timeout <= TFTP_TIMEOUT_MAX: | ||
| if TFTP_TIMEOUT_MIN <= timeout <= TFTP_TIMEOUT_MAX: | ||
| used[TFTP_OPTION_TIMEOUT] = timeout | ||
@@ -480,4 +508,17 @@ else: | ||
| if TFTP_OPTION_WINDOWSIZE in opts: | ||
| windowsize = int(opts[TFTP_OPTION_WINDOWSIZE]) | ||
| if TFTP_WINDOWSIZE_MIN <= windowsize <= TFTP_WINDOWSIZE_MAX: | ||
| used[TFTP_OPTION_WINDOWSIZE] = windowsize | ||
| else: | ||
| return None | ||
| else: | ||
| used[TFTP_OPTION_WINDOWSIZE] = TFTP_DEFAULT_WINDOW_SIZE | ||
| return used | ||
| def get_data_size(blksize): | ||
| """Return the expected size of a DATA packet, in bytes.""" | ||
| return TFTP_OPCODE_LEN + 2 + blksize | ||
| createRRQ = staticmethod(createRRQ) | ||
@@ -497,3 +538,4 @@ createWRQ = staticmethod(createWRQ) | ||
| getOP = staticmethod(getOP) | ||
| get_opcode = staticmethod(get_opcode) | ||
| parse_options = staticmethod(parse_options) | ||
| get_data_size = staticmethod(get_data_size) |
+11
-10
| #!/usr/bin/env python | ||
| # coding=utf-8 | ||
@@ -36,5 +37,5 @@ # Author: David Anderson | ||
| import notify | ||
| import tftpserver | ||
| import dhcpserver | ||
| from . import notify | ||
| from . import tftpserver | ||
| from . import dhcpserver | ||
@@ -82,9 +83,9 @@ l = notify.getLogger('pxed') | ||
| loglevel=options.loglevel, | ||
| format='%(levelname)s(%(name)s): %(message)s') | ||
| fmt='%(levelname)s(%(name)s): %(message)s') | ||
| notify.StreamEngine.install(dhcpserver.l, stream=sys.stdout, | ||
| loglevel=options.loglevel, | ||
| format='%(levelname)s(%(name)s): %(message)s') | ||
| fmt='%(levelname)s(%(name)s): %(message)s') | ||
| notify.StreamEngine.install(tftpserver.l, stream=sys.stdout, | ||
| loglevel=options.loglevel, | ||
| format='%(levelname)s(%(name)s): %(message)s') | ||
| fmt='%(levelname)s(%(name)s): %(message)s') | ||
@@ -95,9 +96,9 @@ try: | ||
| strict_rfc1350=options.strict_rfc1350) | ||
| except tftpserver.TFTPServerConfigurationError, e: | ||
| except tftpserver.TFTPServerConfigurationError as e: | ||
| sys.stderr.write('TFTP server configuration error: %s!\n' % | ||
| e.message) | ||
| e.args) | ||
| return 1 | ||
| except socket.error, e: | ||
| except socket.error as e: | ||
| sys.stderr.write('Socket error (%s): %s!\n' % | ||
| (errno.errorcode[e[0]], e[1])) | ||
| (errno.errorcode[e.args[0]], e.args[1])) | ||
| return 1 | ||
@@ -104,0 +105,0 @@ |
+29
-9
@@ -0,1 +1,2 @@ | ||
| # coding=utf-8 | ||
| # Author: Maxime Petazzoni | ||
@@ -35,3 +36,3 @@ # maxime.petazzoni@bulix.org | ||
| class TFTPState: | ||
| class TFTPState(object): | ||
| """ | ||
@@ -73,3 +74,4 @@ This class represents a peer's state. Because SocketServer is not | ||
| self.opts = { | ||
| proto.TFTP_OPTION_BLKSIZE: proto.TFTP_DEFAULT_PACKET_SIZE | ||
| proto.TFTP_OPTION_BLKSIZE: proto.TFTP_DEFAULT_PACKET_SIZE, | ||
| proto.TFTP_OPTION_WINDOWSIZE: proto.TFTP_DEFAULT_WINDOW_SIZE, | ||
| } | ||
@@ -80,2 +82,4 @@ | ||
| self.packetnum = None # Current data packet number | ||
| self.last_acked = 0 # Packet number of the last acked | ||
| # DATA packet | ||
| self.loop_packetnum = loop_packet # Packet number wraparound toggle | ||
@@ -87,3 +91,3 @@ self.total_packets = 0 # Total number of data packets sent | ||
| self.data = None | ||
| self.tosend = "" | ||
| self.tosend = bytes() | ||
@@ -196,6 +200,6 @@ def extra(self, state): | ||
| if self.mode == 'netascii': | ||
| fromfile = proto.OCTET_TO_NETASCII.sub('\r\n', fromfile) | ||
| fromfile = proto.OCTET_TO_NETASCII.sub(b'\r\n', fromfile) | ||
| self.data = self.tosend + fromfile | ||
| self.tosend = "" | ||
| self.tosend = bytes() | ||
@@ -217,4 +221,15 @@ self.packetnum += 1 | ||
| return proto.TFTPHelper.createDATA(self.packetnum, self.data) | ||
| packet = proto.TFTPHelper.createDATA(self.packetnum, self.data) | ||
| # If the window hasn't been completed yet, send the DATA packet | ||
| # and provide a continuation for the next DATA packet to send in the | ||
| # window. | ||
| next_window = self.last_acked + self.opts[proto.TFTP_OPTION_WINDOWSIZE] | ||
| if self.state == STATE_SEND and self.packetnum < next_window: | ||
| return packet, self.next | ||
| # Otherwise just send this DATA packet and we'll wait for the client to | ||
| # reply with a ACK. | ||
| return packet | ||
| def __next_send_oack(self): | ||
@@ -240,3 +255,3 @@ self.state = STATE_SEND if self.op == proto.OP_RRQ else STATE_RECV | ||
| self.file.write(self.data) | ||
| except IOError, e: | ||
| except IOError as e: | ||
| self.file.close() | ||
@@ -253,4 +268,5 @@ if e.errno == errno.ENOSPC: | ||
| ack = proto.TFTPHelper.createACK(self.packetnum) | ||
| packetnum = self.packetnum | ||
| # Compute next packet number | ||
| if not self.done: | ||
@@ -264,5 +280,9 @@ self.packetnum += 1 | ||
| return ack | ||
| # Only return a ACK if the window size has been reached, or when done | ||
| next_window = self.last_acked + self.opts[proto.TFTP_OPTION_WINDOWSIZE] | ||
| if self.done or packetnum >= next_window: | ||
| self.last_acked = packetnum | ||
| return proto.TFTPHelper.createACK(packetnum) | ||
| def __next_error(self): | ||
| return proto.TFTPHelper.createERROR(self.error) |
+159
-71
| #!/usr/bin/env python | ||
| # coding=utf-8 | ||
@@ -22,2 +23,10 @@ # Author: Maxime Petazzoni | ||
| from __future__ import print_function | ||
| try: | ||
| # noinspection PyShadowingBuiltins | ||
| input = raw_input # Py2 | ||
| except NameError: | ||
| pass # Py3 | ||
| """Simple TFTP client. | ||
@@ -50,3 +59,3 @@ | ||
| # UDP datagram size | ||
| _UDP_TRANSFER_SIZE = 8192 | ||
| _UDP_TRANSFER_SIZE = 2**16 | ||
@@ -59,2 +68,3 @@ _PTFTP_DEFAULT_PORT = 69 | ||
| proto.TFTP_OPTION_BLKSIZE: proto.TFTP_LAN_PACKET_SIZE, | ||
| proto.TFTP_OPTION_WINDOWSIZE: proto.TFTP_LAN_WINDOW_SIZE, | ||
| } | ||
@@ -67,4 +77,68 @@ | ||
| class TFTPClient: | ||
| # noinspection PyPep8Naming | ||
| class UDPMessageSocket(object): | ||
| """A wrapper around a UDP datagram socket that provides per-message | ||
| receival semantics. | ||
| This wrapper is very specific to the TFTP client use case. Messages are | ||
| expected to be received one at a time, expect for DATA messages which may | ||
| come streaming when a window size greater than one is negotiated between | ||
| the client and the server. When that's the case, reading from the socket | ||
| returns more than one message but the client (and its state machine) still | ||
| expects to process them one at a time. | ||
| By utilizing the knowledge of the expected size of those DATA messages | ||
| (based on the negotiated block size) this wrapper slices the received data | ||
| accordingly to expose the desired one-at-a-time semantics. | ||
| """ | ||
| def __init__(self): | ||
| self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
| self.reset() | ||
| @property | ||
| def rport(self): | ||
| return self._rport | ||
| def settimeout(self, timeout): | ||
| self._sock.settimeout(timeout) | ||
| def send(self, message, peer): | ||
| self._sock.sendto(message, peer) | ||
| def reset(self): | ||
| self._buffer = None | ||
| self._rport = None | ||
| def close(self): | ||
| self._sock.close() | ||
| def recv(self, blksize): | ||
| if not self._buffer: | ||
| self._recv() | ||
| if not self._buffer: | ||
| return None | ||
| opcode = proto.TFTPHelper.get_opcode(self._buffer) | ||
| if opcode == proto.OP_DATA: | ||
| size = proto.TFTPHelper.get_data_size(blksize) | ||
| data, self._buffer = self._buffer[:size], self._buffer[size:] | ||
| else: | ||
| data, self._buffer = self._buffer, None | ||
| return data | ||
| def _recv(self): | ||
| (data, (address, port)) = self._sock.recvfrom(_UDP_TRANSFER_SIZE) | ||
| if not self._rport: | ||
| self._rport = port | ||
| elif port != self._rport: | ||
| # Ignore packets from other peers | ||
| return | ||
| self._buffer = data | ||
| # noinspection PyPep8Naming | ||
| class TFTPClient(object): | ||
| """ | ||
| A small and simple TFTP client to pull/push files from a TFTP server. | ||
@@ -76,3 +150,3 @@ """ | ||
| def __init__(self, peer, opts=None, mode='octet', rfc1350=False, | ||
| notification_callbacks={}): | ||
| notification_callbacks=None): | ||
| """ | ||
@@ -92,2 +166,5 @@ Initializes the TFTP client. | ||
| if notification_callbacks is None: | ||
| notification_callbacks = {} | ||
| self.peer = peer | ||
@@ -124,3 +201,3 @@ self.transfer_mode = mode | ||
| self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
| self.sock = UDPMessageSocket() | ||
| self.sock.settimeout(state.STATE_TIMEOUT_SECS) | ||
@@ -150,7 +227,7 @@ print('Connected to {}:{}.'.format(self.peer[0], self.peer[1])) | ||
| while True: | ||
| print() | ||
| try: | ||
| command = raw_input('tftp> ') | ||
| command = input('tftp> ') | ||
| except EOFError: | ||
| print() | ||
| break | ||
@@ -174,2 +251,4 @@ | ||
| self.blksize(cmd_parts[1:]) | ||
| elif cmd_parts[0] in ('w', 'windowsize'): | ||
| self.windowsize(cmd_parts[1:]) | ||
| else: | ||
@@ -188,13 +267,14 @@ print('Unrecognized command. Try help.') | ||
| print 'Available commands:' | ||
| print '? help Display help' | ||
| print 'q quit Quit the TFTP client' | ||
| print 'm mode [newmode] Display or change transfer mode' | ||
| print 'b blksize [newsize] Display or change the transfer block size' | ||
| print 'g get [-f] <filename> Get <filename> from server.' | ||
| print ' (use -f to overwrite local file)' | ||
| print 'p put <filename> Push <filename> to the server' | ||
| print('Available commands:') | ||
| print() | ||
| print('? help Display help') | ||
| print('q quit Quit the TFTP client') | ||
| print('m mode [newmode] Display or change transfer mode') | ||
| print('b blksize [newsize] Display or change the transfer block size') | ||
| print('w windowsize [newsize] Display or change the transfer window size') | ||
| print() | ||
| print('g get [-f] <filename> Get <filename> from server.') | ||
| print(' (use -f to overwrite local file)') | ||
| print('p put <filename> Push <filename> to the server') | ||
| print() | ||
@@ -216,9 +296,4 @@ def handle(self): | ||
| self.error = False | ||
| self.sock.reset() | ||
| # UDP recv size is _UDP_TRANSFER_SIZE or more if required by | ||
| # the used block size. | ||
| recvsize = _UDP_TRANSFER_SIZE | ||
| if self.opts[proto.TFTP_OPTION_BLKSIZE] > recvsize: | ||
| recvsize = self.opts[proto.TFTP_OPTION_BLKSIZE] + 4 | ||
| # Process incoming packet until the state is cleared by the | ||
@@ -228,10 +303,6 @@ # end of a succesfull transmission or an error | ||
| try: | ||
| (request, (raddress, rport)) = self.sock.recvfrom(recvsize) | ||
| if not len(request): | ||
| (request, (raddress, rport)) = self.sock.recvfrom(recvsize) | ||
| # Still nothing? | ||
| if not len(request): | ||
| self.error = (True, 'Communication error.') | ||
| return | ||
| blksize = self.opts[proto.TFTP_OPTION_BLKSIZE] | ||
| request = self.sock.recv(blksize) | ||
| if not request: | ||
| continue | ||
| except socket.timeout: | ||
@@ -242,13 +313,6 @@ self.error = (True, 'Connection timed out.') | ||
| if not self.PTFTP_STATE.tid: | ||
| self.PTFTP_STATE.tid = rport | ||
| self.PTFTP_STATE.tid = self.sock.rport | ||
| print('Communicating with {}:{}.' | ||
| .format(self.peer[0], self.PTFTP_STATE.tid)) | ||
| if self.PTFTP_STATE.tid != rport: | ||
| l.debug( | ||
| 'Ignoring packet from {}:{}, we are connected to {}:{}.' | ||
| .format(raddress, rport, raddress, self.peer[0], | ||
| self.PTFTP_STATE.tid)) | ||
| continue | ||
| # Reset the response packet | ||
@@ -258,3 +322,3 @@ response = None | ||
| # Get the packet opcode and dispatch | ||
| opcode = proto.TFTPHelper.getOP(request) | ||
| opcode = proto.TFTPHelper.get_opcode(request) | ||
| if not opcode: | ||
@@ -267,6 +331,6 @@ self.error = (True, None) | ||
| response = proto.TFTPHelper.createERROR(proto.ERROR_ILLEGAL_OP) | ||
| return | ||
| else: | ||
| try: | ||
| handler = getattr(self, "serve%s" % proto.TFTP_OPS[opcode]) | ||
| response = handler(opcode, request[2:]) | ||
| except AttributeError: | ||
@@ -277,9 +341,6 @@ self.error = (True, 'Operation not supported.') | ||
| if not response: | ||
| response = handler(opcode, request[2:]) | ||
| # Finally, send the response if we have one | ||
| if response: | ||
| self.sock.sendto(response, | ||
| (self.peer[0], self.PTFTP_STATE.tid)) | ||
| peer = (self.peer[0], self.PTFTP_STATE.tid) | ||
| self.sock.send(response, peer) | ||
@@ -480,3 +541,3 @@ def serveOACK(self, op, request): | ||
| self.PTFTP_STATE.state = state.STATE_RECV | ||
| except IOError, e: | ||
| except IOError as e: | ||
| print('Error: {}'.format(os.strerror(e.errno))) | ||
@@ -496,3 +557,3 @@ print('Can\'t write to temporary file {}!' | ||
| # Everything's OK, let's go | ||
| print "Retrieving '%s' from the remote host..." % filename | ||
| print("Retrieving '%s' from the remote host..." % filename) | ||
@@ -502,3 +563,3 @@ packet = proto.TFTPHelper.createRRQ(filepath, self.transfer_mode, opts) | ||
| transfer_start = datetime.today() | ||
| self.sock.sendto(packet, self.peer) | ||
| self.sock.send(packet, self.peer) | ||
| self.handle() | ||
@@ -520,3 +581,3 @@ transfer_time = datetime.today() - transfer_start | ||
| shutil.copy(self.PTFTP_STATE.file.name, filename) | ||
| except IOError, e: | ||
| except IOError as e: | ||
| print('Error: {}'.format(os.strerror(e.errno))) | ||
@@ -560,3 +621,3 @@ print('Can\'t copy temporary file to local file {}!' | ||
| self.PTFTP_STATE.state = state.STATE_SEND | ||
| except IOError, e: | ||
| except IOError as e: | ||
| print('Error: {}'.format(os.strerror(e.errno))) | ||
@@ -575,3 +636,3 @@ print('Can\'t read from local file {}!'.format(filepath)) | ||
| # Everything's OK, let's go | ||
| print "Pushing '%s' to the remote host..." % filepath | ||
| print("Pushing '%s' to the remote host..." % filepath) | ||
@@ -581,3 +642,3 @@ packet = proto.TFTPHelper.createWRQ(filepath, self.transfer_mode, opts) | ||
| transfer_start = datetime.today() | ||
| self.sock.sendto(packet, self.peer) | ||
| self.sock.send(packet, self.peer) | ||
| self.handle() | ||
@@ -633,2 +694,19 @@ transfer_time = datetime.today() - transfer_start | ||
| def windowsize(self, args): | ||
| if len(args) > 1: | ||
| print('Usage: windowsize [newsize]') | ||
| return | ||
| if not len(args): | ||
| print('Current window size: {} packet(s).' | ||
| .format(self.opts[proto.TFTP_OPTION_WINDOWSIZE])) | ||
| return | ||
| try: | ||
| self.opts[proto.TFTP_OPTION_WINDOWSIZE] = int(args[0]) | ||
| print('Window size set to {} packet(s).' | ||
| .format(self.opts[proto.TFTP_OPTION_WINDOWSIZE])) | ||
| except ValueError: | ||
| print('Window size must be a number!') | ||
| def __get_speed(self, filesize, time): | ||
@@ -640,17 +718,20 @@ return (filesize / 1024.0 / | ||
| def usage(): | ||
| print "usage: %s [options]" % sys.argv[0] | ||
| print " -? --help Get help" | ||
| print " -h --host <host> Set TFTP server (default: %s)" % _PTFTP_DEFAULT_HOST | ||
| print " -p --port <port> Define the port to connect to (default: %d)" % _PTFTP_DEFAULT_PORT | ||
| print " -m --mode <mode> Set transfer mode (default: %s)" % _PTFTP_DEFAULT_MODE | ||
| print " Must be one of:", ', '.join(proto.TFTP_MODES) | ||
| print "Available extra options (using the TFTP option extension protocol):" | ||
| print " -b --blksize <n> Set transfer block size (default: %d bytes)" % proto.TFTP_LAN_PACKET_SIZE | ||
| print "To disable the use of TFTP extensions:" | ||
| print " -r --rfc1350 Strictly comply to the RFC1350 only (no extensions)" | ||
| print " This will discard other TFTP option values." | ||
| print("usage: %s [options]" % sys.argv[0]) | ||
| print() | ||
| print(" -? --help Get help") | ||
| print(" -h --host <host> Set TFTP server (default: %s)" % _PTFTP_DEFAULT_HOST) | ||
| print(" -p --port <port> Define the port to connect to (default: %d)" % _PTFTP_DEFAULT_PORT) | ||
| print(" -m --mode <mode> Set transfer mode (default: %s)" % _PTFTP_DEFAULT_MODE) | ||
| print(" Must be one of:", ', '.join(proto.TFTP_MODES)) | ||
| print() | ||
| print("Available extra options (using the TFTP option extension protocol):") | ||
| print(" -b --blksize <n> Set transfer block size (default: %d bytes)" % proto.TFTP_LAN_PACKET_SIZE) | ||
| print(" Must be between %d and %d" % (proto.TFTP_BLKSIZE_MIN, proto.TFTP_BLKSIZE_MAX)) | ||
| print(" -w --windowsize <n> Set streaming window size (default: %d)" % proto.TFTP_LAN_WINDOW_SIZE) | ||
| print(" Must be between %d and %d" % (proto.TFTP_WINDOWSIZE_MIN, proto.TFTP_WINDOWSIZE_MAX)) | ||
| print() | ||
| print("To disable the use of TFTP extensions:") | ||
| print(" -r --rfc1350 Strictly comply to the RFC1350 only (no extensions)") | ||
| print(" This will discard other TFTP option values.") | ||
| print() | ||
@@ -663,6 +744,7 @@ | ||
| try: | ||
| opts, args = getopt.getopt(sys.argv[1:], '?h:p:b:m:r', | ||
| opts, args = getopt.getopt(sys.argv[1:], '?h:p:b:w:m:r', | ||
| ['help', 'host=', | ||
| 'port=', 'blksize=', | ||
| 'mode=', 'rfc1350']) | ||
| 'windowsize=', 'mode=', | ||
| 'rfc1350']) | ||
| except getopt.GetoptError: | ||
@@ -696,2 +778,8 @@ usage() | ||
| return 2 | ||
| if opt in ('-w', '--windowsize'): | ||
| try: | ||
| exts[proto.TFTP_OPTION_WINDOWSIZE] = int(val) | ||
| except ValueError: | ||
| print('Window size must be a number!') | ||
| return 2 | ||
| if opt in ('-m', '--mode'): | ||
@@ -698,0 +786,0 @@ if val in proto.TFTP_MODES: |
+80
-33
| #!/usr/bin/env python | ||
| # coding=utf-8 | ||
@@ -39,3 +40,3 @@ # Author: Maxime Petazzoni | ||
| import socket | ||
| import SocketServer | ||
| import subprocess | ||
| import stat | ||
@@ -50,2 +51,7 @@ import sys | ||
| try: | ||
| import SocketServer as socketserver # Py2 | ||
| except ImportError: | ||
| import socketserver # Py3 | ||
| l = notify.getLogger('tftpd') | ||
@@ -59,5 +65,3 @@ | ||
| def get_ip_config_for_iface(iface): | ||
| """Retrieve and return the IP address/netmask and MAC address of the | ||
| given interface.""" | ||
| """Retrieve and return the IP address/netmask of the given interface.""" | ||
| if iface not in netifaces.interfaces(): | ||
@@ -69,12 +73,18 @@ raise TFTPServerConfigurationError( | ||
| inet = details[netifaces.AF_INET][0] | ||
| link = details[netifaces.AF_LINK][0] | ||
| return inet['addr'], inet['netmask'] | ||
| return inet['addr'], inet['netmask'], link['addr'] | ||
| def get_max_udp_datagram_size(): | ||
| """Retrieve the maximum UDP datagram size allowed by the system.""" | ||
| val = subprocess.check_output(['sysctl', '-n', 'net.inet.udp.maxdgram']) | ||
| return int(val) | ||
| class TFTPServerConfigurationError(Exception): | ||
| """The configuration of the pTFTPd is incorrect.""" | ||
| pass | ||
| class TFTPServerHandler(SocketServer.DatagramRequestHandler): | ||
| # noinspection PyPep8Naming | ||
| class TFTPServerHandler(socketserver.DatagramRequestHandler): | ||
| """ | ||
@@ -89,8 +99,6 @@ The SocketServer UDP datagram handler for the TFTP protocol. | ||
| """ | ||
| request = self.rfile.read() | ||
| response = None | ||
| # Get the packet opcode and dispatch | ||
| opcode = proto.TFTPHelper.getOP(request) | ||
| opcode = proto.TFTPHelper.get_opcode(request) | ||
@@ -104,4 +112,3 @@ if not opcode: | ||
| response = proto.TFTPHelper.createERROR(proto.ERROR_ILLEGAL_OP) | ||
| self.wfile.write(response) | ||
| self.wfile.flush() | ||
| self.send_response(response) | ||
| return | ||
@@ -111,2 +118,3 @@ | ||
| handler = getattr(self, "serve%s" % proto.TFTP_OPS[opcode]) | ||
| response = handler(opcode, request[2:]) | ||
| except AttributeError: | ||
@@ -117,8 +125,31 @@ l.error("Unsupported operation %s" % opcode) | ||
| 'Operation not supported by server.') | ||
| except: | ||
| response = proto.TFTPHelper.createERROR( | ||
| proto.ERROR_UNDEF, | ||
| 'Server error.') | ||
| finally: | ||
| self.send_response(response) | ||
| response = handler(opcode, request[2:]) | ||
| if response: | ||
| self.wfile.write(response) | ||
| self.wfile.flush() | ||
| def send_response(self, response): | ||
| """ | ||
| Send a response to the client. | ||
| Args: | ||
| response (bytes or tuple): the response packet sequence. If the | ||
| argument is a simple bytestring object, it is sent as-is. If it is | ||
| a tuple, it is expected to be a 2-uple containing first a | ||
| bytestring packet, and second a function that, when called, returns | ||
| the next packet sequence to send through this method (recursively). | ||
| """ | ||
| if not response: | ||
| return | ||
| if type(response) == tuple: | ||
| self.send_response(response[0]) | ||
| self.send_response(response[1]()) | ||
| return | ||
| self.wfile.write(response) | ||
| self.wfile.flush() | ||
| def finish_state(self, peer_state): | ||
@@ -173,2 +204,12 @@ self.server.clients[self.client_address] = peer_state | ||
| if opts: | ||
| blksize = opts[proto.TFTP_OPTION_BLKSIZE] | ||
| windowsize = opts[proto.TFTP_OPTION_WINDOWSIZE] | ||
| max_window_size = int( | ||
| get_max_udp_datagram_size() / | ||
| proto.TFTPHelper.get_data_size(blksize)) | ||
| if windowsize > max_window_size: | ||
| l.info('Restricting window size to %d to fit UDP.' % | ||
| max_window_size) | ||
| opts[proto.TFTP_OPTION_WINDOWSIZE] = max_window_size | ||
| # HOOK: this is where we should check that we accept | ||
@@ -184,3 +225,3 @@ # the options requested by the client. | ||
| except IOError, e: | ||
| except IOError as e: | ||
| peer_state.state = state.STATE_ERROR | ||
@@ -244,3 +285,3 @@ | ||
| except IOError, e: | ||
| except IOError as e: | ||
| # Otherwise, if the open failed because the file did not | ||
@@ -323,2 +364,3 @@ # exist, create it and go on | ||
| elif peer_state.packetnum != num: | ||
| # TODO: handle ACK recovery when operating in streaming mode. | ||
| peer_state.state = state.STATE_ERROR | ||
@@ -334,2 +376,3 @@ peer_state.error = proto.ERROR_ILLEGAL_OP | ||
| peer_state.last_acked = num | ||
| return peer_state.next() | ||
@@ -398,3 +441,3 @@ | ||
| next = peer_state.next() | ||
| next_state = peer_state.next() | ||
@@ -414,3 +457,3 @@ if peer_state.done: | ||
| return next | ||
| return next_state | ||
@@ -473,3 +516,3 @@ l.error('Unexpected DATA!', | ||
| for peer, peer_state in self.clients.iteritems(): | ||
| for peer, peer_state in self.clients.items(): | ||
| delta = datetime.today() - peer_state.last_seen | ||
@@ -491,5 +534,9 @@ if delta > timedelta(seconds=state.STATE_TIMEOUT_SECS): | ||
| def __init__(self, iface, root, port=_PTFTPD_DEFAULT_PORT, | ||
| strict_rfc1350=False, notification_callbacks={}): | ||
| strict_rfc1350=False, notification_callbacks=None): | ||
| if notification_callbacks is None: | ||
| notification_callbacks = {} | ||
| self.iface, self.root, self.port, self.strict_rfc1350 = \ | ||
| iface, root, port, strict_rfc1350 | ||
| iface, root, port, strict_rfc1350 | ||
| self.client_registry = {} | ||
@@ -501,4 +548,4 @@ | ||
| self.ip, self.netmask, self.mac = get_ip_config_for_iface(self.iface) | ||
| self.server = SocketServer.UDPServer((self.ip, port), | ||
| self.ip, self.netmask = get_ip_config_for_iface(self.iface) | ||
| self.server = socketserver.UDPServer((self.ip, port), | ||
| TFTPServerHandler) | ||
@@ -514,4 +561,4 @@ self.server.root = self.root | ||
| def serve_forever(self): | ||
| l.info("Serving TFTP requests on %s:%d in %s" % | ||
| (self.iface, self.port, self.root)) | ||
| l.info("Serving TFTP requests on %s/%s:%d in %s" % | ||
| (self.iface, self.ip, self.port, self.root)) | ||
| self.cleanup_thread.start() | ||
@@ -550,3 +597,3 @@ self.server.serve_forever() | ||
| loglevel=options.loglevel, | ||
| format='%(levelname)s(%(name)s): %(message)s') | ||
| fmt='%(levelname)s(%(name)s): %(message)s') | ||
@@ -556,10 +603,10 @@ try: | ||
| server.serve_forever() | ||
| except TFTPServerConfigurationError, e: | ||
| except TFTPServerConfigurationError as e: | ||
| sys.stderr.write('TFTP server configuration error: %s!' % | ||
| e.message) | ||
| e.args) | ||
| return 1 | ||
| except socket.error, e: | ||
| except socket.error as e: | ||
| sys.stderr.write('Error creating a listening socket on port %d: ' | ||
| '%s (%s).\n' % (options.port, e[1], | ||
| errno.errorcode[e[0]])) | ||
| '%s (%s).\n' % (options.port, e.args[1], | ||
| errno.errorcode[e.args[0]])) | ||
| return 1 | ||
@@ -566,0 +613,0 @@ |
@@ -0,2 +1,3 @@ | ||
| # coding=utf-8 | ||
| name = 'ptftpd' | ||
| version = '1.2' | ||
| version = '1.3' |
+17
-19
@@ -1,19 +0,17 @@ | ||
| ptftpd-1.2.dist-info/DESCRIPTION.rst,sha256=YwkcOW3iL0quNYbeZgI4-yIjBuGXfDagaEuo0oP9BhA,2913 | ||
| ptftpd-1.2.dist-info/METADATA,sha256=y_KAwg86OXGSVZPk7GV_c0nbU7ommQQDqylIsK-dCR0,3306 | ||
| ptftpd-1.2.dist-info/RECORD,, | ||
| ptftpd-1.2.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110 | ||
| ptftpd-1.2.dist-info/entry_points.txt,sha256=GHbaAos5ilos__c34JO80WSFVMwOj35D7fJURauuNC8,185 | ||
| ptftpd-1.2.dist-info/metadata.json,sha256=kbA4VaUGZXQkqfC_6Np_V0DiQ9s_9OVn9O-xxVIdUiY,1066 | ||
| ptftpd-1.2.dist-info/pbr.json,sha256=o1sdx0XJ4AG-lMPzxvsraads41T9eKgkCLdVkfAb2zA,47 | ||
| ptftpd-1.2.dist-info/top_level.txt,sha256=bT3CSdhcwUBOuDY05owwMyoebq1zulemqLL7roFudIQ,9 | ||
| ptftpd-1.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 | ||
| ptftplib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| ptftplib/bootpserver.py,sha256=SbJI30_TWjTjF4nb4T1oyY_pZJlXoVqhJKgcQJVzG54,12917 | ||
| ptftplib/dhcpserver.py,sha256=ZVsjDUT7RwrZLhURo1kKIPLl0jW8DA7pLKR9q7_DSBg,15523 | ||
| ptftplib/notify.py,sha256=KnFnnxWob3jZknjtdxQR0udm69UE1QucMf62IxE-Xwc,5816 | ||
| ptftplib/proto.py,sha256=c8q2HBC9myi4UKV0xRl6rIX5v7yBrsdoA4LvGPyu23Q,13939 | ||
| ptftplib/pxeserver.py,sha256=cUScHY3yQJM_5oAyM7rlWFgkF1olHT1IuLCKHNDMTAU,3956 | ||
| ptftplib/state.py,sha256=7rtG685HiAS0_WqlBuZUj3MOtBuNkfQGEucxNNxF5jc,8156 | ||
| ptftplib/tftpclient.py,sha256=fl1aSzpy4Xy7ci-V_Zix2pB6piWBgwMgWSbuLSl6tmk,22941 | ||
| ptftplib/tftpserver.py,sha256=invFFNPWC61hMo_bBu7WCrg5bqUuojJ9l4064rXvhrY,20465 | ||
| ptftplib/version.py,sha256=M2sBwgVbtV7NIfPkYB5s-8O4BEHSF2iIHWz8V0BLsPQ,32 | ||
| ptftpd-1.3.dist-info/METADATA,sha256=BmU8ATgi0MPt3HOfJWc-KiTWRf7MPMorvXF42_bTNEw,3318 | ||
| ptftpd-1.3.dist-info/RECORD,, | ||
| ptftpd-1.3.dist-info/WHEEL,sha256=gduuPyBvFJQSQ0zdyxF7k0zynDXbIbvg5ZBHoXum5uk,110 | ||
| ptftpd-1.3.dist-info/entry_points.txt,sha256=GHbaAos5ilos__c34JO80WSFVMwOj35D7fJURauuNC8,185 | ||
| ptftpd-1.3.dist-info/pbr.json,sha256=o1sdx0XJ4AG-lMPzxvsraads41T9eKgkCLdVkfAb2zA,47 | ||
| ptftpd-1.3.dist-info/top_level.txt,sha256=bT3CSdhcwUBOuDY05owwMyoebq1zulemqLL7roFudIQ,9 | ||
| ptftpd-1.3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 | ||
| ptftplib/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15 | ||
| ptftplib/bootpserver.py,sha256=tBS0vKoj9I4P1SmwcBpVoviGBpxDWZlWh3NLtP7LcJ4,12944 | ||
| ptftplib/dhcpserver.py,sha256=xwjkwvnT4geLKpTxn5Ke5D8p3ylNoYNsB9oBmih9dTk,15543 | ||
| ptftplib/notify.py,sha256=gugA53Wnouk3I_Hb3QJzcGCTZEwdJLjU9imUdgIC6Rs,5865 | ||
| ptftplib/proto.py,sha256=ushqeuuzi7rqD6teerjb-te3GnlbFfEUAdMytmz80Fs,15690 | ||
| ptftplib/pxeserver.py,sha256=tO4YsReIEJfVTU01pMLBNIGjWsjZgRVza1-XO20rXPY,3994 | ||
| ptftplib/state.py,sha256=He8iGLfOjXsr0FhWv1r-wlpH0D1IWSX3ze49Guhw_14,9192 | ||
| ptftplib/tftpclient.py,sha256=fHOHObaD17O1RxDZw0IEzQkwGE9iAtSqM57pWbhf5fM,25947 | ||
| ptftplib/tftpserver.py,sha256=Up_0r2lpvcoLn0jggh_YUnbHU3fbeMRmTbOl6T2UaaA,22347 | ||
| ptftplib/version.py,sha256=7dEo520ne4sCV8iYzhvYOTrfQ4uNlv1uVCSk3cr6lj4,47 |
+1
-1
| Wheel-Version: 1.0 | ||
| Generator: bdist_wheel (0.29.0) | ||
| Generator: bdist_wheel (0.31.1) | ||
| Root-Is-Purelib: true | ||
@@ -4,0 +4,0 @@ Tag: py2-none-any |
-103
| pTFTPd - A pure-Python TFTP tool suite | ||
| ====================================== | ||
| pTFTPd is a collection of tools related to TFTP. It includes a TFTP | ||
| server, a TFTP client, and a complete PXE solution based on this TFTP | ||
| server and a micro-DHCP or BOOTP server. All these tools are written in | ||
| Python and designed to be fast, RFC compliant and easy to use. | ||
| Available tools include: | ||
| - ``bootpd``: a BOOTP server (RFC951 and RFC1497 compliant) | ||
| - ``dhcpd``: a simple, stripped-down DHCP server. | ||
| - ``ptftpd``: the TFTP server (RFC1350, 2347, 2348 and 2349 compliant) | ||
| - ``pxed``: a one-call PXE server using dhcpd and ptftpd. | ||
| - ``ptftp``: a simple TFTP client (RFC1350, 2347, 2348 and 2349 compliant and | ||
| capable) | ||
| They all support the ``--help`` option to present the usage summary to | ||
| the user. | ||
| All tools also understand the ``--rfc1350`` option, which forces them in | ||
| basic TFTP RFC1350 compliance mode, disabling all TFTP extensions for | ||
| increased compatibility would you encouter any problem with your target | ||
| system. | ||
| Installation | ||
| ------------ | ||
| pTFTPd is available on PyPI as the ``ptftpd`` distribution. | ||
| .. code:: | ||
| $ pip install ptftpd | ||
| This will install the ``ptftplib`` Python package, as well as the scripts | ||
| listed above. | ||
| If you use the pTFTPd tool suite outside of a standard distribution | ||
| installation, you may need to specify the Python module search path with | ||
| ``PYTHONPATH`` before executing the binaries: | ||
| .. code:: | ||
| $ export PYTHONPATH=`pwd` | ||
| $ bin/ptftp | ||
| Connected to localhost:69. | ||
| tftp> | ||
| TFTP server and client | ||
| ---------------------- | ||
| The TFTP server, pTFTPd, fully supports the TFTP specification as | ||
| defined in RFC1350. It also supports the TFTP Option Extension protocol | ||
| (per RFC2347), the block size option as defined in RFC2348 and the | ||
| transfer size option from RFC2349. | ||
| For help on how to use pTFTPd, type: | ||
| .. code:: | ||
| $ ptftpd --help | ||
| The port used can be changed using the ``-p`` option. The root path is | ||
| given as a simple argument. For example, to serve ``/var/lib/tftp`` on | ||
| port 6969 through the eth0 network interface: | ||
| .. code:: | ||
| $ ptftpd -p 6969 eth0 /var/lib/tftp | ||
| The TFTP client is an interactive client, just launch it and type | ||
| ``help`` to see the available commands: | ||
| .. code:: | ||
| $ ptftp | ||
| tftp> help | ||
| ... | ||
| PXE solution | ||
| ------------ | ||
| The PXE system is also very easy to use. It takes three arguments: the | ||
| network interface to listen on, the TFTP root path from which to serve | ||
| files, and the PXE boot filename. It will automatically start a TFTP | ||
| server and a DHCP server to serve hosts on the given interface. See | ||
| ``--help`` for more details: | ||
| .. code:: | ||
| $ pxed --help | ||
| Mechanics for using ``pxed.py`` with the BOOTP server are not yet in | ||
| place, but such a solution can easily be constructed manually by | ||
| starting the BOOTP server and the TFTP server manually: | ||
| .. code:: | ||
| $ bootpd <interface> <PXE boot file> & | ||
| $ ptftpd <interface> | ||
| {"classifiers": ["Operating System :: OS Independent", "Programming Language :: Python"], "extensions": {"python.commands": {"wrap_console": {"bootpd": "ptftplib.bootpserver:main", "dhcpd": "ptftplib.dhcpserver:main", "ptftp": "ptftplib.tftpclient:main", "ptftpd": "ptftplib.tftpserver:main", "pxed": "ptftplib.pxeserver:main"}}, "python.details": {"contacts": [{"email": "maxime.petazzoni@bulix.org", "name": "Maxime Petazzoni", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/mpetazzoni/ptftpd"}}, "python.exports": {"console_scripts": {"bootpd": "ptftplib.bootpserver:main", "dhcpd": "ptftplib.dhcpserver:main", "ptftp": "ptftplib.tftpclient:main", "ptftpd": "ptftplib.tftpserver:main", "pxed": "ptftplib.pxeserver:main"}}}, "extras": [], "generator": "bdist_wheel (0.29.0)", "license": "GNU General Public License v2", "metadata_version": "2.0", "name": "ptftpd", "run_requires": [{"requires": ["netifaces"]}], "summary": "pTFTPd, a pure-Python TFTP tool suite that works", "version": "1.2"} |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.