Pull to refresh

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

Reading time 5 min
Views 7.1K
Не так давно была опубликована статья про 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-бота.

В проекте можно очень много чего улучшить — начиная от добавления аутентификации, нормальной поддержки типов запросов, заканчивая работой над производительностью. Очень уж интересно, какой производительности можно достигнуть при такой архитектуре, исследованием чего, возможно, я скоро и займусь.
Tags:
Hubs:
+39
Comments 33
Comments Comments 33

Articles