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

23 Jun 2009
Posted by spirit

Python. Демонстрація до статті "Python bot" from presidentua on Vimeo.

моя стаття з журналу Хакер

Пишем jabber бота на Python для администирирования компом

лейтенант Роман Хоменко aka Spirit (http://tutamc.com)

Вступление

Весна пришла, а с ней и выезды на природу с шашликами. И однажды пока я смотрел на костер, предвкушая начало отдыха, зазвонил телефон, и был это мой начальник срочно вызывающий на роботу, чтобы выдать ему на серваке новый пароль. Так и закончился для меня этот отдых. Как я пожалел, что у меня на телефоне лишь джабер-клиент установлен. Чтобы такого не повторилось было решено написать джабер-бот который могбы исполнять команды на серваке, что бы в любой момент с обычного телефона с установленным джабер-клиентом возможно было решить все проблемы, что могут возникнуть. О создании этого бота я расскажу тебе.

Python & Jabber

Выбор Python'а для реализации бота и джабера в качестве протокола не был сложен. Ведь python это единственный живой(в отличии от Perl'а развитие которого прекращено дефакто) системный(в отличии от вебовского PHP) скриптовый язык програминга. А с протоколов, то джаберовский конкурент аська в нашем случае абсолютно не подходит через отсуствие нативного шифрования, централизованность (ведь ихний сервак может не работать, да и могут начать что-то там мутить со сменой протокола). Про джабер протокол несколько инфы я написал во врезке. Юзать на Питоне джабер-протокол можно сложно и легко. Сложно это читая документацию по протоколу(размещенную по ) и использовать сокеты. Этот вариант больше подходит, если цель также разобраться с протоколом, и если это тебе интересно, то можешь посмотреть как сделан бот от elWaus - исходники есть на диске. Но я быбрал легкий путь и полез в инет искать какие есть библиотеки. Оказалось их много:

Но выбор между ними сложный, но мне почему-то больше понравилась xmppppy от Алексея Нежданова. Вот ею и воспользуется.

Архитектура

Исходя из задачи, нам нужно написать джабер-бота которому можно прислать команду, а он ее исполнит и вернет ответ. Но это все очень просто. Давайте усложним задачу и напишем полноценный бот с поддержкой плагинов, а один с плагинов как раз будет решать нашу задачу администрирования. Система у нас будет состоять с самого бота (bot.py), файла конфигурации (config.ini), папки с плагинами (plugins) и самой библиотеки xmpp. Бот будет имень два типа плагинов, первые которые будут доступны всем, а второй тип плагинов доступны лишь дла админов которые внесены в "белый" список или по паролю.

При проектированию системы с плагинами всегда встает вопрос о том, сколько плагину давать полномочий по управлению самим ботом. С одной стороны, чем меньше прав плагину даем, тем проще их писать, но при этом ограничиваем их возможности. Поэтому пусть плагины будут низко-уровневыми, но зато в каждом плагине можно сделать все что пожелаем.

Система конфигов

Все параметры мы будем хранить в файле config.ini (кстати, амеры весело произноcять ini как "айнай"). Формат ini-файлов простой и исходя с того что нужно хранить параметры доступа к аккаунту, список юзеров что имеют доступ к админке и пароль доступа к админке файл конфигом будет таким:
[connect]
login = spirit@thesecure.biz
password = zaqwesdc
[permission]
allow_password = 123
user_no_pass = user1@a.com,user2@b.com
Для удобной работы с 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()

Запуск бота

Наконец-то перейдем к запуску самого бота. Создадим обьект jid от xmpp.JID передав ему имя пользователя взятого с нашего загруженого конфига. Теперь создание главного обьекта bot производится от xmpp.Client с передачей ей домена на котором находится юзер и пустым списком, чтобы на экран не выводилась отладочная информация (мы же тру хакеры и работаем методом научного тыка):

jid = xmpp.JID(config['login'])
bot = xmpp.Client(jid.getDomain(),debug=[])
Один ньюанс, для того чтобы иметь полный контроль над ботом в любой точке програмы везде нужно передавать наш обьект bot. Но ведь конфиг, список плагинов и другая служебная информация также может понадобиться, поэтому я предлагаю в сам обьект bot сохранить всю информация что может быть интересна. Например конфиг сохраним строкой:
bot.config = config
Теперь можно законектися и пройти аутентификацию:

