FTP сервер с авторизацией через базу данных

    image

    Существует множество готовых FTP серверов для разворачивании у себя на сервере. Но сложилось так что, на сервере уже работает FTP и нужно поднять FTP сервер на альтернативном порту. А также раздать пользователям доступ только к своим папкам с файлами. Решил поинтересоваться, а что можно сделать средствами Python. Поиск быстро выдал библиотеку pyFTPd.


    В готовых примерах данной библиотеки показано как за пару минут поднять свой FTP сервер. Пользователи и путь к файлам, к которым они должны иметь возможность добраться хранятся в базе. Таким образом, было принято решение взять за основу данную библиотеку и связать ее с БД. И получить FTP сервер со своими плюшками :)

    База данных


    Таблица в БД не представляет ничего сверх сложного.
    SQL:
    CREATE TABLE `users` (
      `id` int(11NOT NULL auto_increment,
      `username` varchar(255NOT NULL,
      `password` varchar(32NOT NULL,
      `path` varchar(255NOT NULL,
      `perm` varchar(8default NULL,
      PRIMARY KEY  (`id`),
      KEY `username` (`username`)
    )


    Основные параметры, которые используются — это логин, пароль, права доступа и путь к папке, куда пользователь будет иметь доступ.

    Реализация


    Первое, что было сразу сделано – небольшой класс обертка на работу с БД. Таким образом, переписывая его под свои нужды можно заменить MySQL на любую БД.

    class DB:
        
        init=None
        db=None
        def __init__(self,init_db):
            """ Constructor """
            self.init = init_db
            self.db = self.init()
        
        def doSql(self,sql):
            """ Handle SQL """
            try:
                self.db.execute(sql)
            except:
                try:
                    self.db = self.init()
                    self.db.execute(sql)
                except:
                    print "error:"+sql
        
        def getDB(self):
            """ """
            return self.db
        
        def getLastId(self):
            """Get last insert ID"""
            sql = "select LAST_INSERT_ID() as `id`"
            self.doSql(sql)
            data = self.db.fetchone()
            if 'id' in data:
                return data['id']
            else:
                return None


    Изучив википедию и исходные коды самого сервера были выявлены методы, которые отвечают за авторизацию и выбор пути расположения файлов. Отсюда следовало, что необходимо переопределить эти методы, и задача решена.
    Метод запуска сервера выглядит следующим образом:
    . . .
    def starterver(self):
            """Run server"""
            authorizer = self.ftpserver.DummyAuthorizer()
                  
            authorizer.validate_authentication = self.my_validate_authentication
            authorizer.get_home_dir = self.my_get_home_dir
            authorizer.get_perms = self.my_get_perms
            authorizer.get_msg_login = self.my_get_msg_login
            authorizer.get_msg_quit = self.my_get_msg_quit
            
            authorizer.has_perm = self.my_has_perms
            authorizer.has_user = self.my_has_user
        
            # Instantiate FTP handler class
            ftp_handler = ftpserver.FTPHandler
            ftp_handler.authorizer = authorizer
            ftp_handler.passive_ports = range(63000,63500)
            # Define a customized banner (string returned when client connects)
            ftp_handler.banner = "pyftpdlib %s based ftpd ready." %ftpserver.__ver__
        
            address = ('127.0.0.1'23)
            ftpd = ftpserver.FTPServer(address, ftp_handler)
        
            # set a limit for connections
            ftpd.max_cons = 256
            ftpd.max_cons_per_ip = 5
        
            # start ftp server
            ftpd.serve_forever()
    . . .


    Основные методы, которые были переопределены



    validate_authentication – отвечает за авторизацию пользователя.
    get_home_dir – получение домашней директории, к которой пользователь будет иметь доступ.
    get_perms – получения прав доступа к данным.
    has_perm – проверка прав доступа пользователя к директории
    has_user – проверка на существование пользователя

    Для работы с пользователям был реализован отдельный класс:
    from db import DB
    from config import init_db
    class User:
        
        def __init__(self):
            """Init"""
        
        def auth(self,username,password):
            """Make auth"""
            sql = "select * from `users` where `username`='%s' and `password`='%s'" % (username,password)
            db = DB(init_db)
            db.doSql(sql)
            res = db.getDB().fetchone()
            if res:
                return 1
            else:
                return None
        
        def getPath(self,username):
            """Return path by username"""
            sql = "select `path` from `users` where `username`='%s'" % username
            db = DB(init_db)
            db.doSql(sql)
            uparam = db.getDB().fetchone()
            if uparam:
                return uparam['path']
            else:
                return None
        
        def getPerm(self,username):
            """Return permission by username"""
            sql = "select `perm` from `users` where `username`='%s'" % username
            db = DB(init_db)
            db.doSql(sql)
            uparam = db.getDB().fetchone()
            if uparam:
                return uparam['perm']
            else:
                return ''
        
        def hasUser(self,username):
            """Checj user into DB"""
            sql = "select `id` from `users` where `username`='%s'" % (username)
            db = DB(init_db)
            db.doSql(sql)
            uparam = db.getDB().fetchone()
            if uparam:
                return 1
            else:
                return 0


    Оборачиваем необходимые методы:
    def my_validate_authentication(self,username,password):
        return User().auth(username, password)

    def my_get_home_dir(self,username):
        return User().getPath(username) 

    def my_get_perms(self,username):
        return User().getPerm(username)

    def my_get_msg_login(self,username):
        return 'hello msg login'

    def my_get_msg_quit(self,username):
        return 'byu msg quit'

    def my_has_user(self,username):
        return User().hasUser(username)
        
    def my_has_perms(self,username, perm, path=None):
        return 1   


    Результат


    Пользователь авторизируется через БД и получает доступ к своей директории.

    Что можно улучшить


    Для разгрузки обращений к БД можно использовать кеширование. Например, memcache. Вариантов тут может быть великое множество, например:
    • При логине пользователя записывать всю информацию о пользователе в кеш и потом читать ее оттуда
    • Хранить базу данных о пользователях в кеше и периодически обновлять ее


    Исходный код


    Исходные коды можно скачать тут.

    Источники


    http://en.wikipedia.org/wiki/File_Transfer_Protocol
    http://code.google.com/p/pyftpdlib/
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 56
    • +12
      А колеса-то у Вас квадратные linux.die.net/man/8/pure-ftpd :)
      • 0
        Во-во, pureftpd — наше все ;-)
      • –12
        Юзеры в MySQL. Месье знает толк в извращениях.
        • 0
          Базу можно подобрать под себя любую.
          • +2
            Why not?
            Я вот не вижу каких-то однозначных причин не использовать mysql для этих целей.
            Другое дело пароли в открытом виде, но и для этого могут быть причины.
            • 0
              а кто мешает хранить их в MD5?
              Можно сказать, что это прототип.
              • +1
                да конечно не что не мешает.
                это я написал в тему, что уж если хочется придраться, то лучше к этому чем к mysql — тут хоть какую-то причину придумать можно
                • 0
                  Согласен. В моем понимание это называется конструктивная критика :)
                • +1
                  Иногда нужно интегрировать FTP с существующей базой пользователей на Mysql или другой. В этом случае проще использовать СУБД напрямую чем через md5/sqllite
                • –6
                  А кто сказал что mySQL не подходит. Гвозди тоже можно микроскопом забивать. А какие у mySQL преимущества перед другими бд для хранения паролей?
                  • 0
                    зависит от условий
                    пускай юзеров 100 000 и они пытаются поддерживать фтп соединение по фиговым каналам. лучше в plain text хранить?
                    а если учесть что на сервере скорее всего уже стоит mysql, то чем лучше тот sqlite или plain text?
                    в каком месте myisam на микроскоп для гвоздей похож?
                    • –7
                      Чем mySQL лучше других БД для хранения паролей? И какая разница сколько юзеров?
                      • +1
                        А чем хуже?
                        • –7
                          MySQL — реляционная СУБД. Зачем к примеру вам для хранения паролей все плюшки mySQL? Это и есть забивание гвоздей микроскопом. Или вы считаете что использовать сервер вместо калькулятора тоже гуд?
                          • +1
                            А если так.
                            Есть сервис.
                            В нем есть пользователи, хранятся в БД (например в MySQL).
                            Сервис начинает предоставлять дополнительную услугу — доступ к удаленной директории по FTP. Что лучше держать пользователей в базе и синхронизировать его с файлом, в котором будут хранится пользователи, или добавить пару полей в базу?
                            • –8
                              А причем тут файлы? Я не могу понять зачем Вам для хранения паролей mySQL?
                              • +1
                                Предложите вариант, где хранить пароли пользователей, если все остальные параметры хранятся в БД?
                              • +2
                                Чувствую троллинг, но вмешаюсь.
                                Вот Вам пример, не зачем, а почему ProFTPd использует базу MySQL.
                            • 0
                              за тем, что скорее она уже есть и за тем, что если Вы будете велосипедить подобным образом, то скорее всего для интеграции фтп в некую систему.
                              где хранить имя, фамилию, статистику использования квоты, список загруженного и т.д.?
                              • –7
                                1) С каких это пор mySQL входит в состав хотя бы одной ОС?
                                2) В базе данных.
                                • +3
                                  не надо читать мои комменты между строк — читайте всё.
                                  1. я писал что «скорее всего» она там есть. и не писал что она входит в состав ОС
                                  2. mysql — тоже база данных. класс? :)
                • 0
                  встроеный в питон sqlite наверно подошел бы лучше.
                  • 0
                    это как вариант хранения данных.
                  • +7
                    честно говоря думал такие статьи остались в начале 2000-х
                    • 0
                      хотя, как учебный пример наверное хорошо
                      • +8
                        Да ладно вам, хорошо. Использовать old style классы — моветон, использовать 1 и 0 в качестве булевского выражения — моветон, писать
                                if 'id' in data:
                                    return data['id']
                                else:
                                    return None
                        

                        вместо
                            return data.get('id')
                        

                        бессмысленно и в 4 раза дольше.
                        Про то, что там в sql запросы передается данные переданные пользователем, без всякого эскейпинга, я уже не говорю — sql injection это сейчас модно в обучающих статьях, как я понимаю.
                        Зачем такое использовать в качестве учебного примера?
                        • +2
                          обучение не начинают с sql injection и остального, в обучении главное наглядность, а не фунциональность
                          • 0
                            Делаю смелый вывод что Вы python-программист, и совсем не начального уровня.
                            Если не сложно скиньте несколько ссылок на полезные статьи (лучше в личку может даже).
                            Буду очень благодарен. Я не профи я только учусь, поэтому буду рад любому материалу.
                            • +2
                              Слушайте, это не было наездом на то, что вы чего-то не знаете. Простите если вам так показалось. Мы все вначале пишем плохой код, делаем детские ошибки и прочая — это совершенно нормально. Меня просто удивляют постоянно появляющиеся на главной хабра статьи начинающиеся словами «Я начал изучать python(ruby,java,lisp...) неделю назад и решил написать статью ...».
                              Что касается статей, то черт его знает. Я, помнится, читал что-то из стандартного набора Learninig P. Dive into P. и Python Cookbook.
                              Вообще, мне кажется надо просто читать много чужого кода написанного опытными людьми и параллельно официальную питоновскую документацию. Я, когда начинал, копался в коде twisted. Код самих питоновских библиотек можно смотреть. Иногда забавно в код самого питона заглянуть. Парни из pocoo.org пишут очень хорошие либы. Пользуетесь django — читайте джанго, не знаю, правда, что там.
                              Плюс англоязычные блоги — можно начать с planet python, а потом сами разберетесь кого читать.
                              • 0
                                Ни в коем случае не считал это наездом.
                                Мой комментарий был скорее просьбой.
                                Спасибо за советы.
                                Я постараюсь ими воспользоваться и в следующий раз предлагать более качественный код.
                              • 0
                                Когда только учитесть, старайтесь хотябы первое время свой код не вывешивать на главной хабра, многие могут скопировать неглядя просто или подумать что так и нужно.

                                Я знакомство с питоном отсюда начинал:
                                docs.python.org/tutorial/index.html

                                Потом уже код простеньких программок из Ubuntu смотрел
                              • 0
                                return data.get('id', None)
                                :)
                                • 0
                                  Оно и так по умолчанию None.

                                  Class Docstring:
                                  D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.
                                  • 0
                                    этож бублгум!
                                    в смысле pythonic way — должно быть читабельно и понятно любому, даже не программисту.
                                    Поэтому первые несколько строк были лучше этой одной, но уж если одна, то хотя бы с полным описанием

                                    P.S. PEP8 -> Programming Recommendations
                                    • 0
                                      Я, признаться, не осознал почему написать свой велосипед лучше, чем вызвать наистандартнейшую функцию из core library. Вы другие стандартные функции тоже ручками переписываете для наглядности?

                                      Опять же — перечитал PEP8 -> Programming Recommendations и не нашел ни малейшего упоминания о том, что не рекомендуется использовать значения по умолчанию.

                                      Более того — сделав grep по исходникам питона, я не нашел ни одного случая чтобы там использовали get("...", None) для хешей. Просто вызовов .get(key) зато там навалом. Не все они, конечно, вызываются на dict, но какая-то часть — наверняка.

                                      И вообще мне кажется сомнительной мысль, что разработчики бы реализовывали функцию get, а потом не рекомендовали ее использовать.
                                      • 0
                                        в каком месте была стандартная фукнция переписана?
                                        А по поводу грепа я не понял… толи Вы им пользоваться не умеете, толи врётё… хотя я думаю, просто другие исходники использовали

                                        Python-2.7.1\Doc\tools\rstlint.py (1 hits)
                                        Line 194: checkerlist = checkers.get(ext, None)
                                        • 0
                                          Ну если я удаляю вызов системной функции, и вставляю свои пять строк которые делают то же самое, то я это называю переписывать.

                                          Вы ведь, к примеру, вызываете any, а не итерируете массив? Используете defaultdict, а не делаете вручную проверки и присвоения?

                                          ух ты. Вы правы, видимо не умею. Пропустил пробел после запятой. Причем если бы я не нашел ничего, я бы заподозрил что-то, но увы, питонисты тоже иногда забывают ставить пробелы. Посыпаю голову пеплом и снимаю этот аргумент.

                                          В любом случае — если бы авторы считали что этот параметр нужно явно указывать, они бы не сделали его опциональным.
                                          • 0
                                            да конечно можно не указывать. на питоне можно писать по разному — чего только стоят люмбда функции и создание списков на лету.
                                            НО если мы говорим о качестве кода, то для python есть PEP.
                                            а набирает питон популярность как раз благодаря авторам, которые дают возможность писать как душе угодно
                              • 0
                                > как учебный пример

                                Если только пример «как нельзя», потому что код — ппц.
                              • +2
                                Каждый второй фтп-сервер поддерживает если не внешние аутентификаторы, то базу юзеров во внешней СУБД.

                                Велосипед.
                                • 0
                                  А если нужно повесить триггеры на события по FTP? Например по факту закачки файла или создания директории, выполнять какую-либо функцию. Ни одно стандартное решение не поддерживает это лучше чем путь переопределения методов pyftpd.
                                  • 0
                                    FTP работает с файловой системой. Для слежки за событиями файловой системы есть inotify. И вообще, это дополнительное условие, о котором angry_elf не знал при написании своего комментария. :P
                                • +1
                                  Тем временем обычные пользователи пользуются тем же пресловутым ProFTPd, хранят доступы в базе, да еще и квоты заодно
                                  • +1
                                    Поддержу предыдущего оратора про proftpd. Годы назад для нее уже сделали модуль для работы с mysql. Сам пакет стабилен, и все у него хорошо. Писать собственный — ну разве что из спортивного интереса
                                    • 0
                                      тогда уж лучше vsftpd, имхо
                                      • 0
                                        Опыта общения с vsftpd не имел, поэтому пишу о том, о чем знаю. Кроме того, к proftpd есть графическая морда — gproftpd, которая позволяет достаточно удобно добавлять/удалять пользователей. Так что, если не нужно динамическое добавление, то можно использовать эту утилиту и не привязываться в базам. Одним словом, хорошо подходит для домашнего использования
                                        • 0
                                          Я в общем то тоже не имел с ним дела, но основное преимущество того же ProFTPd в том, что он перкрасно работает «из-коробки» в Debian. Минимум допиливания и конфиги Apache-style доставляю сильнее. На саом деле, когда серверов в проекте переваливает штук за 20-30 — начинает сказываться «цена» обслуживания. Под цено я имею ввиду не только зарплату сисадмина, а еще и время, которое требуется на настройку всего добра. Цепляние же базы к нему — прекрасно описано, и более того — запросы для работы с табличками вы делаете сами — и соответственно можете привязываться вообще к любой существующей табличке любого другого проекта, что мы и сделали лет этак 6 назад
                                          • 0
                                            vsftpd тоже работает прекрасно из коробки. Авторизацию можно прикрутить через pam или встроенными средствами vsftpd. Единственное преимущество proftpd перед vsftpd — это gui морда, имхо. Конфиги — дело привычки.
                                      • 0
                                        Поднимать демон на скриптовом языке python — моветон. ftp-сервера с поддержкой mysql и настройкой портов есть уже много-много лет. Поздравляем Вас с разморозкой.
                                        • 0
                                          Ооо… А еще говорят что в питоне говнокода нет))

                                          Может я немного преувеличиваю, но уж больно страшно смотрится:
                                          * Зачем обертка над БД? db2 API сам себе прекрасная переносимая обертка (т.е. все реляционные БД в Python должны поддерживать этот интерфейс, см www.python.org/dev/peps/pep-0249/). А так получилась универсальная обертка над универсальной оберткой
                                          * SQL инъекции привет! sql = "select * from `users` where `username`='%s' and `password`='%s'" % (username,password) Так в питоне не делают. Используют метод db.execute() с параметрами вроде db.execute(«select * from `users` where `username`='%s' and `password`='%s'», (username,password)) который автоматически экранирование добавляет
                                          * Чтоб получить last insert id существует атрибут .lastrowid курсора (см тот же PEP)
                                          * про if return else return уже заметили
                                          • 0
                                            Когда изучал питон наткнулся на эту библиотеку… Написал свой авторизатор для mysql тогда.
                                            Могу сразу сказать, что Вас ожидает ошибка mysql server has gone away :).
                                            еще у Вас нет setuid и setgid.
                                            • 0
                                              Почему меня ожидает ошибка «mysql server has gone away»?
                                              Про setuid и setgid можно подробнее.
                                              • 0
                                                Прилечу домой скину авторизатор.
                                                Запустите фтпд и авторизуйтесь, после этого оставьте на часов 8 в idle. после этого посыпятся ошибки mysql server has gone away.
                                                • 0
                                                  Как и обещал, полный листинг, дыр я вроде не находил, т.к. все в escapestring. Пароль сами завернуть можете в md5 надеюсь…

                                                  import pwd,os,MySQLdb

                                                  class MysqlAuthorizer(ftpserver.DummyAuthorizer):

                                                  PROCESS_UID = os.getuid()
                                                  PROCESS_GID = os.getgid()
                                                  ml = None
                                                  def __init__(self, ml={'user':'','passwd':'','db':'','host':''}):
                                                  if ml['host'].startswith("/"):
                                                  ml['us'] = ml['host']
                                                  ml['host'] = 'localhost'
                                                  self.ml = ml
                                                  self.connect()
                                                  return

                                                  def connect(self):
                                                  ml = self.ml
                                                  try:
                                                  db=MySQLdb.connect(host=ml['host'],user=ml['user'],passwd=ml['passwd'],db=ml['db'],unix_socket=ml['us'])
                                                  self.c = db.cursor()
                                                  except:
                                                  return
                                                  def add_user():
                                                  return

                                                  def add_anonymous():
                                                  return

                                                  def remove_user():
                                                  return

                                                  def impersonate_user(self, username, password):
                                                  try:
                                                  self.c.execute(«SELECT uid FROM users WHERE user = %s AND passwd = %s LIMIT 1», (username,password))
                                                  except:
                                                  self.connect()
                                                  self.c.execute(«SELECT uid FROM users WHERE user = %s AND passwd = %s LIMIT 1», (username,password))
                                                  re = self.c.fetchone()
                                                  t = pwd.getpwuid(re[0])
                                                  os.setegid(t.pw_gid)
                                                  os.seteuid(t.pw_uid)

                                                  def terminate_impersonation(self):
                                                  os.setegid(self.PROCESS_GID)
                                                  os.seteuid(self.PROCESS_UID)

                                                  def validate_authentication(self, username, password):
                                                  try:
                                                  self.c.execute(«SELECT id FROM users WHERE user = %s AND passwd = %s LIMIT 1», (username,password))
                                                  except:
                                                  self.connect()
                                                  self.c.execute(«SELECT id FROM users WHERE user = %s AND passwd = %s LIMIT 1», (username,password))
                                                  return self.c.rowcount

                                                  def has_user(self, username):
                                                  try:
                                                  self.c.execute(«SELECT id FROM users WHERE user = %s LIMIT 1», (username,))
                                                  except:
                                                  self.connect()
                                                  self.c.execute(«SELECT id FROM users WHERE user = %s LIMIT 1», (username,))
                                                  return self.c.rowcount

                                                  def has_perm(self, username, perm, path=None):
                                                  try:
                                                  self.c.execute(«SELECT id FROM users WHERE user = %s AND perm LIKE %s LIMIT 1», (username,''.join(['%',perm,'%'])))
                                                  except:
                                                  self.connect()
                                                  self.c.execute(«SELECT id FROM users WHERE user = %s AND perm LIKE %s LIMIT 1», (username,''.join(['%',perm,'%'])))
                                                  return self.c.rowcount

                                                  def get_perms(self, username):
                                                  try:
                                                  self.c.execute(«SELECT perm FROM users WHERE user = %s LIMIT 1», (username,))
                                                  except:
                                                  self.connect()
                                                  self.c.execute(«SELECT perm FROM users WHERE user = %s LIMIT 1», (username,))
                                                  re = self.c.fetchone()
                                                  return re[0].strip(",")

                                                  def get_home_dir(self, username):
                                                  try:
                                                  self.c.execute(«SELECT homedir FROM users WHERE user = %s LIMIT 1», (username,))
                                                  except:
                                                  self.connect()
                                                  self.c.execute(«SELECT homedir FROM users WHERE user = %s LIMIT 1», (username,))
                                                  re = self.c.fetchone()
                                                  return re[0]

                                                  def get_msg_login(self, username):
                                                  return «Hallo!»

                                                  def get_msg_quit(self, username):
                                                  return «Bye bye...»
                                                  • 0
                                                    Да, говяно вышло
                                                    Вот в pastie
                                                    pastie.org/1377735

                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.