ftpd correctness:

* winscp mkdir failed because the folder-not-found error got repeated
* rmdir fails after all files in the folder have poofed; that's OK
* add --ftp4 as a precaution
This commit is contained in:
ed 2023-04-28 20:50:45 +00:00
parent d71416437a
commit e4759f86ef
2 changed files with 45 additions and 25 deletions

View file

@ -773,6 +773,7 @@ def add_ftp(ap):
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example \033[32m3921")
ap2.add_argument("--ftps", metavar="PORT", type=int, help="enable FTPS server on PORT, for example \033[32m3990")
ap2.add_argument("--ftpv", action="store_true", help="verbose")
ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4")
ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than SEC seconds ago)")
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, help="the NAT address to use for passive connections")
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000")

View file

@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import argparse
import errno
import logging
import os
import stat
@ -46,6 +47,12 @@ if True: # pylint: disable=using-constant-test
from typing import Any, Optional
class FSE(FilesystemError):
def __init__(self, msg: str, severity: int = 0) -> None:
super(FilesystemError, self).__init__(msg)
self.severity = severity
class FtpAuth(DummyAuthorizer):
def __init__(self, hub: "SvcHub") -> None:
super(FtpAuth, self).__init__()
@ -128,10 +135,6 @@ class FtpFs(AbstractedFS):
self.listdirinfo = self.listdir
self.chdir(".")
def die(self, msg):
self.h.die(msg)
raise Exception()
def v2a(
self,
vpath: str,
@ -145,17 +148,17 @@ class FtpFs(AbstractedFS):
rd, fn = os.path.split(vpath)
if ANYWIN and relchk(rd):
logging.warning("malicious vpath: %s", vpath)
self.die("Unsupported characters in filepath")
raise FSE("Unsupported characters in filepath", 1)
fn = sanitize_fn(fn or "", "", [".prologue.html", ".epilogue.html"])
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if not vfs.realpath:
self.die("No filesystem mounted at this path")
raise FSE("No filesystem mounted at this path", 1)
return os.path.join(vfs.realpath, rem), vfs, rem
except Pebkac as ex:
self.die(str(ex))
raise FSE(str(ex))
def rv2a(
self,
@ -178,7 +181,7 @@ class FtpFs(AbstractedFS):
def validpath(self, path: str) -> bool:
if "/.hist/" in path:
if "/up2k." in path or path.endswith("/dir.txt"):
self.die("Access to this file is forbidden")
raise FSE("Access to this file is forbidden", 1)
return True
@ -195,7 +198,7 @@ class FtpFs(AbstractedFS):
td = 0
if td < -1 or td > self.args.ftp_wt:
self.die("Cannot open existing file for writing")
raise FSE("Cannot open existing file for writing")
self.validpath(ap)
return open(fsenc(ap), mode)
@ -206,7 +209,7 @@ class FtpFs(AbstractedFS):
ap = vfs.canonical(rem)
if not bos.path.isdir(ap):
# returning 550 is library-default and suitable
self.die("Failed to change directory")
raise FSE("No such file or directory")
self.cwd = nwd
(
@ -241,7 +244,11 @@ class FtpFs(AbstractedFS):
vfs_ls.sort()
return vfs_ls
except:
except Exception as ex:
# panic on malicious names
if getattr(ex, "severity", 0):
raise
if vpath:
# display write-only folders as empty
return []
@ -252,31 +259,35 @@ class FtpFs(AbstractedFS):
def rmdir(self, path: str) -> None:
ap = self.rv2a(path, d=True)[0]
bos.rmdir(ap)
try:
bos.rmdir(ap)
except OSError as e:
if e.errno != errno.ENOENT:
raise
def remove(self, path: str) -> None:
if self.args.no_del:
self.die("The delete feature is disabled in server config")
raise FSE("The delete feature is disabled in server config")
vp = join(self.cwd, path).lstrip("/")
try:
self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [])
except Exception as ex:
self.die(str(ex))
raise FSE(str(ex))
def rename(self, src: str, dst: str) -> None:
if not self.can_move:
self.die("Not allowed for user " + self.h.uname)
raise FSE("Not allowed for user " + self.h.uname)
if self.args.no_mv:
self.die("The rename/move feature is disabled in server config")
raise FSE("The rename/move feature is disabled in server config")
svp = join(self.cwd, src).lstrip("/")
dvp = join(self.cwd, dst).lstrip("/")
try:
self.hub.up2k.handle_mv(self.uname, svp, dvp)
except Exception as ex:
self.die(str(ex))
raise FSE(str(ex))
def chmod(self, path: str, mode: str) -> None:
pass
@ -285,7 +296,10 @@ class FtpFs(AbstractedFS):
try:
ap = self.rv2a(path, r=True)[0]
return bos.stat(ap)
except:
except FSE as ex:
if ex.severity:
raise
ap = self.rv2a(path)[0]
st = bos.stat(ap)
if not stat.S_ISDIR(st.st_mode):
@ -305,7 +319,10 @@ class FtpFs(AbstractedFS):
try:
st = self.stat(path)
return stat.S_ISREG(st.st_mode)
except:
except Exception as ex:
if getattr(ex, "severity", 0):
raise
return False # expected for mojibake in ftp_SIZE()
def islink(self, path: str) -> bool:
@ -316,7 +333,10 @@ class FtpFs(AbstractedFS):
try:
st = self.stat(path)
return stat.S_ISDIR(st.st_mode)
except:
except Exception as ex:
if getattr(ex, "severity", 0):
raise
return True
def getsize(self, path: str) -> int:
@ -366,10 +386,6 @@ class FtpHandler(FTPHandler):
# reduce non-debug logging
self.log_cmds_list = [x for x in self.log_cmds_list if x not in ("CWD", "XCWD")]
def die(self, msg):
self.respond("550 {}".format(msg))
raise FilesystemError(msg)
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str]
vp = join(self.fs.cwd, file).lstrip("/")
@ -389,7 +405,7 @@ class FtpHandler(FTPHandler):
0,
"",
):
self.die("Upload blocked by xbu server config")
raise FSE("Upload blocked by xbu server config")
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
ret = FTPHandler.ftp_STOR(self, file, mode)
@ -489,6 +505,9 @@ class Ftpd(object):
if "::" in ips:
ips.append("0.0.0.0")
if self.args.ftp4:
ips = [x for x in ips if ":" not in x]
ioloop = IOLoop()
for ip in ips:
for h, lp in hs: