nobodd.tftpd

Defines several classes for the purposes of constructing TFTP servers. The most useful are TFTPBaseHandler and TFTPBaseServer which are abstract base classes for the construction of a TFTP server with an arbitrary source of files (these are used by nobodd’s main module). In addition, TFTPSimplerHandler and TFTPSimplerServer are provided as a trivial example implementation of a straight-forward TFTP file server.

For example, to start a TFTP server which will serve files from the current directory on (unprivileged) port 1069:

>>> from nobodd.tftpd import SimpleTFTPServer
>>> server = SimpleTFTPServer(('0.0.0.0', 1069), '.')
>>> server.serve_forever()

Handler Classes

class nobodd.tftpd.TFTPBaseHandler(request, client_address, server)[source]

A abstract base handler for building TFTP servers.

Implements do_RRQ() to handle the initial RRQPacket of a transfer. This calls the abstract resolve_path() to obtain the Path-like object representing the requested file. Descendents must (at a minimum) override resolve_path() to implement a TFTP server.

do_ERROR(packet)[source]

Handles ERRORPacket by ignoring it. The only way this should appear on the main port is at the start of a transfer, which would imply we’re not going to start a transfer anyway.

do_RRQ(packet)[source]

Handles packet, the initial RRQPacket of a connection.

If option negotiation succeeds, and resolve_path() returns a valid Path-like object, this method will spin up a TFTPSubServer instance in a background thread (see TFTPSubServers) on an ephemeral port to handle all further interaction with this client.

resolve_path(filename)[source]

Given filename, as requested by a TFTP client, returns a Path-like object.

In the base class, this is an abstract method which raises NotImplementedError. Descendents must override this method to return a Path-like object, specifically one with a working open() method, representing the file requested, or raise an OSError (e.g. FileNotFoundError) if the requested filename is invalid.

class nobodd.tftpd.SimpleTFTPHandler(request, client_address, server)[source]

An implementation of TFTPBaseHandler that overrides uses SimpleTFTPServer.base_path for resolve_path().

resolve_path(filename)[source]

Resolves filename against SimpleTFTPServer.base_path.

Server Classes

class nobodd.tftpd.TFTPBaseServer(address, handler_class, bind_and_activate=True)[source]

A abstract base for building TFTP servers.

To build a concrete TFTP server, make a descendent of TFTPBaseHandler that overrides resolve_path(), then make a descendent of this class that calls super().__init__ with the overridden handler class. See SimpleTFTPHandler and SimpleTFTPServer for examples.

Note

While it is common to combine classes like UDPServer and TCPServer with the threading or fork-based mixins there is little point in doing so with TFTPBaseServer.

Only the initial packet of a TFTP transaction arrives on the “main” port; every packet after this is handled by a background thread with its own ephemeral port. Thus, multi-threading or multi-processing of the initial connection only applies to a single (minimal) packet.

server_close()[source]

Called to clean-up the server.

May be overridden.

class nobodd.tftpd.SimpleTFTPServer(server_address, base_path)[source]

A trivial (pun intended) implementation of TFTPBaseServer that resolves requested paths against base_path (a str or Path).

base_path

The base_path specified in the constructor.

Command Line Use

Just as http.server can be invoked from the command line as a standalone server using the interpreter’s -m option, so nobodd.tftpd can too. To serve the current directory as a TFTP server:

python -m nobodd.tftpd

The server listens to port 6969 by default. This is not the registered port 69 of TFTP, but as that port requires root privileges by default on UNIX platforms, a safer default was selected (the security provenance of this code is largely unknown, and certainly untested at higher privilege levels). The default port can be overridden by passed the desired port number as an argument:

python -m nobodd.tftpd 1069

By default, the server binds to all interfaces. The option -b/--bind specifies an address to which it should bind instead. Both IPv4 and IPv6 addresses are supported. For example, the following command causes the server to bind to localhost only:

python -m nobodd.tftpd --bind 127.0.0.1

By default, the server uses the current directory. The option -d/--directory specifies a directory from which it should serve files instead. For example:

python -m nobodd.tftpd --directory /tmp/

Internal Classes and Exceptions

The following classes and exceptions are entirely for internal use and should never be needed (directly) by applications.

class nobodd.tftpd.TFTPClientState(address, path, mode='octet')[source]

Represents the state of a single transfer with a client. Constructed with the client’s address (format varies according to family), the path of the file to transfer (must be a Path-like object, specifically one with a functioning open() method), and the mode of the transfer (must be either TFTP_BINARY or TFTP_NETASCII).

address

The address of the client.

blocks

An internal mapping of block numbers to blocks. This caches blocks that have been read, transmitted, but not yet acknowledged. As ACK packets are received, blocks are removed from this cache.

block_size

The size, in bytes, of blocks to transfer to the client.

