Не так давно была опубликована статья про 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-бота.
В проекте можно очень много чего улучшить — начиная от добавления аутентификации, нормальной поддержки типов запросов, заканчивая работой над производительностью. Очень уж интересно, какой производительности можно достигнуть при такой архитектуре, исследованием чего, возможно, я скоро и займусь.