Не Совсем Обычный Xmpp-Бот На Python: Туннелирование

Не так давно была опубликована статья о ICQ на Python , что и побудило меня развивать тему, хоть и немного в другом направлении.

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

В результате родилась идея туннелировать HTTP-трафик в XMPP.



Схема

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

Компоненты устроены так: на удаленной машине с доступом в Интернет запускается бот-сервер.

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

  
   
  
  
  
  
  
 
                                      local                                            
 +-----------------------------------------------------------------------------------+
 | http-client (browser, wget) -> http-proxy -> bot-client                           | 
 +-----------------------------------------------------------------------------------+
                                        /\
                                        ||
                                        \/
                                     remote
 +-----------------------------------------------------------------------------------+
 |                               bot-server                                          |
 +-----------------------------------------------------------------------------------+
 
Для взаимодействия с клиентским ботом и серверным ботом используется простой протокол на основе XML. Запрос на загрузку индексной страницы example.com :

$ http_proxy="localhost:3128" wget .



Отвечать:

<url> http://example.com </url>

Ответ состоит из нескольких частей, кусков.

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

закодированные_данные — фрагмент ответа, закодированный в base64. Для большей наглядности представлю схему графически:

<answer chunk="2" count="19"><data>encoded_data</data></answer>



Выполнение



Общая информация
Используется для работы с 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("[email protected]") 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 (кон, сообщение) , аргументами которого будут дескриптор соединения и само сообщение.

Сама функция вызывает обработчик команды из класса Сборщик , который выполняет всю грязную работу и возвращает список фрагментов, переданных клиенту.

Вот и все, на этом работа сервера заканчивается.



Сборщик
Сорт Сборщик реализует саму логику выполнения и кодирования 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

Функция команда_процесса , как вы, наверное, помните, вызывает наш бот-сервер.

Он анализирует XML-запрос, определяет, какой URL-адрес ему необходимо запросить, и делает это, используя urllib2 .

Загруженное сообщение кодируется в формате Base64, чтобы не возникло неожиданных проблем со специальными символами, и делится на равные части, чтобы не достичь ограничения длины сообщения.

Затем каждый фрагмент упаковывается в XML и отправляется.



Клиент
Клиент, по сути, — это всего лишь обратный вызов, который объединяет данные и декодирует их из 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))



Прокси
Для прозрачного использования туннеля реализован HTTP-прокси.

Прокси-сервер привязывается к порту 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; выборка_файла() - спрашивает URL с помощью бот-клиента.



Заключение

Доступен полный исходный код Здесь в виде шар-архива (нужно запустить файл и выполнить его как шелл-скрипт).

Конечно, это скорее прототип, чем полноценное приложение, но прототип работает и, по крайней мере, без проблем скачивает небольшие файлы.

Этого должно быть достаточно для основной цели статьи: продемонстрировать «неинтерактивное» применение IM-бота.

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

Очень интересно, какой производительности можно добиться с помощью такой архитектуры, которую я, возможно, скоро займусь исследованием.

Теги: #python #bot #jabber bot #туннелирование #python

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.