Прием сообщения реализуется через привязку функции к событию прихода сообщения. Тоесть сначала нужно создать функцию, к примеру, message, а потом методом bot.RegisterHandler зарегистрировать ее:

bot.RegisterHandler('message',message)
Дальше идет инициализация данных:
bot.sendInitPresence()

Теперь в цикле необходимо вызывать bot.Process(1) которая принимает входящие сообщения и обрабатывает их. Но все же вечный цикл нам не всегда нужен, поэтому в нашем боте в свойстве online запишем 1 и сделаем цикл до тех пор пока переменная равна 0. Для завершения работы нужно вызвать метод disconnect:

bot.online = 1
while bot.online:
  bot.Process(1)
bot.disconnect()
Теперь наш бот запущен и можем вернутся к написанию функции обработки входящих сообщений. Простейший вариант, который лишь примет сообщения, и в ответ напишет что-то будет такой:
def message(conn,mess):
  global bot
  if ( mess.getBody() == None ):
    return
  bot.send(xmpp.Message(mess.getFrom(),'hello'))

Как видим в начале мы командой "global bot" получаем доступ к обьекту нашего боту. Дальше с идет обработка входяшего сообщения, где как видим командой mess.getBody получем сообщение. Если входящее сообщение равно None, то значить, что пришла служебная команда, например про то что юзер что-то нам печатает, а такие сообщения мы обрабатывать не будет, поэтому просто выйдем с функции. Дальше возпользуемя методом send отправим простое сообщение в ответ. Этот метод в качестве параметра принимает сформированую xml строку. А ее поможет сформировать функция xmpp.Message, которая принимает два параметра: первый - кому нужно отправить сообщения, а второй - текст сообщения. Юзера что прислал нам сообщения достаем командой mess.getFrom().

Плагины

Какая-то поддержка плагинов есть в самой библиотекой xmpp, но мне лень было с ней разбираться. Поэтому решил сделать по своему - пусть каждый плагин должен быть иметь название как имя команды на которую он будет откликаться. К примеру, если мы хотим создать плагин, который на команду - "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) в какую-то переменную. К тему же во время инициализизации создадим два списка в первом будет те плагины которые можно запустить без авторизации, а во втором админские плагины. Для того чтобы потом когда юзер вводит команду легко проверить ее присуствие. А в качестве результата работы функции вернем асоциативный масив с плагинами и списками со всеми командами.

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)

Которая исходя из имени плагина вытаскивает его с переменной plugins и запускает функцию run с плагина. Эти разработку поддержки можно считать почти завершенным, и осталось лишь изменить функцию message что обрабатывает входящие сообщение.

Обработка сообщений

Возвратимся к логике роботы функции message, такая, что она должна сначала выделить с входного сообщения команду (символы от начала до пробела), дальше посмотреть наличие команды в списке публичных команд и если есть, то запустить ее. Если ее нету, то нужно проверить авторизацию и при успехе запустить и наличии команды среди админских - запустить ее. Если же команды нету, то вернем юзеру сообщение, что такой команды нет. Всю функцию можеш посмотреть во врезке. Также замечу один момент, что имя пользователя которое возвращает команда mess.getFrom() не всегда возвратит правильно с нашей точки зрения значения, там формат такой, что сначала идет логин юзера, дальше слеш и дополнительная информация, которая нам не нужная, поэтому ее мы должны обрезать. Теперь имеем полностью рабочее ядро, но все же оно не полноценно без существования двох плагинов:

  • pass (для входа в админку)
  • exit (для выхода из админки)

Исполнение команд

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

На этом разработку нашего бота можна завршить. Мы реализовали все что хотели, да к тому же оставили задел для неограниченого расширения. На диске кроме више перечисленых плагинов, есть некоторые другие, советую глянуть, а чтобы продемонстировать запуск и роботу бота снял видео, котороя также есть на нашем диске.

В чем преимущество Python'a, так это в скорости разработки, ведь даную систему писал несколько часов. Так что дерзай мой друг. Будут вопросы - пиши, координаты на сайте http://tutamc.com



Процедура обработки входящих сообщений

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

Ссылки

 
 
 

Contacts

Роман Хоменко aka PresidentUA
mail/jabber: spirt40@gmail.com

Creative Commons License