4 https://github.com/micahflee/torbrowser-launcher/
6 Copyright (c) 2013 Micah Lee <micah@micahflee.com>
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
31 sys.path.append('/usr/share/torbrowser-launcher/lib/')
35 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
37 from twisted.internet import gtk2reactor
39 from twisted.internet import reactor
45 import os, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil, hashlib, lzma
47 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
48 from twisted.web.http_headers import Headers
49 from twisted.internet.protocol import Protocol
50 from twisted.internet.ssl import ClientContextFactory
51 from twisted.internet.endpoints import TCP4ClientEndpoint
52 from twisted.internet.error import DNSLookupError
54 from txsocksx.client import SOCKS5ClientEndpoint
58 class TryStableException(Exception):
60 class TryDefaultMirrorException(Exception):
62 class DownloadErrorException(Exception):
65 class VerifyTorProjectCert(ClientContextFactory):
67 def __init__(self, torproject_pem):
68 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
70 def getContext(self, host, port):
71 ctx = ClientContextFactory.getContext(self)
72 ctx.set_verify_depth(0)
73 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
76 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
77 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
81 def __init__(self, tbl_version):
82 print _('Initializing Tor Browser Launcher')
83 self.tbl_version = tbl_version
86 self.available_versions = {
87 'stable': _('Tor Browser Bundle - stable'),
88 'alpha': _('Tor Browser Bundle - alpha')
90 self.default_mirror = 'https://www.torproject.org/dist/'
92 self.discover_arch_lang()
94 self.mkdir(self.paths['data_dir'])
97 self.mkdir(self.paths['download_dir'])
98 self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
101 # allow buttons to have icons
103 gtk_settings = gtk.settings_get_default()
104 gtk_settings.props.gtk_button_images = True
108 # discover the architecture and language
109 def discover_arch_lang(self):
110 # figure out the architecture
111 (sysname, nodename, release, version, machine) = os.uname()
112 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
114 # figure out the language
115 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
116 default_locale = locale.getdefaultlocale()[0]
117 if default_locale == None:
118 self.language = 'en-US'
120 self.language = default_locale.replace('_', '-')
121 if self.language not in available_languages:
122 self.language = self.language.split('-')[0]
123 if self.language not in available_languages:
124 for l in available_languages:
125 if l[0:2] == self.language:
127 # if language isn't available, default to english
128 if self.language not in available_languages:
129 self.language = 'en-US'
131 # build all relevant paths
132 def build_paths(self, tbb_version = None):
133 homedir = os.getenv('HOME')
135 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
136 if os.path.exists(homedir) == False:
138 os.mkdir(homedir, 0700)
140 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
141 if not os.access(homedir, os.W_OK):
142 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
144 tbb_data = '%s/.torbrowser' % homedir
147 if tbb_version >= '3.':
149 dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b').replace('-rc-', 'rc')
150 if self.architecture == 'x86_64':
154 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
157 self.paths['tarball_url'] = '{0}torbrowser/'+dirname+'/'+tarball_filename
160 self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
161 self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
162 self.paths['sha256_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt'
163 self.paths['sha256_sig_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt-mikeperry.asc'
166 tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
169 self.paths['tarball_url'] = '{0}torbrowser/linux/'+tarball_filename # {0} will be replaced with the mirror
172 self.paths['tarball_sig_file'] = tbb_data+'/download/'+tarball_filename+'.asc'
173 self.paths['tarball_sig_url'] = '{0}torbrowser/linux/'+tarball_filename+'.asc'
174 self.paths['tarball_sig_filename'] = tarball_filename+'.asc'
176 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
177 self.paths['tarball_filename'] = tarball_filename
181 'tbl_bin': '/usr/bin/torbrowser-launcher',
182 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
183 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
184 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
185 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
186 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
187 'mike_key': '/usr/share/torbrowser-launcher/mike-2013-09.asc',
188 'mirrors_txt': ['/usr/share/torbrowser-launcher/mirrors.txt',
189 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
190 'modem_sound': '/usr/share/torbrowser-launcher/modem.ogg',
191 'data_dir': tbb_data,
192 'download_dir': tbb_data+'/download',
193 'gnupg_homedir': tbb_data+'/gnupg_homedir',
194 'settings_file': tbb_data+'/settings',
195 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
196 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
199 'dir': tbb_data+'/tbb/stable/'+self.architecture,
200 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
201 'vidalia_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
202 'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
203 'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
204 'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
207 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
208 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
209 'vidalia_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
210 'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
211 'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
212 'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
218 def mkdir(self, path):
220 if os.path.exists(path) == False:
221 os.makedirs(path, 0700)
224 print _("Cannot create directory {0}").format(path)
226 if not os.access(path, os.W_OK):
227 print _("{0} is not writable").format(path)
231 # if gnupg_homedir isn't set up, set it up
232 def init_gnupg(self):
233 if not os.path.exists(self.paths['gnupg_homedir']):
234 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
235 self.mkdir(self.paths['gnupg_homedir'])
239 def import_keys(self):
240 print _('Importing keys')
241 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()
244 def load_mirrors(self):
246 for srcfile in self.paths['mirrors_txt']:
247 if not os.path.exists(srcfile):
248 print "Warning: can't load mirrors from %s" % srcfile
250 for mirror in open(srcfile, 'r').readlines():
251 if mirror.strip() not in self.mirrors:
252 self.mirrors.append(mirror.strip())
255 def load_settings(self):
257 'tbl_version': self.tbl_version,
258 'preferred': 'stable',
259 'installed_version': {
267 'update_over_tor': True,
268 'check_for_updates': False,
269 'modem_sound': False,
270 'last_update_check_timestamp': 0,
271 'mirror': self.default_mirror
274 if os.path.isfile(self.paths['settings_file']):
275 settings = pickle.load(open(self.paths['settings_file']))
278 # settings migrations
279 if settings['tbl_version'] == '0.0.1':
280 print '0.0.1 migration'
281 self.settings = default_settings
282 self.settings['installed_version']['alpha'] = settings['installed_version']
286 self.mkdir(self.paths['tbb']['alpha']['dir'])
288 # make sure settings file is up-to-date
289 for setting in default_settings:
290 if setting not in settings:
291 settings[setting] = default_settings[setting]
294 # make sure the version is current
295 if settings['tbl_version'] != self.tbl_version:
296 settings['tbl_version'] = self.tbl_version
299 self.settings = settings
304 self.settings = default_settings
308 def save_settings(self):
309 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
312 # get the process id of a program
313 def get_pid(self, bin_path, python = False):
316 for p in psutil.process_iter():
318 if p.pid != os.getpid():
321 if len(p.cmdline) > 1:
322 if 'python' in p.cmdline[0]:
325 if len(p.cmdline) > 0:
336 # bring program's x window to front
337 def bring_window_to_front(self, pid):
338 # figure out the window id
340 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
341 for line in p.stdout.readlines():
342 line_split = line.split()
343 cur_win_id = line_split[0]
344 cur_win_pid = int(line_split[2])
345 if cur_win_pid == pid:
350 subprocess.call(['wmctrl', '-i', '-a', win_id])
353 def __init__(self, common):
354 print _('Starting settings dialog')
358 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
359 self.window.set_title(_("Tor Browser Launcher Settings"))
360 self.window.set_icon_from_file(self.common.paths['icon_file'])
361 self.window.set_position(gtk.WIN_POS_CENTER)
362 self.window.set_border_width(10)
363 self.window.connect("delete_event", self.delete_event)
364 self.window.connect("destroy", self.destroy)
366 # build the rest of the UI
367 self.box = gtk.VBox(False, 10)
368 self.window.add(self.box)
371 self.hbox = gtk.HBox(False, 10)
372 self.box.pack_start(self.hbox, True, True, 0)
375 self.settings_box = gtk.VBox(False, 10)
376 self.hbox.pack_start(self.settings_box, True, True, 0)
377 self.settings_box.show()
379 self.labels_box = gtk.VBox(False, 10)
380 self.hbox.pack_start(self.labels_box, True, True, 0)
381 self.labels_box.show()
384 self.preferred_box = gtk.HBox(False, 10)
385 self.settings_box.pack_start(self.preferred_box, True, True, 0)
386 self.preferred_box.show()
388 self.preferred_label = gtk.Label(_('I prefer'))
389 self.preferred_label.set_line_wrap(True)
390 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
391 self.preferred_label.show()
393 self.preferred_options = []
394 for i in self.common.available_versions:
395 self.preferred_options.append(self.common.available_versions[i])
396 self.preferred_options.sort()
398 self.preferred = gtk.combo_box_new_text()
399 for option in self.preferred_options:
400 self.preferred.append_text(option)
401 if self.common.settings['preferred'] in self.common.available_versions:
402 self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
404 self.preferred.set_active(0)
405 self.preferred_box.pack_start(self.preferred, True, True, 0)
406 self.preferred.show()
409 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
411 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
412 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
413 if self.common.settings['update_over_tor']:
414 self.tor_update_checkbox.set_active(True)
416 self.tor_update_checkbox.set_active(False)
417 self.tor_update_checkbox.show()
421 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
422 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
423 if self.common.settings['check_for_updates']:
424 self.update_checkbox.set_active(True)
426 self.update_checkbox.set_active(False)
427 self.update_checkbox.show()
430 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
431 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
432 if self.common.settings['modem_sound']:
433 self.modem_checkbox.set_active(True)
435 self.modem_checkbox.set_active(False)
436 self.modem_checkbox.show()
439 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
440 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
442 self.label1 = gtk.Label(_('Not installed'))
443 self.label1.set_line_wrap(True)
444 self.labels_box.pack_start(self.label1, True, True, 0)
447 if(self.common.settings['last_update_check_timestamp'] > 0):
448 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']))))
450 self.label1 = gtk.Label(_('Never checked for updates'))
451 self.label1.set_line_wrap(True)
452 self.labels_box.pack_start(self.label1, True, True, 0)
456 self.mirrors_box = gtk.HBox(False, 10)
457 self.box.pack_start(self.mirrors_box, True, True, 0)
458 self.mirrors_box.show()
460 self.mirrors_label = gtk.Label(_('Mirror'))
461 self.mirrors_label.set_line_wrap(True)
462 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
463 self.mirrors_label.show()
465 self.mirrors = gtk.combo_box_new_text()
466 for mirror in self.common.mirrors:
467 self.mirrors.append_text(mirror)
468 if self.common.settings['mirror'] in self.common.mirrors:
469 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
471 self.preferred.set_active(0)
472 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
476 self.button_box = gtk.HButtonBox()
477 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
478 self.box.pack_start(self.button_box, True, True, 0)
479 self.button_box.show()
481 # save and launch button
482 save_launch_image = gtk.Image()
483 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
484 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
485 self.save_launch_button.set_image(save_launch_image)
486 self.save_launch_button.connect("clicked", self.save_launch, None)
487 self.button_box.add(self.save_launch_button)
488 self.save_launch_button.show()
490 # save and exit button
491 save_exit_image = gtk.Image()
492 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
493 self.save_exit_button = gtk.Button(_("Save & Exit"))
494 self.save_exit_button.set_image(save_exit_image)
495 self.save_exit_button.connect("clicked", self.save_exit, None)
496 self.button_box.add(self.save_exit_button)
497 self.save_exit_button.show()
500 cancel_image = gtk.Image()
501 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
502 self.cancel_button = gtk.Button(_("Cancel"))
503 self.cancel_button.set_image(cancel_image)
504 self.cancel_button.connect("clicked", self.destroy, None)
505 self.button_box.add(self.cancel_button)
506 self.cancel_button.show()
515 def save_launch(self, widget, data=None):
517 p = subprocess.Popen([self.common.paths['tbl_bin']])
521 def save_exit(self, widget, data=None):
527 # figure out the selected preferred option
529 selected = self.preferred_options[self.preferred.get_active()]
530 for i in self.common.available_versions:
531 if self.common.available_versions[i] == selected:
534 self.common.settings['preferred'] = preferred
537 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
538 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
539 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
541 # figure out the selected mirror
542 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
545 self.common.save_settings()
548 def delete_event(self, widget, event, data=None):
550 def destroy(self, widget, data=None):
555 def __init__(self, common):
556 print _('Starting launcher dialog')
560 self.set_gui(None, '', [])
561 self.launch_gui = True
562 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
564 # is vidalia already running and we just need to open a new firefox?
565 if self.common.settings['installed_version']:
566 vidalia_pid = self.common.get_pid('./App/vidalia')
567 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
569 if vidalia_pid and not firefox_pid:
570 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
571 self.common.bring_window_to_front(vidalia_pid)
572 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']])
574 elif vidalia_pid and firefox_pid:
575 print _('Vidalia and Firefox are already open, bringing them to focus')
577 # bring firefox to front, then vidalia
578 self.common.bring_window_to_front(firefox_pid)
579 self.common.bring_window_to_front(vidalia_pid)
583 check_for_updates = False
584 if self.common.settings['check_for_updates']:
585 check_for_updates = True
587 if not check_for_updates:
588 # how long was it since the last update check?
589 # 86400 seconds = 24 hours
590 current_timestamp = int(time.time())
591 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
592 check_for_updates = True
594 if check_for_updates:
596 print 'Checking for update'
597 self.set_gui('task', _("Checking for Tor Browser update."),
598 ['download_update_check',
601 # no need to check for update
602 print _('Checked for update within 24 hours, skipping')
603 self.start_launcher()
607 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
608 self.window.set_title(_("Tor Browser"))
609 self.window.set_icon_from_file(self.common.paths['icon_file'])
610 self.window.set_position(gtk.WIN_POS_CENTER)
611 self.window.set_border_width(10)
612 self.window.connect("delete_event", self.delete_event)
613 self.window.connect("destroy", self.destroy)
615 # build the rest of the UI
618 # download or run TBB
619 def start_launcher(self):
620 # is TBB already installed?
621 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
622 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
624 # verify installed version for newer versions of TBB (#58)
625 if installed_version >= '3.0':
626 versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
627 if os.path.exists(versions_filename):
628 for line in open(versions_filename):
629 if 'TORBROWSER_VERSION' in line:
630 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
632 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
633 if os.path.isfile(start) and os.access(start, os.X_OK):
634 if installed_version == latest_version:
635 print _('Latest version of TBB is installed, launching')
636 # current version of tbb is installed, launch it
638 self.launch_gui = False
639 elif installed_version < latest_version:
640 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
641 # there is a tbb upgrade available
642 if latest_version >= '3.':
643 self.set_gui('task', _("Your Tor Browser is out of date."),
645 'download_sha256_sig',
651 self.set_gui('task', _("Your Tor Browser is out of date."),
652 ['download_tarball_sig',
658 # for some reason the installed tbb is newer than the current version?
659 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
663 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
664 if latest_version >= '3.':
665 self.set_gui('task', _("Downloading and installing Tor Browser."),
667 'download_sha256_sig',
673 self.set_gui('task', _("Downloading and installing Tor Browser."),
674 ['download_tarball_sig',
680 # there are different GUIs that might appear, this sets which one we want
681 def set_gui(self, gui, message, tasks, autostart=True):
683 self.gui_message = message
684 self.gui_tasks = tasks
686 self.gui_autostart = autostart
688 # set all gtk variables to False
690 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
695 self.progressbar = False
696 self.button_box = False
697 self.start_button = False
698 self.exit_button = False
700 # build the application's UI
704 self.box = gtk.VBox(False, 20)
705 self.window.add(self.box)
707 if 'error' in self.gui:
709 self.label = gtk.Label( self.gui_message )
710 self.label.set_line_wrap(True)
711 self.box.pack_start(self.label, True, True, 0)
715 self.button_box = gtk.HButtonBox()
716 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
717 self.box.pack_start(self.button_box, True, True, 0)
718 self.button_box.show()
720 if self.gui != 'error':
722 yes_image = gtk.Image()
723 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
724 self.yes_button = gtk.Button("Yes")
725 self.yes_button.set_image(yes_image)
726 if self.gui == 'error_try_stable':
727 self.yes_button.connect("clicked", self.try_stable, None)
728 elif self.gui == 'error_try_default_mirror':
729 self.yes_button.connect("clicked", self.try_default_mirror, None)
730 elif self.gui == 'error_try_tor':
731 self.yes_button.connect("clicked", self.try_tor, None)
732 self.button_box.add(self.yes_button)
733 self.yes_button.show()
736 exit_image = gtk.Image()
737 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
738 self.exit_button = gtk.Button("Exit")
739 self.exit_button.set_image(exit_image)
740 self.exit_button.connect("clicked", self.destroy, None)
741 self.button_box.add(self.exit_button)
742 self.exit_button.show()
744 elif self.gui == 'task':
746 self.label = gtk.Label( self.gui_message )
747 self.label.set_line_wrap(True)
748 self.box.pack_start(self.label, True, True, 0)
752 self.progressbar = gtk.ProgressBar(adjustment=None)
753 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
754 self.progressbar.set_pulse_step(0.01)
755 self.box.pack_start(self.progressbar, True, True, 0)
758 self.button_box = gtk.HButtonBox()
759 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
760 self.box.pack_start(self.button_box, True, True, 0)
761 self.button_box.show()
764 start_image = gtk.Image()
765 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
766 self.start_button = gtk.Button(_("Start"))
767 self.start_button.set_image(start_image)
768 self.start_button.connect("clicked", self.start, None)
769 self.button_box.add(self.start_button)
770 if not self.gui_autostart:
771 self.start_button.show()
774 exit_image = gtk.Image()
775 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
776 self.exit_button = gtk.Button(_("Exit"))
777 self.exit_button.set_image(exit_image)
778 self.exit_button.connect("clicked", self.destroy, None)
779 self.button_box.add(self.exit_button)
780 self.exit_button.show()
785 if self.gui_autostart:
788 # start button clicked, begin tasks
789 def start(self, widget, data=None):
790 # disable the start button
791 if self.start_button:
792 self.start_button.set_sensitive(False)
794 # start running tasks
797 # run the next task in the task list
801 if self.gui_task_i >= len(self.gui_tasks):
805 task = self.gui_tasks[self.gui_task_i]
807 # get ready for the next task
810 print _('Running task: {0}'.format(task))
811 if task == 'download_update_check':
812 print _('Downloading'), self.common.paths['update_check_url']
813 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
815 if task == 'attempt_update':
816 print _('Checking to see if update is needed')
817 self.attempt_update()
819 elif task == 'download_sha256':
820 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
821 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
823 elif task == 'download_sha256_sig':
824 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
825 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
827 elif task == 'download_tarball_sig':
828 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
829 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
831 elif task == 'download_tarball':
832 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
833 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
835 elif task == 'verify':
836 print _('Verifying signature')
839 elif task == 'extract':
840 print _('Extracting'), self.common.paths['tarball_filename']
844 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
847 elif task == 'start_over':
848 print _('Starting download over again')
851 def response_received(self, response):
852 class FileDownloader(Protocol):
853 def __init__(self, common, file, total, progress, done_cb):
857 self.progress = progress
858 self.all_done = done_cb
860 if response.code != 200:
863 if response.code == 404:
864 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
868 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?"))
870 if common.settings['mirror'] != common.default_mirror:
871 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']))
873 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
875 def dataReceived(self, bytes):
876 self.file.write(bytes)
877 self.so_far += len(bytes)
878 percent = float(self.so_far) / float(self.total)
879 self.progress.set_fraction(percent)
880 amount = float(self.so_far)
882 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
885 amount = amount / float(size)
888 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
890 def connectionLost(self, reason):
891 print _('Finished receiving body:'), reason.getErrorMessage()
892 self.all_done(reason)
894 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
895 response.deliverBody(dl)
897 def response_finished(self, msg):
898 if msg.check(ResponseDone):
899 self.file_download.close()
900 delattr(self, 'current_download_path')
906 print "FINISHED", msg
907 ## FIXME handle errors
909 def download_error(self, f):
910 print _("Download error:"), f.value, type(f.value)
912 if isinstance(f.value, TryStableException):
913 f.trap(TryStableException)
914 self.set_gui('error_try_stable', str(f.value), [], False)
916 elif isinstance(f.value, TryDefaultMirrorException):
917 f.trap(TryDefaultMirrorException)
918 self.set_gui('error_try_default_mirror', str(f.value), [], False)
920 elif isinstance(f.value, DownloadErrorException):
921 f.trap(DownloadErrorException)
922 self.set_gui('error', str(f.value), [], False)
924 elif isinstance(f.value, DNSLookupError):
925 f.trap(DNSLookupError)
926 if common.settings['mirror'] != common.default_mirror:
927 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)
929 self.set_gui('error', str(f.value), [], False)
931 elif isinstance(f.value, ResponseFailed):
932 for reason in f.value.reasons:
933 if isinstance(reason.value, OpenSSL.SSL.Error):
934 # TODO: add the ability to report attack by posting bug to trac.torproject.org
935 if not self.common.settings['update_over_tor']:
936 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)
938 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
941 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
945 def download(self, name, url, path):
946 # keep track of current download
947 self.current_download_path = path
949 # initialize the progress bar
950 mirror_url = url.format(self.common.settings['mirror'])
951 self.progressbar.set_fraction(0)
952 self.progressbar.set_text(_('Downloading {0}').format(name))
953 self.progressbar.show()
956 # default mirror gets certificate pinning, only for requests that use the mirror
957 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
958 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
960 agent = Agent(reactor)
962 # actually, agent needs to follow redirect
963 agent = RedirectAgent(agent)
966 d = agent.request('GET', mirror_url,
967 Headers({'User-Agent': ['torbrowser-launcher']}),
970 self.file_download = open(path, 'w')
971 d.addCallback(self.response_received).addErrback(self.download_error)
973 if not reactor.running:
976 def try_stable(self, widget, data=None):
977 # change preferred to stable and relaunch TBL
978 self.common.settings['preferred'] = 'stable'
979 self.common.save_settings()
980 p = subprocess.Popen([self.common.paths['tbl_bin']])
983 def try_default_mirror(self, widget, data=None):
984 # change preferred to stable and relaunch TBL
985 self.common.settings['mirror'] = self.common.default_mirror
986 self.common.save_settings()
987 p = subprocess.Popen([self.common.paths['tbl_bin']])
990 def try_tor(self, widget, data=None):
991 # set update_over_tor to true and relaunch TBL
992 self.common.settings['update_over_tor'] = True
993 self.common.save_settings()
994 p = subprocess.Popen([self.common.paths['tbl_bin']])
997 def attempt_update(self):
998 # load the update check file
1000 versions = json.load(open(self.common.paths['update_check_file']))
1001 latest_stable = None
1004 # filter linux versions
1007 for version in versions:
1008 if '-Linux' in version:
1009 if 'alpha' in version or 'beta' in version or '-rc' in version:
1010 valid_alphas.append(str(version))
1012 valid_stables.append(str(version))
1014 if len(valid_alphas):
1015 latest_alpha = valid_alphas.pop()
1016 valid_stables.sort()
1017 if len(valid_stables):
1018 latest_stable = valid_stables.pop()
1020 if latest_stable or latest_alpha:
1022 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
1024 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
1025 self.common.settings['last_update_check_timestamp'] = int(time.time())
1026 self.common.settings['check_for_updates'] = False
1027 self.common.save_settings()
1028 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
1029 self.start_launcher()
1032 # failed to find the latest version
1033 self.set_gui('error', _("Error checking for updates."), [], False)
1036 # not a valid JSON object
1037 self.set_gui('error', _("Error checking for updates."), [], False)
1044 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1046 # initialize the progress bar
1047 self.progressbar.set_fraction(0)
1048 self.progressbar.set_text(_('Verifying Signature'))
1049 self.progressbar.show()
1052 if latest_version >= '3.':
1053 # after 3.x we check the sha256 file's sig, and also take the sha256 of the tarball and compare
1054 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1055 self.pulse_until_process_exits(p)
1056 if p.returncode == 0:
1057 # compare with sha256 of the tarball
1058 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1059 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1060 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1064 # before 3.x we just check the tarball sig
1065 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
1066 self.pulse_until_process_exits(p)
1067 if p.returncode == 0:
1073 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1074 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)
1078 if not reactor.running:
1082 # initialize the progress bar
1083 self.progressbar.set_fraction(0)
1084 self.progressbar.set_text(_('Installing'))
1085 self.progressbar.show()
1090 if self.common.paths['tarball_file'][-2:] == 'xz':
1091 # if tarball is .tar.xz
1092 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1093 tf = tarfile.open(fileobj=xz)
1094 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1097 # if tarball is .tar.gz
1098 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1099 tf = tarfile.open(self.common.paths['tarball_file'])
1100 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1106 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1111 # installation is finished, so save installed_version
1112 self.common.settings['installed_version'] = self.common.settings['latest_version']
1113 self.common.save_settings()
1117 def run(self, run_next_task = True):
1118 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1121 if self.common.settings['modem_sound']:
1124 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1131 # make the progress bar pulse until process p (a Popen object) finishes
1132 def pulse_until_process_exits(self, p):
1133 while p.poll() == None:
1135 self.progressbar.pulse()
1138 # start over and download TBB again
1139 def start_over(self):
1140 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1141 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1146 def refresh_gtk(self):
1147 while gtk.events_pending():
1148 gtk.main_iteration(False)
1151 def delete_event(self, widget, event, data=None):
1153 def destroy(self, widget, data=None):
1154 if hasattr(self, 'file_download'):
1155 self.file_download.close()
1156 if hasattr(self, 'current_download_path'):
1157 os.remove(self.current_download_path)
1158 delattr(self, 'current_download_path')
1162 if __name__ == "__main__":
1163 tor_browser_launcher_version = '0.0.6'
1165 print _('Tor Browser Launcher')
1166 print _('By Micah Lee, licensed under GPLv3')
1167 print _('version {0}').format(tor_browser_launcher_version)
1168 print 'https://github.com/micahflee/torbrowser-launcher'
1170 common = TBLCommon(tor_browser_launcher_version)
1172 # is torbrowser-launcher already running?
1173 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1175 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1176 common.bring_window_to_front(tbl_pid)
1179 if '-settings' in sys.argv:
1181 app = TBLSettings(common)
1185 app = TBLLauncher(common)