#!/usr/bin/env python """ Tor Browser Launcher https://github.com/micahflee/torbrowser-launcher/ Copyright (c) 2013 Micah Lee Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import sys sys.path.append('/usr/share/torbrowser-launcher/lib/') import platform import gettext gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale') from twisted.internet import gtk2reactor gtk2reactor.install() from twisted.internet import reactor import pygtk pygtk.require('2.0') import gtk import os, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil, hashlib, lzma from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed from twisted.web.http_headers import Headers from twisted.internet.protocol import Protocol from twisted.internet.ssl import ClientContextFactory from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.internet.error import DNSLookupError from txsocksx.client import SOCKS5ClientEndpoint import OpenSSL class TryStableException(Exception): pass class TryDefaultMirrorException(Exception): pass class DownloadErrorException(Exception): pass class VerifyTorProjectCert(ClientContextFactory): def __init__(self, torproject_pem): self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read()) def getContext(self, host, port): ctx = ClientContextFactory.getContext(self) ctx.set_verify_depth(0) ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname) return ctx def verifyHostname(self, connection, cert, errno, depth, preverifyOK): return cert.digest('sha256') == self.torproject_ca.digest('sha256') class TBLCommon: def __init__(self, tbl_version): print _('Initializing Tor Browser Launcher') self.tbl_version = tbl_version # initialize the app self.available_versions = { 'stable': _('Tor Browser Bundle - stable'), 'alpha': _('Tor Browser Bundle - alpha') } self.default_mirror = 'https://www.torproject.org/dist/' self.discover_arch_lang() self.build_paths() self.mkdir(self.paths['data_dir']) self.load_mirrors() self.load_settings() self.mkdir(self.paths['download_dir']) self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir']) self.init_gnupg() # allow buttons to have icons try: gtk_settings = gtk.settings_get_default() gtk_settings.props.gtk_button_images = True except: pass # discover the architecture and language def discover_arch_lang(self): # figure out the architecture (sysname, nodename, release, version, machine) = os.uname() self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686' # figure out the language available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN'] default_locale = locale.getdefaultlocale()[0] if default_locale == None: self.language = 'en-US' else: self.language = default_locale.replace('_', '-') if self.language not in available_languages: self.language = self.language.split('-')[0] if self.language not in available_languages: for l in available_languages: if l[0:2] == self.language: self.language = l # if language isn't available, default to english if self.language not in available_languages: self.language = 'en-US' # build all relevant paths def build_paths(self, tbb_version = None): homedir = os.getenv('HOME') if not homedir: homedir = '/tmp/.torbrowser-'+os.getenv('USER') if os.path.exists(homedir) == False: try: os.mkdir(homedir, 0700) except: self.set_gui('error', _("Error creating {0}").format(homedir), [], False) if not os.access(homedir, os.W_OK): self.set_gui('error', _("{0} is not writable").format(homedir), [], False) tbb_data = '%s/.torbrowser' % homedir if tbb_version: if tbb_version >= '3.': # tarball filename dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b') if self.architecture == 'x86_64': arch = 'linux64' else: arch = 'linux32' tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz' # tarball self.paths['tarball_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/'+tarball_filename # sig self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt' self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc' self.paths['sha256_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/sha256sums.txt' self.paths['sha256_sig_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/sha256sums.txt.asc-mp' else: # tarball filename tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz' # tarball self.paths['tarball_url'] = '{0}torbrowser/linux/'+tarball_filename # {0} will be replaced with the mirror # sig self.paths['tarball_sig_file'] = tbb_data+'/download/'+tarball_filename+'.asc' self.paths['tarball_sig_url'] = '{0}torbrowser/linux/'+tarball_filename+'.asc' self.paths['tarball_sig_filename'] = tarball_filename+'.asc' self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename self.paths['tarball_filename'] = tarball_filename else: self.paths = { 'tbl_bin': '/usr/bin/torbrowser-launcher', 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm', 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem', 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc', 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc', 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc', 'mike_key': '/usr/share/torbrowser-launcher/mike-2013-09.asc', 'mirrors_txt': '/usr/share/torbrowser-launcher/mirrors.txt', 'data_dir': tbb_data, 'download_dir': tbb_data+'/download', 'gnupg_homedir': tbb_data+'/gnupg_homedir', 'settings_file': tbb_data+'/settings', 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions', 'update_check_file': tbb_data+'/download/RecommendedTBBVersions', 'tbb': { 'stable': { 'dir': tbb_data+'/tbb/stable/'+self.architecture, 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser', 'vidalia_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia', 'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox', 'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile', }, 'alpha': { 'dir': tbb_data+'/tbb/alpha/'+self.architecture, 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser', 'vidalia_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia', 'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox', 'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile', } } } # create a directory def mkdir(self, path): try: if os.path.exists(path) == False: os.makedirs(path, 0700) return True except: print _("Cannot create directory {0}").format(path) return False if not os.access(path, os.W_OK): print _("{0} is not writable").format(path) return False return True # if gnupg_homedir isn't set up, set it up def init_gnupg(self): if not os.path.exists(self.paths['gnupg_homedir']): print _('Creating GnuPG homedir'), self.paths['gnupg_homedir'] self.mkdir(self.paths['gnupg_homedir']) self.import_keys() # import gpg keys def import_keys(self): print _('Importing keys') subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key'], self.paths['sebastian_key'], self.paths['alexandre_key'], self.paths['mike_key']]).wait() # load mirrors def load_mirrors(self): self.mirrors = [] for mirror in open(self.paths['mirrors_txt'], 'r').readlines(): self.mirrors.append(mirror.strip()) # load settings def load_settings(self): default_settings = { 'tbl_version': self.tbl_version, 'preferred': 'stable', 'installed_version': { 'stable': False, 'alpha': False }, 'latest_version': { 'stable': '0', 'alpha': '0' }, 'update_over_tor': True, 'check_for_updates': False, 'last_update_check_timestamp': 0, 'mirror': self.default_mirror } if os.path.isfile(self.paths['settings_file']): settings = pickle.load(open(self.paths['settings_file'])) resave = False # settings migrations if settings['tbl_version'] == '0.0.1': print '0.0.1 migration' self.settings = default_settings self.settings['installed_version']['alpha'] = settings['installed_version'] self.settings['tbl_version'] = self.tbl_version resave = True # move tbb alpha self.mkdir(self.paths['tbb']['alpha']['dir']) # make sure settings file is up-to-date for setting in default_settings: if setting not in settings: settings[setting] = default_settings[setting] resave = True self.settings = settings if resave: self.save_settings() else: self.settings = default_settings self.save_settings() # save settings def save_settings(self): pickle.dump(self.settings, open(self.paths['settings_file'], 'w')) return True # get the process id of a program def get_pid(self, bin_path, python = False): pid = None for p in psutil.process_iter(): try: if p.pid != os.getpid(): exe = None if python: if len(p.cmdline) > 1: if 'python' in p.cmdline[0]: exe = p.cmdline[1] else: if len(p.cmdline) > 0: exe = p.cmdline[0] if exe == bin_path: pid = p.pid except: pass return pid # bring program's x window to front def bring_window_to_front(self, pid): # figure out the window id win_id = None p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE) for line in p.stdout.readlines(): line_split = line.split() cur_win_id = line_split[0] cur_win_pid = int(line_split[2]) if cur_win_pid == pid: win_id = cur_win_id # bring to front if win_id: subprocess.call(['wmctrl', '-i', '-a', win_id]) class TBLSettings: def __init__(self, common): print _('Starting settings dialog') self.common = common # set up the window self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_title(_("Tor Browser Launcher Settings")) self.window.set_icon_from_file(self.common.paths['icon_file']) self.window.set_position(gtk.WIN_POS_CENTER) self.window.set_border_width(10) self.window.connect("delete_event", self.delete_event) self.window.connect("destroy", self.destroy) # build the rest of the UI self.box = gtk.VBox(False, 10) self.window.add(self.box) self.box.show() self.hbox = gtk.HBox(False, 10) self.box.pack_start(self.hbox, True, True, 0) self.hbox.show() self.settings_box = gtk.VBox(False, 10) self.hbox.pack_start(self.settings_box, True, True, 0) self.settings_box.show() self.labels_box = gtk.VBox(False, 10) self.hbox.pack_start(self.labels_box, True, True, 0) self.labels_box.show() # preferred version self.preferred_box = gtk.HBox(False, 10) self.settings_box.pack_start(self.preferred_box, True, True, 0) self.preferred_box.show() self.preferred_label = gtk.Label(_('I prefer')) self.preferred_label.set_line_wrap(True) self.preferred_box.pack_start(self.preferred_label, True, True, 0) self.preferred_label.show() self.preferred_options = [] for i in self.common.available_versions: self.preferred_options.append(self.common.available_versions[i]) self.preferred_options.sort() self.preferred = gtk.combo_box_new_text() for option in self.preferred_options: self.preferred.append_text(option) if self.common.settings['preferred'] in self.common.available_versions: self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) ) else: self.preferred.set_active(0) self.preferred_box.pack_start(self.preferred, True, True, 0) self.preferred.show() # download over tor # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it """ self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)")) self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0) if self.common.settings['update_over_tor']: self.tor_update_checkbox.set_active(True) else: self.tor_update_checkbox.set_active(False) self.tor_update_checkbox.show() """ # check for updates self.update_checkbox = gtk.CheckButton(_("Check for updates next launch")) self.settings_box.pack_start(self.update_checkbox, True, True, 0) if self.common.settings['check_for_updates']: self.update_checkbox.set_active(True) else: self.update_checkbox.set_active(False) self.update_checkbox.show() # labels if(self.common.settings['installed_version'][self.common.settings['preferred']]): self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']])) else: self.label1 = gtk.Label(_('Not installed')) self.label1.set_line_wrap(True) self.labels_box.pack_start(self.label1, True, True, 0) self.label1.show() if(self.common.settings['last_update_check_timestamp'] > 0): self.label1 = gtk.Label(_('Last checked for updates:\n{0}').format(time.strftime("%B %d, %Y %I:%M %P", time.gmtime(self.common.settings['last_update_check_timestamp'])))) else: self.label1 = gtk.Label(_('Never checked for updates')) self.label1.set_line_wrap(True) self.labels_box.pack_start(self.label1, True, True, 0) self.label1.show() # mirrors self.mirrors_box = gtk.HBox(False, 10) self.box.pack_start(self.mirrors_box, True, True, 0) self.mirrors_box.show() self.mirrors_label = gtk.Label(_('Mirror')) self.mirrors_label.set_line_wrap(True) self.mirrors_box.pack_start(self.mirrors_label, True, True, 0) self.mirrors_label.show() self.mirrors = gtk.combo_box_new_text() for mirror in self.common.mirrors: self.mirrors.append_text(mirror) if self.common.settings['mirror'] in self.common.mirrors: self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) ) else: self.preferred.set_active(0) self.mirrors_box.pack_start(self.mirrors, True, True, 0) self.mirrors.show() # button box self.button_box = gtk.HButtonBox() self.button_box.set_layout(gtk.BUTTONBOX_SPREAD) self.box.pack_start(self.button_box, True, True, 0) self.button_box.show() # save and launch button save_launch_image = gtk.Image() save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON) self.save_launch_button = gtk.Button(_("Launch Tor Browser")) self.save_launch_button.set_image(save_launch_image) self.save_launch_button.connect("clicked", self.save_launch, None) self.button_box.add(self.save_launch_button) self.save_launch_button.show() # save and exit button save_exit_image = gtk.Image() save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON) self.save_exit_button = gtk.Button(_("Save & Exit")) self.save_exit_button.set_image(save_exit_image) self.save_exit_button.connect("clicked", self.save_exit, None) self.button_box.add(self.save_exit_button) self.save_exit_button.show() # cancel button cancel_image = gtk.Image() cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON) self.cancel_button = gtk.Button(_("Cancel")) self.cancel_button.set_image(cancel_image) self.cancel_button.connect("clicked", self.destroy, None) self.button_box.add(self.cancel_button) self.cancel_button.show() # show the window self.window.show() # start gtk gtk.main() # save and launch def save_launch(self, widget, data=None): self.save() p = subprocess.Popen([self.common.paths['tbl_bin']]) self.destroy(False) # save and exit def save_exit(self, widget, data=None): self.save() self.destroy(False) # save settings def save(self): # figure out the selected preferred option preferred = None selected = self.preferred_options[self.preferred.get_active()] for i in self.common.available_versions: if self.common.available_versions[i] == selected: preferred = i if preferred: self.common.settings['preferred'] = preferred # checkbox options #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active() self.common.settings['check_for_updates'] = self.update_checkbox.get_active() # figure out the selected mirror self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()] # save them self.common.save_settings() # exit def delete_event(self, widget, event, data=None): return False def destroy(self, widget, data=None): gtk.main_quit() class TBLLauncher: def __init__(self, common): print _('Starting launcher dialog') self.common = common # init launcher self.set_gui(None, '', []) self.launch_gui = True self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']]) # is vidalia already running and we just need to open a new firefox? if self.common.settings['installed_version']: vidalia_pid = self.common.get_pid('./App/vidalia') firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin']) if vidalia_pid and not firefox_pid: print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.') self.common.bring_window_to_front(vidalia_pid) subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'], '-no-remote', '-profile', self.common.paths['tbb'][self.common.settings['preferred']]['firefox_profile']]) return elif vidalia_pid and firefox_pid: print _('Vidalia and Firefox are already open, bringing them to focus') # bring firefox to front, then vidalia self.common.bring_window_to_front(firefox_pid) self.common.bring_window_to_front(vidalia_pid) return # check for updates? check_for_updates = False if self.common.settings['check_for_updates']: check_for_updates = True if not check_for_updates: # how long was it since the last update check? # 86400 seconds = 24 hours current_timestamp = int(time.time()) if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400: check_for_updates = True if check_for_updates: # check for update print 'Checking for update' self.set_gui('task', _("Checking for Tor Browser update."), ['download_update_check', 'attempt_update']) else: # no need to check for update print _('Checked for update within 24 hours, skipping') self.start_launcher() if self.launch_gui: # set up the window self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_title(_("Tor Browser")) self.window.set_icon_from_file(self.common.paths['icon_file']) self.window.set_position(gtk.WIN_POS_CENTER) self.window.set_border_width(10) self.window.connect("delete_event", self.delete_event) self.window.connect("destroy", self.destroy) # build the rest of the UI self.build_ui() # download or run TBB def start_launcher(self): # is TBB already installed? latest_version = self.common.settings['latest_version'][self.common.settings['preferred']] installed_version = self.common.settings['installed_version'][self.common.settings['preferred']] start = self.common.paths['tbb'][self.common.settings['preferred']]['start'] if os.path.isfile(start) and os.access(start, os.X_OK): if installed_version == latest_version: print _('Latest version of TBB is installed, launching') # current version of tbb is installed, launch it self.run(False) self.launch_gui = False elif installed_version < latest_version: print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version)) # there is a tbb upgrade available if latest_version >= '3.': self.set_gui('task', _("Your Tor Browser is out of date."), ['download_sha256', 'download_sha256_sig', 'download_tarball', 'verify', 'extract', 'run']) else: self.set_gui('task', _("Your Tor Browser is out of date."), ['download_tarball_sig', 'download_tarball', 'verify', 'extract', 'run']) else: # for some reason the installed tbb is newer than the current version? self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), []) # not installed else: print _('TBB is not installed, attempting to install {0}'.format(latest_version)) if latest_version >= '3.': self.set_gui('task', _("Downloading and installing Tor Browser."), ['download_sha256', 'download_sha256_sig', 'download_tarball', 'verify', 'extract', 'run']) else: self.set_gui('task', _("Downloading and installing Tor Browser."), ['download_tarball_sig', 'download_tarball', 'verify', 'extract', 'run']) # there are different GUIs that might appear, this sets which one we want def set_gui(self, gui, message, tasks, autostart=True): self.gui = gui self.gui_message = message self.gui_tasks = tasks self.gui_task_i = 0 self.gui_autostart = autostart # set all gtk variables to False def clear_ui(self): if hasattr(self, 'box') and hasattr(self.box, 'destroy'): self.box.destroy() self.box = False self.label = False self.progressbar = False self.button_box = False self.start_button = False self.exit_button = False # build the application's UI def build_ui(self): self.clear_ui() self.box = gtk.VBox(False, 20) self.window.add(self.box) if 'error' in self.gui: # labels self.label = gtk.Label( self.gui_message ) self.label.set_line_wrap(True) self.box.pack_start(self.label, True, True, 0) self.label.show() # button box self.button_box = gtk.HButtonBox() self.button_box.set_layout(gtk.BUTTONBOX_SPREAD) self.box.pack_start(self.button_box, True, True, 0) self.button_box.show() if self.gui != 'error': # yes button yes_image = gtk.Image() yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON) self.yes_button = gtk.Button("Yes") self.yes_button.set_image(yes_image) if self.gui == 'error_try_stable': self.yes_button.connect("clicked", self.try_stable, None) elif self.gui == 'error_try_default_mirror': self.yes_button.connect("clicked", self.try_default_mirror, None) elif self.gui == 'error_try_tor': self.yes_button.connect("clicked", self.try_tor, None) self.button_box.add(self.yes_button) self.yes_button.show() # exit button exit_image = gtk.Image() exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON) self.exit_button = gtk.Button("Exit") self.exit_button.set_image(exit_image) self.exit_button.connect("clicked", self.destroy, None) self.button_box.add(self.exit_button) self.exit_button.show() elif self.gui == 'task': # label self.label = gtk.Label( self.gui_message ) self.label.set_line_wrap(True) self.box.pack_start(self.label, True, True, 0) self.label.show() # progress bar self.progressbar = gtk.ProgressBar(adjustment=None) self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT) self.progressbar.set_pulse_step(0.01) self.box.pack_start(self.progressbar, True, True, 0) # button box self.button_box = gtk.HButtonBox() self.button_box.set_layout(gtk.BUTTONBOX_SPREAD) self.box.pack_start(self.button_box, True, True, 0) self.button_box.show() # start button start_image = gtk.Image() start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON) self.start_button = gtk.Button(_("Start")) self.start_button.set_image(start_image) self.start_button.connect("clicked", self.start, None) self.button_box.add(self.start_button) if not self.gui_autostart: self.start_button.show() # exit button exit_image = gtk.Image() exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON) self.exit_button = gtk.Button(_("Exit")) self.exit_button.set_image(exit_image) self.exit_button.connect("clicked", self.destroy, None) self.button_box.add(self.exit_button) self.exit_button.show() self.box.show() self.window.show() if self.gui_autostart: self.start(None) # start button clicked, begin tasks def start(self, widget, data=None): # disable the start button if self.start_button: self.start_button.set_sensitive(False) # start running tasks self.run_task() # run the next task in the task list def run_task(self): self.refresh_gtk() if self.gui_task_i >= len(self.gui_tasks): self.destroy(False) return task = self.gui_tasks[self.gui_task_i] # get ready for the next task self.gui_task_i += 1 print _('Running task: {0}'.format(task)) if task == 'download_update_check': print _('Downloading'), self.common.paths['update_check_url'] self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file']) if task == 'attempt_update': print _('Checking to see if update is needed') self.attempt_update() elif task == 'download_sha256': print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror']) self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file']) elif task == 'download_sha256_sig': print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror']) self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file']) elif task == 'download_tarball_sig': print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror']) self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file']) elif task == 'download_tarball': print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror']) self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file']) elif task == 'verify': print _('Verifying signature') self.verify() elif task == 'extract': print _('Extracting'), self.common.paths['tarball_filename'] self.extract() elif task == 'run': print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start'] self.run() elif task == 'start_over': print _('Starting download over again') self.start_over() def response_received(self, response): class FileDownloader(Protocol): def __init__(self, common, file, total, progress, done_cb): self.file = file self.total = total self.so_far = 0 self.progress = progress self.all_done = done_cb if response.code != 200: try_stable = False if response.code == 404: if common.settings['preferred'] == 'alpha' and common.language != 'en-US': try_stable = True if try_stable: raise TryStableException(_("It looks like the alpha version of Tor Browser Bundle isn't available for your language. Would you like to try the stable version instead?")) else: if common.settings['mirror'] != common.default_mirror: raise TryDefaultMirrorException(_("Download Error: {0} {1}\n\nYou are currently using a non-default mirror:\n{2}\n\nWould you like to switch back to the default?").format(response.code, response.phrase, common.settings['mirror'])) else: raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase)) def dataReceived(self, bytes): self.file.write(bytes) self.so_far += len(bytes) percent = float(self.so_far) / float(self.total) self.progress.set_fraction(percent) amount = float(self.so_far) units = "bytes" for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]: if amount > size: units = unit amount = amount / float(size) break self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units))) def connectionLost(self, reason): print _('Finished receiving body:'), reason.getErrorMessage() self.all_done(reason) dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished) response.deliverBody(dl) def response_finished(self, msg): if msg.check(ResponseDone): self.file_download.close() delattr(self, 'current_download_path') # next task! self.run_task() else: print "FINISHED", msg ## FIXME handle errors def download_error(self, f): print _("Download error:"), f.value, type(f.value) if isinstance(f.value, TryStableException): f.trap(TryStableException) self.set_gui('error_try_stable', str(f.value), [], False) elif isinstance(f.value, TryDefaultMirrorException): f.trap(TryDefaultMirrorException) self.set_gui('error_try_default_mirror', str(f.value), [], False) elif isinstance(f.value, DownloadErrorException): f.trap(DownloadErrorException) self.set_gui('error', str(f.value), [], False) elif isinstance(f.value, DNSLookupError): f.trap(DNSLookupError) if common.settings['mirror'] != common.default_mirror: self.set_gui('error_try_default_mirror', _("DNS Lookup Error\n\nYou are currently using a non-default mirror:\n{0}\n\nWould you like to switch back to the default?").format(common.settings['mirror']), [], False) else: self.set_gui('error', str(f.value), [], False) elif isinstance(f.value, ResponseFailed): for reason in f.value.reasons: if isinstance(reason.value, OpenSSL.SSL.Error): # TODO: add the ability to report attack by posting bug to trac.torproject.org if not self.common.settings['update_over_tor']: self.set_gui('error_try_tor', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack. Try the download again using Tor?'), [], False) else: self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False) else: self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False) self.build_ui() def download(self, name, url, path): # keep track of current download self.current_download_path = path # initialize the progress bar mirror_url = url.format(self.common.settings['mirror']) self.progressbar.set_fraction(0) self.progressbar.set_text(_('Downloading {0}').format(name)) self.progressbar.show() self.refresh_gtk() # default mirror gets certificate pinning, only for requests that use the mirror if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url: agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem'])) else: agent = Agent(reactor) # actually, agent needs to follow redirect agent = RedirectAgent(agent) # start the request d = agent.request('GET', mirror_url, Headers({'User-Agent': ['torbrowser-launcher']}), None) self.file_download = open(path, 'w') d.addCallback(self.response_received).addErrback(self.download_error) if not reactor.running: reactor.run() def try_stable(self, widget, data=None): # change preferred to stable and relaunch TBL self.common.settings['preferred'] = 'stable' self.common.save_settings() p = subprocess.Popen([self.common.paths['tbl_bin']]) self.destroy(False) def try_default_mirror(self, widget, data=None): # change preferred to stable and relaunch TBL self.common.settings['mirror'] = self.common.default_mirror self.common.save_settings() p = subprocess.Popen([self.common.paths['tbl_bin']]) self.destroy(False) def try_tor(self, widget, data=None): # set update_over_tor to true and relaunch TBL self.common.settings['update_over_tor'] = True self.common.save_settings() p = subprocess.Popen([self.common.paths['tbl_bin']]) self.destroy(False) def attempt_update(self): # load the update check file try: versions = json.load(open(self.common.paths['update_check_file'])) latest_stable = None latest_alpha = None # filter linux versions valid_alphas = [] valid_stables = [] for version in versions: if '-Linux' in version: if 'alpha' in version or 'beta' in version or '-rc' in version: valid_alphas.append(str(version)) else: valid_stables.append(str(version)) valid_alphas.sort() if len(valid_alphas): latest_alpha = valid_alphas.pop() valid_stables.sort() if len(valid_stables): latest_stable = valid_stables.pop() if latest_stable or latest_alpha: if latest_stable: self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')] if latest_alpha: self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')] self.common.settings['last_update_check_timestamp'] = int(time.time()) self.common.settings['check_for_updates'] = False self.common.save_settings() self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']]) self.start_launcher() else: # failed to find the latest version self.set_gui('error', _("Error checking for updates."), [], False) except: # not a valid JSON object self.set_gui('error', _("Error checking for updates."), [], False) # now start over self.clear_ui() self.build_ui() def verify(self): latest_version = self.common.settings['latest_version'][self.common.settings['preferred']] # initialize the progress bar self.progressbar.set_fraction(0) self.progressbar.set_text(_('Verifying Signature')) self.progressbar.show() verified = False if latest_version >= '3.': # after 3.x we check the sha256 file's sig, and also take the sha256 of the tarball and compare p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']]) self.pulse_until_process_exits(p) if p.returncode == 0: # compare with sha256 of the tarball tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest() for line in open(self.common.paths['sha256_file'], 'r').readlines(): if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line: verified = True else: # before 3.x we just check the tarball sig p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']]) self.pulse_until_process_exits(p) if p.returncode == 0: verified = True if verified: self.run_task() else: # TODO: add the ability to report attack by posting bug to trac.torproject.org self.set_gui('task', _("SIGNATURE VERIFICATION FAILED!\n\nYou might be under attack, or there might just be a networking problem. Click Start try the download again."), ['start_over'], False) self.clear_ui() self.build_ui() if not reactor.running: reactor.run() def extract(self): # initialize the progress bar self.progressbar.set_fraction(0) self.progressbar.set_text(_('Installing')) self.progressbar.show() self.refresh_gtk() extracted = False try: if self.common.paths['tarball_file'][-2:] == 'xz': # if tarball is .tar.xz xz = lzma.LZMAFile(self.common.paths['tarball_file']) tf = tarfile.open(fileobj=xz) tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir']) extracted = True else: # if tarball is .tar.gz if tarfile.is_tarfile(self.common.paths['tarball_file']): tf = tarfile.open(self.common.paths['tarball_file']) tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir']) extracted = True except: pass if not extracted: self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False) self.clear_ui() self.build_ui() return # installation is finished, so save installed_version self.common.settings['installed_version'] = self.common.settings['latest_version'] self.common.save_settings() self.run_task() def run(self, run_next_task = True): subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']]) if run_next_task: self.run_task() # make the progress bar pulse until process p (a Popen object) finishes def pulse_until_process_exits(self, p): while p.poll() == None: time.sleep(0.01) self.progressbar.pulse() self.refresh_gtk() # start over and download TBB again def start_over(self): self.label.set_text(_("Downloading Tor Browser Bundle over again.")) self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run'] self.gui_task_i = 0 self.start(None) # refresh gtk def refresh_gtk(self): while gtk.events_pending(): gtk.main_iteration(False) # exit def delete_event(self, widget, event, data=None): return False def destroy(self, widget, data=None): if hasattr(self, 'file_download'): self.file_download.close() if hasattr(self, 'current_download_path'): os.remove(self.current_download_path) delattr(self, 'current_download_path') if reactor.running: reactor.stop() if __name__ == "__main__": tor_browser_launcher_version = '0.0.3' print _('Tor Browser Launcher') print _('By Micah Lee, licensed under GPLv3') print _('version {0}').format(tor_browser_launcher_version) print 'https://github.com/micahflee/torbrowser-launcher' common = TBLCommon(tor_browser_launcher_version) # is torbrowser-launcher already running? tbl_pid = common.get_pid(common.paths['tbl_bin'], True) if tbl_pid: print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid) common.bring_window_to_front(tbl_pid) sys.exit() if '-settings' in sys.argv: # settings mode app = TBLSettings(common) else: # launcher mode app = TBLLauncher(common)