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.
34 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
36 from twisted.internet import gtk2reactor
38 from twisted.internet import reactor
44 import os, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil, hashlib, lzma
46 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
47 from twisted.web.http_headers import Headers
48 from twisted.internet.protocol import Protocol
49 from twisted.internet.ssl import ClientContextFactory
50 from twisted.internet.endpoints import TCP4ClientEndpoint
51 from twisted.internet.error import DNSLookupError
53 from txsocksx.client import SOCKS5ClientEndpoint
58 class TryStableException(Exception):
62 class TryDefaultMirrorException(Exception):
66 class DownloadErrorException(Exception):
70 class VerifyTorProjectCert(ClientContextFactory):
72 def __init__(self, torproject_pem):
73 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
75 def getContext(self, host, port):
76 ctx = ClientContextFactory.getContext(self)
77 ctx.set_verify_depth(0)
78 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
81 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
82 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
87 def __init__(self, tbl_version):
88 print _('Initializing Tor Browser Launcher')
89 self.tbl_version = tbl_version
92 self.available_versions = {
93 'stable': _('Tor Browser Bundle - stable'),
94 'alpha': _('Tor Browser Bundle - alpha')
96 self.default_mirror = 'https://www.torproject.org/dist/'
98 self.discover_arch_lang()
100 self.mkdir(self.paths['data_dir'])
103 self.mkdir(self.paths['download_dir'])
104 self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
107 # allow buttons to have icons
109 gtk_settings = gtk.settings_get_default()
110 gtk_settings.props.gtk_button_images = True
114 # discover the architecture and language
115 def discover_arch_lang(self):
116 # figure out the architecture
117 (sysname, nodename, release, version, machine) = os.uname()
118 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
120 # figure out the language
121 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
122 default_locale = locale.getdefaultlocale()[0]
123 if default_locale is None:
124 self.language = 'en-US'
126 self.language = default_locale.replace('_', '-')
127 if self.language not in available_languages:
128 self.language = self.language.split('-')[0]
129 if self.language not in available_languages:
130 for l in available_languages:
131 if l[0:2] == self.language:
133 # if language isn't available, default to english
134 if self.language not in available_languages:
135 self.language = 'en-US'
137 # build all relevant paths
138 def build_paths(self, tbb_version=None):
139 homedir = os.getenv('HOME')
141 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
142 if not os.path.exists(homedir):
144 os.mkdir(homedir, 0700)
146 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
147 if not os.access(homedir, os.W_OK):
148 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
150 tbb_data = '%s/.torbrowser' % homedir
154 dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b').replace('-rc-', 'rc')
155 if self.architecture == 'x86_64':
159 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
162 self.paths['tarball_url'] = '{0}torbrowser/'+dirname+'/'+tarball_filename
165 self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
166 self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
167 self.paths['sha256_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt'
168 self.paths['sha256_sig_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt-mikeperry.asc'
170 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
171 self.paths['tarball_filename'] = tarball_filename
175 'tbl_bin': '/usr/bin/torbrowser-launcher',
176 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
177 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
178 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
179 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
180 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
181 'mike_key': '/usr/share/torbrowser-launcher/mike-2013-09.asc',
182 'mirrors_txt': ['/usr/share/torbrowser-launcher/mirrors.txt',
183 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
184 'modem_sound': '/usr/share/torbrowser-launcher/modem.ogg',
185 'data_dir': tbb_data,
186 'download_dir': tbb_data+'/download',
187 'gnupg_homedir': tbb_data+'/gnupg_homedir',
188 'settings_file': tbb_data+'/settings',
189 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
190 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
193 'dir': tbb_data+'/tbb/stable/'+self.architecture,
194 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
195 'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Browser/firefox',
196 'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
197 'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
200 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
201 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
202 'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Browser/firefox',
203 'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
204 'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
210 def mkdir(self, path):
212 if not os.path.exists(path):
213 os.makedirs(path, 0700)
216 print _("Cannot create directory {0}").format(path)
218 if not os.access(path, os.W_OK):
219 print _("{0} is not writable").format(path)
223 # if gnupg_homedir isn't set up, set it up
224 def init_gnupg(self):
225 if not os.path.exists(self.paths['gnupg_homedir']):
226 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
227 self.mkdir(self.paths['gnupg_homedir'])
231 def import_keys(self):
232 print _('Importing keys')
233 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()
236 def load_mirrors(self):
238 for srcfile in self.paths['mirrors_txt']:
239 if not os.path.exists(srcfile):
240 print "Warning: can't load mirrors from %s" % srcfile
242 for mirror in open(srcfile, 'r').readlines():
243 if mirror.strip() not in self.mirrors:
244 self.mirrors.append(mirror.strip())
247 def load_settings(self):
249 'tbl_version': self.tbl_version,
250 'preferred': 'stable',
251 'installed_version': {
259 'update_over_tor': True,
260 'check_for_updates': False,
261 'modem_sound': False,
262 'last_update_check_timestamp': 0,
263 'mirror': self.default_mirror
266 if os.path.isfile(self.paths['settings_file']):
267 settings = pickle.load(open(self.paths['settings_file']))
270 # settings migrations
271 if settings['tbl_version'] == '0.0.1':
272 print '0.0.1 migration'
273 self.settings = default_settings
274 self.settings['installed_version']['alpha'] = settings['installed_version']
278 self.mkdir(self.paths['tbb']['alpha']['dir'])
280 # make sure settings file is up-to-date
281 for setting in default_settings:
282 if setting not in settings:
283 settings[setting] = default_settings[setting]
286 # make sure the version is current
287 if settings['tbl_version'] != self.tbl_version:
288 settings['tbl_version'] = self.tbl_version
291 self.settings = settings
296 self.settings = default_settings
300 def save_settings(self):
301 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
304 # get the process id of a program
305 def get_pid(self, bin_path, python=False):
308 for p in psutil.process_iter():
310 if p.pid != os.getpid():
313 if len(p.cmdline) > 1:
314 if 'python' in p.cmdline[0]:
317 if len(p.cmdline) > 0:
328 # bring program's x window to front
329 def bring_window_to_front(self, pid):
330 # figure out the window id
332 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
333 for line in p.stdout.readlines():
334 line_split = line.split()
335 cur_win_id = line_split[0]
336 cur_win_pid = int(line_split[2])
337 if cur_win_pid == pid:
342 subprocess.call(['wmctrl', '-i', '-a', win_id])
346 def __init__(self, common):
347 print _('Starting settings dialog')
351 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
352 self.window.set_title(_("Tor Browser Launcher Settings"))
353 self.window.set_icon_from_file(self.common.paths['icon_file'])
354 self.window.set_position(gtk.WIN_POS_CENTER)
355 self.window.set_border_width(10)
356 self.window.connect("delete_event", self.delete_event)
357 self.window.connect("destroy", self.destroy)
359 # build the rest of the UI
360 self.box = gtk.VBox(False, 10)
361 self.window.add(self.box)
364 self.hbox = gtk.HBox(False, 10)
365 self.box.pack_start(self.hbox, True, True, 0)
368 self.settings_box = gtk.VBox(False, 10)
369 self.hbox.pack_start(self.settings_box, True, True, 0)
370 self.settings_box.show()
372 self.labels_box = gtk.VBox(False, 10)
373 self.hbox.pack_start(self.labels_box, True, True, 0)
374 self.labels_box.show()
377 self.preferred_box = gtk.HBox(False, 10)
378 self.settings_box.pack_start(self.preferred_box, True, True, 0)
379 self.preferred_box.show()
381 self.preferred_label = gtk.Label(_('I prefer'))
382 self.preferred_label.set_line_wrap(True)
383 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
384 self.preferred_label.show()
386 self.preferred_options = []
387 for i in self.common.available_versions:
388 self.preferred_options.append(self.common.available_versions[i])
389 self.preferred_options.sort()
391 self.preferred = gtk.combo_box_new_text()
392 for option in self.preferred_options:
393 self.preferred.append_text(option)
394 if self.common.settings['preferred'] in self.common.available_versions:
395 self.preferred.set_active(self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]))
397 self.preferred.set_active(0)
398 self.preferred_box.pack_start(self.preferred, True, True, 0)
399 self.preferred.show()
402 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
404 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
405 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
406 if self.common.settings['update_over_tor']:
407 self.tor_update_checkbox.set_active(True)
409 self.tor_update_checkbox.set_active(False)
410 self.tor_update_checkbox.show()
414 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
415 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
416 if self.common.settings['check_for_updates']:
417 self.update_checkbox.set_active(True)
419 self.update_checkbox.set_active(False)
420 self.update_checkbox.show()
423 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
424 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
425 if self.common.settings['modem_sound']:
426 self.modem_checkbox.set_active(True)
428 self.modem_checkbox.set_active(False)
429 self.modem_checkbox.show()
432 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
433 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
435 self.label1 = gtk.Label(_('Not installed'))
436 self.label1.set_line_wrap(True)
437 self.labels_box.pack_start(self.label1, True, True, 0)
440 if(self.common.settings['last_update_check_timestamp'] > 0):
441 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']))))
443 self.label1 = gtk.Label(_('Never checked for updates'))
444 self.label1.set_line_wrap(True)
445 self.labels_box.pack_start(self.label1, True, True, 0)
449 self.mirrors_box = gtk.HBox(False, 10)
450 self.box.pack_start(self.mirrors_box, True, True, 0)
451 self.mirrors_box.show()
453 self.mirrors_label = gtk.Label(_('Mirror'))
454 self.mirrors_label.set_line_wrap(True)
455 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
456 self.mirrors_label.show()
458 self.mirrors = gtk.combo_box_new_text()
459 for mirror in self.common.mirrors:
460 self.mirrors.append_text(mirror)
461 if self.common.settings['mirror'] in self.common.mirrors:
462 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
464 self.preferred.set_active(0)
465 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
469 self.button_box = gtk.HButtonBox()
470 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
471 self.box.pack_start(self.button_box, True, True, 0)
472 self.button_box.show()
474 # save and launch button
475 save_launch_image = gtk.Image()
476 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
477 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
478 self.save_launch_button.set_image(save_launch_image)
479 self.save_launch_button.connect("clicked", self.save_launch, None)
480 self.button_box.add(self.save_launch_button)
481 self.save_launch_button.show()
483 # save and exit button
484 save_exit_image = gtk.Image()
485 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
486 self.save_exit_button = gtk.Button(_("Save & Exit"))
487 self.save_exit_button.set_image(save_exit_image)
488 self.save_exit_button.connect("clicked", self.save_exit, None)
489 self.button_box.add(self.save_exit_button)
490 self.save_exit_button.show()
493 cancel_image = gtk.Image()
494 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
495 self.cancel_button = gtk.Button(_("Cancel"))
496 self.cancel_button.set_image(cancel_image)
497 self.cancel_button.connect("clicked", self.destroy, None)
498 self.button_box.add(self.cancel_button)
499 self.cancel_button.show()
508 def save_launch(self, widget, data=None):
510 p = subprocess.Popen([self.common.paths['tbl_bin']])
514 def save_exit(self, widget, data=None):
520 # figure out the selected preferred option
522 selected = self.preferred_options[self.preferred.get_active()]
523 for i in self.common.available_versions:
524 if self.common.available_versions[i] == selected:
527 self.common.settings['preferred'] = preferred
530 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
531 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
532 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
534 # figure out the selected mirror
535 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
538 self.common.save_settings()
541 def delete_event(self, widget, event, data=None):
544 def destroy(self, widget, data=None):
549 def __init__(self, common):
550 print _('Starting launcher dialog')
554 self.set_gui(None, '', [])
555 self.launch_gui = True
556 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
558 # is firefox already running?
559 if self.common.settings['installed_version']:
560 firefox_pid = self.common.get_pid('./Browser/firefox')
562 print _('Firefox are is open, bringing to focus')
563 # bring firefox to front
564 self.common.bring_window_to_front(firefox_pid)
568 check_for_updates = False
569 if self.common.settings['check_for_updates']:
570 check_for_updates = True
572 if not check_for_updates:
573 # how long was it since the last update check?
574 # 86400 seconds = 24 hours
575 current_timestamp = int(time.time())
576 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
577 check_for_updates = True
579 if check_for_updates:
581 print 'Checking for update'
582 self.set_gui('task', _("Checking for Tor Browser update."),
583 ['download_update_check',
586 # no need to check for update
587 print _('Checked for update within 24 hours, skipping')
588 self.start_launcher()
592 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
593 self.window.set_title(_("Tor Browser"))
594 self.window.set_icon_from_file(self.common.paths['icon_file'])
595 self.window.set_position(gtk.WIN_POS_CENTER)
596 self.window.set_border_width(10)
597 self.window.connect("delete_event", self.delete_event)
598 self.window.connect("destroy", self.destroy)
600 # build the rest of the UI
603 # download or run TBB
604 def start_launcher(self):
605 # is TBB already installed?
606 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
607 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
609 # verify installed version for newer versions of TBB (#58)
610 if installed_version >= '3.0':
611 versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
612 if os.path.exists(versions_filename):
613 for line in open(versions_filename):
614 if 'TORBROWSER_VERSION' in line:
615 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
617 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
618 if os.path.isfile(start) and os.access(start, os.X_OK):
619 if installed_version == latest_version:
620 print _('Latest version of TBB is installed, launching')
621 # current version of tbb is installed, launch it
623 self.launch_gui = False
624 elif installed_version < latest_version:
625 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
626 # there is a tbb upgrade available
627 self.set_gui('task', _("Your Tor Browser is out of date."),
629 'download_sha256_sig',
635 # for some reason the installed tbb is newer than the current version?
636 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
640 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
641 self.set_gui('task', _("Downloading and installing Tor Browser."),
643 'download_sha256_sig',
649 # there are different GUIs that might appear, this sets which one we want
650 def set_gui(self, gui, message, tasks, autostart=True):
652 self.gui_message = message
653 self.gui_tasks = tasks
655 self.gui_autostart = autostart
657 # set all gtk variables to False
659 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
664 self.progressbar = False
665 self.button_box = False
666 self.start_button = False
667 self.exit_button = False
669 # build the application's UI
673 self.box = gtk.VBox(False, 20)
674 self.window.add(self.box)
676 if 'error' in self.gui:
678 self.label = gtk.Label(self.gui_message)
679 self.label.set_line_wrap(True)
680 self.box.pack_start(self.label, True, True, 0)
684 self.button_box = gtk.HButtonBox()
685 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
686 self.box.pack_start(self.button_box, True, True, 0)
687 self.button_box.show()
689 if self.gui != 'error':
691 yes_image = gtk.Image()
692 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
693 self.yes_button = gtk.Button("Yes")
694 self.yes_button.set_image(yes_image)
695 if self.gui == 'error_try_stable':
696 self.yes_button.connect("clicked", self.try_stable, None)
697 elif self.gui == 'error_try_default_mirror':
698 self.yes_button.connect("clicked", self.try_default_mirror, None)
699 elif self.gui == 'error_try_tor':
700 self.yes_button.connect("clicked", self.try_tor, None)
701 self.button_box.add(self.yes_button)
702 self.yes_button.show()
705 exit_image = gtk.Image()
706 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
707 self.exit_button = gtk.Button("Exit")
708 self.exit_button.set_image(exit_image)
709 self.exit_button.connect("clicked", self.destroy, None)
710 self.button_box.add(self.exit_button)
711 self.exit_button.show()
713 elif self.gui == 'task':
715 self.label = gtk.Label(self.gui_message)
716 self.label.set_line_wrap(True)
717 self.box.pack_start(self.label, True, True, 0)
721 self.progressbar = gtk.ProgressBar(adjustment=None)
722 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
723 self.progressbar.set_pulse_step(0.01)
724 self.box.pack_start(self.progressbar, True, True, 0)
727 self.button_box = gtk.HButtonBox()
728 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
729 self.box.pack_start(self.button_box, True, True, 0)
730 self.button_box.show()
733 start_image = gtk.Image()
734 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
735 self.start_button = gtk.Button(_("Start"))
736 self.start_button.set_image(start_image)
737 self.start_button.connect("clicked", self.start, None)
738 self.button_box.add(self.start_button)
739 if not self.gui_autostart:
740 self.start_button.show()
743 exit_image = gtk.Image()
744 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
745 self.exit_button = gtk.Button(_("Exit"))
746 self.exit_button.set_image(exit_image)
747 self.exit_button.connect("clicked", self.destroy, None)
748 self.button_box.add(self.exit_button)
749 self.exit_button.show()
754 if self.gui_autostart:
757 # start button clicked, begin tasks
758 def start(self, widget, data=None):
759 # disable the start button
760 if self.start_button:
761 self.start_button.set_sensitive(False)
763 # start running tasks
766 # run the next task in the task list
770 if self.gui_task_i >= len(self.gui_tasks):
774 task = self.gui_tasks[self.gui_task_i]
776 # get ready for the next task
779 print _('Running task: {0}'.format(task))
780 if task == 'download_update_check':
781 print _('Downloading'), self.common.paths['update_check_url']
782 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
784 if task == 'attempt_update':
785 print _('Checking to see if update is needed')
786 self.attempt_update()
788 elif task == 'download_sha256':
789 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
790 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
792 elif task == 'download_sha256_sig':
793 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
794 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
796 elif task == 'download_tarball_sig':
797 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
798 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
800 elif task == 'download_tarball':
801 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
802 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
804 elif task == 'verify':
805 print _('Verifying signature')
808 elif task == 'extract':
809 print _('Extracting'), self.common.paths['tarball_filename']
813 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
816 elif task == 'start_over':
817 print _('Starting download over again')
820 def response_received(self, response):
821 class FileDownloader(Protocol):
822 def __init__(self, common, file, total, progress, done_cb):
826 self.progress = progress
827 self.all_done = done_cb
829 if response.code != 200:
832 if response.code == 404:
833 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
837 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?"))
839 if common.settings['mirror'] != common.default_mirror:
840 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']))
842 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
844 def dataReceived(self, bytes):
845 self.file.write(bytes)
846 self.so_far += len(bytes)
847 percent = float(self.so_far) / float(self.total)
848 self.progress.set_fraction(percent)
849 amount = float(self.so_far)
851 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
854 amount = amount / float(size)
857 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
859 def connectionLost(self, reason):
860 print _('Finished receiving body:'), reason.getErrorMessage()
861 self.all_done(reason)
863 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
864 response.deliverBody(dl)
866 def response_finished(self, msg):
867 if msg.check(ResponseDone):
868 self.file_download.close()
869 delattr(self, 'current_download_path')
875 print "FINISHED", msg
876 ## FIXME handle errors
878 def download_error(self, f):
879 print _("Download error:"), f.value, type(f.value)
881 if isinstance(f.value, TryStableException):
882 f.trap(TryStableException)
883 self.set_gui('error_try_stable', str(f.value), [], False)
885 elif isinstance(f.value, TryDefaultMirrorException):
886 f.trap(TryDefaultMirrorException)
887 self.set_gui('error_try_default_mirror', str(f.value), [], False)
889 elif isinstance(f.value, DownloadErrorException):
890 f.trap(DownloadErrorException)
891 self.set_gui('error', str(f.value), [], False)
893 elif isinstance(f.value, DNSLookupError):
894 f.trap(DNSLookupError)
895 if common.settings['mirror'] != common.default_mirror:
896 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)
898 self.set_gui('error', str(f.value), [], False)
900 elif isinstance(f.value, ResponseFailed):
901 for reason in f.value.reasons:
902 if isinstance(reason.value, OpenSSL.SSL.Error):
903 # TODO: add the ability to report attack by posting bug to trac.torproject.org
904 if not self.common.settings['update_over_tor']:
905 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)
907 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
910 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
914 def download(self, name, url, path):
915 # keep track of current download
916 self.current_download_path = path
918 # initialize the progress bar
919 mirror_url = url.format(self.common.settings['mirror'])
920 self.progressbar.set_fraction(0)
921 self.progressbar.set_text(_('Downloading {0}').format(name))
922 self.progressbar.show()
925 # default mirror gets certificate pinning, only for requests that use the mirror
926 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
927 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
929 agent = Agent(reactor)
931 # actually, agent needs to follow redirect
932 agent = RedirectAgent(agent)
935 d = agent.request('GET', mirror_url,
936 Headers({'User-Agent': ['torbrowser-launcher']}),
939 self.file_download = open(path, 'w')
940 d.addCallback(self.response_received).addErrback(self.download_error)
942 if not reactor.running:
945 def try_stable(self, widget, data=None):
946 # change preferred to stable and relaunch TBL
947 self.common.settings['preferred'] = 'stable'
948 self.common.save_settings()
949 p = subprocess.Popen([self.common.paths['tbl_bin']])
952 def try_default_mirror(self, widget, data=None):
953 # change preferred to stable and relaunch TBL
954 self.common.settings['mirror'] = self.common.default_mirror
955 self.common.save_settings()
956 p = subprocess.Popen([self.common.paths['tbl_bin']])
959 def try_tor(self, widget, data=None):
960 # set update_over_tor to true and relaunch TBL
961 self.common.settings['update_over_tor'] = True
962 self.common.save_settings()
963 p = subprocess.Popen([self.common.paths['tbl_bin']])
966 def attempt_update(self):
967 # load the update check file
969 versions = json.load(open(self.common.paths['update_check_file']))
973 # filter linux versions
976 for version in versions:
977 if '-Linux' in version:
978 if 'alpha' in version or 'beta' in version or '-rc' in version:
979 valid_alphas.append(str(version))
981 valid_stables.append(str(version))
983 if len(valid_alphas):
984 latest_alpha = valid_alphas.pop()
986 if len(valid_stables):
987 latest_stable = valid_stables.pop()
989 if latest_stable or latest_alpha:
991 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
993 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
994 self.common.settings['last_update_check_timestamp'] = int(time.time())
995 self.common.settings['check_for_updates'] = False
996 self.common.save_settings()
997 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
998 self.start_launcher()
1001 # failed to find the latest version
1002 self.set_gui('error', _("Error checking for updates."), [], False)
1005 # not a valid JSON object
1006 self.set_gui('error', _("Error checking for updates."), [], False)
1013 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1015 # initialize the progress bar
1016 self.progressbar.set_fraction(0)
1017 self.progressbar.set_text(_('Verifying Signature'))
1018 self.progressbar.show()
1021 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1022 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1023 self.pulse_until_process_exits(p)
1024 if p.returncode == 0:
1025 # compare with sha256 of the tarball
1026 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1027 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1028 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1034 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1035 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)
1039 if not reactor.running:
1043 # initialize the progress bar
1044 self.progressbar.set_fraction(0)
1045 self.progressbar.set_text(_('Installing'))
1046 self.progressbar.show()
1051 if self.common.paths['tarball_file'][-2:] == 'xz':
1052 # if tarball is .tar.xz
1053 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1054 tf = tarfile.open(fileobj=xz)
1055 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1058 # if tarball is .tar.gz
1059 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1060 tf = tarfile.open(self.common.paths['tarball_file'])
1061 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1067 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1072 # installation is finished, so save installed_version
1073 self.common.settings['installed_version'] = self.common.settings['latest_version']
1074 self.common.save_settings()
1078 def run(self, run_next_task=True):
1079 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1082 if self.common.settings['modem_sound']:
1085 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1092 # make the progress bar pulse until process p (a Popen object) finishes
1093 def pulse_until_process_exits(self, p):
1094 while p.poll() is None:
1096 self.progressbar.pulse()
1099 # start over and download TBB again
1100 def start_over(self):
1101 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1102 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1107 def refresh_gtk(self):
1108 while gtk.events_pending():
1109 gtk.main_iteration(False)
1112 def delete_event(self, widget, event, data=None):
1115 def destroy(self, widget, data=None):
1116 if hasattr(self, 'file_download'):
1117 self.file_download.close()
1118 if hasattr(self, 'current_download_path'):
1119 os.remove(self.current_download_path)
1120 delattr(self, 'current_download_path')
1124 if __name__ == "__main__":
1125 tor_browser_launcher_version = open('/usr/share/torbrowser-launcher/version').read().strip()
1127 print _('Tor Browser Launcher')
1128 print _('By Micah Lee, licensed under GPLv3')
1129 print _('version {0}').format(tor_browser_launcher_version)
1130 print 'https://github.com/micahflee/torbrowser-launcher'
1132 common = TBLCommon(tor_browser_launcher_version)
1134 # is torbrowser-launcher already running?
1135 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1137 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1138 common.bring_window_to_front(tbl_pid)
1141 if '-settings' in sys.argv:
1143 app = TBLSettings(common)
1147 app = TBLLauncher(common)