Модуль pyftpdlib
реализует серверную часть протокола FTP, как определено в RFC-959. По своей природе pyftpdlib
является асинхронным. Это означает, что он использует один процесс/поток для обработки нескольких клиентских подключений и передачи файлов. Вот почему он такой быстрый, легкий и масштабируемый.
Кто ищет реализацию клиентской части протокола FTP на Python, то смотрите документацию по модулю стандартной библиотеке
ftplib
.
НО! У асинхронной модели есть один большой недостаток: не должно быть блокировок на длительный период времени, иначе зависнет весь FTP-сервер. Таким образом асинхронная модель может не подходить для медленной файловой системы (например, сетевая файловая система samba
).
Изменить асинхронную модель легко. Для этого нужно вместо класса FTPServer
использовать классы ThreadedFTPServer
или MultiprocessFTPServer
. На практике это означает, что можно не бояться длительных блокирующих операции, а следовательно использовать FTP-сервер на медленных файловых системах.
pyftpdlib
в виртуальное окружение;pyftpdlib
в виртуальное окружение.Так как модуль pyftpdlib
не входит в стандартную библиотеку Python, его необходимо установить отдельно. Сделать это можно с помощью менеджера пакетов pip
.
# создаем виртуальное окружение, если нет $ python3 -m venv .venv --prompt VirtualEnv # активируем виртуальное окружение $ source .venv/bin/activate # обновляем `pip` (VirtualEnv):~$ python3 -m pip install -U pip # ставим модуль `pyftpdlib` (VirtualEnv):~$ python3 -m pip install pyftpdlib -U
Модуль pyftpdlib
можно запускать как простой автономный сервер с помощью опции python3 -m
, что особенно полезно, когда необходимо быстро поделиться каталогом.
Анонимный FTPd
, использующий текущий каталог:
$ python3 -m pyftpdlib
Анонимный FTPd
с разрешением на запись:
$ python3 -m pyftpdlib -w
Установка другого адреса/порта и домашнего каталога:
$ python3 -m pyftpdlib -i localhost -p 8021 -d /home/someone
Можно использовать следующие опции:
-i ADDRESS, --interface=ADDRESS
: указать интерфейс для запуска (по умолчанию все интерфейсы);-p PORT, --port=PORT
: укажите номер порта для запуска (по умолчанию 2121);-w, --write
: предоставляет доступ на запись для вошедшего в систему пользователя (по умолчанию только для чтения);-d FOLDER, --directory=FOLDER
: каталог для общего доступа (текущий каталог по умолчанию);-n ADDRESS, --nat-address=ADDRESS
: адрес NAT для пассивных подключений;-r FROM-TO, --range=FROM-TO
: диапазон TCP-портов, используемых для пассивных подключений (например, -r 8000-9000
);-D, --debug
: уровень ведения журнала DEBUG
;-V, --verbose
: активировать более подробное ведение журнала;-u USERNAME, --username=USERNAME
: имя пользователя для входа в систему (анонимный вход будет отключен, и потребуется пароль, если он указан);-P PASSWORD, --password=PASSWORD
: пароль для входа в систему (имя пользователя должно быть полезным).В приведенном ниже сценарии используется базовая конфигурация, и это, вероятно, лучшая отправная точка для понимания того, как все работает. FTP-сервер использует базовый DummyAuthorizer
для добавления группы "виртуальных" пользователей и устанавливает лимит на входящие соединения и диапазон пассивных портов.
import os from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer def main(): # экземпляр фиктивного средства авторизации # для управления "виртуальными" пользователями authorizer = DummyAuthorizer() # добавляем нового пользователя, имеющего полные права доступа `r/w` # и анонимного пользователя, для которого FS доступна только для чтения authorizer.add_user('user', '12345', '/home/user/some-dir', perm='elradfmwMT') authorizer.add_anonymous(os.getcwd()) # экземпляр класса обработчика FTP handler = FTPHandler handler.authorizer = authorizer # настраиваемый баннер (строка, возвращаемая при подключении клиента) handler.banner = "pyftpdlib основанный на ftpd." # masquerade-адрес и диапазон портов, которые будут использоваться # для пассивных подключений. Строки ниже нужно раскомментировать # если вы находитесь за NAT (masquerade_address укажите свой). # handler.masquerade_address = '151.25.42.11' # handler.passive_ports = range(60000, 65535) # экземпляр класса FTP-сервера, который слушает `0.0.0.0:2121` address = ('', 2121) server = FTPServer(address, handler) # лимиты на соединения server.max_cons = 256 server.max_cons_per_ip = 5 server.serve_forever() if __name__ == '__main__': main()
Модуль pyftpdlib
использует модуль logging для ведения журнала логов. Если не настроить логирование, то журнал будет выводится в stderr
. Настроить ведение журнала необходимо до вызова server.serve_forever()
.
Пример записи логов в файл:
import logging from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.', perm='elradfmwMT') handler = FTPHandler handler.authorizer = authorizer # запись логов в файл '/var/log/pyftpd.log' logging.basicConfig(filename='/var/log/pyftpd.log', level=logging.INFO) server = FTPServer(('', 2121), handler) server.serve_forever()
Режим ведение журнала logging.DEBUG
будет выводить все команды и ответы, которыми обмениваются клиент и FTP-сервера. Режим DEBUG
также регистрирует внутренние ошибки, которые могут возникнуть при вызовах, связанных с сокетами, таких как send()
и recv()
.
Для включения режима DEBUG
из кода, используйте:
logging.basicConfig(level=logging.DEBUG)
Для включения режима DEBUG
из командной строки, используйте:
$ python -m pyftpdlib -D
handler = FTPHandler handler.log_prefix = 'XXX [%(username)s]@%(remote_ip)s' server = FTPServer(('localhost', 2121), handler) server.serve_forever()
Использование FTP-сервера по умолчанию с DummyAuthorizer
означает, что пароли будут храниться в открытом виде, а хранение паролей в открытом виде, конечно, нежелательно. Самый простой способ избежать подобного сценария - сначала создать новых пользователей, а затем сохранить их имена пользователей и пароли в виде хэш-дайджестов в файле или там, где это удобно. В приведенном ниже примере показано, как хранить пароли в виде односторонних хешей с использованием алгоритма SHA1 (hashlib.sha1
).
import os import sys from hashlib import sha1 from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer, AuthenticationFailed class DummySHA1Authorizer(DummyAuthorizer): def validate_authentication(self, username, password, handler): hash = sha1(password.encode('utf-8')).hexdigest() try: if self.user_table[username]['pwd'] != hash: raise KeyError except KeyError: raise AuthenticationFailed def main(): # хеш-дайджест из текста пароля hash = sha1('12345'.encode('utf-8')).hexdigest() authorizer = DummySHA1Authorizer() authorizer.add_user('user', hash, os.getcwd(), perm='elradfmwMT') authorizer.add_anonymous(os.getcwd()) handler = FTPHandler handler.authorizer = authorizer server = FTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main()
Важной особенностью FTP-сервера является ограничение скорости загрузки и выгрузки, влияющее на канал передачи данных. Для установки таких ограничений можно использовать класс ThrottledDTPHandler
. Основная идея заключается в том, чтобы обернуть отправку и получение в счетчик данных, а когда трафик превышает в среднем X
Кбит/с, то pyftpdlib
будет временно блокировать передачу на определенное количество секунд.
import os from pyftpdlib.handlers import FTPHandler, ThrottledDTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmwMT') authorizer.add_anonymous(os.getcwd()) dtp_handler = ThrottledDTPHandler dtp_handler.read_limit = 30720 # 30 Kb/sec (30 * 1024) dtp_handler.write_limit = 30720 # 30 Kb/sec (30 * 1024) ftp_handler = FTPHandler ftp_handler.authorizer = authorizer # обработчик ftp использует альтернативный класс обработчика dtp ftp_handler.dtp_handler = dtp_handler server = FTPServer(('', 2121), ftp_handler) server.serve_forever() if __name__ == '__main__': main()
Модуль pyftpdlib
включает полную поддержку FTPS, реализующую протоколы TLS и SSL, а также команды AUTH
, PBSZ
и PROT
, как определено в RFC-4217. Это поведение реализовано с помощью модуля PyOpenSSL
, необходимого для запуска приведенного ниже кода. Для класса TLS_FTPHandler
требуется указать как минимум файл сертификата и, возможно, ключевой файл.
""" Асинхронный FTPS-сервер, поддерживающий SSL и TLS. Требуется модуль PyOpenSSL (http://pypi.python.org/pypi/pyOpenSSL). """ from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import TLS_FTPHandler def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.', perm='elradfmwMT') authorizer.add_anonymous('.') handler = TLS_FTPHandler handler.certfile = 'cert.pem' handler.authorizer = authorizer # requires SSL for both control and data channel #handler.tls_control_required = True #handler.tls_data_required = True server = FTPServer(('', 21), handler) server.serve_forever() if __name__ == '__main__': main()
Создать самоподписанный SSL-сертификат с помощью OpenSSL можно командой:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365
Если не надо защищать закрытый ключ парольной фразой, то можно добавить опцию -nodes
(сокращение от "no DES"). В противном случае будет предложено ввести пароль не менее 4 символов.
Опцию -days 365
(дата истечения срока действия) можно заменить любым числом. Затем будет предложено ввести такие сведения, как "Название страны" и т.д. - это можно игнорировать просто нажав "Enter", тем самым принять значения по умолчанию.
Чтобы исключить вопросы о содержимом сертификата, можно добавить опцию -subj '/CN=localhost'
(замените localhost
на желаемый домен).
Самоподписанные сертификат не проверяются какой-либо третьей стороной, если предварительно не импортировать их в браузер. Если нужна дополнительная безопасность, то необходимо использовать сертификат, подписанный центром сертификации.
FTP-сервер с авторизацией пользователей Linux.
На Unix системах можно настроить свой FTP-сервер с поддержкой реальных пользователей, существующих в системе, и перемещаться по реальной файловой системе. В приведенном ниже примере для этого используются классы UnixAuthorizer
и UnixFilesystem
.
from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import UnixAuthorizer from pyftpdlib.filesystems import UnixFilesystem def main(): authorizer = UnixAuthorizer(rejected_users=["root"], require_valid_shell=True) handler = FTPHandler handler.authorizer = authorizer handler.abstracted_fs = UnixFilesystem server = FTPServer(('', 21), handler) server.serve_forever() if __name__ == "__main__": main()
В следующем коде показано, как реализовать базовый авторизатор для рабочей станции Windows для аутентификации по существующим учетным записям пользователей Windows. Этот код требует установки модуля pywin32
.
from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from pyftpdlib.authorizers import WindowsAuthorizer def main(): authorizer = WindowsAuthorizer() # для обработки анонимных сеансов можно использовать пользователя # Guest с пустым паролем. Дополнительно можно указать каталог профиля. # authorizer = WindowsAuthorizer(anonymous_user="Guest", anonymous_password="") handler = FTPHandler handler.authorizer = authorizer server = FTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main()
По своей природе pyftpdlib
является асинхронным. Это означает, что он использует один процесс/поток для обработки нескольких клиентских подключений и передачи файлов. Вот почему он такой быстрый, легкий и масштабируемый. Однако у асинхронной модели есть один большой недостаток: в коде не должно быть инструкций, блокирующих на длительный период времени, иначе зависнет весь FTP-сервер. Таким образом, пользователь должен избегать таких вызовов, как time.sleep(3)
, тяжелых запросов к базе данных и т. д. Более того, есть случаи, когда асинхронная модель не подходит - медленная файловая система (например, сетевая файловая система samba
).
Если файловая система медленная (например, open(file, 'r').read(8192)
занимает 2 секунды), то FTP-сервер застрянет. Модуль pyftpdlib
поддерживает 2 класса, которые изменяют модель параллелизма по умолчанию, добавляя несколько потоков или процессов. С технической точки зрения это означает, что когда клиент подключается, создается отдельный поток/процесс, и внутри него выполняется собственный цикл ввода-вывода. На практике это означает, что можно использовать блокирующие операции, которые неограничены по времени.
Изменить асинхронную модель легко. Для этого нужно вместо класса FTPServer
использовать ThreadedFTPServer
или MultiprocessFTPServer
.
Пример на основе потоков:
from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import ThreadedFTPServer # <- from pyftpdlib.authorizers import DummyAuthorizer def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.') handler = FTPHandler handler.authorizer = authorizer server = ThreadedFTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main()
Пример на основе процессоров:
from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import MultiprocessFTPServer # <- from pyftpdlib.authorizers import DummyAuthorizer def main(): authorizer = DummyAuthorizer() authorizer.add_user('user', '12345', '.') handler = FTPHandler handler.authorizer = authorizer server = MultiprocessFTPServer(('', 2121), handler) server.serve_forever() if __name__ == "__main__": main()