mode

The transfer mode. One of TFTP_BINARY or TFTP_NETASCII.

source

The file-like object opened from the specified path.

timeout

The timeout, in nano-seconds, to use before re-transmitting packets to the client.

ack(block_num)[source]

Specifies that block_num has been acknowledged by the client and can be removed from blocks, the internal block cache.

close()[source]

Closes the source file associated with the client state. This method is idempotent.

get_block(block_num)[source]

Returns the bytes of the specified block_num.

If the block_num has not been read yet, this will cause the source to be read. Otherwise, it will be returned from the as-yet unacknowledged block cache (in blocks). If the block has already been acknowledged, which may happen asynchronously, this will raise AlreadyAcknowledged.

A ValueError is raised if an invalid block is requested.

get_size()[source]

Attempts to calculate the size of the transfer. This is used when negotiating the tsize option.

At first, os.fstat() is attempted on the open file; if this fails (e.g. because there’s no valid fileno), the routine will attempt to seek() to the end of the file briefly to determine its size. Raises OSError in the case that the size cannot be determined.

negotiate(options)[source]

Called with options, a mapping of option names to values (both str) that the client wishes to negotiate.

Currently supported options are defined in nobodd.tftp.TFTP_OPTIONS. The original options mapping is left unchanged. Returns a new options mapping containing only those options that we understand and accept, and with values adjusted to those that we can support.

Raises BadOptions in the case that the client requests pathologically silly or dangerous options.

property finished

Indicates whether the transfer has completed or not. A transfer is considered complete when the final (under-sized) block has been sent to the client and acknowledged.

property transferred

Returns the number of bytes transferred to client and successfully acknowledged.

class nobodd.tftpd.TFTPHandler(request, client_address, server)[source]

Abstract base handler for TFTP transfers.

This handles decoding TFTP packets with the classes defined in nobodd.tftp. If the decoding is successful, it attempts to call a corresponding do_ method (e.g. do_RRQ(), do_ACK()) with the decoded packet. The handler must return a nobodd.tftp.Packet in response.

This base class defines no do_ methods itself; see TFTPBaseHandler and TFTPSubHandler.

finish()[source]

Overridden to send the response written to wfile. Returns the number of bytes written.

Note

In contrast to the usual DatagramRequestHandler, this method does not send an empty packet in the event that wfile has no content, as that confused several TFTP clients.

handle()[source]

Attempts to decode the incoming Packet and dispatch it to an appropriately named do_ method. If the method returns another Packet, it will be sent as the response.

setup()[source]

Overridden to set up the rfile and wfile objects.

class nobodd.tftpd.TFTPSubHandler(request, client_address, server)[source]

Handler for all client interaction after the initial RRQPacket.

Only the initial packet goes to the “main” TFTP port (69). After that, each transfer communicates between the client’s original port (presumably in the ephemeral range) and an ephemeral server port, specific to that transfer. This handler is spawned by the main handler (a descendent of TFTPBaseHandler) and deals with all further client communication. In practice this means it only handles ACKPacket and ERRORPacket.

do_ACK(packet)[source]

Handles ACKPacket by calling TFTPClientState.ack(). Terminates the thread for this sub-handler if the transfer is complete, and otherwise sends the next DATAPacket in response.

do_ERROR(packet)[source]

Handles ERRORPacket by terminating the transfer (in accordance with the spec.)

finish()[source]

Overridden to note the last time we communicated with this client. This is used by the re-transmit algorithm.

handle()[source]

Overridden to verify that the incoming packet came from the address (and port) that originally spawned this sub-handler. Logs and otherwise ignores all packets that do not meet this criteria.

class nobodd.tftpd.TFTPSubServer(main_server, client_state)[source]

The server class associated with TFTPSubHandler.

You should never need to instantiate this class yourself. The base handler should create an instance of this to handle all communication with the client after the initial RRQ packet.

service_actions()[source]

Overridden to handle re-transmission after a timeout.

class nobodd.tftpd.TFTPSubServers[source]

Manager class for the threads running TFTPSubServer.

TFTPBaseServer creates an instance of this to keep track of the background threads that are running transfers with TFTPSubServer.

add(server)[source]

Add server, a TFTPSubServer instance, as a new background thread to be tracked.

run()[source]

Watches background threads for completed or otherwise terminated transfers. Shuts down all remaining servers (and their corresponding threads) at termination.

exception nobodd.tftpd.TransferDone[source]

Exception raised internally to signal that a transfer has been completed.

exception nobodd.tftpd.AlreadyAcknowledged[source]

Exception raised internally to indicate that a particular data packet was already acknowledged, and does not require repeated acknowlegement.

exception nobodd.tftpd.BadOptions[source]

Exception raised when a client passes invalid options in a RRQPacket.