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, 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.error import DNSLookupError
55 class TryStableException(Exception):
59 class TryDefaultMirrorException(Exception):
63 class DownloadErrorException(Exception):
67 class VerifyTorProjectCert(ClientContextFactory):
69 def __init__(self, torproject_pem):
70 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
72 def getContext(self, host, port):
73 ctx = ClientContextFactory.getContext(self)
74 ctx.set_verify_depth(0)
75 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
78 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
79 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
84 def __init__(self, tbl_version):
85 print _('Initializing Tor Browser Launcher')
86 self.tbl_version = tbl_version
89 self.available_versions = {
90 'stable': _('Tor Browser Bundle - stable'),
91 'alpha': _('Tor Browser Bundle - alpha')
93 self.default_mirror = 'https://www.torproject.org/dist/'
95 self.discover_arch_lang()
97 self.mkdir(self.paths['data_dir'])
100 self.mkdir(self.paths['download_dir'])
101 self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
104 # allow buttons to have icons
106 gtk_settings = gtk.settings_get_default()
107 gtk_settings.props.gtk_button_images = True
111 # discover the architecture and language
112 def discover_arch_lang(self):
113 # figure out the architecture
114 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
116 # figure out the language
117 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
118 default_locale = locale.getdefaultlocale()[0]
119 if default_locale is None:
120 self.language = 'en-US'
122 self.language = default_locale.replace('_', '-')
123 if self.language not in available_languages:
124 self.language = self.language.split('-')[0]
125 if self.language not in available_languages:
126 for l in available_languages:
127 if l[0:2] == self.language:
129 # if language isn't available, default to english
130 if self.language not in available_languages:
131 self.language = 'en-US'
133 # build all relevant paths
134 def build_paths(self, tbb_version=None):
135 homedir = os.getenv('HOME')
137 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
138 if not os.path.exists(homedir):
140 os.mkdir(homedir, 0700)
142 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
143 if not os.access(homedir, os.W_OK):
144 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
146 tbb_data = '%s/.torbrowser' % homedir
150 dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b').replace('-rc-', 'rc')
151 if self.architecture == 'x86_64':
155 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
158 self.paths['tarball_url'] = '{0}torbrowser/'+dirname+'/'+tarball_filename
159 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
160 self.paths['tarball_filename'] = tarball_filename
163 self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
164 self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
165 self.paths['sha256_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt'
166 self.paths['sha256_sig_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt-mp.asc'
169 'tbl_bin': '/usr/bin/torbrowser-launcher',
170 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
171 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
172 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
173 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
174 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
175 'mike_key': '/usr/share/torbrowser-launcher/mike-2013-09.asc',
176 'mirrors_txt': ['/usr/share/torbrowser-launcher/mirrors.txt',
177 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
178 'modem_sound': '/usr/share/torbrowser-launcher/modem.ogg',
179 'data_dir': tbb_data,
180 'download_dir': tbb_data+'/download',
181 'gnupg_homedir': tbb_data+'/gnupg_homedir',
182 'settings_file': tbb_data+'/settings',
183 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
184 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
187 'dir': tbb_data+'/tbb/stable/'+self.architecture,
188 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
189 'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
192 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
193 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
194 'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
203 if not os.path.exists(path):
204 os.makedirs(path, 0700)
207 print _("Cannot create directory {0}").format(path)
209 if not os.access(path, os.W_OK):
210 print _("{0} is not writable").format(path)
214 # if gnupg_homedir isn't set up, set it up
215 def init_gnupg(self):
216 if not os.path.exists(self.paths['gnupg_homedir']):
217 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
218 self.mkdir(self.paths['gnupg_homedir'])
222 def import_keys(self):
223 print _('Importing keys')
224 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()
227 def load_mirrors(self):
229 for srcfile in self.paths['mirrors_txt']:
230 if not os.path.exists(srcfile):
231 print "Warning: can't load mirrors from %s" % srcfile
233 for mirror in open(srcfile, 'r').readlines():
234 if mirror.strip() not in self.mirrors:
235 self.mirrors.append(mirror.strip())
238 def load_settings(self):
240 'tbl_version': self.tbl_version,
241 'preferred': 'stable',
242 'installed_version': {
250 'update_over_tor': True,
251 'check_for_updates': False,
252 'modem_sound': False,
253 'last_update_check_timestamp': 0,
254 'mirror': self.default_mirror
257 if os.path.isfile(self.paths['settings_file']):
258 settings = pickle.load(open(self.paths['settings_file']))
261 # settings migrations
262 if settings['tbl_version'] == '0.0.1':
263 print '0.0.1 migration'
264 self.settings = default_settings
265 self.settings['installed_version']['alpha'] = settings['installed_version']
269 self.mkdir(self.paths['tbb']['alpha']['dir'])
271 # make sure settings file is up-to-date
272 for setting in default_settings:
273 if setting not in settings:
274 settings[setting] = default_settings[setting]
277 # make sure the version is current
278 if settings['tbl_version'] != self.tbl_version:
279 settings['tbl_version'] = self.tbl_version
282 self.settings = settings
287 self.settings = default_settings
291 def save_settings(self):
292 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
295 # get the process id of a program
297 def get_pid(bin_path, python=False):
300 for p in psutil.process_iter():
302 if p.pid != os.getpid():
305 if len(p.cmdline) > 1:
306 if 'python' in p.cmdline[0]:
309 if len(p.cmdline) > 0:
320 # bring program's x window to front
322 def bring_window_to_front(pid):
323 # figure out the window id
325 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
326 for line in p.stdout.readlines():
327 line_split = line.split()
328 cur_win_id = line_split[0]
329 cur_win_pid = int(line_split[2])
330 if cur_win_pid == pid:
335 subprocess.call(['wmctrl', '-i', '-a', win_id])
339 def __init__(self, common):
340 print _('Starting settings dialog')
344 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
345 self.window.set_title(_("Tor Browser Launcher Settings"))
346 self.window.set_icon_from_file(self.common.paths['icon_file'])
347 self.window.set_position(gtk.WIN_POS_CENTER)
348 self.window.set_border_width(10)
349 self.window.connect("delete_event", self.delete_event)
350 self.window.connect("destroy", self.destroy)
352 # build the rest of the UI
353 self.box = gtk.VBox(False, 10)
354 self.window.add(self.box)
357 self.hbox = gtk.HBox(False, 10)
358 self.box.pack_start(self.hbox, True, True, 0)
361 self.settings_box = gtk.VBox(False, 10)
362 self.hbox.pack_start(self.settings_box, True, True, 0)
363 self.settings_box.show()
365 self.labels_box = gtk.VBox(False, 10)
366 self.hbox.pack_start(self.labels_box, True, True, 0)
367 self.labels_box.show()
370 self.preferred_box = gtk.HBox(False, 10)
371 self.settings_box.pack_start(self.preferred_box, True, True, 0)
372 self.preferred_box.show()
374 self.preferred_label = gtk.Label(_('I prefer'))
375 self.preferred_label.set_line_wrap(True)
376 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
377 self.preferred_label.show()
379 self.preferred_options = []
380 for i in self.common.available_versions:
381 self.preferred_options.append(self.common.available_versions[i])
382 self.preferred_options.sort()
384 self.preferred = gtk.combo_box_new_text()
385 for option in self.preferred_options:
386 self.preferred.append_text(option)
387 if self.common.settings['preferred'] in self.common.available_versions:
388 self.preferred.set_active(self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]))
390 self.preferred.set_active(0)
391 self.preferred_box.pack_start(self.preferred, True, True, 0)
392 self.preferred.show()
395 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
397 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
398 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
399 if self.common.settings['update_over_tor']:
400 self.tor_update_checkbox.set_active(True)
402 self.tor_update_checkbox.set_active(False)
403 self.tor_update_checkbox.show()
407 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
408 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
409 if self.common.settings['check_for_updates']:
410 self.update_checkbox.set_active(True)
412 self.update_checkbox.set_active(False)
413 self.update_checkbox.show()
416 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
417 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
418 if self.common.settings['modem_sound']:
419 self.modem_checkbox.set_active(True)
421 self.modem_checkbox.set_active(False)
422 self.modem_checkbox.show()
425 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
426 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
428 self.label1 = gtk.Label(_('Not installed'))
429 self.label1.set_line_wrap(True)
430 self.labels_box.pack_start(self.label1, True, True, 0)
433 if(self.common.settings['last_update_check_timestamp'] > 0):
434 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']))))
436 self.label1 = gtk.Label(_('Never checked for updates'))
437 self.label1.set_line_wrap(True)
438 self.labels_box.pack_start(self.label1, True, True, 0)
442 self.mirrors_box = gtk.HBox(False, 10)
443 self.box.pack_start(self.mirrors_box, True, True, 0)
444 self.mirrors_box.show()
446 self.mirrors_label = gtk.Label(_('Mirror'))
447 self.mirrors_label.set_line_wrap(True)
448 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
449 self.mirrors_label.show()
451 self.mirrors = gtk.combo_box_new_text()
452 for mirror in self.common.mirrors:
453 self.mirrors.append_text(mirror)
454 if self.common.settings['mirror'] in self.common.mirrors:
455 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
457 self.preferred.set_active(0)
458 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
462 self.button_box = gtk.HButtonBox()
463 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
464 self.box.pack_start(self.button_box, True, True, 0)
465 self.button_box.show()
467 # save and launch button
468 save_launch_image = gtk.Image()
469 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
470 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
471 self.save_launch_button.set_image(save_launch_image)
472 self.save_launch_button.connect("clicked", self.save_launch, None)
473 self.button_box.add(self.save_launch_button)
474 self.save_launch_button.show()
476 # save and exit button
477 save_exit_image = gtk.Image()
478 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
479 self.save_exit_button = gtk.Button(_("Save & Exit"))
480 self.save_exit_button.set_image(save_exit_image)
481 self.save_exit_button.connect("clicked", self.save_exit, None)
482 self.button_box.add(self.save_exit_button)
483 self.save_exit_button.show()
486 cancel_image = gtk.Image()
487 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
488 self.cancel_button = gtk.Button(_("Cancel"))
489 self.cancel_button.set_image(cancel_image)
490 self.cancel_button.connect("clicked", self.destroy, None)
491 self.button_box.add(self.cancel_button)
492 self.cancel_button.show()
501 def save_launch(self, widget, data=None):
503 subprocess.Popen([self.common.paths['tbl_bin']])
507 def save_exit(self, widget, data=None):
513 # figure out the selected preferred option
515 selected = self.preferred_options[self.preferred.get_active()]
516 for i in self.common.available_versions:
517 if self.common.available_versions[i] == selected:
520 self.common.settings['preferred'] = preferred
523 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
524 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
525 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
527 # figure out the selected mirror
528 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
531 self.common.save_settings()
534 def delete_event(self, widget, event, data=None):
537 def destroy(self, widget, data=None):
542 def __init__(self, common):
543 print _('Starting launcher dialog')
547 self.set_gui(None, '', [])
548 self.launch_gui = True
549 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
551 # is firefox already running?
552 if self.common.settings['installed_version']:
553 firefox_pid = self.common.get_pid('./Browser/firefox')
555 print _('Firefox are is open, bringing to focus')
556 # bring firefox to front
557 self.common.bring_window_to_front(firefox_pid)
561 check_for_updates = False
562 if self.common.settings['check_for_updates']:
563 check_for_updates = True
565 if not check_for_updates:
566 # how long was it since the last update check?
567 # 86400 seconds = 24 hours
568 current_timestamp = int(time.time())
569 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
570 check_for_updates = True
572 if check_for_updates:
574 print 'Checking for update'
575 self.set_gui('task', _("Checking for Tor Browser update."),
576 ['download_update_check',
579 # no need to check for update
580 print _('Checked for update within 24 hours, skipping')
581 self.start_launcher()
585 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
586 self.window.set_title(_("Tor Browser"))
587 self.window.set_icon_from_file(self.common.paths['icon_file'])
588 self.window.set_position(gtk.WIN_POS_CENTER)
589 self.window.set_border_width(10)
590 self.window.connect("delete_event", self.delete_event)
591 self.window.connect("destroy", self.destroy)
593 # build the rest of the UI
596 # download or run TBB
597 def start_launcher(self):
598 # is TBB already installed?
599 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
600 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
602 # verify installed version for newer versions of TBB (#58)
603 if installed_version >= '3.0':
604 versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
605 if os.path.exists(versions_filename):
606 for line in open(versions_filename):
607 if 'TORBROWSER_VERSION' in line:
608 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
610 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
611 if os.path.isfile(start) and os.access(start, os.X_OK):
612 if installed_version == latest_version:
613 print _('Latest version of TBB is installed, launching')
614 # current version of tbb is installed, launch it
616 self.launch_gui = False
617 elif installed_version < latest_version:
618 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
619 # there is a tbb upgrade available
620 self.set_gui('task', _("Your Tor Browser is out of date."),
622 'download_sha256_sig',
628 # for some reason the installed tbb is newer than the current version?
629 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
633 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
634 self.set_gui('task', _("Downloading and installing Tor Browser."),
636 'download_sha256_sig',
642 # there are different GUIs that might appear, this sets which one we want
643 def set_gui(self, gui, message, tasks, autostart=True):
645 self.gui_message = message
646 self.gui_tasks = tasks
648 self.gui_autostart = autostart
650 # set all gtk variables to False
652 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
657 self.progressbar = False
658 self.button_box = False
659 self.start_button = False
660 self.exit_button = False
662 # build the application's UI
666 self.box = gtk.VBox(False, 20)
667 self.window.add(self.box)
669 if 'error' in self.gui:
671 self.label = gtk.Label(self.gui_message)
672 self.label.set_line_wrap(True)
673 self.box.pack_start(self.label, True, True, 0)
677 self.button_box = gtk.HButtonBox()
678 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
679 self.box.pack_start(self.button_box, True, True, 0)
680 self.button_box.show()
682 if self.gui != 'error':
684 yes_image = gtk.Image()
685 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
686 self.yes_button = gtk.Button("Yes")
687 self.yes_button.set_image(yes_image)
688 if self.gui == 'error_try_stable':
689 self.yes_button.connect("clicked", self.try_stable, None)
690 elif self.gui == 'error_try_default_mirror':
691 self.yes_button.connect("clicked", self.try_default_mirror, None)
692 elif self.gui == 'error_try_tor':
693 self.yes_button.connect("clicked", self.try_tor, None)
694 self.button_box.add(self.yes_button)
695 self.yes_button.show()
698 exit_image = gtk.Image()
699 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
700 self.exit_button = gtk.Button("Exit")
701 self.exit_button.set_image(exit_image)
702 self.exit_button.connect("clicked", self.destroy, None)
703 self.button_box.add(self.exit_button)
704 self.exit_button.show()
706 elif self.gui == 'task':
708 self.label = gtk.Label(self.gui_message)
709 self.label.set_line_wrap(True)
710 self.box.pack_start(self.label, True, True, 0)
714 self.progressbar = gtk.ProgressBar(adjustment=None)
715 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
716 self.progressbar.set_pulse_step(0.01)
717 self.box.pack_start(self.progressbar, True, True, 0)
720 self.button_box = gtk.HButtonBox()
721 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
722 self.box.pack_start(self.button_box, True, True, 0)
723 self.button_box.show()
726 start_image = gtk.Image()
727 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
728 self.start_button = gtk.Button(_("Start"))
729 self.start_button.set_image(start_image)
730 self.start_button.connect("clicked", self.start, None)
731 self.button_box.add(self.start_button)
732 if not self.gui_autostart:
733 self.start_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()
747 if self.gui_autostart:
750 # start button clicked, begin tasks
751 def start(self, widget, data=None):
752 # disable the start button
753 if self.start_button:
754 self.start_button.set_sensitive(False)
756 # start running tasks
759 # run the next task in the task list
763 if self.gui_task_i >= len(self.gui_tasks):
767 task = self.gui_tasks[self.gui_task_i]
769 # get ready for the next task
772 print _('Running task: {0}'.format(task))
773 if task == 'download_update_check':
774 print _('Downloading'), self.common.paths['update_check_url']
775 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
777 if task == 'attempt_update':
778 print _('Checking to see if update is needed')
779 self.attempt_update()
781 elif task == 'download_sha256':
782 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
783 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
785 elif task == 'download_sha256_sig':
786 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
787 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
789 elif task == 'download_tarball':
790 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
791 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
793 elif task == 'verify':
794 print _('Verifying signature')
797 elif task == 'extract':
798 print _('Extracting'), self.common.paths['tarball_filename']
802 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
805 elif task == 'start_over':
806 print _('Starting download over again')
809 def response_received(self, response):
810 class FileDownloader(Protocol):
811 def __init__(self, common, file, total, progress, done_cb):
815 self.progress = progress
816 self.all_done = done_cb
818 if response.code != 200:
821 if response.code == 404:
822 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
826 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?"))
828 if common.settings['mirror'] != common.default_mirror:
829 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']))
831 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
833 def dataReceived(self, bytes):
834 self.file.write(bytes)
835 self.so_far += len(bytes)
836 percent = float(self.so_far) / float(self.total)
837 self.progress.set_fraction(percent)
838 amount = float(self.so_far)
840 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
843 amount = amount / float(size)
846 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
848 def connectionLost(self, reason):
849 print _('Finished receiving body:'), reason.getErrorMessage()
850 self.all_done(reason)
852 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
853 response.deliverBody(dl)
855 def response_finished(self, msg):
856 if msg.check(ResponseDone):
857 self.file_download.close()
858 delattr(self, 'current_download_path')
864 print "FINISHED", msg
865 ## FIXME handle errors
867 def download_error(self, f):
868 print _("Download error:"), f.value, type(f.value)
870 if isinstance(f.value, TryStableException):
871 f.trap(TryStableException)
872 self.set_gui('error_try_stable', str(f.value), [], False)
874 elif isinstance(f.value, TryDefaultMirrorException):
875 f.trap(TryDefaultMirrorException)
876 self.set_gui('error_try_default_mirror', str(f.value), [], False)
878 elif isinstance(f.value, DownloadErrorException):
879 f.trap(DownloadErrorException)
880 self.set_gui('error', str(f.value), [], False)
882 elif isinstance(f.value, DNSLookupError):
883 f.trap(DNSLookupError)
884 if common.settings['mirror'] != common.default_mirror:
885 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)
887 self.set_gui('error', str(f.value), [], False)
889 elif isinstance(f.value, ResponseFailed):
890 for reason in f.value.reasons:
891 if isinstance(reason.value, OpenSSL.SSL.Error):
892 # TODO: add the ability to report attack by posting bug to trac.torproject.org
893 if not self.common.settings['update_over_tor']:
894 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)
896 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
899 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
903 def download(self, name, url, path):
904 # keep track of current download
905 self.current_download_path = path
907 # initialize the progress bar
908 mirror_url = url.format(self.common.settings['mirror'])
909 self.progressbar.set_fraction(0)
910 self.progressbar.set_text(_('Downloading {0}').format(name))
911 self.progressbar.show()
914 # default mirror gets certificate pinning, only for requests that use the mirror
915 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
916 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
918 agent = Agent(reactor)
920 # actually, agent needs to follow redirect
921 agent = RedirectAgent(agent)
924 d = agent.request('GET', mirror_url,
925 Headers({'User-Agent': ['torbrowser-launcher']}),
928 self.file_download = open(path, 'w')
929 d.addCallback(self.response_received).addErrback(self.download_error)
931 if not reactor.running:
934 def try_stable(self, widget, data=None):
935 # change preferred to stable and relaunch TBL
936 self.common.settings['preferred'] = 'stable'
937 self.common.save_settings()
938 p = subprocess.Popen([self.common.paths['tbl_bin']])
941 def try_default_mirror(self, widget, data=None):
942 # change preferred to stable and relaunch TBL
943 self.common.settings['mirror'] = self.common.default_mirror
944 self.common.save_settings()
945 subprocess.Popen([self.common.paths['tbl_bin']])
948 def try_tor(self, widget, data=None):
949 # set update_over_tor to true and relaunch TBL
950 self.common.settings['update_over_tor'] = True
951 self.common.save_settings()
952 subprocess.Popen([self.common.paths['tbl_bin']])
955 def attempt_update(self):
956 # load the update check file
958 versions = json.load(open(self.common.paths['update_check_file']))
962 # filter linux versions
965 for version in versions:
966 if '-Linux' in version:
967 if 'alpha' in version or 'beta' in version or '-rc' in version:
968 valid_alphas.append(str(version))
970 valid_stables.append(str(version))
972 if len(valid_alphas):
973 latest_alpha = valid_alphas.pop()
975 if len(valid_stables):
976 latest_stable = valid_stables.pop()
978 if latest_stable or latest_alpha:
980 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
982 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
983 self.common.settings['last_update_check_timestamp'] = int(time.time())
984 self.common.settings['check_for_updates'] = False
985 self.common.save_settings()
986 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
987 self.start_launcher()
990 # failed to find the latest version
991 self.set_gui('error', _("Error checking for updates."), [], False)
994 # not a valid JSON object
995 self.set_gui('error', _("Error checking for updates."), [], False)
1002 # initialize the progress bar
1003 self.progressbar.set_fraction(0)
1004 self.progressbar.set_text(_('Verifying Signature'))
1005 self.progressbar.show()
1008 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1009 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1010 self.pulse_until_process_exits(p)
1011 if p.returncode == 0:
1012 # compare with sha256 of the tarball
1013 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1014 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1015 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1021 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1022 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)
1026 if not reactor.running:
1030 # initialize the progress bar
1031 self.progressbar.set_fraction(0)
1032 self.progressbar.set_text(_('Installing'))
1033 self.progressbar.show()
1038 if self.common.paths['tarball_file'][-2:] == 'xz':
1039 # if tarball is .tar.xz
1040 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1041 tf = tarfile.open(fileobj=xz)
1042 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1045 # if tarball is .tar.gz
1046 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1047 tf = tarfile.open(self.common.paths['tarball_file'])
1048 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1054 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1059 # installation is finished, so save installed_version
1060 self.common.settings['installed_version'] = self.common.settings['latest_version']
1061 self.common.save_settings()
1065 def run(self, run_next_task=True):
1066 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1069 if self.common.settings['modem_sound']:
1072 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1079 # make the progress bar pulse until process p (a Popen object) finishes
1080 def pulse_until_process_exits(self, p):
1081 while p.poll() is None:
1083 self.progressbar.pulse()
1086 # start over and download TBB again
1087 def start_over(self):
1088 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1089 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1094 def refresh_gtk(self):
1095 while gtk.events_pending():
1096 gtk.main_iteration(False)
1099 def delete_event(self, widget, event, data=None):
1102 def destroy(self, widget, data=None):
1103 if hasattr(self, 'file_download'):
1104 self.file_download.close()
1105 if hasattr(self, 'current_download_path'):
1106 os.remove(self.current_download_path)
1107 delattr(self, 'current_download_path')
1111 if __name__ == "__main__":
1112 tor_browser_launcher_version = open('/usr/share/torbrowser-launcher/version').read().strip()
1114 print _('Tor Browser Launcher')
1115 print _('By Micah Lee, licensed under GPLv3')
1116 print _('version {0}').format(tor_browser_launcher_version)
1117 print 'https://github.com/micahflee/torbrowser-launcher'
1119 common = TBLCommon(tor_browser_launcher_version)
1121 # is torbrowser-launcher already running?
1122 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1124 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1125 common.bring_window_to_front(tbl_pid)
1128 if '-settings' in sys.argv:
1130 app = TBLSettings(common)
1134 app = TBLLauncher(common)