Не совсем обычный XMPP-бот на Python: туннелирование

    Не так давно была опубликована статья про ICQ на Python, которая меня подтолкнула развить тему, правда в несколько другом направлении. Несколько лет назад у меня были трудности с домашним интернетом: доступ только в локальную сеть, из связи с внешним миром только ICQ и локальный Jabber сервер; никакой другой возможности попасть наружу не было. В результате чего родилась идея туннелировать HTTP трафик в XMPP.



    Схема


    Схема базируется на трех основных компонентах:

    • бот-сервер: принимает сообщения с HTTP-запросами, выполняет, кодирует и высылает клиенту результат
    • бот-клиент: отправляет серверу информацию о HTTP запросах, которые нужно выполнить, ждет результата, обрабатывает и возвращает готовый к дальнейшему использованию результат выполнения запроса
    • http-proxy: прокси сервер, который обрабатывает HTTP запросы, используя бота-клиента


    Компоненты располагаются так: на удаленной машине с доступом в интернет запускается бот-сервер. На localhost запускаются бот-клиент и прокси; клиентские приложения настраиваются на использование нашего прокси, например:

    $ http_proxy="localhost:3128" wget ...


    Для взаимодействия бота-клиента с ботом-сервером используется простенький, основанный на XML, протокольчик.

    Запрос на скачку индексной страницы example.com:
    <url>http://example.com</url>
    


    Ответ:
    <answer chunk="2" count="19"><data>encoded_data</data></answer>
    


    Ответ состоит из нескольких частей, chunk'ов. Здесь chunk — номер chunk'а, count — общее количество чанков, на которое был разбит ответ на запрос. encoded_data — закодированный в base64 кусок ответа.

    Для пущей наглядности представлю схему графически:

                                         local                                            
    +-----------------------------------------------------------------------------------+
    | http-client (browser, wget) -> http-proxy -> bot-client                           | 
    +-----------------------------------------------------------------------------------+
                                           /\
                                           ||
                                           \/
                                        remote
    +-----------------------------------------------------------------------------------+
    |                               bot-server                                          |
    +-----------------------------------------------------------------------------------+
    


    Реализация


    Общие сведения

    Для работы с XMPP использован xmpppy. Никаких хитрых возможности не требуется, нужно лишь обрабатывать входящие сообщения и отправлять ответы. XML парсится и генерируется средствами стандартной библиотеки — xml.dom.minidom.

    Бот-сервер

    Задача сервера — получать запросы на закачку, отдавать их в библиотеку, которая уже сама разберется, что нужно скачивать, и вернет результат, а сервер переправит этот результат клиенту.

    В упрощенной схеме обработка сообщений на стороне сервера выглядит таким образом:

    import xmpp
    
    from Fetcher import Fetcher
    
    fetcher = None
    
    def message_callback(con, msg):
        global fetcher
       
        if msg.getBody():
            try:
                ret = fetcher.process_command(msg.getBody())
            except:
                ret = ["failed to process command"]
    
            for i in ret:
                reply = xmpp.Message(msg.getFrom(), i)
                reply.setType('chat')
                con.send(reply)
    
    if __name__ == "__main__":
        jid = xmpp.JID("my@server.jid")
        user = jid.getNode()
        server = jid.getDomain()
        password = "secret"
    
        conn = xmpp.Client(server, debug=[])
        conres = conn.connect()
    
        authres = conn.auth(user, password, resource="foo")
    
        conn.RegisterHandler('message', message_callback)
        conn.sendInitPresence()
    
        fetcher = Fetcher()
    
        while True:
             conn.Process(1)
    


    Я намеренно убрал обработку ошибок и захардкодил значения, чтобы код был компактнее и легче читаем. Итак, что здесь происходит? Мы подключаемся к jabber-серверу и вешаем обработчик сообщений:

        conn.RegisterHandler('message', message_callback)
    


    Таким образом, на каждое новое входящее сообщение будет вызываться наша функция message_callback(con, msg), аргументами которой будет хэндл подключения и само сообщение. Сама же функция вызывает обработчик команд из класса Fetcher, который делает всю «черную» работу и возвращает список чанков, отдаваемых клиенту. Вот и все, на этом работа сервера заканчивается.

    Fetcher

    Класс Fetcher реализует саму логику выполнения и кодирования HTTP запросов. Целиком код его приводить не буду, его можно будет посмотреть в архиве, приложенном к статье, опишу лишь основные моменты:

        def process_command(self, command):
            doc = xml.dom.minidom.parseString(command)
    
            url = self._gettext(doc.getElementsByTagName("url")[0].childNodes)
    
            try:
                f = urllib2.urlopen(url)
            except Exception, err:
                return ["%s" % str(err)]
    
            lines = base64.b64encode(f.read())
        
            ret = []
            chunk_size = 1024
            x = 0 
            n = 1 
            chunk_count = (len(lines) + chunk_size - 1) / chunk_size
    
            while x < len(lines):
                ret.append(self._prepare_chunk(n, chunk_count, lines[x:x + chunk_size]))
                x += chunk_size
                n += 1
    
            return ret
    


    Функцию process_command, как вы наверно помните, вызывает наш бот-сервер. Она парсит XML-запрос, определяет, какой url ей нужно запросить и делает это с помощью urllib2. Скачанное кодируется в base64, чтобы не было никаких неожиданных проблем со спец-символами, и разбивается на равные части для того, чтобы не упереться в ограничение на длину сообщения. Затем каждый чанк оборачивается в XML и отправляется наружу.

    Клиент

    Клиент, по сути, представляет из себя один лишь callback, который склеивает данные и декодит из base64:

    def message_callback(con, msg):
        global fetcher, output, result
    
        if msg.getBody():
            message = msg.getBody()
    
            chunks, count, data = fetcher.parse_answer(message)
    
            output.append(data)
    
            if chunks == count:
                result = base64.b64decode(''.join(output))
    


    Proxy

    Для того, чтобы туннель можно было использовать прозрачно, реализован HTTP-proxy. Прокси-сервер биндится на порт 3128/tcp и ждет запросов. Полученные запросы передаются на обработку бот-серверу, результат декодируется и отдается клиенту. С точки зрения клиентских приложений, наш прокси ничем не отличается от «обыкновенных».

    Для создания TCP сервера используется SocketServer.StreamRequestHandler из стандартной библиотеки.

    class RequestHandler(SocketServer.StreamRequestHandler):
    
        def handle(self):
            data = self.request.recv(1024)
    
            method, url, headers = parse_http_request(data)
    
            if url is not None:
                response = fetch_file(server_jid, client_jid, password, url)
                                                                                                                                
                self.wfile.write(response)
    
            self.request.close()
    


    Функция parse_http_request() парсит HTTP-запрос, вытаскивая из него url, заголовки и http version; fetch_file() — запрашивает url, используя бота-клиента.

    Заключение


    Полный исходный код доступен здесь в виде shar архива (нужно запустить файл и выполнить его как шелл-скрипт). Конечно, это больше прототип, чем полноценное приложение, однако прототип рабочий и как минимум небольшие файлы скачивает без проблем. Этого должно быть достаточно для основной цели статьи: продемонстрировать «не-интерактивное» применение IM-бота.

    В проекте можно очень много чего улучшить — начиная от добавления аутентификации, нормальной поддержки типов запросов, заканчивая работой над производительностью. Очень уж интересно, какой производительности можно достигнуть при такой архитектуре, исследованием чего, возможно, я скоро и займусь.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 33
    • НЛО прилетело и опубликовало эту надпись здесь
      • +5
        www.linux.org.ru/gallery/screenshots/2445958 PPPOJ покруче таки будет :)
        • +1
          Вот верная ссылка, там и исходники есть:
          code.google.com/p/pppoj/

          Ну и исходник сразу, почитать, если интересно:
          snipt.org/wkpgh
          • –1
            Да, здорово, почитаю на досуге, как реализовано.
          • 0
            Помню, лет 8 назад у меня был лимитный интернет, а вот протокол icq работал без лимита. Я очень долго искал информацию, как использовать протокол icq для выхода в Интернет, но так и не нашел ничего подобного… ;)
            • 0
              Сталкивался я с подобным в общежитии СибГУТИ. Там ICQ давали всем и бесплатно. На деле просто прокси позволял подключаться на порт 5190 на любом интернет сервере. Правда я дальше подключения jabber на jabber.ru не пошёл. Думаю если бы была возможность повешать прокси на 5190 порт, вполне бы мог бесплатно в интернет ходить.
            • +4
              Если бы не jabber, а skype, то Вы бы наверное туннелировали в видео поток и «пожимали бы» Интернет при помощи h264?
              А если бы был только голос — наверное бы сделали бота работающего как dialup модем?
              Только не нужно воспринимать это как идею и начинать реализовывать )

              А если серьезно, то такие ситуации, когда что-то запрещено, но очень хочется — неплохо нагружают мозги, заставляют альтернативно мыслить, выходить за рамки обыденности. А им (мозгам) это полезно! Удачи в дальнейших мозголомках и извращениях!
              • –15
                1. Код на гитхаб. Как его вантузтники то открывать будут, вы думали? Я его тоже открывать не буду, запускать не пойми что не в моих правилах.
                2. Когда что-то запретили, значит нефиг лазить.
                • +8
                  >Код на гитхаб. Как его вантузтники то открывать будут, вы думали?

                  Так же, как и не вантузятники: git-клиентом. Кому надо — тот возьмёт, кому не надо — тому не надо.

                  >Я его тоже открывать не буду, запускать не пойми что не в моих правилах.

                  *автор кода с рёвом выбежал из комнаты и больше не вернулся*
                  • –12
                    Так же, как и не вантузятники: git-клиентом. Кому надо — тот возьмёт, кому не надо — тому не надо.

                    Чукча писатель? Откуда они буду открывать его git-клиентом, можно узнать?

                    *автор кода с рёвом выбежал из комнаты и больше не вернулся*

                    Какой ужас, печаль моя неизбывна.

                    Я так вижу по отметке в карме, мы уже общались. Видимо ничего с той поры не изменилось в чукча-сознании.
                    • 0
                      Да, невнимателен был. В принципе, виндузятникам и так предстоит выполнить неплохой квест по установке питона и всех необходимых модулей, так что плюс-минус Cygwin для шелл-скрипта — невелик довесок. Ну а если бы чукча был читателем и сходил по ссылке, то он бы заметил, что шелл там нужен постольку-поскольку, и код выдёргивается за 10 секунд в любом человеческом текстовом редакторе.
                      Поэтому я могу лишь повторить: «Кому надо — тот возьмёт, кому не надо — тому не надо».

                      >Видимо ничего с той поры не изменилось в чукча-сознании.

                      Судя по вашей зацикленности на собственной персоне — действительно, ничего не изменилось. А посему предлагаю не тратить время на очередной обмен любезностями. Разумеется, вы вправе отказаться и продолжить общение в виде монолога (a.k.a. «диалога с уважаемым человеком»).
                      • –11
                        Я так понял, что это были своего рода извинения. Ну спасибо и на том, от такого персонажа лучшего не получить все равно.
                        Подсказка: не стоит писать столько отмазок — признали ошибку, и можно не пытаться оскорбить собеседника дальше ;-)
                        • 0
                          >Я так понял, что это были своего рода извинения.

                          Вы поняли неправильно. В данном комментарии я не вижу ровным счётом ничего, что требовало бы извинений.
                          JFYI: «признание неправоты и невнимательности» != «извинение».

                          >… можно не пытаться оскорбить собеседника дальше

                          Если мне не изменяет зрение, то оскорбления начались где-то примерно вот с этого комментария, однако извинений «от такого персонажа» я даже не ожидаю.
                          • –9
                            О нет, оскорбления началось тут — habrahabr.ru/blogs/python/111971/#comment_3584348
                            Так как считать собеседника идиотом величайшее из оскорблений.
                            • –1
                              Видеть оскорбления там, где их даже не предполагалось — это высший пилотаж %)

                              >Так как считать собеседника идиотом величайшее из оскорблений.

                              С таким подходом оскорбления [в сторону автора] начались примерно тут, т.к. претензии в таком тоне — откровенное хамство, а хамство в адрес того, кто безвозмездно раздаёт довольно неплохую работу — «величайшее из оскорблений» %)

                              Берите-ка лучше пример с меня: меня не колышат оскорбления не то что там, где их не предполагалось, но даже там, где они предполагались :)
                              • –8
                                Ой, теперь роль защитника обиженных и оскорбленных: ) А как же мой монолог? Что вы мешаете мне разговаривать с уважаемым человеком, что вы вмешиваетесь постоянно?

                                Переходить на обсуждение автора я не стану, оставьте свою подачу себе. Я уже написал все, что имел автору сообщить.

                                Но чисто спортивный интерес мне не дает покоя — когда же вы все таки оставите этот ужасный, абсолютно неинтересный вам, разговор ни о чем? Вам тоже интересна моя персона? Я знал, я всем интересен!: )
                                • –1
                                  >Ой, теперь роль защитника обиженных и оскорбленных :)

                                  Исключительно в целях указания на бревно в глазу того, кто возмущается по поводу «оскорблений» — не более.

                                  >А как же мой монолог? Что вы мешаете мне разговаривать с уважаемым человеком, что вы вмешиваетесь постоянно?

                                  Я не вмешиваюсь, я суфлирую. А если не хотите видеть суфлёров — исполняйте свой монолог в менее публичном месте :)

                                  >… когда же вы все таки оставите этот ужасный, абсолютно неинтересный вам, разговор ни о чем?

                                  Когда мне надоест валять ваньку на работе и я поеду домой, где меня ждут более интересные занятия. Ну или когда мне наскучит окончательно — смотря что случится раньше :)

                                  >Вам тоже интересна моя персона? Я знал, я всем интересен! :)

                                  Спасибо, конечно, за комплимент, но я вынужден буду отказаться от чести действовать от имени «всех». Мой «интерес» — он исключительно мой, не нужно на его основании делать столь далеко идущих выводов.
                    • +3
                      Вот это да, вот это народ пошел тупой.
                  • +1
                    Делал ip over irc с приятелем в году примерно 2002-2003, идея была два бота общающихся через irc кидающие друг-дгугу куски base64, c другой стороны бота — tun интерфейс, так что работало все. На ирц сервере правда был rate limit, потому более 10KB/s не получалось.
                    • 0
                      Делал то же самое, только на java, года три назад — воспоминания аж нахлынули приятные =)
                      • 0
                        А вот после этой статьи — на работах начнут резать и XMPP :(
                        • +1
                          У любого админа на работе, есть интернет. Его как правило нет у бухгалтера.
                          • 0
                            Вы бы знали, как достают админов иногда товарищи, которые что-то там в компьютерах «шарят». Вот они вполне могут утянуть подобный скрипт, и мешать работе XMPP сервера.
                            • 0
                              Сейчас сотрудники продвинутые пошли, это да.
                              Но есть yota или чтонить по проще, я видел, пользуются и довольно удачно.
                              Мне так вообще это не нужно.
                              Я делаю либо ssh тунель или rdp до дома :)) а и еще есть vpn
                        • 0
                          немного не в тему но касательно xmpppy
                          msg.getBody() мы используем что бы увидеть что написал юзер, а как получать сервисные сообщения типа: id вошел в конференцию, id сменил статус и т.п.?
                          я вот что-то в доках не нашел совсем =(
                          • 0
                            client.RegisterHandler('presence', presenceCallBack)

                            Не оно?
                            • 0
                              Если это действительно то, что вам было нужно, то я не понимаю, как вы его «не нашли». Я нашёл это менее, чем за минуту, не имев до этого дела с xmpppy вообще. Главная страница XMPPPY — Examples — вот оно.
                              • 0
                                я не могу найти ничего об этом здесь, почему?
                                • +1
                                  Это автоматически сгенерированная документация, потому такая запутанная. Но там есть то что нужно. В левом верхнем выбираете «xmpp.protocol» в левом нижнем «Presence» попадаете сюда xmpppy.sourceforge.net/apidocs/xmpp.protocol.Presence-class.html
                          • +4
                            По поводу кода+производительности…
                            1) Раз уж вы разбиваете сообщения на чанки, то может есть смысл генерировать их в поточном режиме как то так?
                            CHUNK_SIZE=1024*0.75#base64 add ~30%owerhead
                            f = urllib2.urlopen(url)
                            lines = base64.b64encode(f.read())
                            
                            chunk=f.read(CHUNK_SIZE)
                            while chunk:
                                yield self._new_prepare_chunk(base64.b64encode(chunk))
                                chunk=f.read(CHUNK_SIZE)
                            Только prepare_chunk придется переделать т.к. кол-во чанков не знаем (хотя, если не лень, можем посчитать исходя из заголовка «Content-length»)
                            Если качаем большой файл — сэкономим память. Плюс зарботает Long-pooling

                            2) Зачем вам HTTP прокси? передавали бы прям весь HTTP запрос и ответ как есть (+base64) — заголовки и тело. К тому же не нужно было бы свой протокол изобретать. Только на сервере остается распарсить первую строчку заголовков чтоб узнать по какому адресу сокет подключить и писать в этот сокет raw HTTP.

                            3) Судя по коду прокси, не поддерживаются POST запросы. Да и заголовки вообще не передаются. Легко лечится п.2

                            4) Не уверен, но кажется не поддерживаются параллельные запросы. Если поднапрячься, то можно добавить.

                            Наверняка есть еще что улучшить — архив не смотрел, только статью
                            • 0
                              Спасибо, ценные замечания, именно в этом направлении и собираюсь двигаться.
                            • 0
                              Я когда-то давно давно во времена лимитных Интернетов думал об этой же идее, но не позволяла реализовать квалификация и лень…
                              Правда я думал почему-то о реализации в виде транспорта в XMPP
                              • +1
                                Провайдеры подключенные к Яндекс.Локальная сеть и яндексовский жаббер сервер негодуют?

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