Hello, Habr!
In this article, I would like to share my experience in creating a daemon for remote control of a computer via e-mail.
Introduction
In my work, I use many remote machines. Often, access to them is limited by IP filter, so you have to use long chains from hosts to enter the machine.
After completing this quest once again, in order to fulfill a couple of teams, I realized that I needed to change something. Of course, the simplest solution would be to create a direct SSH tunnel and forget about all the difficulties, but, firstly, this is hindered by a strict security policy, and, secondly, I would like to have a flexible and independent system.
Over time, a number of requirements emerged:
- system security;
- simple access to the system without unnecessary gestures (from a telephone, someone else's computer, etc.);
- history of executed commands and execution results.
After a detailed analysis of the requirements, I came to the conclusion that the most suitable solution is a bot that works through email. After all, it is very convenient. Nowadays, you can send E-mail from any device that has a screen and a keyboard (computer, phone, Kindle, even TVs already know how to do it). The list of letters is always available on the server and you can conveniently analyze the change in the state of the system.
Among the ready-made solutions, nothing suitable was found, so I decided to do it myself.
')
Implementation
Python was chosen as the programming language; not only the flexibility of the language itself, but also the long-standing desire to use it in practice served as the selection criterion.
The program algorithm is quite simple:
- Receive commands by e-mail
- Command execution
- Send results back to user
1. Receive commands by e-mail
To begin with we establish connection with the server, there are two options: POP3 or IMAP4. The choice depends on the supported protocols on the mail server and on the openness of the ports on the target machine.
Connection to server for POP3 protocol
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"))
Connection to server for IMAP4 protocol
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"))
After the connection is established, you need to filter out of all messages commands for our bot. I decided to use three-level filtering:
- filtering by subject;
- filtering senders by white and black lists;
- login + password authorization.
The algorithm for filtering by topic in the case of POP3 is the following: receive only message headers, check the “Subject:” field, if the topic is correct, we receive the message completely and pass it on to further processing.
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']:
In the case of IMAP, everything is a bit simpler, the protocol allows you to perform selections on the server side, i.e., it is enough for us to specify the subject, and the server will give us all the appropriate letters.
session.select(self.get_param_str('Mail', 'IMAP_MAILBOX_NAME')) typ, data = session.search(None, 'SUBJECT', self.get_param_str("Main", "SUBJECT_CODE_PHRASE"))
The next step is to filter the sender by white and black lists (regular expressions can be used)
And the last bastion is authorization for a pair of login: password, which should go in the first line of the letter with the command.
On the client, instead of passwords, only md5 hashes are stored.
Yes, I understand that this is too paranoid, on the other hand, is it possible to talk about excessive paranoia in matters of security?
2. Execution of commands
Since potentially the execution of some commands may take considerable time, it was decided to execute each command in a separate process. It was also introduced a top limit on the number of active processes.
The disadvantage of executing arbitrary commands is the ability to suspend the system by launching an interactive program (mc, htop, etc). So far, I have not figured out how to deal with this.
3. Send results back to user
After the user command completes, a report will be sent to the user containing all command output and the return code.
For sending the smtplib module is used.
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)
This class was used to create the daemon.
Conclusion
As an example, send a command to the bot:

After some time we see the answer:
Project code is available on github
I hope that this information will be useful to someone.
Thank you for your attention, waiting for your comments.
UPD: fixed a bug related to incorrect processing of multi-part messages, thanks to github user megres.
UPD2: added the ability to set an arbitrary timeout for the command. To use, you must add the prefix ": time = x" before the command, i.e. ": time = 10 make", will give 10 seconds to build, and then shoot it off.
Thank you
tanenn for the idea.