Не так давно была опубликована статья про ICQ на Python, которая меня подтолкнула развить тему, правда в несколько другом направлении. Несколько лет назад у меня были трудности с домашним интернетом: доступ только в локальную сеть, из связи с внешним миром только ICQ и локальный Jabber сервер; никакой другой возможности попасть наружу не было. В результате чего родилась идея туннелировать HTTP трафик в XMPP.
Схема базируется на трех основных компонентах:
Компоненты располагаются так: на удаленной машине с доступом в интернет запускается бот-сервер. На localhost запускаются бот-клиент и прокси; клиентские приложения настраиваются на использование нашего прокси, например:
Для взаимодействия бота-клиента с ботом-сервером используется простенький, основанный на XML, протокольчик.
Запрос на скачку индексной страницы example.com:
Ответ:
Ответ состоит из нескольких частей, chunk'ов. Здесь chunk — номер chunk'а, count — общее количество чанков, на которое был разбит ответ на запрос. encoded_data — закодированный в base64 кусок ответа.
Для пущей наглядности представлю схему графически:
Для работы с XMPP использован xmpppy. Никаких хитрых возможности не требуется, нужно лишь обрабатывать входящие сообщения и отправлять ответы. XML парсится и генерируется средствами стандартной библиотеки — xml.dom.minidom.
Задача сервера — получать запросы на закачк��, отдавать их в библиотеку, которая уже сама разберется, что нужно скачивать, и вернет результат, а сервер переправит этот результат клиенту.
В упрощенной схеме обработка сообщений на стороне сервера выглядит таким образом:
Я намеренно убрал обработку ошибок и захардкодил значения, чтобы код был компактнее и легче читаем. Итак, что здесь происходит? Мы подключаемся к jabber-серверу и вешаем обработчик сообщений:
Таким образом, на каждое новое входящее сообщение будет вызываться наша функция message_callback(con, msg), аргументами которой будет хэндл подключения и само сообщение. Сама же функция вызывает обработчик команд из класса Fetcher, который делает всю «черную» работу и возвращает список чанков, отдаваемых клиенту. Вот и все, на этом работа сервера заканчивается.
Класс Fetcher реализует саму логику выполнения и кодирования HTTP запросов. Целиком код его приводить не буду, его можно будет посмотреть в архиве, приложенном к статье, опишу лишь основные моменты:
Функцию process_command, как вы наверно помните, вызывает наш бот-сервер. Она парсит XML-запрос, определяет, какой url ей нужно запросить и делает это с помощью urllib2. Скачанное кодируется в base64, чтобы не было никаких неожиданных проблем со спец-символами, и разбивается на равные части для того, чтобы не упереться в ограничение на длину сообщения. Затем каждый чанк оборачивается в XML и отправляется наружу.
Клиент, по сути, представляет из себя один лишь callback, который склеивает данные и декодит из base64:
Для того, чтобы туннель можно было использовать прозрачно, реализован HTTP-proxy. Прокси-сервер биндится на порт 3128/tcp и ждет запросов. Полученные запросы передаются на обработку бот-серверу, результат декодируется и отдается клиенту. С точки зрения клиентских приложений, наш прокси ничем не отличается от «обыкновенных».
Для создания TCP сервера используется SocketServer.StreamRequestHandler из стандартной библиотеки.
Функция parse_http_request() парсит HTTP-запрос, вытаскивая из него url, заголовки и http version; fetch_file() — запрашивает url, используя бота-клиента.
Полный исходный код доступен здесь в виде shar архива (нужно запустить файл и выполнить его как шелл-скрипт). Конечно, это больше прототип, чем полноценное приложение, однако прототип рабочий и как минимум небольшие файлы скачивает без проблем. Этого должно быть достаточно для основной цели статьи: продемонстрировать «не-интерактивное» применение IM-бота.
В проекте можно очень много чего улучшить — начиная от добавления аутентификации, нормальной поддержки типов запросов, заканчивая работой над производительностью. Очень уж интересно, какой производительности можно достигнуть при такой архитектуре, исследованием чего, возможно, я скоро и займусь.
Схема
Схема базируется на трех основных компонентах:
- бот-сервер: принимает сообщения с 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-бота.
В проекте можно очень много чего улучшить — начиная от добавления аутентификации, нормальной поддержки типов запросов, заканчивая работой над производительностью. Очень уж интересно, какой производительности можно достигнуть при такой архитектуре, исследованием чего, возможно, я скоро и займусь.