mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 00:52:16 -06:00
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:
parent
d71416437a
commit
e4759f86ef
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue