import os import re import sys import subprocess import time from threading import Thread import distutils import ox import stem from stem.control import Controller from Crypto.PublicKey import RSA import settings import state import utils import logging logging.getLogger('stem').setLevel(logging.ERROR) logger = logging.getLogger(__name__) class TorDaemon(Thread): installing = False running = True def __init__(self): self._status = [] Thread.__init__(self) self.daemon = True self.start() def create_torrc(self): defaults = os.path.join(settings.data_path, 'torrc-defaults') torrc = os.path.join(settings.data_path, 'torrc') if not os.path.exists(defaults): with open(defaults, 'w') as fd: fd.write(''' AvoidDiskWrites 1 # Where to send logging messages. Format is minSeverity[-maxSeverity] # (stderr|stdout|syslog|file FILENAME). Log notice stdout SocksPort 9830 ControlPort 9831 CookieAuthentication 1 '''.strip()) if not os.path.exists(torrc): with open(torrc, 'w') as fd: fd.write(''' DataDirectory {base}/TorData DirReqStatistics 0 '''.strip().format(base=settings.data_path)) else: with open(torrc, 'r') as fd: data = fd.read() modified_data = re.sub('DataDirectory.*?/TorData', 'DataDirectory {base}/TorData'.format(base=settings.data_path), data) if data != modified_data: with open(torrc, 'w') as fd: fd.write(modified_data) return defaults, torrc def run(self): defaults, torrc = self.create_torrc() tor = get_tor() if not tor: self._status.append('No tor binary found. Please install TorBrowser or tor') self.installing = True install_tor() self.installing = False tor = get_tor() if tor: cmd = [tor, '--defaults-torrc', defaults, '-f', torrc] while self.running: self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, close_fds=True, start_new_session=True) for line in self.p.stdout: self._status.append(line) logger.debug(line) time.sleep(0.5) self.p = None def kill(self): self.running = False if self.p: self.p.kill() def status(self, max_lines=50): return ''.join(self._status[-max_lines:]) class Tor(object): _shutdown = False connected = False controller = None daemon = None socks_port = 9150 def __init__(self): if not self.connect(): self.reconnect() def connect(self): self.connected = False self.dir = os.path.join(settings.data_path, 'tor') connected = False for port in (9831, 9151): try: self.controller = Controller.from_port('127.0.0.1', port) connected = True break except stem.SocketError: pass if not connected: if not self.daemon: logger.debug("Start tor") self.daemon = TorDaemon() return self.connect() if not self.daemon.installing: logger.debug("Failed to connect to tor") return False try: self.controller.authenticate() except stem.connection.MissingPassword: logger.debug("TOR requires a password") return False except stem.connection.PasswordAuthFailed: logger.debug("invalid tor password") return False self.controller.add_event_listener(self.event_listener) self.controller.add_status_listener(self.status_listener) self.connected = True self.socks_port = int(self.controller.get_conf('SocksPort').split(' ')[0]) self.publish() state.online = self.is_online() return True def reconnect(self): if not self.connect(): if state.main: state.main.call_later(1, self.reconnect) def status_listener(self, controller, status, timestamp): if status == 'Closed': if not self._shutdown: self.connected = False state.online = False self.reconnect() else: logger.debug('unknonw change %s', status) def event_listener(self, event): print('EVENT', event) def shutdown(self): self._shutdown = True try: self.depublish() if self.controller: #self.controller.remove_event_listener(self.connection_change) self.controller.close() if self.daemon: self.daemon.kill() except: logger.debug('shutdown exception', exc_info=True) pass self.connected = False def publish(self): logger.debug("publish tor node") if not self.connected: return False controller = self.controller if controller.get_version() >= stem.version.Requirement.ADD_ONION: with open(settings.ssl_key_path, 'rb') as fd: private_key = fd.read() key_content = RSA.importKey(private_key).exportKey().decode() key_content = ''.join(key_content.strip().split('\n')[1:-1]) ports = {9851: settings.server['node_port']} controller.remove_ephemeral_hidden_service(settings.USER_ID) response = controller.create_ephemeral_hidden_service(ports, key_type='RSA1024', key_content=key_content, detached=True) if response.is_ok(): logger.debug('published node as https://%s.onion:%s', settings.USER_ID, settings.server_defaults['node_port']) else: logger.debug('failed to publish node to tor') else: controller.remove_hidden_service(self.dir) result = controller.create_hidden_service( self.dir, settings.server_defaults['node_port'], target_port=settings.server['node_port'] ) logger.debug('published node as https://%s:%s', result.hostname, settings.server_defaults['node_port']) def depublish(self): if not self.connected: return False if self.controller: try: self.controller.remove_hidden_service(self.dir) except: logger.debug('self.controller.remove_hidden_service fail', exc_info=True) state.online = False def is_online(self): return self.connected and self.controller.is_alive() and utils.can_connect_dns() def torbrowser_url(): import re from ox.cache import read_url base_url = 'https://dist.torproject.org/torbrowser/' r = re.compile('href="(\d\.\d\.\d/)"') current = sorted(r.findall(read_url(base_url, timeout=3*24*60*60).decode()))[-1] url = base_url + current if sys.platform.startswith('linux'): import platform if platform.architecture()[0] == '64bit': osname = 'linux64' else: osname = 'linux32' ext = 'xz' elif sys.platform == 'darwin': osname = 'osx64' ext = 'dmg' elif sys.platform == 'win32': osname = 'install' ext = 'exe' else: logger.debug('no way to get torbrowser url for %s', sys.platform) return None r = re.compile('href="(.*?{osname}.*?en.*?{ext})"'.format(osname=osname,ext=ext)) torbrowser = sorted(r.findall(read_url(url).decode()))[-1] url += torbrowser return url def get_tor(): if sys.platform == 'darwin': for path in ( '/Applications/TorBrowser.app/TorBrowser/Tor/tor', os.path.join(settings.base_dir, 'tor', 'TorBrowser.app', 'TorBrowser', 'Tor', 'tor') ): if os.path.isfile(path) and os.access(path, os.X_OK): return path start = os.path.expanduser('~/.local/share/applications/start-tor-browser.desktop') if os.path.exists(start): with open(start) as fd: e = [line for line in fd.read().split('\n') if line.startswith('Exec')] if e: try: base = os.path.dirname(e[0].split('"')[1]) path = os.path.join(base, 'TorBrowser', 'Tor', 'tor') if os.path.isfile(path) and os.access(path, os.X_OK): return path except: pass local_tor = os.path.normpath(os.path.join(settings.base_dir, '..', 'tor', 'tor-browser_en-US', 'Browser', 'TorBrowser', 'Tor', 'tor')) if os.path.exists(local_tor): return local_tor return distutils.spawn.find_executable('tor') def install_tor(): import tarfile import update import shutil # only install if tor can not be found if get_tor(): logger.debug('found existing tor installation') return url = torbrowser_url() target = os.path.normpath(os.path.join(settings.base_dir, '..', 'tor')) if url: logger.debug('downloading and installing tor') if sys.platform.startswith('linux'): ox.makedirs(target) try: tar_file = os.path.join(target, os.path.basename(url)) update.get(url, tar_file) tar = tarfile.open(tar_file) tar.extractall(target) desktop = os.path.join(target, tar.members[0].path, 'start-tor-browser.desktop') tar.close() subprocess.call([desktop, '--register-app'], cwd=os.path.dirname(desktop)) os.unlink(tar_file) except: logger.debug('tor installation failed', exc_info=True) elif sys.platform == 'darwin': ox.makedirs(target) dmg = os.path.join(target, os.path.basename(url)) name = 'TorBrowser.app' if os.access('/Applications', os.W_OK): target = os.path.join('/Applications', name) else: target = os.path.join(target, name) if not os.path.exists(target): try: update.get(url, dmg) r = subprocess.check_output(['hdiutil', 'mount', dmg]) volume = r.decode().strip().split('\t')[-1] app = os.path.join(volume, name) shutil.copytree(app, target) subprocess.call(['hdiutil', 'unmount', volume]) os.unlink(dmg) except: logger.debug('tor installation failed', exc_info=True) else: logger.debug('no way to install TorBrowser on %s so far', sys.platform)