Skip to content

Commit

Permalink
tftp: support ipv6 and utf-8 filenames + ...
Browse files Browse the repository at this point in the history
* fix winexe
* missing newline after dirlist
* optimizations
  • Loading branch information
9001 committed Feb 17, 2024
1 parent 39cc92d commit 0504b01
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 19 deletions.
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -954,17 +954,24 @@ a TFTP server (read/write) can be started using `--tftp 3969` (you probably wan
* based on [partftpy](https://github.com/9001/partftpy)
* no accounts; read from world-readable folders, write to world-writable, overwrite in world-deletable
* needs a dedicated port (cannot share with the HTTP/HTTPS API)
* run as root to use the spec-recommended port `69` (nice)
* run as root (or see below) to use the spec-recommended port `69` (nice)
* can reply from a predefined portrange (good for firewalls)
* only supports the binary/octet/image transfer mode (no netascii)
* [RFC 7440](https://datatracker.ietf.org/doc/html/rfc7440) is **not** supported, so will be extremely slow over WAN
* expect 1100 KiB/s over 1000BASE-T, 400-500 KiB/s over wifi, 200 on bad wifi
* assuming default blksize (512), expect 1100 KiB/s over 100BASE-T, 400-500 KiB/s over wifi, 200 on bad wifi
most clients expect to find TFTP on port 69, but on linux and macos you need to be root to listen on that. Alternatively, listen on 3969 and use NAT on the server to forward 69 to that port;
* on linux: `iptables -t nat -A PREROUTING -i eth0 -p udp --dport 69 -j REDIRECT --to-port 3969`
some recommended TFTP clients:
* curl (cross-platform, read/write)
* get: `curl --tftp-blksize 1428 tftp://127.0.0.1:3969/firmware.bin`
* put: `curl --tftp-blksize 1428 -T firmware.bin tftp://127.0.0.1:3969/`
* windows: `tftp.exe` (you probably already have it)
* `tftp -i 127.0.0.1 put firmware.bin`
* linux: `tftp-hpa`, `atftp`
* `tftp 127.0.0.1 3969 -v -m binary -c put firmware.bin`
* `curl tftp://127.0.0.1:3969/firmware.bin` (read-only)
* `atftp --option "blksize 1428" 127.0.0.1 3969 -p -l firmware.bin -r firmware.bin`
* `tftp -v -m binary 127.0.0.1 3969 -c put firmware.bin`
## smb server
Expand Down Expand Up @@ -997,7 +1004,7 @@ known client bugs:
* however smb1 is buggy and is not enabled by default on win10 onwards
* windows cannot access folders which contain filenames with invalid unicode or forbidden characters (`<>:"/\|?*`), or names ending with `.`
the smb protocol listens on TCP port 445, which is a privileged port on linux and macos, which would require running copyparty as root. However, this can be avoided by listening on another port using `--smb-port 3945` and then using NAT to forward the traffic from 445 to there;
the smb protocol listens on TCP port 445, which is a privileged port on linux and macos, which would require running copyparty as root. However, this can be avoided by listening on another port using `--smb-port 3945` and then using NAT on the server to forward the traffic from 445 to there;
* on linux: `iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 445 -j REDIRECT --to-port 3945`
authenticate with one of the following:
Expand Down
1 change: 1 addition & 0 deletions copyparty/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,7 @@ def add_tftp(ap):
ap2.add_argument("--tftp", metavar="PORT", type=int, help="enable TFTP server on \033[33mPORT\033[0m, for example \033[32m69 \033[0mor \033[32m3969")
ap2.add_argument("--tftpv", action="store_true", help="verbose")
ap2.add_argument("--tftpvv", action="store_true", help="verboser")
ap2.add_argument("--tftp-no-fast", action="store_true", help="debug: disable optimizations")
ap2.add_argument("--tftp-lsf", metavar="PTN", type=u, default="\\.?(dir|ls)(\\.txt)?", help="return a directory listing if a file with this name is requested and it does not exist; defaults matches .ls, dir, .dir.txt, ls.txt, ...")
ap2.add_argument("--tftp-nols", action="store_true", help="if someone tries to download a directory, return an error instead of showing its directory listing")
ap2.add_argument("--tftp-ipa", metavar="PFX", type=u, default="", help="only accept connections from IP-addresses starting with \033[33mPFX\033[0m; specify [\033[32many\033[0m] to disable inheriting \033[33m--ipa\033[0m. Example: [\033[32m127., 10.89., 192.168.\033[0m]")
Expand Down
100 changes: 91 additions & 9 deletions copyparty/tftpd.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,33 @@ def __init__(self, **attr):
self.__dict__.update(attr)


import inspect
import logging
import os
import re
import socket
import stat
import threading
import time
from datetime import datetime

from partftpy import TftpContexts, TftpServer, TftpStates
try:
import inspect
except:
pass

from partftpy import (
TftpContexts,
TftpPacketFactory,
TftpPacketTypes,
TftpServer,
TftpStates,
)
from partftpy.TftpShared import TftpException

from .__init__ import PY2, TYPE_CHECKING
from .__init__ import EXE, TYPE_CHECKING
from .authsrv import VFS
from .bos import bos
from .util import BytesIO, Daemon, exclude_dotfiles, runhook, undot
from .util import BytesIO, Daemon, exclude_dotfiles, min_ex, runhook, undot

if True: # pylint: disable=using-constant-test
from typing import Any, Union
Expand All @@ -35,6 +49,10 @@ def __init__(self, **attr):
debug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error)


def noop(*a, **ka) -> None:
pass


def _serverInitial(self, pkt: Any, raddress: str, rport: int) -> bool:
info("connection from %s:%s", raddress, rport)
ret = _orig_serverInitial(self, pkt, raddress, rport)
Expand All @@ -56,6 +74,7 @@ def __init__(self, hub: "SvcHub") -> None:
self.args = hub.args
self.asrv = hub.asrv
self.log = hub.log
self.mutex = threading.Lock()

_hub[:] = []
_hub.append(hub)
Expand All @@ -65,6 +84,38 @@ def __init__(self, hub: "SvcHub") -> None:
lgr = logging.getLogger(x)
lgr.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO)

if not self.args.tftpv and not self.args.tftpvv:
# contexts -> states -> packettypes -> shared
# contexts -> packetfactory
# packetfactory -> packettypes
Cs = [
TftpPacketTypes,
TftpPacketFactory,
TftpStates,
TftpContexts,
TftpServer,
]
cbak = []
if not self.args.tftp_no_fast and not EXE:
try:
import inspect

ptn = re.compile(r"(^\s*)log\.debug\(.*\)$")
for C in Cs:
cbak.append(C.__dict__)
src1 = inspect.getsource(C).split("\n")
src2 = "\n".join([ptn.sub("\\1pass", ln) for ln in src1])
cfn = C.__spec__.origin
exec (compile(src2, filename=cfn, mode="exec"), C.__dict__)
except Exception:
t = "failed to optimize tftp code; run with --tftp-noopt if there are issues:\n"
self.log("tftp", t + min_ex(), 3)
for n, zd in enumerate(cbak):
Cs[n].__dict__ = zd

for C in Cs:
C.log.debug = noop

# patch vfs into partftpy
TftpContexts.open = self._open
TftpStates.open = self._open
Expand Down Expand Up @@ -102,21 +153,52 @@ def __init__(self, hub: "SvcHub") -> None:
self.log("tftp", "IPv6 not supported for tftp; listening on 0.0.0.0", 3)
ip = "0.0.0.0"

self.ip = ip
self.port = int(self.args.tftp)
self.srv = TftpServer.TftpServer("/", self._ls)
self.stop = self.srv.stop
self.srv = []
self.ips = []

ports = []
if self.args.tftp_pr:
p1, p2 = [int(x) for x in self.args.tftp_pr.split("-")]
ports = list(range(p1, p2 + 1))

Daemon(self.srv.listen, "tftp", [self.ip, self.port], ka={"ports": ports})
ips = self.args.i
if "::" in ips:
ips.append("0.0.0.0")

if self.args.ftp4:
ips = [x for x in ips if ":" not in x]

for ip in ips:
name = "tftp_%s" % (ip,)
Daemon(self._start, name, [ip, ports])
time.sleep(0.2) # give dualstack a chance

def nlog(self, msg: str, c: Union[int, str] = 0) -> None:
self.log("tftp", msg, c)

def _start(self, ip, ports):
fam = socket.AF_INET6 if ":" in ip else socket.AF_INET
srv = TftpServer.TftpServer("/", self._ls)
with self.mutex:
self.srv.append(srv)
self.ips.append(ip)
try:
srv.listen(ip, self.port, af_family=fam, ports=ports)
except OSError:
with self.mutex:
self.srv.remove(srv)
self.ips.remove(ip)
if ip != "0.0.0.0" or "::" not in self.ips:
raise

def stop(self):
with self.mutex:
srvs = self.srv[:]

for srv in srvs:
srv.stop()

def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]:
vpath = vpath.replace("\\", "/").lstrip("/")
if not perms:
Expand Down Expand Up @@ -190,7 +272,7 @@ def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any:
retl = ["# permissions: %s" % (", ".join(perms),)]
retl += [fmt.format(*x) for x in ls]
ret = "\n".join(retl).encode("utf-8", "replace")
return BytesIO(ret)
return BytesIO(ret + b"\n")

