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 asCONare 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,overwriteandmax_uploaddecide which uploads are accepted, and an announcedtsizelarger 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().