From 6ce0e2cd5be65f1c6c3a9ec9e04c22d930c269a5 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 13 Feb 2022 15:46:33 +0100 Subject: [PATCH] ftpd: add ftps --- README.md | 8 +++--- copyparty/__main__.py | 3 ++- copyparty/ftpd.py | 61 +++++++++++++++++++++++++++++++++++-------- copyparty/svchub.py | 4 +-- setup.py | 1 + 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ea8efae9..3179d232 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down * [other tricks](#other-tricks) * [searching](#searching) - search by size, date, path/name, mp3-tags, ... * [server config](#server-config) - using arguments or config files, or a mix of both + * [ftp-server](#ftp-server) - an FTP server can be started using `--ftp 3921` * [file indexing](#file-indexing) * [upload rules](#upload-rules) - set upload rules using volume flags * [compress uploads](#compress-uploads) - files can be autocompressed on upload @@ -626,12 +627,13 @@ using arguments or config files, or a mix of both: ## ftp-server -an FTP server can be started using `--ftp 3921` (or any other port) +an FTP server can be started using `--ftp 3921`, and/or `--ftps` for explicit TLS (ftpes) * based on [pyftpdlib](https://github.com/giampaolo/pyftpdlib) * needs a dedicated port (cannot share with the HTTP/HTTPS API) -* runs in active mode by default, you probably want `--ftp-r` -* uploads are not resumable +* uploads are not resumable -- delete and restart if necessary +* runs in active mode by default, you probably want `--ftp-pr 12000-13000` + * if you enable both `ftp` and `ftps`, the port-range will be divided in half ## file indexing diff --git a/copyparty/__main__.py b/copyparty/__main__.py index fd8af7f1..8e317481 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -447,9 +447,10 @@ def run_argparse(argv, formatter): ap2 = ap.add_argument_group('FTP options') ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example 3921") + ap2.add_argument("--ftps", metavar="PORT", type=int, help="enable FTPS server on PORT, for example 3990") ap2.add_argument("--ftp-dbg", action="store_true", help="enable debug logging") ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, help="the NAT address to use for passive connections") - ap2.add_argument("--ftp-r", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example 12000-13000") + ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example 12000-13000") ap2 = ap.add_argument_group('opt-outs') ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)") diff --git a/copyparty/ftpd.py b/copyparty/ftpd.py index 404366d5..8b5b654e 100644 --- a/copyparty/ftpd.py +++ b/copyparty/ftpd.py @@ -2,6 +2,7 @@ from __future__ import print_function, unicode_literals import os +import sys import stat import time import logging @@ -14,6 +15,7 @@ from pyftpdlib.servers import FTPServer from pyftpdlib.ioloop import IOLoop from pyftpdlib.log import config_logging +from .__init__ import E from .util import Pebkac, fsenc, exclude_dotfiles from .bos import bos from .authsrv import AuthSrv @@ -275,30 +277,67 @@ class FtpHandler(FTPHandler): ) +try: + from pyftpdlib.handlers import TLS_FTPHandler + + class SftpHandler(FtpHandler, TLS_FTPHandler): + pass + +except: + pass + + class Ftpd(object): def __init__(self, hub): self.hub = hub self.args = hub.args - h = FtpHandler - h.hub = hub - h.args = hub.args - h.authorizer = FtpAuth() - h.authorizer.hub = hub + hs = [] + if self.args.ftp: + hs.append([FtpHandler, self.args.ftp]) + if self.args.ftps: + try: + h = SftpHandler + except: + m = "\nftps requires pyopenssl;\nplease run the following:\n\n {} -m pip install --user pyopenssl\n" + print(m.format(sys.executable)) + sys.exit(1) - if self.args.ftp_r: - p1, p2 = [int(x) for x in self.args.ftp_r.split("-")] - h.passive_ports = list(range(p1, p2 + 1)) + h.certfile = os.path.join(E.cfg, "cert.pem") + h.tls_control_required = True + h.tls_data_required = True - if self.args.ftp_nat: - h.masquerade_address = self.args.ftp_nat + hs.append([h, self.args.ftps]) + + for h in hs: + h, lp = h + h.hub = hub + h.args = hub.args + h.authorizer = FtpAuth() + h.authorizer.hub = hub + + if self.args.ftp_pr: + p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")] + if self.args.ftp and self.args.ftps: + # divide port range in half + d = int((p2 - p1) / 2) + if lp == self.args.ftp: + p2 = p1 + d + else: + p1 += d + 1 + + h.passive_ports = list(range(p1, p2 + 1)) + + if self.args.ftp_nat: + h.masquerade_address = self.args.ftp_nat if self.args.ftp_dbg: config_logging(level=logging.DEBUG) ioloop = IOLoop() for ip in self.args.i: - FTPServer((ip, int(self.args.ftp)), h, ioloop) + for h, lp in hs: + FTPServer((ip, int(lp)), h, ioloop) t = threading.Thread(target=ioloop.loop) t.daemon = True diff --git a/copyparty/svchub.py b/copyparty/svchub.py index cf9956bc..366c5990 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -105,9 +105,9 @@ class SvcHub(object): args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage) - if args.ftp: + if args.ftp or args.ftps: from .ftpd import Ftpd - + self.ftpd = Ftpd(self) # decide which worker impl to use diff --git a/setup.py b/setup.py index bbc60abc..37b828b9 100755 --- a/setup.py +++ b/setup.py @@ -116,6 +116,7 @@ args = { "thumbnails": ["Pillow"], "audiotags": ["mutagen"], "ftpd": ["pyftpdlib"], + "ftps": ["pyopenssl"], }, "entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]}, "scripts": ["bin/copyparty-fuse.py", "bin/up2k.py"],