# 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 `server` 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)

  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)

    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)

    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)

    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)

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

  resolve_path(filename)

    Resolves filename against `SimpleTFTPServer.base_path`.

## Server classes

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

  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()

    Called to clean-up the server.

    May be overridden.

class nobodd.tftpd.SimpleTFTPServer(server_address, base_path)

  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 passing 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/

## Support 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')

  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)

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

  close()

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

  get_block(block_num)

    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()

    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)

    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)

  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()

    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()

    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()

    Overridden to set up the `rfile` and `wfile` objects.

class nobodd.tftpd.TFTPSubHandler(request, client_address, server)

  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)

    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)

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

  finish()

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

  handle()

    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)

  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()

    Overridden to handle re-transmission after a timeout.

class nobodd.tftpd.TFTPSubServers

  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)

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

  run()

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

exception nobodd.tftpd.TransferDone

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

exception nobodd.tftpd.AlreadyAcknowledged

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

exception nobodd.tftpd.BadOptions

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