Pull to refresh

Демон для удаленного управления компьтером через e-mail

Python *
Sandbox
Здравствуй, Хабр!

В данной статье я хотел бы поделиться опытом в создании демона для удаленного управления компьютером командами через электронную почту.

Введение


В своей работе я использую множество удаленных машин. Зачастую доступ к ним ограничен фильтром по IP, поэтому приходится использовать длинные цепочки из хостов для входа на машину.
После прохождения в очередной раз этого квеста, ради выполнения пары команд, я понял, что нужно что-то менять. Конечно, самым простым решением было бы создать прямой SSH-тоннель и забыть про все трудности, но, во-первых, этому мешает строгая политика безопасности, а, во-вторых, хотелось бы иметь гибкую и независимую систему.

Со временем выработался ряд требований:
  • безопасность системы;
  • простой доступ к системе без лишних телодвижений (с телефона, чужого компьютера и т. д.);
  • история выполненных команд и результатов выполнения.

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

Реализация


В качестве языка программирования был выбран Python, критерием выбора послужила не только гибкость самого языка, но и давнее желание использовать его на практике.
Алгоритм программы довольно простой:
  1. Получение команд по E-Mail
  2. Выполнение команд
  3. Отправка результатов обратно пользователю

1. Получение команд по E-Mail

Для начала устанавливаем соединение с сервером, существует два варианта: POP3 или IMAP4. Выбор зависит как от поддерживаемых протоколов на почтовом сервере, так и от открытости портов на целевой машине.
Соединение с сервером для POP3 протокола
if is_enabled(self.get_param_str("Mail", "USE_SSL")):
    session = poplib.POP3_SSL(self.get_param_str("Mail", "POP_SERVER"),
                              self.get_param_int("Mail", "POP_SSL_PORT"))
else:
    session = poplib.POP3(self.get_param_str("Mail", "POP_SERVER"),
                          self.get_param_int("Mail", "POP_PORT"))
#if
if is_enabled(self.get_param_str("Debug", "NETWORK_COMM_LOGGING")):
    session.set_debuglevel(10)
#if
try:
    session.user(self.get_param_str("Mail", "EMAIL_USER"))
    session.pass_(self.get_param_str("Mail", "EMAIL_PASS"))
except poplib.error_proto as e:
    sys.stderr.write("Got an error while connecting to POP server: '%s'\n"  % (e))
    return False
#try
    

Соединение с сервером для IMAP4 протокола
if is_enabled(self.get_param_str("Mail", "USE_SSL")):
    session = imaplib.IMAP4_SSL(self.get_param_str("Mail", "IMAP_SERVER"),
                                self.get_param_int("Mail", "IMAP_SSL_PORT"))
else:
    session = imaplib.IMAP4(self.get_param_str("Mail", "IMAP_SERVER"),
                            self.get_param_int("Mail", "IMAP_PORT"))
#if
if is_enabled(self.get_param_str("Debug", "NETWORK_COMM_LOGGING")):
    session.debug = 10
#if
try:
    session.login(self.get_param_str("Mail", "EMAIL_USER"),
                  self.get_param_str("Mail", "EMAIL_PASS"))
except imaplib.IMAP4.error as e:
    sys.stderr.write("Got an error while connecting to IMAP server: '%s'\n" % (e))
    return False
#try
   


После того как соединение установлено, нужно отфильтровать из всех сообщений команды для нашего бота. Я решил использовать трехуровневую фильтрацию:
  • фильтрация по теме сообщения;
  • фильтрация отправителей по белым и черным спискам;
  • авторизация по login+password.

Алгоритм для фильтрации по теме в случае POP3 следующий: получить только заголовки сообщения, проверить поле «Subject:», если тема правильная — получаем сообщение полностью и передаем на дальнейшую обработку.
numMessages = len(session.list()[1])
for i in range(numMessages):
    m_parsed = Parser().parsestr("\n".join(session.top(i+1, 0)[1]))
    if self.get_param_str('Main', 'SUBJECT_CODE_PHRASE') == m_parsed['subject']:
        #Looks like valid cmd for bot, continue
        if self._process_msg("\n".join(session.retr(i+1)[1])):
            session.dele(i+1)
        #if
    #if
#for
    

В случае с IMAP все немного проще, протокол позволяет выполнять выборки на стороне сервера, т. е. нам достаточно указать тему, и сервер сам выдаст нам все подходящие письма.
session.select(self.get_param_str('Mail', 'IMAP_MAILBOX_NAME'))
typ, data = session.search(None,
                           'SUBJECT', self.get_param_str("Main", "SUBJECT_CODE_PHRASE"))
    


Следующий шаг — фильтрация отправителя по белым и черным спискам (можно использовать регулярные выражения)

И последний бастион — авторизация по паре login:password, которые должны идти в первой строке письма с командой.
На клиенте вместо паролей хранятся только md5 хеши.

Да, я понимаю, что это чересчур параноидально, с другой стороны, разве можно говорить об излишней паранойе в вопросах безопасности?

2. Выполнение команд

Так как потенциально выполнение некоторых команд может занять значительное время, то было решено каждую команду выполнять в отдельном процессе. Так же было введено ограничение сверху на количество активных процессов.
Минусом выполнения произвольных команд является возможность подвесить систему запуском интерактивной программы (mc, htop, etc). Пока что не придумал как с этим бороться.

3. Отправка результатов обратно пользователю

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

self.__send_lock.acquire()
if not msg is None:
    print "[%s] Sending response to '%s'" % (datetime.today().strftime('%d/%m/%y %H:%M'), email_from)
    recipients = [email_from, self.get_param_str('Mail', 'SEND_COPY_TO')]
    message = "%s%s%s\n%s" % ('From: %s \n' % (self.get_param_str('Main', 'BOT_NAME')),
                              'To: %s \n' % (email_from),
                              'Subject: Report %s \n' % (datetime.today().strftime('%d/%m/%y %H:%M')),
                               msg)
    # Currently in python SMTP_SSL is broken, so always using usual version
    session = smtplib.SMTP(self.get_param_str("Mail", "SMTP_SERVER"),
                           self.get_param_int("Mail", "SMTP_PORT"))
    if is_enabled(self.get_param_str("Debug", "NETWORK_COMM_LOGGING")):
        session.set_debuglevel(10)
    #if
    session.login(self.get_param_str("Mail", "EMAIL_USER"),
                  self.get_param_str("Mail", "EMAIL_PASS"))
    session.sendmail(self.get_param_str("Mail", "EMAIL_USER"),
                     recipients,
                     message)
    session.quit()
#if
self.__send_lock.release()
    


Для создания демона был использован вот этот класс.

Заключение


В качестве примера посылаем команду боту:


Спустя некоторое время видим ответ:


Код проекта доступен на github

Надеюсь что кому-нибудь данная информация будет полезна.

Спасибо за внимание, жду ваших комментариев.

UPD: исправлен баг связанный с некорректной обработкой multi-part сообщений, спасибо github юзеру megres.

UPD2: добавлена возможность выставления произвольного таймаута для комманды. Для использования необходимо добавить префикс ":time=x" перед коммандой, т.е. ":time=10 make", даст 10 секунда на сборку, а потом отстрелит.
Спасибо tanenn за идею.
Tags:
Hubs:
Total votes 60: ↑53 and ↓7 +46
Views 5.1K
Comments Comments 47