Стаття з 6-того випуску журнала Хакер. Звичайно приємніше читати все в гарно зверстаному печатному вигляді ;)
[python] Демонстрация бота для администрирования from presidentua on Vimeo.
Пишем jabber-бота на Python'е для администрирования сервака
лейтенант Роман 'Spirit' Хоменко (http://tutamc.com)Джабер-бот должен быть у каждого ленивого сис.админа. Ведь это он спасет в экстремальных ситуациях, когда под рукой нету putty, а лишь джабер-клиент. В статье обсудим создание джабер-бота на Питоне.
Весна пришла, а с ней и выезды на природу с шашлыками. И пока я смотрел на костер, предвкушая начало отдыха, зазвонил телефон (в соответствии с приказами он должен быть всегда при каждом сотруднике милиции на случай тревоги или команды 'збор'), и был это полковник срочно вызывающий на работу, чтобы выдать ему на серваке новый пароль. Работы то на пару минут, но где же я в лесу достану комп с инетом... На этом отдых закончился :(. Вечером вспомнил, что в то время на телефоне был джабер-месенджер, и если бы на серваке установить джабер-бота, что может исполнять команды - таких ситуаций больше не повторится. Вот тогда-то перед глазами держались образы шашлыка и граненого стаканчика с кока-колой, а руки кодили джабер-бота. За несколько часов уже было готово ядро системы. Но разберем все по-порядку.
Python & Jabber
Данный бот будем реализовывать на Python'е. Ведь это единственный развивающийся (в отличие от Perl'а развитие которого де-факто прекращено) системный (в отличие от вебовского PHP) скриптовый язык програминга. Выбор протокола еще проще, ведь у джабера нету сейчас конкурентов для такого бота. К примеру, аська не подходит через отсутствие нативного шифрования и централизованность (ведь их сервак может не работать или начнут что-то там мутить со сменой протокола). Про джабер несколько инфы читай во врезке.
Юзать на Питоне джабер-протокол можно сложно и легко. Сложно это читая документацию по протоколу (размещенную по xmpp.org/rfcs/) и использовать сокеты. Этот вариант больше подходит, если цель также разобраться с протоколом. При интересе к этому советую посмотреть как сделан бот от eLWAux - исходники есть на диске. Легкий путь - это юзать уже написанные либы. Я сразу же выбрал легкий путь и полез в инет искать библиотеки. Оказалось что их много, вот некоторые:
- Twisted Words (http://twistedmatrix.com/projects/words/);
- jabber.py (http://jabberpy.sourceforge.net/);
- xmppppy (http://xmpppy.sourceforge.net/).
Выбор между ними сложный, но мне почему-то больше понравилась xmppppy от Алексея Нежданова. Вот ею и воспользуемся.
Архитектура
Итак, нам нужно написать джабер-бота, которому можно прислать команду, а он ее исполнит и вернет ответ. Но поскольку это все очень просто - усложним задачу и напишем полноценный бот с поддержкой плагинов, а один из плагинов как раз будет решать нашу задачу администрирования.
Система у нас будет состоять с бота (bot.py), файла конфигурации (config.ini), папки с плагинами (plugins) и библиотеки xmpppy. Бот будет иметь два типа плагинов, первые которые будут доступны всем, а вторые доступны лишь для админов внесенных в "белый" список или по паролю.
При проектировании системы с плагинами всегда встает вопрос об их полномочиях по управлению ботом. С одной стороны, чем меньше прав плагину даем, тем проще их писать, но этим ограничиваем возможности. Поэтому пусть плагины будут низкоуровневыми, но в каждом плагине можно сделать все что пожелаем с ботом.
Система конфигов
Програминг бота начнем с чтения конфига, что будет храниться в известном всем ini-формате в файле config.ini (кстати, амеры весело произноcят ini как "айнай"), где в секции connect разместим параметры доступа к аккаунту на котором будет висеть бот, а в секции permission - список юзеров, что имеют доступ к админке и пароль к админке.
Удобную работу с ini-файлами в Питоне обеспечиват библиотека ConfigParser. Для чтения параметров нам пригодятся две функции из нее. Первая, read - для чтения конфигурационного файла, имя которого передается как параметр. Вторая функция, get нужна чтобы достать какой-то параметр, она принимает секцию, где он находится, и имя параметра. Функция, что читает конфиг для бота такая:
def loadConfig():
import ConfigParser
config = ConfigParser.ConfigParser()
config.read('config.ini')
login = config.get('connect', 'login')
password = config.get('connect', 'password')
allow_password =
config.get('permission', 'allow_password')
user_no_pass =
config.get('permission', 'user_no_pass')
user_no_pass = user_no_pass.split(',')
return {'login':login,'password':password,
'allow_password':allow_password,
'user_no_pass':user_no_pass}
Как видим, когда считали переменную со списком юзеров, которым разрешен доступ в админку, то ее, методом split по запятой как разделителю, превращаем в список. Дальше все параметры возвращаем упакованными в ассоциативный массив (на языке Питона - словарь).
Теперь конфиг можно прочитать:
config = loadConfig()
Запуск бота
Перейдем к использованию xmpppy и запуску бота. Сперва создадим объект jid от xmpp.JID передав имя пользователя взятого с нашего загруженного конфига. Теперь создание главного объекта bot производится от xmpp.Client с передачей домена на котором находится юзер и пустым списком, чтобы на экран не выводилась отладочная информация (тру хакеры работают только методом научного тыка :).
jid = xmpp.JID(config['login'])
bot = xmpp.Client(jid.getDomain(),debug=[])
Для того чтобы иметь полный контроль над ботом в любой точке программы (тоесть и в главном цикле и в плагинах) будем передавать везде наш объект bot. Но ведь конфиг, список плагинов и другая служебная информация также может понадобиться (например, плагину help нужно знать какие плагины установлены), поэтому в объект bot сохраним всю информация что может быть интересна. Например, конфиг сохраним строкой:
bot.config = config
Теперь можно законектиться и пройти аутентификацию:
bot.connect()
bot.auth(jid.getNode(),bot.config['password'])
Прием сообщения в xmpppy реализуется через привязку функции к событию прихода сообщения. Сначала нужно создать функцию, к примеру, message, а потом методом bot.RegisterHandler зарегистрировать ее:
bot.RegisterHandler('message',message)
Теперь в цикле необходимо вызывать bot.Process(1) который принимает входящие сообщения и обрабатывает их. Но все же вечный цикл нам не всегда нужен, поэтому в нашем боте в свойстве online запишем 1 и сделаем цикл до тех пор пока переменная равна 1:
bot.online = 1
while bot.online:
bot.Process(1)
bot.disconnect()
Если же когда-то нужно остановить бота, то стоит присвоить свойству online 0 и бот корректно отсоединиться от сервера и завершит работу.
Теперь наш бот запущен и можем вернутся к написанию функции обработки входящих сообщений. Простейший вариант, который лишь примет сообщения, и в ответ напишет что-то такой:
def message(conn,mess):
global bot
if ( mess.getBody() == None ):
return
bot.send(xmpp.Message(mess.getFrom(),'hello'))
Как видим вначале мы командой "global bot" получаем доступ к объекту нашего боту. Дальше идет обработка входящего сообщения, где командой mess.getBody получим сообщение. Если входящее сообщение равно None, то значить, что пришла служебная команда, например про то что юзер что-то нам печатает, а такие сообщения обрабатывать не будет, поэтому просто выйдем с функции.
Дальше воспользуемся методом send отправим простое сообщение в ответ. Этот метод в качестве параметра принимает объект Message библиотеки xmpppy. При ее создании передаем два параметра: первый - кому нужно отправить сообщения, а второй - текст сообщения.
Плагины
Какая-то простая поддержка плагинов есть в самой библиотекой xmpppy, но лучше сделаем по своему - напишем свою архитектуру плагинов. Сначала определимся со структурой плагина. Пусть каждый должен иметь название такое, как имя команды на которую будет откликаться. К примеру, если мы хотим создать плагин, что на команду - "echo some text" посылает сообщение с этим же текстом, то плагин должен называться echo и размещаться в файл echo.py в каталоге plugins. В каждом плагине должны быть две функции. Одна, init, может проводить некую предварительную инициализацию и обязательно возвращать 1, если плагин можно использовать лишь админам, и 0, если всем юзерам. Вторая же обязательная функция run, которая в качестве входного параметра принимает ссылку на наш бот, и входящее сообщение. К примеру, плагин echo выглядит так:
import xmpp
def init():
return 0
def run(bot,mess):
bot.send(
xmpp.Message(mess.getFrom(),mess.getBody()))
Теперь напишем функцию что загрузит наши плагины. Поскольку решили, что они должны находится в папке plugins, то в этом каталоге поместим пустой файл __init__.py - такое требование Питон, что позволит эти файлы импортировать как модуль функцией __import__. Итак, нам нужно загрузить все файлы размещенные в папке plugins (кроме __init__.py) в какую-то переменную. К тому же во время инициализации создадим два списка, что бы потом знать что мы загрузили. В первом (public_commands) будут те плагины, которые можно запустить без авторизации, а во втором (commands) админские плагины. В качестве результата работы функции вернем ассоциативный массив с плагинами и списками командами.
def loadPlugins():
import os
commands = []
public_commands = []
#перебираем все файлы с папки plugins
for fname in os.listdir('plugins/'):
#если файл заканчивается на '.py'
if fname.endswith('.py'):
#обрезаем последнее 3 буквы
plugin_name = fname[:-3]
#если имя файла не '__init__'
if plugin_name != '__init__':
#загружаем плагин в переменную
plugins=__import__('plugins.'+plugin_name)
#достаем плагин с переменной
plugin = getattr(plugins,plugin_name)
#если плагин админский
if plugin.init():
commands.append(plugin_name)
else:
public_commands.append(plugin_name)
#возвращаем ассоциативный словарь
return {'plugins':plugins,'commands':commands,
'public_commands':public_commands}
Эта функция очень важна для понимания работы Питона по поддержке загрузки плагинов на лету - советую очень подробно в ней разобраться. Как видим, после загрузки в переменную plugins плагина, его оттуда можно вытянуть функцией getattr с указанием в первом параметре имя переменной, где хранятся плагины, а во втором параметре имя плагина. В результате функция возвращает сам плагин, из которого уже потом можно вызывать функции что находятся в нем, к примеру, в вышеуказаной функции загрузки плагинов мы запускаем с каждого из них функцию init и в зависимости от результата имя плагина добавляем или в список public_commands или в commands.
Теперь написанную загрузку плагинов можно заюзать и при этом записать результат в свойство бота:
bot.plugins = loadPlugins()
Для того чтобы вызывать наш плагин напишем маленькую функцию, что принимает имя команды, ссылку на бота и входящее сообщение:
def runPlugin(command,bot,mess):
plugin = getattr(bot.plugins['plugins'],command)
plugin.run(bot,mess)
Эта функция исходя из имени плагина (параметр command) вытаскивает его с bot.plugins['plugins'] и запускает функцию run.
Обработка сообщений
Осталось изменить функцию message, что обрабатывает входящие сообщения. Логика ее работы такая: она должна сначала выделить с входного сообщения команду (символы от начала до пробела), дальше посмотреть наличие команды в списке публичных команд и если ее нету, то нужно проверить авторизацию и наличии команды среди админских. Если команда нашлась, то запустить ее, в противном случае вернуть юзеру сообщение, что команды нет. Также замечу один момент, что имя пользователя которое возвращает команда mess.getFrom() не всегда возвратит правильное значение с точки зрения проверки авторизации, ведь там может быть имя ресурса (смотри врезку про Джабер) которое нам не нужно, поэтому ресурс мы должны обрезать, например, вызвать метод строки split('/') и потом взять первый элемент списка. Полностью функция есть во врезке.
Исполнение команд
А мы же совсем забыли о цели создания нашего бота. Ведь еще не написали плагин, что будет выполнять системные команды. Пусть он будет называться cmd. Его работа такая что во входящем сообщении, должен сначала обрезать начальные 4 символа (к примеру, 'cmd ls' -> 'ls'). Дальше команду передаем на цепочку функций os.popen(cmd).read(). Полученный результат переконвертируем при необходимости в utf-8 (при инициализации бота мы не указывали кодировку, а она по умолчании будет юникод). После возвратим пользователю результат. Вот что получилось:
import xmpp
import os
def init():
return 1
def run(bot,mess):
cmd = mess.getBody()
cmd = cmd[4:]
output = os.popen(cmd).read()
if not isinstance(output, unicode):
output = unicode(output,'utf-8','ignore')
bot.send(xmpp.Message(mess.getFrom(),output))
Happy end
На этом разработку бота можно считать завершенной. Мы реализовали все что хотели, да к тому же оставили задел для неограниченного расширения при необходимости в виде плагинов. На диске кроме вышеперечисленных плагинов, есть некоторые другие, советую их глянуть, а чтобы продемонстрировать запуск и работу бота снял видео. Если что-то будет не понятно - спрашивай.
Ссылки на объекты и переменные
В Питоне (да и в других языка, например в ПХП), есть один нюанс, про который часто забывают.
Вот есть пример работы с переменными:
a = 1; b = a; b = 2
print a #1
print b #2
Он выведет на экран сразу 1, а потом 2, и все это закономерно. Но посмотрим на такое же, только переменная будет в объекте:
class Obj():
def __init__(self):
pass
a = Obj(); a.var = 1; b = a; b.var = 2
print a.var #2
print b.var #2
И на экран выведется везде 2! А случается это потому что операция "=" при работе с объектами не копирует их как при других типах переменных, а создает ссылку. Вот именно используя данную фичу в боте переменную bot всегда обычно передавали в функции как бы по значению, но на самом деле все передавалось как ссылка.
Процедура обработки входящих сообщений
def message(conn,mess):
global bot
text = mess.getBody()
#если сообщение служебное - выходим
if ( text == None ):
return
#с входного сообщения достаем команду
command = text.split(' ')
command = command[0]
#если команда в списке публичных - запускаем
if command in bot.plugins['public_commands']:
#запускаем команду
runPlugin(command,bot,mess)
return
#достаем имя пользователя
user=mess.getFrom()
user=str(user).split('/')
user=user[0]
#если юзер не админ - говорим "команды нет"
if user not in bot.config['user_no_pass']:
text = "wrong command. try 'help'"
bot.send(xmpp.Message(mess.getFrom(),text))
return
#если команда есть в админских командах
if command in bot.plugins['commands']:
runPlugin(command,bot,mess)
else:
text = "wrong command. try 'help'"
bot.send(xmpp.Message(mess.getFrom(),text))
пару слово о Jabber
Jabber — система мгновенного обмена сообщениями и информацией о присутствии на основе открытого протокола XMPP. Проект Jabber был основан Джереми Миллером в начале 1998 года с разработки сервера jabberd. Сейчас есть некоторая непонятность в соотношении слов jabber и xmpp. Даже в английской википедии с jabber стоит переадресация на xmpp. Эта непонятность в первую очередь связана с тем, что под именем xmpp протокол был стандартизирован в IETF. Но думаю не стоит сильно с этим заморачиваться ;)
Каждый пользователь в джабер-сети имеет уникальный идентификатор — Jabber ID (сокращенно JID). Адрес JID, подобно адресу электронной почты, содержит имя пользователя и доменное имя сервера, на котором зарегистрирован пользователь, разделённые знаком @. Например, пользователь user, зарегистрированный на сервере example.com, будет иметь адрес: user@example.com.
Пользователь может иметь одновременно несколько подключений, для различения которых используется дополнительное значение JID, называемое ресурсом и добавляемое через слэш в конец адреса. К примеру, пусть полный адрес пользователя будет user@example.com/work, тогда сообщения, посланные на адрес user@example.com, дойдут на указанный адрес вне зависимости от имени ресурса, но сообщения для user@example.com/work дойдут на указанный адрес только при соответствующем подключенном ресурсе.
Библиотека xmpppy имеет много полезных объектов. Некоторые ниже кратно описаны с самыми полезными параметрами и методами.
объект JID для работы с Jabber ID. При создании принимает параметр - Jabber-идентификатор.
Методы:
- getDomain, возвращает домен;
- getNode, возвращаем имя пользователя;
- getResource, возвращает ресурс.
главный объект Client. При создании принимает домен джабер-сервера и переменную для отладочной информации.
Методы:
- connect, подключение к серверу;
- auth, авторизация, принимает параметры: имя пользователя и пароль;
- RegisterHandler, привязка функций к событиям, принимает параметры тип события(message, presence, iq) и имя функции;
- sendInitPresence, отправка начальных запросов, нужно запускать после авторизации;
- send, отправка сообщений, принимает объект Message;
- Process, запустить обработку входных сообщений;
- disconnect, отключиться от сервера.
Message - объект сообщения. Принимает параметры - имя юзера и текст сообщения
Методы:
- getBody, возвращает текст сообщения;
- getFrom, возвращает имя пользователя от кого сообщение.