def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any:
rd = wr = False
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ thumbnails2 = ["pyvips"]
audiotags = ["mutagen"]
ftpd = ["pyftpdlib"]
ftps = ["pyftpdlib", "pyopenssl"]
tftpd = ["partftpy>=0.2.0"]
tftpd = ["partftpy>=0.3.0"]
pwhash = ["argon2-cffi"]

[project.scripts]
Expand Down
4 changes: 2 additions & 2 deletions scripts/make-sfx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,9 @@ necho() {
mv pyftpdlib ftp/

necho collecting partftpy
f="../build/partftpy-0.2.0.tar.gz"
f="../build/partftpy-0.3.0.tar.gz"
[ -e "$f" ] ||
(url=https://files.pythonhosted.org/packages/64/4a/360dde1e7277758a4ccb0d6434ec661042d9d745aa6c3baa9ec0699df3e9/partftpy-0.2.0.tar.gz;
(url=https://files.pythonhosted.org/packages/06/ce/531978c831c47f79bc72d5bbb3f12757daf1602d1fffad012305f2d270f6/partftpy-0.3.0.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)

tar -zxf $f
Expand Down
2 changes: 1 addition & 1 deletion scripts/pyinstaller/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ rm -rf $TEMP/pe-copyparty*
python copyparty-sfx.py --version

rm -rf mods; mkdir mods
cp -pR $TEMP/pe-copyparty/copyparty/ $TEMP/pe-copyparty/{ftp,j2}/* mods/
cp -pR $TEMP/pe-copyparty/{copyparty,partftpy}/ $TEMP/pe-copyparty/{ftp,j2}/* mods/
[ $w10 ] && rm -rf mods/{jinja2,markupsafe}

af() { awk "$1" <$2 >tf; mv tf "$2"; }
Expand Down
36 changes: 36 additions & 0 deletions scripts/test/tftp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/bash
set -ex

# PYTHONPATH=.:~/dev/partftpy/ taskset -c 0 python3 -m copyparty -v srv::r -v srv/junk:junk:A --tftp 3969

get_src=~/dev/copyparty/srv/palette.flac
get_fn=${get_src##*/}

put_src=~/Downloads/102.zip
put_dst=~/dev/copyparty/srv/junk/102.zip

cd /dev/shm

echo curl get 1428 v4; curl --tftp-blksize 1428 tftp://127.0.0.1:3969/$get_fn | cmp $get_src || exit 1
echo curl get 1428 v6; curl --tftp-blksize 1428 tftp://[::1]:3969/$get_fn | cmp $get_src || exit 1

echo curl put 1428 v4; rm -f $put_dst && curl --tftp-blksize 1428 -T $put_src tftp://127.0.0.1:3969/junk/ && cmp $put_src $put_dst || exit 1
echo curl put 1428 v6; rm -f $put_dst && curl --tftp-blksize 1428 -T $put_src tftp://[::1]:3969/junk/ && cmp $put_src $put_dst || exit 1

echo atftp get 1428; rm -f $get_fn && ~/src/atftp/atftp --option "blksize 1428" -g -r $get_fn 127.0.0.1 3969 && cmp $get_fn $get_src || exit 1

echo atftp put 1428; rm -f $put_dst && ~/src/atftp/atftp --option "blksize 1428" 127.0.0.1 3969 -p -l $put_src -r junk/102.zip && cmp $put_src $put_dst || exit 1

echo tftp-hpa get; rm -f $put_dst && tftp -v -m binary 127.0.0.1 3969 -c get $get_fn && cmp $get_src $get_fn || exit 1

echo tftp-hpa put; rm -f $put_dst && tftp -v -m binary 127.0.0.1 3969 -c put $put_src junk/102.zip && cmp $put_src $put_dst || exit 1

echo curl get 512; curl tftp://127.0.0.1:3969/$get_fn | cmp $get_src || exit 1

echo curl put 512; rm -f $put_dst && curl -T $put_src tftp://127.0.0.1:3969/junk/ && cmp $put_src $put_dst || exit 1

echo atftp get 512; rm -f $get_fn && ~/src/atftp/atftp -g -r $get_fn 127.0.0.1 3969 && cmp $get_fn $get_src || exit 1

echo atftp put 512; rm -f $put_dst && ~/src/atftp/atftp 127.0.0.1 3969 -p -l $put_src -r junk/102.zip && cmp $put_src $put_dst || exit 1

echo nice
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def run(self):
"audiotags": ["mutagen"],
"ftpd": ["pyftpdlib"],
"ftps": ["pyftpdlib", "pyopenssl"],
"tftpd": ["partftpy>=0.2.0"],
"tftpd": ["partftpy>=0.3.0"],
"pwhash": ["argon2-cffi"],
},
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
Expand Down

0 comments on commit 0504b01

Please sign in to comment.