Retrieving tor bridges from GMail using Python

The previous article covered installation and configuration tor in OS Linux Mint 21.3and also considered several ways to obtain bridges obfs4 by using site and telegrams bot. A question was asked in the comments about automatically receiving and updating bridge data. If you use the previously discussed methods, you most likely will not be able to obtain bridges automatically. But there is another, quite interesting and not very often used method, which we will consider in this article in a little more detail.

Another alternative way to get bridges obfs4 is to send an email to bridges@torproject.org. Here's what the website says about it:

The only limitation in this method is that you receive bridges no more than once every 3 hours. That is, if you made a request for bridges and received them safely, then the next request can be made only after three hours. All subsequent messages before the expiration of this time interval will be simply ignored. And it’s most likely not worth flooding the server with unnecessary requests. He may decide that the letters from your mailbox are spam and it will no longer be possible to receive bridges using this method.

The first email service is not available to us. And you can only get there by roundabout routes. But there is also GMail, which, so far, is functioning successfully for us. Therefore, some alternative is available – without an alternative, which we will use.

Next comes the crooked Python code.

Retrieving an application password

Since we will be using python to send and receive mail using a script, we need to obtain the application password. How to do this is described in sufficient detail in Google Help, therefore, I think there is no need to dwell on this step in the article. Once you have received the application password, you can move on.

Installing external libraries

For the script to work, you will need to install third-party libraries. They can be installed using the command:

pip install python-decouple opencv-python pyzbar IMAPClient

Since we will use the application password, as well as the login from the Google account, it is advisable not to use it in the code. Therefore, it is necessary to create a “.env” file with approximate content:

login = "test@gmail.com"
pwd = "gffg sbbp jccj vhhi"

In order to read data from this file, we need the decouple library. To read an image with a QR code, and we will use it, since it is more convenient than parsing the body of the letter, you will need the opencv library. Well, in order to read the data encoded in a QR code, we use the pyzbar library. Instead of the imap client that comes with Python out of the box, we will use the third-party library IMAPClient, with which it is more convenient to work with messages on the mail server.

Importing libraries

After the libraries are installed, import them and other libraries necessary for work into the script:

import email
import platform
import re
import shutil
import smtplib
from socket import gaierror
import subprocess
import time
from datetime import datetime
from os import getuid

import cv2
from decouple import config
from imapclient import IMAPClient
from pyzbar.pyzbar import decode

Reading data from a QR code

Let's create a function qr_read(path), which gets the path to the image with the code. We will open and convert it to ndarray using cv2, after which we will immediately pass it for decoding to the decode function of the pyzbar library.

def qr_read(path):
    try:
        if img := decode(cv2.imread(path)):
            briges = re.sub("['\[\]]", "", img[0][0].decode('utf-8')).split(", ")
            return briges if briges else False
        return False
    except TypeError:
        return False

Let's handle the exception if we can't read the image and return False from the function. We also convert the resulting bridged string, which looks like a list but is not, into a list by removing extra characters from it and splitting it with a comma and a space, and then returning it from the function.

Sending letter

Let's write a function mail_send(), which will send a letter to the specified address with text to receive bridges. Here we will use the standard smtplib library with SSL encryption. We take the password and login from the “.env” file, and use config(“login”), config(“pwd”) to substitute it into the authorization function. In the case of Gmail, the login and email address will be the same (in my case). We log in and send the contents of message. Then we close the connection, display a message that the letter has been sent and return the sending time from the function. It will be needed in the future.

def mail_send():
    try:
        s = smtplib.SMTP_SSL("smtp.gmail.com", 465)
        s.login(config("login"), config("pwd"))
        message = "\r\n".join([
            f"From:{config('login')}",
            f"To: bridges@torproject.org",
            f"Subject: ",
            "",
            '"get transport obfs4"'])
        s.sendmail(config('login'), "bridges@torproject.org", message)
        s.quit()
        return datetime.utcnow().time()
    except gaierror:
        exit('Ошибка соединения')
    except smtplib. SMTPAuthenticationError:
        exit("Ошибка аутентификации. Проверьте логин или пароль")

Reading mail

In order to receive the letter we need and take bridges from it, we need to obtain the contents of the mailbox. The IMAPClient library will help us with this.

We specify the address of the imap server, establish a connection by specifying the login and password. Select the “INBOX” folder to search for letters. We are looking for unread messages, the “UNSEEN” flag from the address “bridges@torproject.org”. In a loop we iterate through the messages found. Using the email library, we convert the message into a form more convenient for parsing. We get the date and time of the message. Here you need to get the current date. It should be noted that the date in the messages is indicated in GMT. That is, we need to get the date our message was sent to the Tor server, as well as the current date with a given offset.

Next, we compare the dates of the message we sent and the message received from the server. If the date of the message from the server and the date of sending coincide, we check the time of the message. It must be greater than the time we sent our message to the server. If all conditions are met, we iterate through the message and take the name of the attachment from it, in our case, the attachment is a QR code and save it to disk in the folder with the script. We return the name of the saved attachment from the function.

If the attempt to receive mail was unsuccessful, return False.

def mail_read(send_time):
    with IMAPClient(host="imap.gmail.com") as client:
        client.login(config('login'), config("pwd"))
        client.select_folder('INBOX')
        messages = client.search(["UNSEEN", ['FROM', 'bridges@torproject.org']])
        for uid, message_data in client.fetch(messages, "RFC822").items():
            email_message = email.message_from_bytes(message_data[b"RFC822"])
            mail_date = datetime.strptime(email_message.get("Date"), "%a, %d %b %Y %H:%M:%S %z").date()
            mail_time = datetime.strptime(email_message.get("Date"), "%a, %d %b %Y %H:%M:%S %z").time()
            now_date = datetime.utcnow().date()
            if mail_date == now_date and mail_time > send_time:
                for ct in email_message.walk():
                    if filename := ct.get_filename():
                        with open(filename, 'wb') as file:
                            file.write(ct.get_payload(decode=True))
                        return filename
            return False

Copying torrc settings

If we received the email and also extracted the bridges from the QR code, we need to replace them in the tor settings file. But before we do that, let's make a backup copy. Suddenly, something goes wrong. Therefore, we create a function copy_config(), which will copy the settings file into exactly the same one, with the “.bak” extension.

def copy_config():
    try:
        shutil.copy2("/etc/tor/torrc", "/etc/tor/torrc.bak")
        return True
    except Exception as e:
        print(e)
        return False

Updating the Settings File

After we have made a copy of the settings, we open the file for reading. We read it line by line and add it to the list, skipping the lines that start with “Bridge” and “UseBridges”, since we will add new ones instead. After this, we add the resulting bridges to the resulting list, as well as the line “UseBridges”. We save the resulting list to a settings file.

def read_write_config(bridges):
    with open("/etc/tor/torrc", "r", encoding="utf-8") as torrc:
        new_torrc = [x.strip() for x in torrc.readlines() if not x.startswith(("Bridge", "UseBridges")) and x.strip()]
        new_torrc.extend([f'Bridge {bridge}' for bridge in bridges])
        new_torrc.append("UseBridges 1")
    with open("/etc/tor/torrc", "w", encoding="utf-8") as n_torrc:
        n_torrc.write("\n".join(new_torrc))

main function

Since the code will change the settings of the system service, it is required that the script be run as a superuser. First, let's check if this is true. If not, we inform the user and complete the work. Also, if the OS is not Linux, we terminate the script.

Then we send the mail and get the sending time. We pause for 20 seconds. Then we launch an endless loop that will periodically check the mail for the necessary response letter. Here I am not sure about the waiting time until the next request for the contents of the box. I think the time needs to be increased from 20 to a higher value. Since you shouldn’t contact the Google server too often either.

After the letter is received, we read the data from the QR code, make a copy of the settings file, update it and save the new values. After this, we restart the service and display its status in the terminal.

def main():
    if platform.system() == "Linux":
        if not getuid() == 0:
            exit('\n[!] Запустите скрипт с правами суперпользователя!\n')

        print("Отправка письма")
        send_time = mail_send()
        print("Письмо отправлено")
        print(f"\rПолучаем почту", end="")
        time.sleep(20)
        i = True
        while i:
            if filename := mail_read(send_time):
                print(f"\rПочта получена", end="")
                print("\nQR-код сохранен")
                if bridges := qr_read(filename):
                    print(f"Мосты получены:\n  {bridges}")
                    print("Делаем резервную копию torrc")
                    if copy_config():
                        print("Резервная копия torrc сохранена\nМеняем мосты")
                        read_write_config(bridges)
                        print("Мосты torrc изменены")
                        subprocess.call("sudo /etc/init.d/tor restart", shell=True)
                        subprocess.call("/etc/init.d/tor status", shell=True)
                else:
                    print("\nНе удалось прочитать мосты из QR-кода")
                i = False
            time.sleep(20)
    else:
        exit('\n[!] Данный скрипт работает только в ОС Linux\n')


if __name__ == "__main__":
    main()

In general, what happened is what happened. Testing this script is complicated by the fact that you can receive emails with bridges once every 3 hours. And if something goes wrong, you have to wait for the next time.

However, the script runs successfully. And in the end we get the following output:

"""
pip install python-decouple opencv-python pyzbar IMAPClient
"""
import email
import platform
import re
import shutil
import smtplib
from socket import gaierror
import subprocess
import time
from datetime import datetime
from os import getuid

import cv2
from decouple import config
from imapclient import IMAPClient
from pyzbar.pyzbar import decode


def qr_read(path):
    try:
        if img := decode(cv2.imread(path)):
            briges = re.sub("['\[\]]", "", img[0][0].decode('utf-8')).split(", ")
            return briges if briges else False
        return False
    except TypeError:
        return False


def mail_send():
    try:
        s = smtplib.SMTP_SSL("smtp.gmail.com", 465)
        s.login(config("login"), config("pwd"))
        message = "\r\n".join([
            f"From:{config('login')}",
            f"To: bridges@torproject.org",
            f"Subject: ",
            "",
            '"get transport obfs4"'])
        s.sendmail(config('login'), "bridges@torproject.org", message)
        s.quit()
        return datetime.utcnow().time()
    except gaierror:
        exit('Ошибка соединения')
    except smtplib. SMTPAuthenticationError:
        exit("Ошибка аутентификации. Проверьте логин или пароль")


def mail_read(send_time):
    with IMAPClient(host="imap.gmail.com") as client:
        client.login(config('login'), config("pwd"))
        client.select_folder('INBOX')
        messages = client.search(["UNSEEN", ['FROM', 'bridges@torproject.org']])
        for uid, message_data in client.fetch(messages, "RFC822").items():
            email_message = email.message_from_bytes(message_data[b"RFC822"])
            mail_date = datetime.strptime(email_message.get("Date"), "%a, %d %b %Y %H:%M:%S %z").date()
            mail_time = datetime.strptime(email_message.get("Date"), "%a, %d %b %Y %H:%M:%S %z").time()
            now_date = datetime.utcnow().date()
            if mail_date == now_date and mail_time > send_time:
                for ct in email_message.walk():
                    if filename := ct.get_filename():
                        with open(filename, 'wb') as file:
                            file.write(ct.get_payload(decode=True))
                        return filename
            return False


def copy_config():
    try:
        shutil.copy2("/etc/tor/torrc", "/etc/tor/torrc.bak")
        return True
    except Exception as e:
        print(e)
        return False


def read_write_config(bridges):
    with open("/etc/tor/torrc", "r", encoding="utf-8") as torrc:
        new_torrc = [x.strip() for x in torrc.readlines() if not x.startswith(("Bridge", "UseBridges")) and x.strip()]
        new_torrc.extend([f'Bridge {bridge}' for bridge in bridges])
        new_torrc.append("UseBridges 1")
    with open("/etc/tor/torrc", "w", encoding="utf-8") as n_torrc:
        n_torrc.write("\n".join(new_torrc))


def main():
    if platform.system() == "Linux":
        if not getuid() == 0:
            exit('\n[!] Запустите скрипт с правами суперпользователя!\n')

        print("Отправка письма")
        send_time = mail_send()
        print("Письмо отправлено")
        print(f"\rПолучаем почту", end="")
        time.sleep(20)
        i = True
        while i:
            if filename := mail_read(send_time):
                print(f"\rПочта получена", end="")
                print("\nQR-код сохранен")
                if bridges := qr_read(filename):
                    print(f"Мосты получены:\n  {bridges}")
                    print("Делаем резервную копию torrc")
                    if copy_config():
                        print("Резервная копия torrc сохранена\nМеняем мосты")
                        read_write_config(bridges)
                        print("Мосты torrc изменены")
                        subprocess.call("sudo /etc/init.d/tor restart", shell=True)
                        subprocess.call("/etc/init.d/tor status", shell=True)
                else:
                    print("\nНе удалось прочитать мосты из QR-кода")
                i = False
            time.sleep(20)
    else:
        exit('\n[!] Данный скрипт работает только в ОС Linux\n')


if __name__ == "__main__":
    main()

And that's all. Thank you for your attention. I hope this information will be useful to you.

Subscribe to our telegram channel, there is a lot of useful information there.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *