Files
dragonion-server/dragonion_server/utils/onion/onion.py
2023-06-26 14:02:29 +03:00

321 lines
10 KiB
Python

from stem.control import Controller
from stem import SocketClosed, ProtocolError
import textwrap
import socket
import random
import os
import psutil
import shlex
import subprocess
import tempfile
import platform
import time
import base64
import nacl.public
from dragonion_server.utils.core import dirs
from dragonion_server.utils import config
from dragonion_server.utils.config.db import services
def get_available_port(min_port: int = 1000, max_port: int = 65535):
with socket.socket() as tmpsock:
while True:
try:
tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port)))
break
except OSError:
pass
_, port = tmpsock.getsockname()
return port
def key_str(key):
key_bytes = bytes(key)
key_b32 = base64.b32encode(key_bytes)
assert key_b32[-4:] == b"===="
key_b32 = key_b32[:-4]
s = key_b32.decode("utf-8")
return s
class Onion(object):
def __init__(self):
self.tor_data_directory_name = None
self.tor_control_socket = None
self.tor_control_port = None
self.tor_torrc = None
self.tor_socks_port = None
self.tor_cookie_auth_file = None
self.tor_data_directory = None
self.tor_path = dirs.get_tor_paths()
self.tor_proc = None
# noinspection PyTypeChecker
self.c: Controller = None
self.connected_to_tor = False
self.auth_string = None
self.graceful_close_onions = []
def kill_same_tor(self):
for proc in psutil.process_iter(["pid", "name", "username"]):
try:
cmdline = proc.cmdline()
if (
cmdline[0] == self.tor_path
and cmdline[1] == "-f"
and cmdline[2] == self.tor_torrc
):
proc.terminate()
proc.wait()
break
except Exception as e:
assert e
def fill_torrc(self, tor_data_directory_name):
torrc_template = textwrap.dedent("""
DataDirectory {data_directory}
SocksPort {socks_port}
CookieAuthentication 1
CookieAuthFile {cookie_auth_file}
AvoidDiskWrites 1
Log notice stdout
""")
self.tor_cookie_auth_file = os.path.join(tor_data_directory_name, "cookie")
try:
self.tor_socks_port = get_available_port(1000, 65535)
except Exception as e:
print(f"Cannot bind any port for socks proxy: {e}")
self.tor_torrc = os.path.join(tor_data_directory_name, "torrc")
self.kill_same_tor()
if platform.system() in ["Windows", "Darwin"]:
torrc_template += "ControlPort {control_port}\n"
try:
self.tor_control_port = get_available_port(1000, 65535)
except Exception as e:
print(f"Cannot bind any control port: {e}")
self.tor_control_socket = None
else:
torrc_template += "ControlSocket {control_socket}\n"
self.tor_control_port = None
self.tor_control_socket = os.path.join(
tor_data_directory_name, "control_socket"
)
torrc_template = torrc_template.format(
data_directory=tor_data_directory_name,
control_port=str(self.tor_control_port),
control_socket=str(self.tor_control_socket),
cookie_auth_file=self.tor_cookie_auth_file,
socks_port=str(self.tor_socks_port)
)
with open(self.tor_torrc, "w") as f:
f.write(torrc_template)
def connect(self, connect_timeout=120):
self.c = None
self.tor_data_directory = tempfile.TemporaryDirectory(
dir=dirs.build_tmp_dir()
)
self.tor_data_directory_name = self.tor_data_directory.name
self.fill_torrc(self.tor_data_directory_name)
start_ts = time.time()
if platform.system() == "Windows":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
self.tor_proc = subprocess.Popen(
[self.tor_path, "-f", self.tor_torrc],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
)
else:
env = {"LD_LIBRARY_PATH": os.path.dirname(self.tor_path)}
self.tor_proc = subprocess.Popen(
[self.tor_path, "-f", self.tor_torrc],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)
time.sleep(2)
if platform.system() in ["Windows", "Darwin"]:
self.c = Controller.from_port(port=self.tor_control_port)
self.c.authenticate()
else:
self.c = Controller.from_socket_file(path=self.tor_control_socket)
self.c.authenticate()
while True:
try:
res = self.c.get_info("status/bootstrap-phase")
except SocketClosed:
raise
res_parts = shlex.split(res)
progress = res_parts[2].split("=")[1]
summary = res_parts[4].split("=")[1]
print(
f"\rConnecting to the Tor network: {progress}% - {summary}\033[K",
end="",
)
if summary == "Done":
print("")
break
time.sleep(0.2)
if time.time() - start_ts > connect_timeout:
print("")
try:
self.tor_proc.terminate()
print(
"Taking too long to connect to Tor. Maybe you aren't connected to the Internet, "
"or have an inaccurate system clock?"
)
raise
except FileNotFoundError:
pass
self.connected_to_tor = True
@staticmethod
def write_onion_service(name: str, port: int):
if name in services.keys():
service: config.models.ServiceModel = services[name]
service.port = port
services[name] = service
return services[name]
client_auth_priv_key_raw = nacl.public.PrivateKey.generate()
client_auth_priv_key = key_str(client_auth_priv_key_raw)
client_auth_pub_key = key_str(
client_auth_priv_key_raw.public_key
)
services[name] = config.models.ServiceModel(
port=port,
client_auth_priv_key=client_auth_priv_key,
client_auth_pub_key=client_auth_pub_key
)
return services[name]
def start_onion_service(self, name):
if name not in services.keys():
raise 'Service not created'
service: config.models.ServiceModel = services[name]
try:
res = self.c.create_ephemeral_hidden_service(
{80: service.port},
await_publication=True,
key_type=service.key_type,
key_content=service.key_content,
client_auth_v3=service.client_auth_pub_key,
)
except ProtocolError as e:
print("Tor error: {}".format(e.args[0]))
raise
onion_host = res.service_id + ".onion"
self.graceful_close_onions.append(res.service_id)
if service.key_type == "NEW":
service.service_id = res.service_id
service.key_type = "ED25519-V3"
service.key_content = res.private_key
self.auth_string = \
base64.b64encode(f'{res.service_id}:descriptor:x25519:{service.client_auth_priv_key}'.encode()).decode()
services[name] = service
return onion_host
def stop_onion_service(self, name):
service: config.models.ServiceModel = services[name]
if service.service_id:
try:
self.c.remove_ephemeral_hidden_service(
service.service_id
)
except Exception as e:
print(e)
def is_authenticated(self):
if self.c is not None:
return self.c.is_authenticated()
else:
return False
def cleanup(self):
if self.tor_proc:
try:
rendezvous_circuit_ids = []
for c in self.c.get_circuits():
if (
c.purpose == "HS_SERVICE_REND"
and c.rend_query in self.graceful_close_onions
):
rendezvous_circuit_ids.append(c.id)
symbols = list("\\|/-")
symbols_i = 0
while True:
num_rend_circuits = 0
for c in self.c.get_circuits():
if c.id in rendezvous_circuit_ids:
num_rend_circuits += 1
if num_rend_circuits == 0:
print(
"\rTor rendezvous circuits have closed" + " " * 20
)
break
if num_rend_circuits == 1:
circuits = "circuit"
else:
circuits = "circuits"
print(
f"\rWaiting for {num_rend_circuits} Tor rendezvous {circuits} to close {symbols[symbols_i]} ",
end="",
)
symbols_i = (symbols_i + 1) % len(symbols)
time.sleep(1)
except Exception as e:
print(e)
self.tor_proc.terminate()
time.sleep(0.2)
if self.tor_proc.poll() is None:
try:
self.tor_proc.kill()
time.sleep(0.2)
except Exception as e:
print(e)
self.tor_proc = None
self.connected_to_tor = False
try:
self.tor_data_directory.cleanup()
except Exception as e:
print(f'Cannot cleanup temporary directory: {e}')
def get_tor_socks_port(self):
return "127.0.0.1", self.tor_socks_port