Сообщить об ошибке.

Модуль pyftpdlib в Python, FTP-сервер

FTP-сервер на Python по быстрому

Модуль pyftpdlib реализует серверную часть протокола FTP, как определено в RFC-959. По своей природе pyftpdlib является асинхронным. Это означает, что он использует один процесс/поток для обработки нескольких клиентских подключений и передачи файлов. Вот почему он такой быстрый, легкий и масштабируемый.

Кто ищет реализацию клиентской части протокола FTP на Python, то смотрите документацию по модулю стандартной библиотеке ftplib.

НО! У асинхронной модели есть один большой недостаток: не должно быть блокировок на длительный период времени, иначе зависнет весь FTP-сервер. Таким образом асинхронная модель может не подходить для медленной файловой системы (например, сетевая файловая система samba).

Изменить асинхронную модель легко. Для этого нужно вместо класса FTPServer использовать классы ThreadedFTPServer или MultiprocessFTPServer. На практике это означает, что можно не бояться длительных блокирующих операции, а следовательно использовать FTP-сервер на медленных файловых системах.

Содержание:


Установка 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

Использование командной строки для запуска FTP-сервера.

Модуль 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-сервер.

В приведенном ниже сценарии используется базовая конфигурация, и это, вероятно, лучшая отправная точка для понимания того, как все работает. 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()

Ведение логов FTP-сервера.

Модуль 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()

FTPS-сервер (FTP через TLS/SSL).

Модуль 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()

FTP-сервер с авторизацией пользователей Windows.

В следующем коде показано, как реализовать базовый авторизатор для рабочей станции 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()

Изменение модели параллелизма FTP-сервера.

По своей природе 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()