tftp 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tftp/AGENTS.md +761 -0
- tftp/README.md +218 -0
- tftp/__init__.py +180 -0
- tftp/__main__.py +6 -0
- tftp/_sockets.py +36 -0
- tftp/aio/__init__.py +22 -0
- tftp/aio/bridge.py +214 -0
- tftp/aio/client.py +426 -0
- tftp/aio/server.py +314 -0
- tftp/backends/__init__.py +19 -0
- tftp/backends/http.py +142 -0
- tftp/backends/memory.py +93 -0
- tftp/backends/pipe.py +192 -0
- tftp/backends/proxy.py +162 -0
- tftp/capture/__init__.py +74 -0
- tftp/capture/events.py +161 -0
- tftp/capture/filters.py +149 -0
- tftp/capture/flows.py +290 -0
- tftp/capture/frames.py +189 -0
- tftp/capture/live.py +54 -0
- tftp/capture/pcap.py +225 -0
- tftp/cli/__init__.py +56 -0
- tftp/cli/capture.py +130 -0
- tftp/cli/common.py +240 -0
- tftp/cli/handlers.py +147 -0
- tftp/cli/relay.py +117 -0
- tftp/cli/serve.py +237 -0
- tftp/cli/transfer.py +174 -0
- tftp/client.py +769 -0
- tftp/errors.py +205 -0
- tftp/listing.py +123 -0
- tftp/netascii.py +149 -0
- tftp/options/__init__.py +89 -0
- tftp/options/base.py +175 -0
- tftp/options/builtin.py +318 -0
- tftp/options/negotiate.py +134 -0
- tftp/options/policy.py +96 -0
- tftp/options/profiles.py +74 -0
- tftp/options/registry.py +64 -0
- tftp/packet/__init__.py +40 -0
- tftp/packet/codec.py +194 -0
- tftp/packet/enums.py +32 -0
- tftp/path/__init__.py +34 -0
- tftp/path/_stream.py +195 -0
- tftp/path/local.py +221 -0
- tftp/path/uri.py +94 -0
- tftp/py.typed +0 -0
- tftp/relay/__init__.py +24 -0
- tftp/relay/core.py +429 -0
- tftp/relay/routing.py +148 -0
- tftp/relay/session.py +165 -0
- tftp/result.py +88 -0
- tftp/server/__init__.py +20 -0
- tftp/server/base.py +321 -0
- tftp/server/core.py +430 -0
- tftp/server/handler.py +290 -0
- tftp/server/listener.py +198 -0
- tftp/server/policy.py +63 -0
- tftp/server/session.py +320 -0
- tftp/server/stats.py +63 -0
- tftp/transfer/__init__.py +31 -0
- tftp/transfer/base.py +241 -0
- tftp/transfer/receiver.py +212 -0
- tftp/transfer/sender.py +175 -0
- tftp/uri.py +95 -0
- tftp-0.0.0.dist-info/METADATA +260 -0
- tftp-0.0.0.dist-info/RECORD +70 -0
- tftp-0.0.0.dist-info/WHEEL +4 -0
- tftp-0.0.0.dist-info/entry_points.txt +5 -0
- tftp-0.0.0.dist-info/licenses/LICENSE +21 -0
tftp/AGENTS.md
ADDED
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
# `tftp` — public API header
|
|
2
|
+
|
|
3
|
+
Header-file-style reference for the `tftp` package: every public export with
|
|
4
|
+
its signature, contract and gotchas, so the package can be used without
|
|
5
|
+
reading its source. It ships inside the package and is self-contained; read it
|
|
6
|
+
(or the README beside it) with `importlib.resources.files("tftp")`.
|
|
7
|
+
Development documentation lives with the source at
|
|
8
|
+
<https://github.com/jose-pr/pytftp>.
|
|
9
|
+
|
|
10
|
+
Everything below is importable from `tftp` directly. The subpackages only
|
|
11
|
+
group it: `tftp.packet` (wire format), `tftp.options` (negotiation),
|
|
12
|
+
`tftp.transfer` (the I/O-free engine), `tftp.client`, `tftp.server`,
|
|
13
|
+
`tftp.netascii`, `tftp.errors`, `tftp.result`, and `tftp.cli` (needs the `cli`
|
|
14
|
+
extra). Modules starting with `_` are internal.
|
|
15
|
+
|
|
16
|
+
`tftp.__version__` — the installed distribution's version.
|
|
17
|
+
|
|
18
|
+
## Protocol coverage
|
|
19
|
+
|
|
20
|
+
| Spec | What | Notes |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| RFC 1350 | RRQ, WRQ, DATA, ACK, ERROR; octet and netascii | `mail` mode is refused with ERROR 4 |
|
|
23
|
+
| RFC 1123 4.2.3.1 | Sorcerer's Apprentice fix | duplicate ACKs never trigger a resend (windowsize 1) |
|
|
24
|
+
| RFC 1123 4.2.3.2 | exponential backoff | each consecutive retransmission waits `backoff` times longer, capped |
|
|
25
|
+
| RFC 1123 4.2.3.4 | broadcast requests ignored | server, when pktinfo reports the destination |
|
|
26
|
+
| RFC 2347 | option extension, OACK, ERROR 8 | unknown options are ignored, as the RFC requires |
|
|
27
|
+
| RFC 2348 | `blksize` 8..65464 | server clamps to its `max_blksize` |
|
|
28
|
+
| RFC 2349 | `timeout` (1..255 s), `tsize` | `tsize` 0 is never sent in an OACK (curl rejects it) |
|
|
29
|
+
| RFC 7440 | `windowsize` 1..65535 | server clamps to its `max_windowsize` (default 64) |
|
|
30
|
+
| tftp-hpa | `blksize2`, `utimeout`, `rollover`, `cookie` | **off unless a server allows them** (`ServerOptions(allowed=...)`, a profile) |
|
|
31
|
+
| Microsoft | `mstfwindow` (bootmgr/WDS variable window) | off unless allowed; runs a fixed window of 4 (the in-transfer resize is unpublished) |
|
|
32
|
+
| pytftp | `x-list` (directory listing), `x-mtime` (modification time) | off unless allowed (`LISTING_OPTIONS`); other servers ignore them |
|
|
33
|
+
| — | block-number rollover | blocks wrap after 65535 (to 0, or 1 with `rollover`); file size is unlimited |
|
|
34
|
+
| RFC 1350 §6 | dallying | server re-ACKs a repeated last DATA for one timeout |
|
|
35
|
+
|
|
36
|
+
Not implemented: RFC 2090 multicast, PXE MTFTP.
|
|
37
|
+
|
|
38
|
+
## Client
|
|
39
|
+
|
|
40
|
+
**`Client(host, port=69, *, timeout=1.0, retries=5, blksize=1428, windowsize=None, tsize=True, rollover=None, timeout_option=True, family=0, local_address=None, fallback=True, dally=False, backoff=2.0, max_timeout=None, max_duration=None, strict_source=True, utimeout=False, extra_options=None, registry=None, on_negotiated=None)`**
|
|
41
|
+
|
|
42
|
+
- `host` — a string (name or address; `"[v6]"`, `"host:port"` and
|
|
43
|
+
`"[v6]:port"` are accepted, and a port written there overrides `port`), an
|
|
44
|
+
`ipaddress` address or interface (its address), or a `netimps.Host`. Kept
|
|
45
|
+
as given in `client.host`.
|
|
46
|
+
- `timeout` — seconds before a retransmission. Requested from the server as
|
|
47
|
+
`timeout` when whole (1..255); a fractional one is requested as
|
|
48
|
+
`utimeout` only with `utimeout=True`, otherwise not at all. Nothing is
|
|
49
|
+
requested with `timeout_option=False`.
|
|
50
|
+
- `retries` — retransmissions of one packet before `TransferTimeout`.
|
|
51
|
+
- `backoff`, `max_timeout` — each consecutive retransmission (of the
|
|
52
|
+
request too) waits `backoff` times longer, up to `max_timeout` (default
|
|
53
|
+
8 × `timeout`); progress resets the wait. `backoff=1` disables it.
|
|
54
|
+
- `max_duration` — seconds a whole transfer may take; `None` is unlimited.
|
|
55
|
+
- `strict_source` — the first answer must come from the address the request
|
|
56
|
+
was sent to. `False` accepts a multi-homed server answering from another
|
|
57
|
+
address (the transfer then locks on to that address and port).
|
|
58
|
+
- `blksize` — requested; `None` asks for nothing (512). The default 1428 fits
|
|
59
|
+
one Ethernet frame on IPv4 and IPv6. `"mtu"` sizes it to the MTU of the
|
|
60
|
+
interface the route to the server uses (1428 when unknown).
|
|
61
|
+
- `on_negotiated(negotiated, peer)` — called once the server has answered
|
|
62
|
+
the request (OACK, first DATA or ACK 0) and **before any data moves**;
|
|
63
|
+
`peer` is the server's transfer address. If it raises, the server is sent
|
|
64
|
+
an ERROR (the `TftpError`'s code, else 0) and the exception propagates.
|
|
65
|
+
- `extra_options` — further options to request verbatim (`{"blksize2": 4096}`,
|
|
66
|
+
`{"cookie": "x"}`, custom ones). A known option's answer is validated by
|
|
67
|
+
its handler (`registry`, default `DEFAULT_REGISTRY`); an unknown one's
|
|
68
|
+
answer lands in `result.negotiated.extra`. Naming a built-in twice (here
|
|
69
|
+
and as a keyword) raises `ValueError`.
|
|
70
|
+
- `windowsize` — requested RFC 7440 window; `None` asks for nothing (1).
|
|
71
|
+
- `tsize` — ask for the size (RRQ) or announce it (WRQ, when the source size
|
|
72
|
+
can be determined).
|
|
73
|
+
- `rollover` — request rollover to 0 or 1; `None` asks for nothing, wraps to 0,
|
|
74
|
+
and follows a server that wraps to 1.
|
|
75
|
+
- `family` — `socket.AF_INET`/`AF_INET6` to force one; `0` is whatever the host
|
|
76
|
+
resolves to first.
|
|
77
|
+
- `local_address` — `(address, port)` to send from; the address in any form
|
|
78
|
+
`host` takes.
|
|
79
|
+
- `fallback` — if the server answers the *request* with ERROR 8 (options
|
|
80
|
+
refused), ask again once without options.
|
|
81
|
+
- `dally` — after the last ACK of a download, keep re-ACKing a repeated last
|
|
82
|
+
DATA for one timeout. Costs that much time per download.
|
|
83
|
+
- Options are validated at construction: an out-of-range `blksize`,
|
|
84
|
+
`windowsize` or `rollover` raises `ValueError` there.
|
|
85
|
+
- **Not thread-safe**: one `Client` per thread. Each transfer opens its own
|
|
86
|
+
socket, so sequential transfers on one client are fine.
|
|
87
|
+
|
|
88
|
+
Methods (each returns a `TransferResult` unless noted):
|
|
89
|
+
|
|
90
|
+
- **`download(filename, dest, *, mode="octet", progress=None)`** — `dest` is a
|
|
91
|
+
path or a writable binary file. A path is created/truncated and **removed
|
|
92
|
+
again if the transfer fails**.
|
|
93
|
+
- **`get(filename, *, mode="octet") -> bytes`** — download into memory.
|
|
94
|
+
- **`upload(filename, source, *, mode="octet", progress=None)`** — `source` is
|
|
95
|
+
a path, a readable binary file (`readinto` or `read`), or bytes-like.
|
|
96
|
+
- **`put(filename, data, *, mode="octet")`** — upload bytes.
|
|
97
|
+
- **`size(filename, *, mode="octet") -> int | None`** — the file's size
|
|
98
|
+
without transferring it: an RRQ asking only for `tsize`, abandoned with
|
|
99
|
+
ERROR 8 once the server answers (a server counts that as `declined`, not
|
|
100
|
+
failed). `None` when the server reports no sizes, unless the file fits in
|
|
101
|
+
the first 512-byte block. Raises like a download (`FileNotFound`...).
|
|
102
|
+
- **`stat(filename, *, mode="octet") -> RemoteStat(size, mtime, is_dir)`** —
|
|
103
|
+
one probe like `size()`, also asking for `x-mtime` and `x-list`: a server
|
|
104
|
+
allowing them reports the modification time (seconds since the epoch) and
|
|
105
|
+
recognises a directory (`size` is then `None`); others leave `mtime`
|
|
106
|
+
`None` and report a directory as `FileNotFound`. A request refused with
|
|
107
|
+
ERROR 8 is retried as a plain size probe (with `fallback`). `filename`
|
|
108
|
+
`""` is the root.
|
|
109
|
+
- **`listdir(dirname="") -> list[ListEntry(name, is_dir, size, mtime)]`** —
|
|
110
|
+
a directory listing from a server speaking `x-list`. `NotADirectoryError`
|
|
111
|
+
when the name is a file (the transfer is abandoned at once);
|
|
112
|
+
`FileNotFound` when it does not exist — which is also what a server
|
|
113
|
+
without listing support answers for a directory.
|
|
114
|
+
- **`path(*segments, mode="octet") -> TftpPath`** — see Paths (needs the
|
|
115
|
+
`path` extra).
|
|
116
|
+
- `mode` is `"octet"` or `"netascii"`; `"binary"`/`"ascii"` are aliases. Any
|
|
117
|
+
other value raises `ValueError`.
|
|
118
|
+
- `progress(done_bytes, total_or_None)` is called after each packet that moved
|
|
119
|
+
data; `total` is the negotiated `tsize`.
|
|
120
|
+
- Raises `RemoteError` (the server sent ERROR), `TransferTimeout`,
|
|
121
|
+
(as the subclass for its code: `FileNotFound`, `AccessViolation`, ...),
|
|
122
|
+
`ProtocolError` (the server broke the protocol, e.g. an OACK with a larger
|
|
123
|
+
`blksize` than requested — the client sends ERROR 8 first), or `OSError` for
|
|
124
|
+
local failures (resolution, the local file).
|
|
125
|
+
|
|
126
|
+
**`download(host, filename, dest, *, port=69, mode="octet", progress=None, **client_options)`**
|
|
127
|
+
and **`upload(host, filename, source, ...)`** — one-shot wrappers;
|
|
128
|
+
`client_options` go to `Client`.
|
|
129
|
+
|
|
130
|
+
**`MODES`** — `("octet", "netascii")`.
|
|
131
|
+
|
|
132
|
+
## Server
|
|
133
|
+
|
|
134
|
+
**`Server(root_or_handler, host=None, port=69, *, writable=False, create=True, overwrite=False, timeout=1.0, retries=5, options=None, max_sessions=None, reply_from_request_address=True, dally=True, on_complete=None, limits=None, ignore_broadcast=True, backoff=2.0, max_timeout=None, open_in_thread=None, workers=8, port_range=None, interface=None)`**
|
|
135
|
+
|
|
136
|
+
- `root_or_handler` — a directory (wrapped in `FileSystemHandler` with
|
|
137
|
+
`writable`, `create`, `overwrite`) or any handler object (see below).
|
|
138
|
+
- `host` — `None` (default) or `"::"` listens on IPv6 **and IPv4** with a dual-stack
|
|
139
|
+
socket, falling back to `"0.0.0.0"` on a host without IPv6. `"0.0.0.0"` is
|
|
140
|
+
IPv4 only; a specific address (`"192.0.2.10"`, `"[fe80::1%eth0]"`) listens
|
|
141
|
+
there alone. An `ipaddress` address or `netimps.Host` works too.
|
|
142
|
+
- `port` — `0` picks a free port; read it from `server_address`.
|
|
143
|
+
- `timeout`, `retries` — per transfer, unless the client negotiates `timeout`.
|
|
144
|
+
- `options` — a `ServerOptions` policy.
|
|
145
|
+
- `max_sessions` — concurrent transfers; beyond it a request gets ERROR 0
|
|
146
|
+
`"server busy"`. `None` is unlimited, **except on Windows, where it defaults
|
|
147
|
+
to 500** because `select()` handles at most 512 sockets.
|
|
148
|
+
- `reply_from_request_address` — each transfer's socket is bound to the
|
|
149
|
+
address the request was sent to (pktinfo), so a multi-homed host or a VIP
|
|
150
|
+
answers from the address the client used. Where the platform cannot report
|
|
151
|
+
it, replies come from the listening address, or from the routing table's
|
|
152
|
+
choice when listening on a wildcard. Check `supports_pktinfo`.
|
|
153
|
+
- `dally` — keep a finished upload's socket for one timeout to re-ACK a
|
|
154
|
+
repeated last DATA.
|
|
155
|
+
- `on_complete(result)` — called with a `TransferResult` after **every**
|
|
156
|
+
transfer, refused and failed ones included (`result.error` set). Exceptions
|
|
157
|
+
it raises are logged and swallowed.
|
|
158
|
+
- `limits` — a `ServerLimits` (below).
|
|
159
|
+
- `ignore_broadcast` — silently drop requests addressed to a broadcast
|
|
160
|
+
(limited or subnet) or multicast address. Needs pktinfo to see the
|
|
161
|
+
destination; without it every request looks unicast.
|
|
162
|
+
- `backoff`, `max_timeout` — as for `Client`, per transfer.
|
|
163
|
+
- `open_in_thread`, `workers` — call the handler's `open_read`/`open_write`
|
|
164
|
+
in a pool of `workers` threads so a handler that blocks (HTTP, an upstream
|
|
165
|
+
server) never stalls other transfers. `None` decides per handler: one
|
|
166
|
+
with `_tftp_fast_open_ = True` (`FileSystemHandler`, `MemoryHandler`)
|
|
167
|
+
opens inline on the loop, anything else in a worker. A retransmitted
|
|
168
|
+
request is still recognised while its open is pending.
|
|
169
|
+
- `interface` — listen on one network adapter: a name (`"eth0"`), a
|
|
170
|
+
`netimps.Interface`, its MAC, or one of its addresses. The listener binds
|
|
171
|
+
**one address** of it: the one named, else the adapter's primary IPv4
|
|
172
|
+
address (non-loopback preferred), else its IPv6 one; `host="0.0.0.0"` or
|
|
173
|
+
`"::"` picks the family. Another `host`, or an adapter netimps cannot
|
|
174
|
+
resolve, is a `ValueError`. Requests to the adapter's other addresses are
|
|
175
|
+
not received; run one server per family or address as needed.
|
|
176
|
+
- `port_range` — `(low, high)` inclusive, a `range`, or a `PortRange`:
|
|
177
|
+
transfer sockets take their ports from it (round-robin, so a just-released
|
|
178
|
+
port is reused last), for a firewall to allow. With every port taken a
|
|
179
|
+
request gets ERROR 0 `"server busy"` (counted `refused`). `None` lets the
|
|
180
|
+
OS choose.
|
|
181
|
+
- Raises `OSError` if the port cannot be bound (port 69 needs privileges on
|
|
182
|
+
POSIX).
|
|
183
|
+
|
|
184
|
+
Lifecycle:
|
|
185
|
+
|
|
186
|
+
- **`serve_forever()`** — run the event loop in the calling thread until
|
|
187
|
+
`shutdown()`. One thread serves every transfer.
|
|
188
|
+
- **`shutdown()`** — stop `serve_forever`; safe from any thread, a handler or
|
|
189
|
+
`on_complete`. Transfers in flight get ERROR 0 `"server shutting down"`
|
|
190
|
+
(their result's `error` is `TransferAborted`).
|
|
191
|
+
- **`start() -> Server`** — run `serve_forever` in a daemon thread.
|
|
192
|
+
- **`stop(timeout=5.0)`** — `shutdown()` and join the `start()` thread.
|
|
193
|
+
- **`close()`** — stop and release every socket. Also the context-manager
|
|
194
|
+
exit. A closed server cannot serve again.
|
|
195
|
+
|
|
196
|
+
Statistics: **`stats`** (a `Stats`: `stats["completed"]`, `snapshot()`)
|
|
197
|
+
counts `requests`, `refused` (refused before a transfer, including limits
|
|
198
|
+
and `server busy`), `started`, `completed`, `failed`, `bytes_sent`,
|
|
199
|
+
`bytes_received`, `retransmits`; **`stats_snapshot()`** returns them plus
|
|
200
|
+
`active`. Thread-safe; meant for a metrics exporter. A `Relay` has the same,
|
|
201
|
+
with `bytes_to_clients`/`bytes_from_clients`.
|
|
202
|
+
|
|
203
|
+
Properties: `server_address` (the bound `(host, port, ...)`, valid after
|
|
204
|
+
close), `supports_pktinfo`, `dual_stack`, `active_sessions`.
|
|
205
|
+
|
|
206
|
+
Behaviour worth knowing:
|
|
207
|
+
|
|
208
|
+
- Every transfer runs on its **own UDP socket** (its transfer ID). A packet
|
|
209
|
+
from any other address/port gets ERROR 5 and does not disturb the transfer.
|
|
210
|
+
- A repeated request from the same client address and port while its transfer
|
|
211
|
+
is running is ignored (it is a retransmission).
|
|
212
|
+
- Datagrams on the listening port that are not RRQ/WRQ are **dropped without
|
|
213
|
+
reply** (no reflection). A malformed RRQ/WRQ gets ERROR 4.
|
|
214
|
+
- A v4 client of a dual-stack listener is served from a plain IPv4 socket, so
|
|
215
|
+
`result.peer` shows `"192.0.2.7"`, never `"::ffff:192.0.2.7"`.
|
|
216
|
+
- Socket buffers are grown to hold two windows (Windows defaults to 64 KiB,
|
|
217
|
+
which a large window overflows).
|
|
218
|
+
|
|
219
|
+
**`PortRange(low, high)`** — `PortRange.of(value)` takes `None`, a pair, a
|
|
220
|
+
`range` or a `PortRange`; `len()`, iteration (every port once, from the
|
|
221
|
+
round-robin cursor). `ValueError` outside 1..65535 or with `low > high`.
|
|
222
|
+
|
|
223
|
+
**`ServerLimits(max_request_size=1024, max_filename_length=512, max_options=16, max_option_length=255, max_sessions_per_client=None, max_duration=None)`**
|
|
224
|
+
— bounds on untrusted input. A request over a size/name/option limit gets
|
|
225
|
+
ERROR 4 from the listening port; a client over `max_sessions_per_client`
|
|
226
|
+
(counted per address, any port) gets ERROR 0 `"server busy"`.
|
|
227
|
+
`max_duration` ends a transfer that runs longer (ERROR 0 to the peer,
|
|
228
|
+
`TransferTimeout` in the result).
|
|
229
|
+
|
|
230
|
+
**`ServerOptions(max_blksize=65464, max_windowsize=64, max_window_bytes=4 MiB, allowed=None, refused=(), fit_mtu=False, registry=None)`**
|
|
231
|
+
— the negotiation policy.
|
|
232
|
+
|
|
233
|
+
- A larger `blksize`/`windowsize` request is answered with the maximum
|
|
234
|
+
(which the RFCs allow). `windowsize` is also lowered so one window of the
|
|
235
|
+
negotiated block size fits `max_window_bytes` — the memory a sender holds.
|
|
236
|
+
- `allowed` — option names ever acknowledged; `None` is `STANDARD_OPTIONS`
|
|
237
|
+
(`blksize`, `timeout`, `tsize`, `windowsize`). Add `EXTENSION_OPTIONS`
|
|
238
|
+
names to accept extensions: tftp-hpa's `blksize2`, `utimeout`,
|
|
239
|
+
`rollover`, `cookie`; Microsoft's `mstfwindow`; pytftp's `x-list`,
|
|
240
|
+
`x-mtime` (`LISTING_OPTIONS`). A name not in `registry` raises
|
|
241
|
+
`ValueError`.
|
|
242
|
+
- `refused` — never acknowledged even if allowed: for firmware that asks for
|
|
243
|
+
an option and then mishandles it (`refused={"windowsize"}`).
|
|
244
|
+
- `fit_mtu` — lower `blksize` so a DATA packet fits the MTU of the interface
|
|
245
|
+
the request arrived on (IP + UDP + 4-byte header): 1468 on IPv4 and 1448
|
|
246
|
+
on IPv6 for a 1500 MTU (`netimps.max_udp_payload` − 4). Needs pktinfo for
|
|
247
|
+
the interface; boot ROMs often cannot reassemble fragments.
|
|
248
|
+
- `accepts(name)` — `name` in `allowed` and not in `refused`.
|
|
249
|
+
|
|
250
|
+
## Options, registry and profiles
|
|
251
|
+
|
|
252
|
+
Every option is an **`OptionHandler`** (`name`, `standard`) with
|
|
253
|
+
`negotiate(value, ctx) -> str | None` (server: the value to acknowledge, or
|
|
254
|
+
`None` to leave it out — RFC 2347's refusal; set fields on `ctx.result`) and
|
|
255
|
+
`accept(requested, acked, ctx)` (client: validate and apply; raise
|
|
256
|
+
`tftp.options.refuse(msg)` for ERROR 8). `ServerContext` carries `result`,
|
|
257
|
+
`requested`, `acked`, `policy`, `is_read`, `size`, `mtu`, `ipv6`, `stream`
|
|
258
|
+
(what the handler opened for an RRQ, else `None`) and `max_blksize` (policy
|
|
259
|
+
limit after `fit_mtu`); `ClientContext` carries
|
|
260
|
+
`result`, `requested`, `is_read`. Custom values go in `ctx.result.extra`.
|
|
261
|
+
|
|
262
|
+
**`OptionRegistry(handlers=BUILTIN_OPTIONS)`** — `register(handler,
|
|
263
|
+
replace=False)` (a duplicate name raises), `unregister(name)`, `get(name)`,
|
|
264
|
+
`in`, iteration, `names()`, `standard()`, `copy()`. **Registration order is
|
|
265
|
+
negotiation order** (`windowsize` after the block size it depends on).
|
|
266
|
+
`DEFAULT_REGISTRY` is what servers and clients use unless given another;
|
|
267
|
+
`register_option(handler)` adds to it. A custom option must also be in a
|
|
268
|
+
server's `allowed`.
|
|
269
|
+
|
|
270
|
+
Built-ins: `blksize`, `timeout`, `tsize`, `windowsize` (standard), and
|
|
271
|
+
`blksize2` (largest power of two ≤ the request and the limit; ignored when
|
|
272
|
+
`blksize` was acknowledged), `utimeout` (10 000..255 000 000 µs),
|
|
273
|
+
`rollover` (0/1), `cookie` (echoed unchanged; a client refuses a changed
|
|
274
|
+
one), `mstfwindow` (`Mstfwindow`: answers `31416` with `27182` and runs a
|
|
275
|
+
window of 4 unless `windowsize` was also acknowledged; a client refuses any
|
|
276
|
+
other answer), `x-list` (`XList`: acknowledged `1` only when the RRQ's stream
|
|
277
|
+
is a listing, `_tftp_listing_`), `x-mtime` (`XMtime`: an RRQ's OACK carries
|
|
278
|
+
the stream's `mtime` attribute or `fstat` time, whole seconds; omitted when
|
|
279
|
+
unknown). `stream_mtime(stream)` is that lookup.
|
|
280
|
+
|
|
281
|
+
**Profiles** — `Profile(name, server, client)`: `.server` is a
|
|
282
|
+
`ServerOptions`, `.client` a fresh dict of `Client` keyword arguments.
|
|
283
|
+
`PROFILES` maps names to the five presets:
|
|
284
|
+
|
|
285
|
+
| profile | server | client |
|
|
286
|
+
| --- | --- | --- |
|
|
287
|
+
| `STRICT` | standard options | `fallback=False` |
|
|
288
|
+
| `DEFAULT` | standard options | library defaults |
|
|
289
|
+
| `PXE` | standard + `rollover`, `utimeout`; `fit_mtu=True` | `blksize=1428` |
|
|
290
|
+
| `HPA` | tftp-hpa's extensions | `utimeout=True` |
|
|
291
|
+
| `LEGACY` | standard, `windowsize` refused | no options at all, `strict_source=False` |
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
tftp.Server("/srv/tftp", options=tftp.PXE.server)
|
|
295
|
+
tftp.Client("192.0.2.1", **tftp.LEGACY.client)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Handlers
|
|
299
|
+
|
|
300
|
+
A handler is any object with:
|
|
301
|
+
|
|
302
|
+
- **`open_read(context) -> reader`** — a binary reader with `readinto` or
|
|
303
|
+
`read`, and `close`. `tsize` is answered when the reader has an integer
|
|
304
|
+
`size` attribute, a real `fileno()`, or is seekable.
|
|
305
|
+
- **`open_write(context, size) -> writer`** — a binary writer with `write` and
|
|
306
|
+
`close`. `size` is the client's announced `tsize` or `None`. **`close` is
|
|
307
|
+
called after the last block is written and before it is acknowledged**, so
|
|
308
|
+
an exception there reaches the client as ERROR. If the writer has `abort()`,
|
|
309
|
+
a failed transfer calls it instead of `close()`.
|
|
310
|
+
|
|
311
|
+
**Slow or asynchronous streams.** A reader's `readinto`/`read`, a writer's
|
|
312
|
+
`write` and its `close` may raise `tftp.WouldBlock` when nothing is ready:
|
|
313
|
+
the transfer pauses (a writer's block stays unacknowledged, which is
|
|
314
|
+
backpressure on the client) without blocking the loop. A stream that has
|
|
315
|
+
`set_wakeup(callback)` is given a thread-safe callback to call when it can
|
|
316
|
+
make progress again; without it a stalled transfer only resumes when the
|
|
317
|
+
peer retransmits. A paused transfer is subject to `max_duration`, not to
|
|
318
|
+
peer retries.
|
|
319
|
+
|
|
320
|
+
Either may raise `TftpError(code, message)` to refuse with that ERROR, or
|
|
321
|
+
`OSError`, mapped by errno (`ENOENT` → 1, `EACCES`/`EPERM` → 2, `ENOSPC` → 3,
|
|
322
|
+
`EEXIST` → 6). The OS message is **not** sent (it could disclose paths). Any
|
|
323
|
+
other exception is logged and sent as ERROR 0. Handlers run on the event-loop
|
|
324
|
+
thread: **a slow handler stalls every transfer**.
|
|
325
|
+
|
|
326
|
+
`write` receives a `memoryview` of a reused buffer when the writer is a
|
|
327
|
+
standard file object (`io.IOBase`) or declares `_tftp_copies_ = True`, and
|
|
328
|
+
`bytes` otherwise — so a writer that keeps references is safe by default.
|
|
329
|
+
|
|
330
|
+
**`RequestContext`** — `request` (the `Request`), `peer` (client address
|
|
331
|
+
tuple), `local_address` (the destination address as text, or `None` without
|
|
332
|
+
pktinfo), `interface_index` (or 0), `interface` (that interface as a
|
|
333
|
+
`netimps.Interface` — name, addresses, MTU — or `None`), `listing` (the RRQ asks for `x-list=1`
|
|
334
|
+
and the server allows it); shortcuts `filename`, `mode`, `options`.
|
|
335
|
+
|
|
336
|
+
**`FileSystemHandler(root, *, writable=False, create=True, overwrite=False, backslash=True, max_upload=None)`**
|
|
337
|
+
|
|
338
|
+
- Serves files under `root` (which must exist). Leading `/` is stripped, so
|
|
339
|
+
`/boot/x` is `root/boot/x`. With `backslash`, `\` is a separator too (Windows
|
|
340
|
+
boot loaders send `\boot\bcd`).
|
|
341
|
+
- **Containment**: `..` components are refused (ERROR 2), and the resolved
|
|
342
|
+
path, symlinks included, must stay inside `root` (ERROR 2). On Windows,
|
|
343
|
+
drive letters, `:` streams and reserved device names (`CON`, `NUL`, ...) are
|
|
344
|
+
refused too.
|
|
345
|
+
- Reads: a missing file is ERROR 1, and so is a directory — unless
|
|
346
|
+
`context.listing`, when a directory (`""`, `.` or `/` for the root) is
|
|
347
|
+
answered with a `DirectoryListing`.
|
|
348
|
+
- Writes: refused unless `writable` (ERROR 2); an existing file needs
|
|
349
|
+
`overwrite` (else ERROR 6); a new file needs `create` (else ERROR 1); the
|
|
350
|
+
directory must exist (directories are never created). An announced `tsize`
|
|
351
|
+
above `max_upload` or the free disk space is ERROR 3, and so is an upload
|
|
352
|
+
that grows past `max_upload`.
|
|
353
|
+
- Uploads go through `AtomicWriter`: **a failed or partial upload never
|
|
354
|
+
appears or replaces anything**.
|
|
355
|
+
- `resolve(filename) -> str` exposes the mapping (raises `TftpError`).
|
|
356
|
+
|
|
357
|
+
**`tftp.listing`** — the `x-list` format: UTF-8 lines `<f|d> <size> <mtime|-> <name>`
|
|
358
|
+
(`%`, CR, LF in names as `%25`, `%0D`, `%0A`). **`ListEntry(name, is_dir,
|
|
359
|
+
size, mtime=None)`**, **`format_listing(entries) -> bytes`**,
|
|
360
|
+
**`parse_listing(data) -> list`** (malformed lines skipped),
|
|
361
|
+
**`DirectoryListing(directory, root=None)`** (a `BytesIO` with `size`,
|
|
362
|
+
`mtime`, `_tftp_listing_`; sorted by name; leaves out symlinks resolving
|
|
363
|
+
outside `root` and in-progress uploads `.name.*.part`), `LIST_OPTION`,
|
|
364
|
+
`MTIME_OPTION`.
|
|
365
|
+
|
|
366
|
+
**`AtomicWriter(path, overwrite=True)`** — writes to a hidden temp file beside
|
|
367
|
+
`path`; `close()` renames it into place (refusing with ERROR 6 if
|
|
368
|
+
`overwrite=False` and `path` appeared meanwhile); `abort()` deletes it.
|
|
369
|
+
|
|
370
|
+
## asyncio (`tftp.aio`)
|
|
371
|
+
|
|
372
|
+
**`AsyncClient(...)`** — `Client`'s arguments and rules (options, backoff,
|
|
373
|
+
fallback, `trace`, `on_negotiated`); coroutine methods `download(filename,
|
|
374
|
+
dest, *, mode, progress)`, `get`, `upload(filename, source, *, mode,
|
|
375
|
+
progress)`, `put`, `size`, `stat` (both in the executor), `listdir`, and the async generator **`stream(filename, *, mode,
|
|
376
|
+
buffer=1 MiB)`** yielding chunks as they arrive (a slow consumer holds ACKs
|
|
377
|
+
back; at most `buffer` bytes are held). Name resolution runs in the
|
|
378
|
+
executor. Destinations: a path, a binary file (written on the loop — fine
|
|
379
|
+
for local files), or an **async writer** (`async write(data)`, or `write` +
|
|
380
|
+
`async drain()` like `asyncio.StreamWriter`); `download` returns only once
|
|
381
|
+
an async writer has taken every byte (the writer is not closed). Sources: a
|
|
382
|
+
path, bytes, a binary file, or an **async reader** (`async read(n)`) or async
|
|
383
|
+
iterable of bytes. Cancelling the task sends the server ERROR 0. Each
|
|
384
|
+
attempt (including the option fallback) uses a fresh socket.
|
|
385
|
+
|
|
386
|
+
**`AsyncServer(root_or_handler, host=None, port=69, *, executor=None, **server_options)`**
|
|
387
|
+
— `Server`'s arguments except `open_in_thread`/`workers`. `async with
|
|
388
|
+
AsyncServer(...) as server: await server.serve_forever()`, or `await
|
|
389
|
+
server.start()` … `await server.stop()`; `shutdown()` is thread-safe; `await
|
|
390
|
+
close()`. Handlers:
|
|
391
|
+
|
|
392
|
+
- `open_read`/`open_write` may be `async def` (awaited on the loop), or
|
|
393
|
+
synchronous: those marked `_tftp_fast_open_` (file, memory) run inline,
|
|
394
|
+
others in `executor` (default: the loop's) — so `HttpHandler` and
|
|
395
|
+
`UpstreamHandler` work unchanged (their `Pipe` wake-ups are thread-safe).
|
|
396
|
+
- The returned stream may be an async reader/iterable (RRQ) or async writer
|
|
397
|
+
(WRQ); a writer is closed after the last block, and the final ACK waits
|
|
398
|
+
until everything is written.
|
|
399
|
+
- pktinfo is kept on every loop: the listener is read with netimps'
|
|
400
|
+
`UdpEndpoint.arecv` (`add_reader`, or a readiness thread where the loop
|
|
401
|
+
has none — Windows' Proactor loop).
|
|
402
|
+
- On Windows the default `max_sessions` is 500 (a selector loop's
|
|
403
|
+
`select()`).
|
|
404
|
+
|
|
405
|
+
**`AsyncReaderBridge(source, capacity=1 MiB, size=None)`** /
|
|
406
|
+
**`AsyncWriterBridge(sink, capacity=1 MiB, close_sink=True)`** — the
|
|
407
|
+
adapters doing that (engine-side `readinto`/`write`/`close` raising
|
|
408
|
+
`WouldBlock`, `set_wakeup`; writer `await finish()`); create them on the
|
|
409
|
+
running loop. `is_async_reader(obj)` / `is_async_writer(obj)` say which
|
|
410
|
+
objects they take.
|
|
411
|
+
|
|
412
|
+
## Paths (`tftp.path`, `path` extra)
|
|
413
|
+
|
|
414
|
+
`pip install tftp[path]` adds `pathlib-next[uri]`. Importing `tftp` never
|
|
415
|
+
loads it.
|
|
416
|
+
|
|
417
|
+
**`TftpPath(*segments, client, mode="octet")`** — a `pathlib_next.Path`
|
|
418
|
+
bound to a `Client` (`client.path("boot", "x")`). Joined like a
|
|
419
|
+
`PurePosixPath` (`\` counts as `/`); the path text is the filename sent,
|
|
420
|
+
so `/boot/x` and `boot/x` stay distinct. Joining onto a `TftpPath` keeps its
|
|
421
|
+
client; `with_client()`, `with_mode()`; `client`, `transfer_mode`. Equality
|
|
422
|
+
and hashing include the server (host, port). `as_uri()` is the `tftp://`
|
|
423
|
+
URL. `relative_to()` works on the path text.
|
|
424
|
+
|
|
425
|
+
**`TftpUriPath`** — the `tftp://host[:port]/path[;mode=netascii]` scheme
|
|
426
|
+
for `pathlib_next.uri.UriPath`, registered through the
|
|
427
|
+
`pathlib_next.schemes` entry point, so `UriPath("tftp://...")` returns one
|
|
428
|
+
without importing `tftp`. `filename`, `transfer_mode`;
|
|
429
|
+
`with_options(**client_options)` / `with_client(client)` choose the
|
|
430
|
+
`Client` (default: the URI's host and port, library defaults). The URI host
|
|
431
|
+
may be an address object; `Client` accepts one.
|
|
432
|
+
|
|
433
|
+
Both support what TFTP can do, plus listing against a server speaking
|
|
434
|
+
`x-list`:
|
|
435
|
+
|
|
436
|
+
- `open("r")` streams a download; `open("w")` streams an upload that
|
|
437
|
+
completes (and raises the server's error) on `close()`; `open("x")`
|
|
438
|
+
first probes the size and raises `FileExistsError` if the file exists
|
|
439
|
+
(not atomic). `a` and `+` modes raise `NotImplementedError`. `open()`
|
|
440
|
+
returns only once the server has answered, so `FileNotFoundError`/
|
|
441
|
+
`PermissionError` surface there. At most 1 MiB is buffered; an abandoned
|
|
442
|
+
read sends the server an ERROR.
|
|
443
|
+
- `read_bytes`, `read_text`, `write_bytes`, `write_text` (text newline
|
|
444
|
+
handling is pathlib's), `copy()`/`move()` to and from any pathlib_next path.
|
|
445
|
+
- `stat()` is one `Client.stat()` probe (`FileStat`: `st_size` and
|
|
446
|
+
`st_mtime` 0 when unknown, directory mode for a listed directory);
|
|
447
|
+
`exists()`, `is_file()`, `is_dir()`.
|
|
448
|
+
- `iterdir()`, `walk()`, `glob()`/`rglob()` and `copy(..., recursive=True)`
|
|
449
|
+
need a server allowing `x-list` (one listing per directory, with each
|
|
450
|
+
entry's type, size and time, so no per-entry probe). Against other servers
|
|
451
|
+
a directory raises `FileNotFoundError`; listing a file raises
|
|
452
|
+
`NotADirectoryError`.
|
|
453
|
+
- Deleting, renaming, creating directories and permissions raise
|
|
454
|
+
`NotImplementedError`.
|
|
455
|
+
- TFTP errors become pathlib's: `FileNotFoundError`, `PermissionError`,
|
|
456
|
+
`FileExistsError`, `OSError(ENOSPC)`, `TimeoutError`, else `OSError(EIO)`.
|
|
457
|
+
|
|
458
|
+
## Backends (`tftp.backends`)
|
|
459
|
+
|
|
460
|
+
- **`MemoryHandler(files=None, *, writable=False, overwrite=True, max_upload=None)`**
|
|
461
|
+
— serves `files` (name → bytes; names normalised so `/a\\b` is `a/b`);
|
|
462
|
+
uploads replace an entry only when complete. Thread-safe `files` updates.
|
|
463
|
+
- **`HttpHandler(base_url=None, *, url_for=None, writable=False, headers=None, timeout=10.0, buffer=1 MiB, opener=None)`**
|
|
464
|
+
— TFTP-to-HTTP(S) gateway on `urllib`: GET `base_url + quote(name)`
|
|
465
|
+
(or `url_for(context)`), streamed; `Content-Length` answers `tsize`;
|
|
466
|
+
WRQ → `PUT` (chunked without `tsize`) when `writable`; the final ACK
|
|
467
|
+
waits for the PUT's response. HTTP 404/410 → ERROR 1, 401/403 → 2,
|
|
468
|
+
409 → 6, 413/507 → 3, other → 0. `..` in a name → ERROR 2.
|
|
469
|
+
- **`UpstreamHandler(upstream, *, client_options=None, buffer=1 MiB, stall_timeout=30.0, writable=False)`**
|
|
470
|
+
— terminating proxy: `upstream` is `"host"`, `"host:port"`, an address
|
|
471
|
+
object or `netimps.Host`, `(host, port)`, or `upstream(context)` returning
|
|
472
|
+
one. Each side
|
|
473
|
+
negotiates independently (`client_options` for the upstream `Client`);
|
|
474
|
+
only bytes cross, through a bounded pipe, so a slow client slows the
|
|
475
|
+
upstream. The RRQ is answered after the upstream answers: its ERROR
|
|
476
|
+
reaches the client with the same code and text, its `tsize` passes
|
|
477
|
+
through. A WRQ's final ACK waits for the upstream's final ACK.
|
|
478
|
+
- **`Pipe(capacity=1 MiB, size=None)`** — the bounded byte pipe between a
|
|
479
|
+
worker thread and a transfer, for your own handlers. Transfer side
|
|
480
|
+
(non-blocking, `WouldBlock`): `readinto`, `write`, `close`, `abort`,
|
|
481
|
+
`set_wakeup`. Worker side (blocking): `put(data, timeout)`,
|
|
482
|
+
`get(n, timeout)`/`read(n)`, `finish(error=None)`. `for_upload()` makes
|
|
483
|
+
`close()` wait (`WouldBlock`) until the worker calls
|
|
484
|
+
`set_result(error=None)`, and re-raise its error. `size` answers `tsize`.
|
|
485
|
+
|
|
486
|
+
## Relay (`tftp.relay`)
|
|
487
|
+
|
|
488
|
+
**`Relay(route, host=None, port=69, *, idle_timeout=30.0, max_lifetime=3600.0, linger=2.0, upstream_source=None, limits=None, max_sessions=None, ignore_broadcast=True, reply_from_request_address=True, trace=None, on_session_end=None, port_range=None, interface=None)`**
|
|
489
|
+
— a transparent application relay (there is no standard TFTP relay). The
|
|
490
|
+
request is forwarded **byte for byte** from a fresh upstream-side socket; the
|
|
491
|
+
upstream's TID is learned from its first answer (from the address asked);
|
|
492
|
+
datagrams then cross unchanged between `client <-> relay(client-side TID)` and
|
|
493
|
+
`relay(upstream-side TID) <-> upstream`, so options and extensions this
|
|
494
|
+
library does not know work end to end. Each side negotiates nothing with the
|
|
495
|
+
relay — the client sees the upstream's OACK. For different settings per side
|
|
496
|
+
use `UpstreamHandler` (a terminating proxy) instead.
|
|
497
|
+
|
|
498
|
+
- `interface` — listen on one adapter, as for `Server`.
|
|
499
|
+
- `port_range` — as for `Server`, for both sockets of each relayed transfer
|
|
500
|
+
(client side and upstream side).
|
|
501
|
+
- `route` — an upstream (`"host"`, `"host:port"`, `"[v6]:port"`, an address
|
|
502
|
+
object or `netimps.Host`, `(host, port)`, `Upstream`) or `route(request, context) -> upstream | None`;
|
|
503
|
+
`None` refuses with ERROR 2 `"no route"`. Hostnames are resolved per
|
|
504
|
+
request (cached 60 s). Routes run on the relay's loop: keep them fast.
|
|
505
|
+
- A transfer ends on: an ERROR either way (after ≤1 s), the final DATA/ACK
|
|
506
|
+
exchange (the block size followed from an OACK's `blksize`/`blksize2`, then
|
|
507
|
+
`linger`), `idle_timeout` without traffic, or `max_lifetime`. Keep
|
|
508
|
+
`idle_timeout` above the longest timeout × retries a peer may use.
|
|
509
|
+
- A repeated request from the same client address/port is forwarded again
|
|
510
|
+
only while the upstream has not answered. Strays on either leg get ERROR 5.
|
|
511
|
+
- Shutdown sends ERROR 0 `"relay shutting down"` to both sides of each
|
|
512
|
+
transfer. Windows caps `max_sessions` at 250 by default (two sockets each).
|
|
513
|
+
- `on_session_end(RelaySummary)` — `session`, `client`, `upstream` (the
|
|
514
|
+
learned TID), `filename`, `operation`, `mode`, `bytes_to_client`,
|
|
515
|
+
`bytes_from_client`, `packets`, `duration`, `reason` (`"complete"`,
|
|
516
|
+
`"error"`, `"idle"`, `"lifetime"`, `"shutdown"`), `error` (`(code,
|
|
517
|
+
message)` of an ERROR that passed through, or `None`).
|
|
518
|
+
- Lifecycle and properties as for `Server`: `serve_forever`, `shutdown`,
|
|
519
|
+
`start`, `stop`, `close`, context manager; `server_address`,
|
|
520
|
+
`supports_pktinfo`, `active_sessions`.
|
|
521
|
+
|
|
522
|
+
Routing helpers (`tftp.relay`): **`upstream(value) -> Upstream(host, port)`**
|
|
523
|
+
(`host` normalised by `netimps.normalize_host`: brackets dropped, a
|
|
524
|
+
`"host:port"` split);
|
|
525
|
+
**`by_subnet({network: upstream})`** (keys: anything `netimps.parse(...,
|
|
526
|
+
IPNetwork)` takes — CIDR strings, `ipaddress` networks, interfaces, addresses
|
|
527
|
+
as /32 or /128, `(address, prefix)`; client address, longest prefix; mapped
|
|
528
|
+
v4 matches v4 networks); **`by_prefix({prefix: upstream})`** (filename,
|
|
529
|
+
longest prefix, `\` = `/`, leading separators ignored); **`by_interface({key:
|
|
530
|
+
upstream})`** (keys: an interface index, a `netimps.Interface`, or the local
|
|
531
|
+
address the request was sent to — string, `ipaddress` address or interface,
|
|
532
|
+
`netimps.Host`; needs pktinfo); **`RouteTable(routes, default=None)`** (first
|
|
533
|
+
match wins).
|
|
534
|
+
|
|
535
|
+
## Packet events (`tftp.capture`)
|
|
536
|
+
|
|
537
|
+
**`PacketEvent(time, direction, local, remote, data, role="capture", session=None, leg=None)`**
|
|
538
|
+
— one datagram. `direction` is `"in"`/`"out"` from `role`'s view
|
|
539
|
+
(`"seen"` for a passive capture); `session` correlates one transfer's events;
|
|
540
|
+
`leg` is the relay side. Properties: `opcode`, `opcode_name`, `block`,
|
|
541
|
+
`payload_size`, `summary` (one line, never raises), `source`, `destination`;
|
|
542
|
+
methods `decode()`, `format()` (a human line), `to_dict(payload=False)` (JSON-
|
|
543
|
+
ready metadata; DATA payloads only as hex with `payload=True`).
|
|
544
|
+
`summarize(data)` is the one-line description on its own.
|
|
545
|
+
|
|
546
|
+
**Trace hooks** — `Client(trace=)`, `Server(trace=)`, `Relay(trace=)` take
|
|
547
|
+
`trace(PacketEvent)`, called for every datagram received and sent, on the
|
|
548
|
+
thread doing the I/O; exceptions it raises are logged, never propagated.
|
|
549
|
+
Roles are `"client"`, `"server"`, `"relay"`; one `session` id per transfer
|
|
550
|
+
(`c…`, `s…`, `r…`). A server traces a transfer's request as arriving at the
|
|
551
|
+
listening address. Requests a server refuses before a transfer exists are
|
|
552
|
+
not traced. Off (`None`) costs nothing per packet.
|
|
553
|
+
|
|
554
|
+
**`PcapWriter(path_or_binary_file)`** — writes events (it is a ready trace
|
|
555
|
+
hook: `Server(..., trace=PcapWriter("t.pcap"))`) or `write(time, source,
|
|
556
|
+
destination, payload)` as pcap, link type RAW, with synthesized IPv4/IPv6 and
|
|
557
|
+
UDP headers and valid checksums, so Wireshark/tshark decode it as TFTP
|
|
558
|
+
(v4-mapped addresses are written as IPv4). Context manager; `close()`.
|
|
559
|
+
|
|
560
|
+
**Reading captures** — pcap and pcapng (both byte orders, µs/ns and
|
|
561
|
+
`if_tsresol`, several interfaces), from a path or a stream (a live
|
|
562
|
+
`tcpdump -i eth0 -U -w - udp` pipe). Link types: Ethernet (VLAN/QinQ),
|
|
563
|
+
RAW, IPv4, IPv6, Linux SLL/SLL2, BSD NULL/LOOP. IPv4 and IPv6 fragments are
|
|
564
|
+
reassembled (a large `blksize` fragments on the wire). Non-UDP is skipped.
|
|
565
|
+
|
|
566
|
+
- **`read_frames(source) -> Iterator[(time, linktype, frame)]`**,
|
|
567
|
+
**`read_datagrams(source) -> Iterator[UdpDatagram(time, source, destination, payload)]`**
|
|
568
|
+
— `CaptureFormatError` (a `ValueError`) for anything that is not a capture.
|
|
569
|
+
- **`FlowTracker(ports=(69,), keep_payloads=True)`** — `feed(datagram) ->
|
|
570
|
+
PacketEvent | None` (role `"capture"`, direction `"seen"`; `None` for UDP
|
|
571
|
+
that is neither TFTP traffic of a known transfer nor to/from a request
|
|
572
|
+
port), `feed_all(datagrams)`, `.transfers`. A transfer starts at an
|
|
573
|
+
RRQ/WRQ to a request port and follows the server's answer (its TID; from
|
|
574
|
+
another address too, within 10 s) between that TID and the client's
|
|
575
|
+
address/port. `::ffff:a.b.c.d` and `a.b.c.d` count as one host.
|
|
576
|
+
- **`CapturedTransfer`** — `session`, `client`, `server`, `server_tid`,
|
|
577
|
+
`filename`, `mode`, `operation`, `requested`, `acknowledged`, `blksize`,
|
|
578
|
+
`windowsize`, `tsize`, `error` (`(code, message, "client"|"server")`),
|
|
579
|
+
`complete` (final DATA seen and ACKed), `packets`, `retransmissions` (DATA
|
|
580
|
+
seen again), `request_retransmissions`, `bytes`, `missing_blocks`,
|
|
581
|
+
`started`, `ended`, `duration`; `data(decode_netascii=True)` returns the
|
|
582
|
+
file up to the first gap (block numbers followed across rollover);
|
|
583
|
+
`to_dict()`.
|
|
584
|
+
- **`analyze(source, *, ports=(69,), filter=None, keep_payloads=True) -> Analysis(events, transfers)`**
|
|
585
|
+
— a whole capture (path, stream, or datagrams) at once.
|
|
586
|
+
- **`sniff(interface=None, *, stop=None) -> Iterator[UdpDatagram]`** — live,
|
|
587
|
+
Linux only (`AF_PACKET`, needs `CAP_NET_RAW`); `live_capture_supported()`.
|
|
588
|
+
Elsewhere pipe `tcpdump`/`dumpcap -w -` into `read_datagrams`.
|
|
589
|
+
|
|
590
|
+
**Filters** — **`compile_filter(text) -> predicate(event)`**: `key=value`
|
|
591
|
+
clauses joined by `and`, `,` for "any of", `!=` to negate; empty matches all.
|
|
592
|
+
Keys: `op` (opcode name), `host`/`src`/`dst` (address, CIDR, `addr:port`,
|
|
593
|
+
`[v6]:port`, `:port`; mapped v4 matches v4), `port`, `file` (shell pattern,
|
|
594
|
+
requests only), `block`, `code` (ERROR code), `session`, `leg`, `direction`.
|
|
595
|
+
`FilterError` (a `ValueError`) for an unknown key or malformed value.
|
|
596
|
+
|
|
597
|
+
## Results and errors
|
|
598
|
+
|
|
599
|
+
**`TransferResult`** — `filename`, `operation` (`"read"` for RRQ, `"write"`
|
|
600
|
+
for WRQ, on both sides), `mode`, `peer`, `local`, `bytes` (payload bytes on
|
|
601
|
+
the wire; netascii-encoded size in netascii mode), `blocks`, `retransmits`,
|
|
602
|
+
`duration` (seconds), `negotiated`, `error` (or `None`); properties `ok` and
|
|
603
|
+
`throughput` (bytes/s).
|
|
604
|
+
|
|
605
|
+
**`Negotiated`** — what a transfer ran with: `blksize`, `windowsize`,
|
|
606
|
+
`timeout` (seconds), `tsize` (or `None`), `rollover`, and `options` (the OACK
|
|
607
|
+
as sent/received; empty when RFC 1350 defaults applied).
|
|
608
|
+
|
|
609
|
+
Exceptions: **`TftpError(code=0, message="")`** (base; `.code` is an
|
|
610
|
+
`ErrorCode` when the value is known, `.message` defaults to the code's
|
|
611
|
+
standard text), **`RemoteError`** (the peer sent ERROR; always raised as
|
|
612
|
+
the subclass for its code — `FileNotFound` 1, `AccessViolation` 2,
|
|
613
|
+
`DiskFull` 3, `IllegalOperation` 4, `UnknownTransferId` 5,
|
|
614
|
+
`FileAlreadyExists` 6, `NoSuchUser` 7, `OptionNegotiationError` 8 — and
|
|
615
|
+
plain `RemoteError` for 0 and unknown codes; `RemoteError.from_code(code,
|
|
616
|
+
message)` builds one), **`ProtocolError`** (the peer broke the protocol;
|
|
617
|
+
code 4, or 8 for option problems), **`TransferTimeout`** (also a
|
|
618
|
+
`TimeoutError`; retries exhausted or `max_duration` passed),
|
|
619
|
+
**`TransferAborted`** (cancelled locally: `abort()`, server shutdown).
|
|
620
|
+
`MalformedPacket` is a `ValueError`.
|
|
621
|
+
|
|
622
|
+
## Wire format
|
|
623
|
+
|
|
624
|
+
`Opcode` (`RRQ`=1 … `OACK`=6) and `ErrorCode` (`NOT_DEFINED`=0 …
|
|
625
|
+
`OPTION_REFUSED`=8) are `IntEnum`s. Packet types are `NamedTuple`s:
|
|
626
|
+
`Request(opcode, filename, mode, options, raw=b"")` (`.is_read`; option
|
|
627
|
+
names lower-cased, first occurrence wins, mode lower-cased; `raw` is the
|
|
628
|
+
datagram as received, `.raw_options` every pair as sent — original case,
|
|
629
|
+
order, duplicates — and `.encode()` returns `raw` or a fresh encoding, so a
|
|
630
|
+
relay forwards unknown options untouched), `Data(block, data)`, `Ack(block)`,
|
|
631
|
+
`Error(code, message)`, `OptionAck(options)`.
|
|
632
|
+
|
|
633
|
+
- **`decode(bytes) -> Packet`** — raises `MalformedPacket`. Tolerates a
|
|
634
|
+
missing final NUL and drops a dangling option name.
|
|
635
|
+
- **`encode_request(opcode, filename, mode="octet", options=None)`**,
|
|
636
|
+
**`encode_data(block, data)`**, **`encode_ack(block)`**,
|
|
637
|
+
**`encode_error(code, message="")`**, **`encode_oack(options)`** — option
|
|
638
|
+
values are sent as `str(value)`; NUL inside a string raises `ValueError`.
|
|
639
|
+
- Strings are UTF-8 with `surrogateescape`, so any byte sequence round-trips
|
|
640
|
+
and real UTF-8 names decode naturally.
|
|
641
|
+
- Constants: `DEFAULT_BLKSIZE` 512, `MIN_BLKSIZE` 8, `MAX_BLKSIZE` 65464,
|
|
642
|
+
`MAX_WINDOWSIZE` 65535, `STANDARD_OPTIONS`, `EXTENSION_OPTIONS`,
|
|
643
|
+
`LISTING_OPTIONS` (`x-list`, `x-mtime`), `SUPPORTED_OPTIONS` (standard and
|
|
644
|
+
extensions).
|
|
645
|
+
|
|
646
|
+
## URIs (RFC 3617)
|
|
647
|
+
|
|
648
|
+
- **`parse_url(url) -> TftpURL(host, port, filename, mode)`** — the file name
|
|
649
|
+
is the percent-decoded path after the authority's `/`; `;mode=netascii`
|
|
650
|
+
selects the mode (default `octet`); the port defaults to 69. Anything else
|
|
651
|
+
(another scheme, no host or file, an unknown parameter, `mode=mail`) raises
|
|
652
|
+
`ValueError`.
|
|
653
|
+
- **`format_url(host, filename, port=69, mode="octet")`** — the inverse
|
|
654
|
+
(`str(TftpURL)` too); `host` may be an address or interface object or a
|
|
655
|
+
`netimps.Host`; IPv6 hosts are bracketed.
|
|
656
|
+
- **`download_url(url, dest, *, progress=None, **client_options)`**,
|
|
657
|
+
**`upload_url(url, source, ...)`** — one-shot transfers by URL.
|
|
658
|
+
|
|
659
|
+
## Netascii
|
|
660
|
+
|
|
661
|
+
Local text uses LF; on the wire a line ends CR LF and a bare CR is CR NUL.
|
|
662
|
+
The translation is lossless both ways (a local `\r\n` travels as `\r\0\r\n`).
|
|
663
|
+
**`NetasciiReader(raw)`** (`readinto`, `close`) encodes a binary reader;
|
|
664
|
+
**`NetasciiWriter(raw)`** (`write`, `flush`, `close`, `abort`) decodes into a
|
|
665
|
+
binary writer, holding a trailing CR until it sees what follows. Both handle a
|
|
666
|
+
CR on any block boundary. `tftp.netascii` also has `encode`, `decode` and
|
|
667
|
+
`encoded_size(fileobj)` (seekable; position preserved).
|
|
668
|
+
|
|
669
|
+
## Transfer engine
|
|
670
|
+
|
|
671
|
+
**`Sender(send, read, negotiated, retries, now, oack=None, **kw)`** and
|
|
672
|
+
**`Receiver(send, write, negotiated, retries, now, reply=None, complete=None, **kw)`**
|
|
673
|
+
(`kw`: `backoff=2.0`, `max_timeout=None`, `expires=None` — an absolute
|
|
674
|
+
deadline in the caller's clock)
|
|
675
|
+
are one side of the DATA/ACK exchange **without any I/O**: packets leave
|
|
676
|
+
through `send(packet)`, arrive through `handle(buffer, n, now)`, and the caller
|
|
677
|
+
calls `on_timeout(now)` once `deadline` (in the caller's clock) passes, and
|
|
678
|
+
`resume(now)` when a stalled source/sink (`WouldBlock`) is ready again;
|
|
679
|
+
`abort(message)` cancels. State: `done`, `error`, `stalled`, `bytes`,
|
|
680
|
+
`blocks`, `retransmits`, `deadline`. A repeated OACK is tolerated: a
|
|
681
|
+
receiver re-sends its ACK 0, a sender ignores it (its own timeout resends
|
|
682
|
+
DATA 1). Build `read`
|
|
683
|
+
and `write` with `tftp.transfer.as_readinto(fileobj)` / `as_write(fileobj)`.
|
|
684
|
+
This is what the client and server drive; use it to run TFTP over another
|
|
685
|
+
transport or event loop.
|
|
686
|
+
|
|
687
|
+
## Command line (`cli` extra)
|
|
688
|
+
|
|
689
|
+
`pip install tftp[cli]` adds duho. The `pytftp` console script is installed
|
|
690
|
+
either way; without the extra it prints one line naming the extra and exits 1.
|
|
691
|
+
`python -m tftp` is equivalent.
|
|
692
|
+
|
|
693
|
+
```
|
|
694
|
+
pytftp get HOST REMOTE [LOCAL|-] [-p PORT] [-m octet|netascii] [-b BLKSIZE] [-w WINDOW]
|
|
695
|
+
pytftp get tftp://HOST[:PORT]/FILE [LOCAL|-] [-t TIMEOUT] [-r RETRIES] [--no-tsize] [--no-options]
|
|
696
|
+
[--compat PROFILE] [-4|-6] [--json] [--trace] [--pcap FILE]
|
|
697
|
+
pytftp put HOST LOCAL|- [REMOTE] (same options; or put tftp://HOST/FILE LOCAL|-)
|
|
698
|
+
pytftp ls HOST [DIR] | tftp://HOST/DIR (same options) [--json]
|
|
699
|
+
pytftp serve [ROOT] [--http URL | --upstream HOST[:PORT]] [-l ADDRESS | --interface NIC] [-p PORT] [-W/--write]
|
|
700
|
+
[--overwrite] [--no-create] [--compat PROFILE | --max-blksize N --max-windowsize N
|
|
701
|
+
--allow OPTION... --refuse OPTION... --fit-mtu] [--listing] [--max-sessions N]
|
|
702
|
+
[--max-per-client N] [--port-range LOW:HIGH] [--per-client] [--ignore-case]
|
|
703
|
+
[--remap REGEX=REPLACEMENT]... [--json] [--trace] [--pcap FILE]
|
|
704
|
+
pytftp relay [UPSTREAM] [--route-subnet CIDR=HOST[:PORT]]... [--route-prefix PREFIX=HOST[:PORT]]...
|
|
705
|
+
[-l ADDRESS | --interface NIC] [-p PORT] [--idle-timeout S] [--max-sessions N] [--port-range LOW:HIGH]
|
|
706
|
+
[--json] [--trace] [--pcap FILE]
|
|
707
|
+
pytftp capture FILE|- | -i IFACE [-p PORT]... [-f FILTER] [--no-packets] [--transfers]
|
|
708
|
+
[--extract DIR] [--json] [--payload]
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
- `-b 0` / `-w 0` request no `blksize` / `windowsize`; defaults are 1428 and 0.
|
|
712
|
+
`--compat PROFILE` (`strict`, `default`, `pxe`, `hpa`, `legacy`) replaces the
|
|
713
|
+
option flags with the profile's settings (client and server alike).
|
|
714
|
+
- `get` writes to the remote file's basename by default; `-` is stdout.
|
|
715
|
+
`put` from `-` (stdin) needs a remote name. A `tftp://` URL replaces HOST and
|
|
716
|
+
the remote name (and sets the mode with `;mode=netascii`).
|
|
717
|
+
- `--trace` prints every datagram on stderr; `--pcap FILE` writes them as a
|
|
718
|
+
capture (Wireshark-readable).
|
|
719
|
+
- `serve --http URL` is the HTTP gateway, `serve --upstream` the terminating
|
|
720
|
+
proxy; `--write` enables uploads for both. `--allow` adds extension options
|
|
721
|
+
to the standard four; `--refuse` removes any.
|
|
722
|
+
- `--interface NIC` (serve, relay) listens on that adapter's IPv4 address
|
|
723
|
+
(`-l ::` for its IPv6 one).
|
|
724
|
+
- `serve --listing` allows `x-list`/`x-mtime` (with `--compat` too), which
|
|
725
|
+
`pytftp ls` and `TftpPath.iterdir()` need. `--port-range` pins transfer
|
|
726
|
+
ports. Directory serving only: `--per-client` serves `ROOT/<client
|
|
727
|
+
address>/` when it exists (IPv6 `:` written `-`), else `ROOT`;
|
|
728
|
+
`--ignore-case` finds names whatever their case (the exact name wins).
|
|
729
|
+
`--remap REGEX=REPLACEMENT` (repeatable, split at the first `=`) rewrites
|
|
730
|
+
requested names with the first matching rule, for any source. These are
|
|
731
|
+
CLI conveniences, not library API.
|
|
732
|
+
- `ls` prints `type size time name` per entry (`--json`: a list of entries);
|
|
733
|
+
exit 1 for a file, a missing name, or a server without listing.
|
|
734
|
+
- `relay` needs an UPSTREAM (the default route) or routes; prefix routes are
|
|
735
|
+
tried before subnet routes. `--json` prints one line per finished transfer
|
|
736
|
+
(its `RelaySummary`).
|
|
737
|
+
- `capture` reads a pcap/pcapng file, a live pipe on stdin (`tcpdump -i eth0 -U
|
|
738
|
+
-w - udp | pytftp capture -`), or (Linux) an interface. Packets print as they
|
|
739
|
+
are decoded; `--transfers` adds a summary per transfer at the end;
|
|
740
|
+
`--extract DIR` writes each transfer's file as `<session>-<name>`
|
|
741
|
+
(`.partial` when incomplete). Ctrl-C ends a live capture and still prints
|
|
742
|
+
the summaries.
|
|
743
|
+
- Exit codes: 0 success, 1 the transfer failed or the port could not be
|
|
744
|
+
bound (error text on stderr), 2 a caller error (bad argument, missing file,
|
|
745
|
+
unreadable capture, bad filter).
|
|
746
|
+
- `--json`: one JSON object on stdout per transfer, session or packet;
|
|
747
|
+
diagnostics always go to stderr.
|
|
748
|
+
- `serve` and `relay` log each transfer at INFO on stderr (`-v`/`-q` adjust)
|
|
749
|
+
and their final counters when stopped.
|
|
750
|
+
|
|
751
|
+
## Dependencies
|
|
752
|
+
|
|
753
|
+
`netimps` (required, no dependencies of its own; imported lazily, on the
|
|
754
|
+
first transfer or server): pktinfo receive and reply sockets (`UdpEndpoint`,
|
|
755
|
+
also `arecv` for `AsyncServer`), broadcast/multicast checks, MTU payload
|
|
756
|
+
sizing, the retransmission timer (`Backoff`), socket binding,
|
|
757
|
+
host:port parsing, address/network types (`AddressLike`, `IPNetworkLike`,
|
|
758
|
+
`Host`, `Interface`: what the address-taking parameters accept), bind-error
|
|
759
|
+
hints. Importing netimps installs its additive `recvmsg`/`sendmsg`
|
|
760
|
+
socket patch on Windows unless `NETIMPS_NO_SOCKET_PATCH=1` is set first.
|
|
761
|
+
`duho` (optional, `cli` extra).
|