Avoid giving out bridges with broken tor versions.
[tor-bridgedb.git] / scripts / nagios-email-check
blob51c63a174c0f427c9c8d957cbfbf39e6af3d5cbb
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
4 # This file is part of  BridgeDB, a Tor bridge distribution system.
6 # This script sends an email request for bridges to BridgeDB and then checks if
7 # it got a response.  The result is written to STATUS_FILE, which is consumed
8 # by Nagios.  Whenever BridgeDB fails to respond with bridges, we will get a
9 # Nagios alert.
11 # Run this script via crontab every three hours as follows:
12 #   0 */3 * * * path/to/nagios-email-check $(cat path/to/gmail.key)
14 # You can provide the Gmail key as an argument (as exemplified above) or by
15 # using the environment variable BRIDGEDB_GMAIL_KEY, e.g.:
16 #   BRIDGEDB_GMAIL_KEY=foo path/to/nagios-email-check $(cat path/to/gmail.key)
18 import os
19 import sys
20 import smtplib
21 import time
22 import imaplib
23 import email
24 import email.utils
26 # Standard Nagios return codes
27 OK, WARNING, CRITICAL, UNKNOWN = range(4)
29 FROM_EMAIL = "testbridgestorbrowser@gmail.com"
30 TO_EMAIL = "bridges@torproject.org"
31 SMTP_SERVER = "imap.gmail.com"
32 SMTP_PORT = 993
34 MESSAGE_FROM = TO_EMAIL
35 MESSAGE_BODY = "Here are your bridges:"
37 STATUS_FILE = "/srv/bridges.torproject.org/check/status"
39 # This will contain our test email's message ID.  We later make sure that this
40 # message ID is referenced in the In-Reply-To header of BridgeDB's response.
41 MESSAGE_ID = None
44 def log(*args, **kwargs):
45     """
46     Generic log function.
47     """
49     print("[+]", *args, file=sys.stderr, **kwargs)
52 def get_email_response(password):
53     """
54     Open our Gmail inbox and see if we got a response.
55     """
57     log("Checking for email response.")
58     mail = imaplib.IMAP4_SSL(SMTP_SERVER)
59     try:
60         mail.login(FROM_EMAIL, password)
61     except Exception as e:
62         return WARNING, str(e)
64     mail.select("INBOX")
66     _, data = mail.search(None, "ALL")
67     email_ids = data[0].split()
68     if len(email_ids) == 0:
69         log("Found no response.")
70         return CRITICAL, "No emails from BridgeDB found"
72     return check_email(mail, email_ids)
75 def check_email(mail, email_ids):
76     """
77     Check if we got our expected email response.
78     """
80     log("Checking {:,} emails.".format(len(email_ids)))
81     for email_id in email_ids:
82         _, data = mail.fetch(email_id, "(RFC822)")
84         # The variable `data` contains the full email object fetched by imaplib
85         # <https://docs.python.org/3/library/imaplib.html#imaplib.IMAP4.fetch>
86         # We are only interested in the response part containing the email
87         # envelope.
88         for response_part in data:
89             if isinstance(response_part, tuple):
90                 m = str(response_part[1], "utf-8")
91                 msg = email.message_from_string(m)
92                 email_from = "{}".format(msg["From"])
93                 email_body = "{}".format(msg.as_string())
94                 email_reply_to = "{}".format(msg["In-Reply-To"])
96                 if (MESSAGE_FROM == email_from) and \
97                    (MESSAGE_BODY in email_body) and \
98                    (MESSAGE_ID == email_reply_to):
99                     mail.store(email_id, '+X-GM-LABELS', '\\Trash')
100                     mail.expunge()
101                     mail.close()
102                     mail.logout()
103                     log("Found correct response (referencing {})."
104                         .format(MESSAGE_ID))
105                     return OK, "BridgeDB's email responder works"
106                 else:
107                     mail.store(email_id, '+X-GM-LABELS', '\\Trash')
108     mail.expunge()
109     mail.close()
110     mail.logout()
111     log("Found no response.")
112     return WARNING, "No emails from BridgeDB found"
115 def send_email_request(password):
116     """
117     Attempt to send a bridge request over Gmail.
118     """
120     subject = "Bridges"
121     body = "get bridges"
123     log("Sending email.")
124     global MESSAGE_ID
125     MESSAGE_ID = email.utils.make_msgid(idstring="test-bridgedb",
126                                         domain="gmail.com")
127     email_text = "From: %s\r\nTo: %s\r\nMessage-ID: %s\r\nSubject: %s\r\n" \
128                  "\r\n%s" % (FROM_EMAIL, TO_EMAIL, MESSAGE_ID, subject, body)
130     try:
131         mail = smtplib.SMTP_SSL("smtp.gmail.com", 465)
132         mail.login(FROM_EMAIL, password)
133         mail.sendmail(FROM_EMAIL, TO_EMAIL, email_text)
134         mail.close()
135         log("Email successfully sent (message ID: %s)." % MESSAGE_ID)
136         return OK, "Sent email bridge request"
137     except Exception as e:
138         log("Error while sending email: %s" % err)
139         return UNKNOWN, str(e)
142 def write_status_file(status, message):
143     """
144     Write the given `status` and `message` to our Nagios status file.
145     """
147     codes = {
148         0: "OK",
149         1: "WARNING",
150         2: "CRITICAL",
151         3: "UNKNOWN"
152     }
153     code = codes.get(status, UNKNOWN)
155     with open(STATUS_FILE, "w") as fd:
156         fd.write("{}\n{}: {}\n".format(code, status, message))
157     log("Wrote status='%s', message='%s' to status file." % (status, message))
160 if __name__ == "__main__":
161     status, message = None, None
163     # Our Gmail password should be in sys.argv[1].
165     if len(sys.argv) == 2:
166         password = sys.argv[1]
167     else:
169         # Try to find our password in an environment variable.
171         try:
172             password = os.environ["BRIDGEDB_GMAIL_KEY"]
173         except KeyError:
174             log("No email password provided.")
175             write_status_file(UNKNOWN, "No email password provided")
176             sys.exit(1)
178     # Send an email request to BridgeDB.
180     try:
181         status, message = send_email_request(password)
182     except Exception as e:
183         write_status_file(UNKNOWN, repr(e))
184         sys.exit(1)
186     wait_time = 60
187     log("Waiting %d seconds for a response." % wait_time)
188     time.sleep(wait_time)
190     # Check if we've received an email response.
192     try:
193         status, message = get_email_response(password)
194     except KeyboardInterrupt:
195         status, message = CRITICAL, "Caught Control-C..."
196     except Exception as e:
197         status = CRITICAL
198         message = repr(e)
199     finally:
200         write_status_file(status, message)
201         sys.exit(status)