import os import re import sys import subprocess import time from threading import Thread import distutils import zipfile import ox import stem from stem import Signal 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()) tor_data = os.path.join(settings.data_path, 'TorData') 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 'TorBrowser' in tor and sys.platform.startswith('linux'): env = { 'LD_LIBRARY_PATH': os.path.dirname(tor) } else: env = None cmd = [tor, '--defaults-torrc', defaults, '-f', torrc, 'DataDirectory', tor_data] cmd += self.geoip(tor) cwd = os.path.dirname(tor) while self.running: if sys.platform == 'win32': 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 geoip(self, tor): geo = [] tordir = os.path.dirname(os.path.dirname(tor)) gepipfile = os.path.join(tordir, 'Data', 'Tor', 'geoip') gepipv6file = os.path.join(tordir, 'Data', 'Tor', 'geoip6') if os.path.exists(gepipfile): geo += ['GeoIPFile', gepipfile] if os.path.exists(gepipv6file): geo += ['GeoIPv6File', gepipv6file] return geo 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): 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(sys_platform=None): import re from ox.cache import read_url if not sys_platform: sys_platform = sys.platform 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'): 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': 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 ( '/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 = [] 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 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) 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)