import os import sys import subprocess from threading import Thread import distutils import ox import stem from stem.control import Controller import settings import state import utils import logging logging.getLogger('stem').setLevel(logging.ERROR) logger = logging.getLogger(__name__) class TorDaemon(Thread): installing = False def __init__(self): self._status = [] Thread.__init__(self) self.daemon = True self.start() def create_torrc(self): defaults = os.path.join(settings.config_path, 'torrc-defaults') torrc = os.path.join(settings.config_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.config_path)) 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] 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) self.p = None def shutdown(self): 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.config_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.shutdown() except: logger.debug('shutdown exception', exc_info=1) pass self.connected = False def publish(self): logger.debug("publish tor node") if not self.connected: return False controller = self.controller logger.debug("FIXME: dont remove/add service if already defined") 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']) ''' with open(settings.ssl_key_path) as fd: key_content = fd.read() ports = {9851: settings.server['node_port']} response = controller.create_ephemeral_hidden_service(ports, key_type='RSA1024', key_content=key_content, detached=True, await_publication = True) logger.debug('published node as https://%s.onion:%s', settings.USER_ID, 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=1) 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=1) 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=1) else: logger.debug('no way to install TorBrowser on %s so far', sys.platform)