from threading import Thread import distutils import os import platform import re import subprocess import sys import time import zipfile from Crypto.PublicKey import RSA import ox import stem from stem import Signal 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 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()) tor_data = os.path.join(settings.data_path, 'TorData') if sys.platform == 'win32': tor_data = os.path.normpath(tor_data).replace(os.SEP, '/') if not os.path.exists(torrc): with open(torrc, 'w') as fd: fd.write(''' DataDirectory {tor_data} DirReqStatistics 0 '''.strip().format(tor_data=tor_data)) else: with open(torrc, 'r') as fd: data = fd.read() modified_data = re.sub('DataDirectory.*?TorData', 'DataDirectory {tor_data}'.format(tor_data=tor_data), 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_data = os.path.join(settings.data_path, 'TorData') 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: if sys.platform.startswith('linux') and ( 'TorBrowser' in tor or tor.startswith(os.path.dirname(settings.base_dir)) ): env = { 'LD_LIBRARY_PATH': os.path.dirname(tor) } else: env = None cmd = [tor, '--defaults-torrc', defaults, '-f', torrc, 'DataDirectory', tor_data] cmd += get_geoip(tor) cwd = os.path.dirname(tor) while self.running: if sys.platform == 'win32': cwd = settings.data_path startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, start_new_session=True, env=env, cwd=cwd, startupinfo=startupinfo) else: self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, start_new_session=True, env=env, cwd=cwd) 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): logger.debug('EVENT', event) def shutdown(self): self._shutdown = True try: self.depublish() if self.daemon and self.controller: self.controller.signal(Signal.SHUTDOWN) 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): 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']} if settings.preferences.get('enableReadOnlyService'): ports[80] = settings.server['public_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']) if settings.preferences.get('enableReadOnlyService'): logger.debug('published readonly version as hidden servers: http://%s.onion', settings.USER_ID) 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']) if settings.preferences.get('enableReadOnlyService'): logger.error('can not publish read-only version, please update TOR') 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(sys_platform=None): import re from ox.cache import read_url if not sys_platform: sys_platform = sys.platform if sys_platform.startswith('linux'): machine = platform.machine() if machine not in ('x86_64', 'i386', 'i686'): return base_url = 'https://dist.torproject.org/torbrowser/' data = read_url(base_url, timeout=3*24*60*60).decode() versions = [] for r in ( re.compile('href="(\d\.\d\.\d/)"'), re.compile('href="(\d\.\d/)"'), ): versions += r.findall(data) current = sorted(versions)[-1] url = base_url + current language = '.*?en' if sys_platform.startswith('linux'): if platform.architecture()[0] == '64bit': osname = 'linux64' else: osname = 'linux32' ext = 'xz' elif sys_platform == 'darwin': osname = 'osx64' ext = 'dmg' elif sys_platform == 'win32': language = '' osname = '' ext = 'zip' else: logger.debug('no way to get torbrowser url for %s', sys.platform) return None r = re.compile('href="(.*?{osname}{language}.*?{ext})"'.format(osname=osname,language=language,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 ( os.path.join(settings.base_dir, '..', 'platform_darwin64', 'tor', 'tor'), '/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 elif sys.platform == 'win32': paths = [ os.path.join(settings.base_dir, '..', 'platform_win32', 'tor', 'tor.exe') ] exe = os.path.join('Tor Browser', 'Browser', 'TorBrowser', 'Tor', 'tor.exe') for prefix in ( os.path.join(os.path.expanduser('~'), 'Desktop'), os.path.join('C:', 'Program Files'), os.path.join('C:', 'Program Files (x86)'), ): path = os.path.join(prefix, exe) paths.append(path) paths.append(os.path.join(settings.base_dir, '..', 'tor', 'Tor', 'tor.exe')) for path in paths: if os.path.isfile(path) and os.access(path, os.X_OK): return os.path.normpath(path) elif sys.platform.startswith('linux'): for path in ( os.path.join(settings.base_dir, '..', 'platform_linux64', 'tor', 'tor'), os.path.join(settings.base_dir, '..', 'platform_linux32', 'tor', 'tor'), os.path.join(settings.base_dir, '..', 'platform_linux_armv7l', 'tor', 'tor'), os.path.join(settings.base_dir, '..', 'platform_linux_aarch64', 'tor', 'tor'), ): if os.path.isfile(path) and os.access(path, os.X_OK): return os.path.normpath(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 get_geoip(tor): geo = [] for tordir in ( os.path.normpath(os.path.join(settings.base_dir, '..', 'platform', 'tor')), os.path.join(os.path.dirname(os.path.dirname(tor)), 'Data', 'Tor') ): gepipfile = os.path.join(tordir, 'geoip') gepipv6file = os.path.join(tordir, 'geoip6') if os.path.exists(gepipfile): geo += ['GeoIPFile', gepipfile] if os.path.exists(gepipv6file): geo += ['GeoIPv6File', gepipv6file] if geo: break return geo 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) elif sys.platform == 'win32': try: ox.makedirs(target) zipf = os.path.join(target, os.path.basename(url)) update.get(url, zipf) f = zipfile.ZipFile(zipf) f.extractall(target) os.unlink(zipf) except: logger.debug('tor installation failed', exc_info=True) else: logger.debug('no way to install TorBrowser on %s so far', sys.platform)