Daemon for remote control of a laptop via e-mail

Hello, Habr!

In this article, I would like to share my experience in creating a daemon for remotely controlling computer commands via email.

Introduction


In my work I use many remote machines. Often access to them is limited by an IP filter, so you have to use long chains from hosts to enter the machine.
After completing this quest once again, in order to complete a couple of commands, I realized that something needs to be changed. Of course, the simplest solution would be to create a direct SSH tunnel and forget about all the difficulties, but, firstly, this is hampered by a strict security policy, and secondly, I would like to have a flexible and independent system.

Over time, a number of requirements developed:
  • system security;
  • easy access to the system without unnecessary gestures (from the phone, someone else's computer, etc.);
  • history of executed commands and results of execution.

After a detailed analysis of the requirements, I came to the conclusion that the bot that works through e-mail is the most suitable solution. After all, it is very convenient. Nowadays, you can send E-mail from any device that has a screen and keyboard (computer, phone, Kindle, even televisions already know how to do this). 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, the selection criterion was not only the flexibility of the language itself, but also a long-standing desire to use it in practice.
The program algorithm is quite simple:
  1. Receiving E-Mail Commands
  2. Command execution
  3. Sending results back to user

1. Receiving commands by e-mail

First, we establish a connection to the server, there are two options: POP3 or IMAP4. The choice depends on the supported protocols on the mail server, as well as on the open ports on the target machine.
Server connection 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"))
#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
    

Server connection 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"))
#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
   


After the connection is established, you need to filter from all messages the commands for our bot. I decided to use three-level filtering:
  • filtering by subject of the message;
  • filtering senders according to white and black lists;
  • login + password authorization.

The algorithm for filtering by topic in the case of POP3 is as follows: get only the message headers, check the “Subject:” field, if the topic is correct, we get the message completely and send it for 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']:
        #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
    

In the case of IMAP, everything is a little simpler, the protocol allows you to perform selections on the server side, that is, we just need to specify the subject, and the server itself 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 filtering the sender by white and black lists (regular expressions can be used)

And the last bastion is authorization by the login: password pair, which should go in the first line of the command letter.
Instead of passwords, only md5 hashes are stored on the client.

Yes, I understand that this is too paranoid, on the other hand, can we talk about excessive paranoia in security matters?

2. Execution of commands

Since potentially the execution of some commands can take considerable time, it was decided to execute each command in a separate process. A restriction was also introduced from above on the number of active processes.
The disadvantage of executing arbitrary commands is the ability to suspend the system by running an interactive program (mc, htop, etc). I have not yet figured out how to deal with this.

3. Sending the results back to the user

After the user command completes, the user will be sent a report containing all the output of the commands and return code.
The smtplib module is used for sending

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()
    


This class was used to create the daemon .

Conclusion


As an example, we send a command to the bot:


After some time, we see the answer:


The 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 github to user megres.

UPD2: added the ability to set an arbitrary timeout for the command. To use it is necessary to add the prefix ": time = x" before the command, i.e. ": time = 10 make", will give 10 seconds to build, and then shoot.
Thanks tanenn for the idea.

Also popular now: