Skip to content

Serving files

A directory

import tftp

server = tftp.TFTPServer("/srv/tftp", port=69, writable=True, overwrite=False)
server.serve_forever()

FilesystemBackend maps a filename to a path under the root:

  • a leading / is stripped, and \ is a separator too (Windows boot loaders send \boot\bcd);
  • .. is refused, and the resolved path, symlinks included, must stay inside the root; on Windows drive letters, : streams and device names such as CON are refused as well;
  • uploads are written to a temporary file and renamed into place only when the last block has arrived, so a failed upload leaves nothing behind;
  • create, overwrite and max_upload decide which uploads are accepted, and an announced tsize larger than the free space is refused up front.

Generated content

A handler is any object with open_read(context) and open_write(context, size). Subclass FilesystemBackend to generate some files and serve the rest from disk:

import io
import tftp

class BootMenu(tftp.FilesystemBackend):
    def open_read(self, context):
        if context.filename == "pxelinux.cfg/default":
            return io.BytesIO(b"DEFAULT linux\n# for %s\n" % context.peer[0].encode())
        return super().open_read(context)

context carries the parsed request, the client's address, and the address and interface the request arrived on. Raise tftp.TFTPError(code, message) to refuse with a specific ERROR, or let an OSError through to have it mapped by errno.

A handler's open_read and open_write run in a worker thread, unless the handler sets opens_fast = True (FilesystemBackend and MemoryBackend do), when they run on the server's single event-loop thread and must return quickly. The streams they return are read and written on that thread: a stream with nothing ready raises tftp.WouldBlock instead of blocking.

Composing handlers

Handlers wrap each other. tftp.backends has three that compose over a directory (pytftp serve --per-client, --remap and --ignore-case are these three; see Backends):

import tftp
from tftp.backends import CaseInsensitive, PerClient, Remap

def make(directory):
    return CaseInsensitive(directory, writable=True)

handler = Remap(PerClient("/srv/tftp", make, fallback=False), [r"^/?pxelinux/=boot/"])
tftp.TFTPServer(handler).serve_forever()

Your own is a class with open_read(context) and open_write(context, size) that picks another handler and calls it.

Behind a firewall

Every transfer runs on its own UDP port. Pin them to a range a firewall can allow:

tftp.TFTPServer("/srv/tftp", port_range=(50000, 50100))

(pytftp serve --port-range 50000:50100; the relay takes the same.) A request arriving with every port in use is answered "server busy".

Where to listen

listen says which sockets the server opens, in the grammar every netimps listener reads. With none, the server listens on every address of both families ("*" and "::": two sockets, the IPv6 one left out on a host with no IPv6), on port. A host name is never resolved: "localhost" is an adapter name.

tftp.TFTPServer("/srv/tftp", listen="eth0")                    # eth0's IPv4 wildcard socket, limited to eth0
tftp.TFTPServer("/srv/tftp", listen=["10.0.0.5", "[fd00::5]:6969"])   # two addresses, the second on its own port

An entry is an address ("10.0.0.5", "[fd00::5]:6969"), an adapter name, a netimps.Interface or a MAC (a wildcard socket limited to that adapter, which also hears its broadcasts), or "*"; several are a list or joined by commas (pytftp serve --listen eth0 --listen 10.0.0.5:6969, repeatable or comma-joined). port is the port an entry without one gets. Every socket answers its own requests from the address they were sent to; max_sessions and the other limits count across all of them, and server_addresses lists where each is bound.

Directory listings

TFTPServerOptions(allowed=tftp.options.STANDARD_OPTIONS | tftp.options.LISTING_OPTIONS) (or pytftp serve --listing) lets pytftp clients list directories and see modification times; see Paths.

Watching transfers

def report(result):
    print(result.operation, result.filename, result.peer[0], result.bytes, result.error or "ok")

tftp.TFTPServer("/srv/tftp", on_complete=report).serve_forever()

on_complete sees every transfer, including refused and failed ones.

Running in the background

server = tftp.TFTPServer("/srv/tftp", port=0).start()   # binds, then serves on a daemon thread
print(server.server_address)
...
server.shutdown()      # ask it to stop; returns at once
server.wait_closed()   # block until it has
server.close()         # release the sockets; final

The constructor opens nothing. bind() opens the socket (start(), serve_forever() and with server: call it), so a privileged port can be taken before dropping privileges, and server_address is None until then. A handler that wants the server down calls shutdown(), never close().