diff options
Diffstat (limited to 'network/tor_manager.py')
| -rw-r--r-- | network/tor_manager.py | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/network/tor_manager.py b/network/tor_manager.py new file mode 100644 index 0000000..8f91b68 --- /dev/null +++ b/network/tor_manager.py @@ -0,0 +1,196 @@ +import os +import subprocess +import asyncio +import socket +import socks +from typing import Optional +import shutil +import platform +import sys +import stat + +class TorManager: + def __init__(self): + self.tor_process: Optional[subprocess.Popen] = None + self.onion_address: Optional[str] = None + self.socks_port = 9052 + self.control_port = 9053 + self.base_dir = self._get_base_dir() + + def _get_base_dir(self) -> str: + """Get the portable base directory for Tor files""" + if getattr(sys, 'frozen', False): + # If running as compiled executable + base_path = os.path.dirname(sys.executable) + else: + # If running from source + base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + # Use a hidden directory in the user's home + return os.path.join(os.path.expanduser("~"), ".tormesh") + + def _secure_dir_permissions(self, path: str): + """Set secure permissions on directory""" + os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) # 700 + + def _get_tor_path(self) -> str: + """Get platform-specific Tor binary path with fallbacks""" + if platform.system() == 'Windows': + tor_names = ['tor.exe'] + else: + tor_names = ['tor'] + + # Search in system paths first + search_paths = [ + "/usr/bin", + "/usr/local/bin", + "/opt/homebrew/bin", # For MacOS + os.path.join(self.base_dir, "bin") + ] + + # Add PATH directories + if os.environ.get('PATH'): + search_paths.extend(os.environ['PATH'].split(os.pathsep)) + + for path in search_paths: + for name in tor_names: + tor_path = os.path.join(path, name) + if os.path.exists(tor_path) and os.access(tor_path, os.X_OK): + return tor_path + + raise FileNotFoundError( + "Tor binary not found. Please ensure Tor is installed." + ) + + async def start(self): + """Start Tor with robust error handling and retries""" + # Create base directory with secure permissions + os.makedirs(self.base_dir, exist_ok=True) + self._secure_dir_permissions(self.base_dir) + + # Create necessary subdirectories + data_dir = os.path.join(self.base_dir, "data") + hidden_service_dir = os.path.join(self.base_dir, "hidden_service") + + for directory in [data_dir, hidden_service_dir]: + os.makedirs(directory, exist_ok=True) + self._secure_dir_permissions(directory) + + # Create torrc configuration + torrc_path = os.path.join(self.base_dir, "torrc") + with open(torrc_path, "w") as f: + f.write(f""" +SocksPort 127.0.0.1:{self.socks_port} +ControlPort 127.0.0.1:{self.control_port} +DataDirectory {data_dir} +HiddenServiceDir {hidden_service_dir} +HiddenServicePort 12345 127.0.0.1:12345 +# Improve anonymity +UseEntryGuards 1 +NumEntryGuards 4 +# Improve resilience +CircuitBuildTimeout 60 +LearnCircuitBuildTimeout 1 +MaxCircuitDirtiness 600 +NewCircuitPeriod 300 +""") + self._secure_dir_permissions(os.path.dirname(torrc_path)) + + # Start Tor process with retries + max_retries = 3 + retry_delay = 2 + + for attempt in range(max_retries): + try: + tor_path = self._get_tor_path() + print(f"Starting Tor from: {tor_path}") + + self.tor_process = subprocess.Popen( + [tor_path, "-f", torrc_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # Wait for Tor to start and verify it's running + if await self._wait_for_tor(): + print(f"Tor started successfully on attempt {attempt + 1}") + return + + except Exception as e: + print(f"Tor start attempt {attempt + 1} failed: {str(e)}") + if self.tor_process: + self.tor_process.terminate() + await asyncio.sleep(1) + + if attempt < max_retries - 1: + await asyncio.sleep(retry_delay) + else: + raise Exception(f"Failed to start Tor after {max_retries} attempts") + + async def _wait_for_tor(self) -> bool: + """Wait for Tor to start with improved verification""" + hostname_file = os.path.join( + self.base_dir, "hidden_service", "hostname" + ) + + for _ in range(30): # Wait up to 30 seconds + # Check if process is still running + if self.tor_process and self.tor_process.poll() is not None: + if self.tor_process.stderr: + stderr = self.tor_process.stderr.read().decode() + raise Exception(f"Tor process died: {stderr}") + raise Exception("Tor process died unexpectedly") + + # Check for hostname file + if os.path.exists(hostname_file): + try: + with open(hostname_file, "r") as f: + self.onion_address = f.read().strip() + + # Verify SOCKS port is listening + sock = socket.socket() + try: + sock.connect(("127.0.0.1", self.socks_port)) + sock.close() + return True + except: + pass + except: + pass + + await asyncio.sleep(1) + + return False + + async def stop(self): + """Stop Tor with graceful shutdown""" + if self.tor_process: + try: + self.tor_process.terminate() + try: + await asyncio.wait_for( + asyncio.create_task(self.tor_process.wait()), + timeout=5 + ) + except asyncio.TimeoutError: + self.tor_process.kill() + + # Clear sensitive data + if self.onion_address: + self.onion_address = None + + # Clean up temporary files securely + if os.path.exists(self.base_dir): + try: + # Securely delete sensitive files + for root, dirs, files in os.walk(self.base_dir): + for f in files: + path = os.path.join(root, f) + with open(path, 'wb') as fd: + fd.write(os.urandom(os.path.getsize(path))) + # Remove directory tree + shutil.rmtree(self.base_dir) + except: + pass + except: + pass # Ensure cleanup doesn't raise errors |
