Files
DECNET/decnet/templates/tftp/server.py
anti 6708f26e6b fix(packaging): move templates/ into decnet/ package so they ship with pip install
The docker build contexts and syslog_bridge.py lived at repo root, which
meant setuptools (include = ["decnet*"]) never shipped them. Agents
installed via `pip install $RELEASE_DIR` got site-packages/decnet/** but no
templates/, so every deploy blew up in deployer._sync_logging_helper with
FileNotFoundError on templates/syslog_bridge.py.

Move templates/ -> decnet/templates/ and declare it as setuptools
package-data. Path resolutions in services/*.py and engine/deployer.py drop
one .parent since templates now lives beside the code. Test fixtures,
bandit exclude path, and coverage omit glob updated to match.
2026-04-19 19:30:04 -04:00

81 lines
2.3 KiB
Python

#!/usr/bin/env python3
"""
TFTP server (UDP 69).
Parses RRQ (read) and WRQ (write) requests, logs filename and transfer mode,
then responds with an error packet. Logs all requests as JSON.
"""
import asyncio
import os
import struct
from syslog_bridge import syslog_line, write_syslog_file, forward_syslog
NODE_NAME = os.environ.get("NODE_NAME", "tftpserver")
SERVICE_NAME = "tftp"
LOG_TARGET = os.environ.get("LOG_TARGET", "")
# TFTP opcodes
_RRQ = 1
_WRQ = 2
_ERROR = 5
# TFTP Error packet: opcode(2) + error_code(2) + error_msg + NUL
def _error_pkt(code: int, msg: str) -> bytes:
return struct.pack(">HH", _ERROR, code) + msg.encode() + b"\x00"
def _log(event_type: str, severity: int = 6, **kwargs) -> None:
line = syslog_line(SERVICE_NAME, NODE_NAME, event_type, severity, **kwargs)
write_syslog_file(line)
forward_syslog(line, LOG_TARGET)
class TFTPProtocol(asyncio.DatagramProtocol):
def __init__(self):
self._transport = None
def connection_made(self, transport):
self._transport = transport
def datagram_received(self, data: bytes, addr):
if len(data) < 4:
return
opcode = struct.unpack(">H", data[:2])[0]
if opcode in (_RRQ, _WRQ):
# Filename and mode are NUL-terminated strings after the opcode
parts = data[2:].split(b"\x00")
filename = parts[0].decode(errors="replace") if parts else ""
mode = parts[1].decode(errors="replace") if len(parts) > 1 else ""
_log(
"request",
src=addr[0],
src_port=addr[1],
op="RRQ" if opcode == _RRQ else "WRQ",
filename=filename,
mode=mode,
)
self._transport.sendto(_error_pkt(2, "Access violation"), addr)
else:
_log("unknown_opcode", src=addr[0], opcode=opcode, data=data[:32].hex())
def error_received(self, exc):
pass
async def main():
_log("startup", msg=f"TFTP server starting as {NODE_NAME}")
loop = asyncio.get_running_loop()
transport, _ = await loop.create_datagram_endpoint(
TFTPProtocol, local_addr=("0.0.0.0", 69) # nosec B104
)
try:
await asyncio.sleep(float("inf"))
finally:
transport.close()
if __name__ == "__main__":
asyncio.run(main())