Бот для администрирования

16 Липня 2009

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

 
 
spirt40@gmail.com