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 SHARE = os.getenv('TBL_SHARE', '/usr/share')
36 gettext.install('torbrowser-launcher', os.path.join(SHARE, 'torbrowser-launcher/locale'))
38 from twisted.internet import gtk2reactor
40 from twisted.internet import reactor
46 import subprocess, locale, time, pickle, json, tarfile, psutil, hashlib, lzma
48 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
49 from twisted.web.http_headers import Headers
50 from twisted.internet.protocol import Protocol
51 from twisted.internet.ssl import ClientContextFactory
52 from twisted.internet.error import DNSLookupError
57 class TryStableException(Exception):
61 class TryDefaultMirrorException(Exception):
65 class DownloadErrorException(Exception):
69 class VerifyTorProjectCert(ClientContextFactory):
71 def __init__(self, torproject_pem):
72 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
74 def getContext(self, host, port):
75 ctx = ClientContextFactory.getContext(self)
76 ctx.set_verify_depth(0)
77 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
80 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
81 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
86 def __init__(self, tbl_version):
87 print _('Initializing Tor Browser Launcher')
88 self.tbl_version = tbl_version
91 self.available_versions = {
92 'stable': _('Tor Browser Bundle - stable'),
93 'alpha': _('Tor Browser Bundle - alpha')
95 self.default_mirror = 'https://www.torproject.org/dist/'
97 self.discover_arch_lang()
99 self.mkdir(self.paths['data_dir'])
102 self.mkdir(self.paths['download_dir'])
103 self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
106 # allow buttons to have icons
108 gtk_settings = gtk.settings_get_default()
109 gtk_settings.props.gtk_button_images = True
113 # discover the architecture and language
114 def discover_arch_lang(self):
115 # figure out the architecture
116 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
118 # figure out the language
119 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
120 default_locale = locale.getdefaultlocale()[0]
121 if default_locale is None:
122 self.language = 'en-US'
124 self.language = default_locale.replace('_', '-')
125 if self.language not in available_languages:
126 self.language = self.language.split('-')[0]
127 if self.language not in available_languages:
128 for l in available_languages:
129 if l[0:2] == self.language:
131 # if language isn't available, default to english
132 if self.language not in available_languages:
133 self.language = 'en-US'
135 # build all relevant paths
136 def build_paths(self, tbb_version=None):
137 homedir = os.getenv('HOME')
139 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
140 if not os.path.exists(homedir):
142 os.mkdir(homedir, 0700)
144 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
145 if not os.access(homedir, os.W_OK):
146 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
148 tbb_data = '%s/.torbrowser' % homedir
152 dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b').replace('-rc-', 'rc')
153 if self.architecture == 'x86_64':
157 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
160 self.paths['tarball_url'] = '{0}torbrowser/'+dirname+'/'+tarball_filename
161 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
162 self.paths['tarball_filename'] = 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'
171 'tbl_bin': '/usr/bin/torbrowser-launcher',
172 'icon_file': os.path.join(SHARE, 'pixmaps/torbrowser80.xpm'),
173 'torproject_pem': os.path.join(SHARE, 'torbrowser-launcher/torproject.pem'),
174 'erinn_key': os.path.join(SHARE, 'torbrowser-launcher/erinn.asc'),
175 'sebastian_key': os.path.join(SHARE, 'torbrowser-launcher/sebastian.asc'),
176 'alexandre_key': os.path.join(SHARE, 'torbrowser-launcher/alexandre.asc'),
177 'mike_key': os.path.join(SHARE, 'torbrowser-launcher/mike-2013-09.asc'),
178 'mirrors_txt': [os.path.join(SHARE, 'torbrowser-launcher/mirrors.txt'),
179 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
180 'modem_sound': os.path.join(SHARE, 'torbrowser-launcher/modem.ogg'),
181 'data_dir': tbb_data,
182 'download_dir': tbb_data+'/download',
183 'gnupg_homedir': tbb_data+'/gnupg_homedir',
184 'settings_file': tbb_data+'/settings',
185 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
186 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
189 'dir': tbb_data+'/tbb/stable/'+self.architecture,
190 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
191 'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
194 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
195 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
196 'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
205 if not os.path.exists(path):
206 os.makedirs(path, 0700)
209 print _("Cannot create directory {0}").format(path)
211 if not os.access(path, os.W_OK):
212 print _("{0} is not writable").format(path)
216 # if gnupg_homedir isn't set up, set it up
217 def init_gnupg(self):
218 if not os.path.exists(self.paths['gnupg_homedir']):
219 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
220 self.mkdir(self.paths['gnupg_homedir'])
224 def import_keys(self):
225 print _('Importing keys')
226 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()
229 def load_mirrors(self):
231 for srcfile in self.paths['mirrors_txt']:
232 if not os.path.exists(srcfile):
233 print "Warning: can't load mirrors from %s" % srcfile
235 for mirror in open(srcfile, 'r').readlines():
236 if mirror.strip() not in self.mirrors:
237 self.mirrors.append(mirror.strip())
240 def load_settings(self):
242 'tbl_version': self.tbl_version,
243 'preferred': 'stable',
244 'installed_version': {
252 'update_over_tor': True,
253 'check_for_updates': False,
254 'modem_sound': False,
255 'last_update_check_timestamp': 0,
256 'mirror': self.default_mirror
259 if os.path.isfile(self.paths['settings_file']):
260 settings = pickle.load(open(self.paths['settings_file']))
263 # settings migrations
264 if settings['tbl_version'] == '0.0.1':
265 print '0.0.1 migration'
266 self.settings = default_settings
267 self.settings['installed_version']['alpha'] = settings['installed_version']
271 self.mkdir(self.paths['tbb']['alpha']['dir'])
273 # make sure settings file is up-to-date
274 for setting in default_settings:
275 if setting not in settings:
276 settings[setting] = default_settings[setting]
279 # make sure the version is current
280 if settings['tbl_version'] != self.tbl_version:
281 settings['tbl_version'] = self.tbl_version
284 self.settings = settings
289 self.settings = default_settings
293 def save_settings(self):
294 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
297 # get the process id of a program
299 def get_pid(bin_path, python=False):
302 for p in psutil.process_iter():
304 if p.pid != os.getpid():
307 if len(p.cmdline) > 1:
308 if 'python' in p.cmdline[0]:
311 if len(p.cmdline) > 0:
322 # bring program's x window to front
324 def bring_window_to_front(pid):
325 # figure out the window id
327 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
328 for line in p.stdout.readlines():
329 line_split = line.split()
330 cur_win_id = line_split[0]
331 cur_win_pid = int(line_split[2])
332 if cur_win_pid == pid:
337 subprocess.call(['wmctrl', '-i', '-a', win_id])
341 def __init__(self, common):
342 print _('Starting settings dialog')
346 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
347 self.window.set_title(_("Tor Browser Launcher Settings"))
348 self.window.set_icon_from_file(self.common.paths['icon_file'])
349 self.window.set_position(gtk.WIN_POS_CENTER)
350 self.window.set_border_width(10)
351 self.window.connect("delete_event", self.delete_event)
352 self.window.connect("destroy", self.destroy)
354 # build the rest of the UI
355 self.box = gtk.VBox(False, 10)
356 self.window.add(self.box)
359 self.hbox = gtk.HBox(False, 10)
360 self.box.pack_start(self.hbox, True, True, 0)
363 self.settings_box = gtk.VBox(False, 10)
364 self.hbox.pack_start(self.settings_box, True, True, 0)
365 self.settings_box.show()
367 self.labels_box = gtk.VBox(False, 10)
368 self.hbox.pack_start(self.labels_box, True, True, 0)
369 self.labels_box.show()
372 self.preferred_box = gtk.HBox(False, 10)
373 self.settings_box.pack_start(self.preferred_box, True, True, 0)
374 self.preferred_box.show()
376 self.preferred_label = gtk.Label(_('I prefer'))
377 self.preferred_label.set_line_wrap(True)
378 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
379 self.preferred_label.show()
381 self.preferred_options = []
382 for i in self.common.available_versions:
383 self.preferred_options.append(self.common.available_versions[i])
384 self.preferred_options.sort()
386 self.preferred = gtk.combo_box_new_text()
387 for option in self.preferred_options:
388 self.preferred.append_text(option)
389 if self.common.settings['preferred'] in self.common.available_versions:
390 self.preferred.set_active(self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]))
392 self.preferred.set_active(0)
393 self.preferred_box.pack_start(self.preferred, True, True, 0)
394 self.preferred.show()
397 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
399 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
400 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
401 if self.common.settings['update_over_tor']:
402 self.tor_update_checkbox.set_active(True)
404 self.tor_update_checkbox.set_active(False)
405 self.tor_update_checkbox.show()
409 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
410 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
411 if self.common.settings['check_for_updates']:
412 self.update_checkbox.set_active(True)
414 self.update_checkbox.set_active(False)
415 self.update_checkbox.show()
418 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
419 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
420 if self.common.settings['modem_sound']:
421 self.modem_checkbox.set_active(True)
423 self.modem_checkbox.set_active(False)
424 self.modem_checkbox.show()
427 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
428 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
430 self.label1 = gtk.Label(_('Not installed'))
431 self.label1.set_line_wrap(True)
432 self.labels_box.pack_start(self.label1, True, True, 0)
435 if(self.common.settings['last_update_check_timestamp'] > 0):
436 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']))))
438 self.label1 = gtk.Label(_('Never checked for updates'))
439 self.label1.set_line_wrap(True)
440 self.labels_box.pack_start(self.label1, True, True, 0)
444 self.mirrors_box = gtk.HBox(False, 10)
445 self.box.pack_start(self.mirrors_box, True, True, 0)
446 self.mirrors_box.show()
448 self.mirrors_label = gtk.Label(_('Mirror'))
449 self.mirrors_label.set_line_wrap(True)
450 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
451 self.mirrors_label.show()
453 self.mirrors = gtk.combo_box_new_text()
454 for mirror in self.common.mirrors:
455 self.mirrors.append_text(mirror)
456 if self.common.settings['mirror'] in self.common.mirrors:
457 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
459 self.preferred.set_active(0)
460 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
464 self.button_box = gtk.HButtonBox()
465 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
466 self.box.pack_start(self.button_box, True, True, 0)
467 self.button_box.show()
469 # save and launch button
470 save_launch_image = gtk.Image()
471 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
472 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
473 self.save_launch_button.set_image(save_launch_image)
474 self.save_launch_button.connect("clicked", self.save_launch, None)
475 self.button_box.add(self.save_launch_button)
476 self.save_launch_button.show()
478 # save and exit button
479 save_exit_image = gtk.Image()
480 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
481 self.save_exit_button = gtk.Button(_("Save & Exit"))
482 self.save_exit_button.set_image(save_exit_image)
483 self.save_exit_button.connect("clicked", self.save_exit, None)
484 self.button_box.add(self.save_exit_button)
485 self.save_exit_button.show()
488 cancel_image = gtk.Image()
489 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
490 self.cancel_button = gtk.Button(_("Cancel"))
491 self.cancel_button.set_image(cancel_image)
492 self.cancel_button.connect("clicked", self.destroy, None)
493 self.button_box.add(self.cancel_button)
494 self.cancel_button.show()
503 def save_launch(self, widget, data=None):
505 subprocess.Popen([self.common.paths['tbl_bin']])
509 def save_exit(self, widget, data=None):
515 # figure out the selected preferred option
517 selected = self.preferred_options[self.preferred.get_active()]
518 for i in self.common.available_versions:
519 if self.common.available_versions[i] == selected:
522 self.common.settings['preferred'] = preferred
525 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
526 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
527 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
529 # figure out the selected mirror
530 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
533 self.common.save_settings()
536 def delete_event(self, widget, event, data=None):
539 def destroy(self, widget, data=None):
544 def __init__(self, common):
545 print _('Starting launcher dialog')
549 self.set_gui(None, '', [])
550 self.launch_gui = True
551 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
553 # is firefox already running?
554 if self.common.settings['installed_version']:
555 firefox_pid = self.common.get_pid('./Browser/firefox')
557 print _('Firefox are is open, bringing to focus')
558 # bring firefox to front
559 self.common.bring_window_to_front(firefox_pid)
563 check_for_updates = False
564 if self.common.settings['check_for_updates']:
565 check_for_updates = True
567 if not check_for_updates:
568 # how long was it since the last update check?
569 # 86400 seconds = 24 hours
570 current_timestamp = int(time.time())
571 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
572 check_for_updates = True
574 if check_for_updates:
576 print 'Checking for update'
577 self.set_gui('task', _("Checking for Tor Browser update."),
578 ['download_update_check',
581 # no need to check for update
582 print _('Checked for update within 24 hours, skipping')
583 self.start_launcher()
587 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
588 self.window.set_title(_("Tor Browser"))
589 self.window.set_icon_from_file(self.common.paths['icon_file'])
590 self.window.set_position(gtk.WIN_POS_CENTER)
591 self.window.set_border_width(10)
592 self.window.connect("delete_event", self.delete_event)
593 self.window.connect("destroy", self.destroy)
595 # build the rest of the UI
598 # download or run TBB
599 def start_launcher(self):
600 # is TBB already installed?
601 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
602 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
604 # verify installed version for newer versions of TBB (#58)
605 if installed_version >= '3.0':
606 versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
607 if os.path.exists(versions_filename):
608 for line in open(versions_filename):
609 if 'TORBROWSER_VERSION' in line:
610 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
612 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
613 if os.path.isfile(start) and os.access(start, os.X_OK):
614 if installed_version == latest_version:
615 print _('Latest version of TBB is installed, launching')
616 # current version of tbb is installed, launch it
618 self.launch_gui = False
619 elif installed_version < latest_version:
620 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
621 # there is a tbb upgrade available
622 self.set_gui('task', _("Your Tor Browser is out of date."),
624 'download_sha256_sig',
630 # for some reason the installed tbb is newer than the current version?
631 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
635 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
636 self.set_gui('task', _("Downloading and installing Tor Browser."),
638 'download_sha256_sig',
644 # there are different GUIs that might appear, this sets which one we want
645 def set_gui(self, gui, message, tasks, autostart=True):
647 self.gui_message = message
648 self.gui_tasks = tasks
650 self.gui_autostart = autostart
652 # set all gtk variables to False
654 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
659 self.progressbar = False
660 self.button_box = False
661 self.start_button = False
662 self.exit_button = False
664 # build the application's UI
668 self.box = gtk.VBox(False, 20)
669 self.window.add(self.box)
671 if 'error' in self.gui:
673 self.label = gtk.Label(self.gui_message)
674 self.label.set_line_wrap(True)
675 self.box.pack_start(self.label, True, True, 0)
679 self.button_box = gtk.HButtonBox()
680 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
681 self.box.pack_start(self.button_box, True, True, 0)
682 self.button_box.show()
684 if self.gui != 'error':
686 yes_image = gtk.Image()
687 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
688 self.yes_button = gtk.Button("Yes")
689 self.yes_button.set_image(yes_image)
690 if self.gui == 'error_try_stable':
691 self.yes_button.connect("clicked", self.try_stable, None)
692 elif self.gui == 'error_try_default_mirror':
693 self.yes_button.connect("clicked", self.try_default_mirror, None)
694 elif self.gui == 'error_try_tor':
695 self.yes_button.connect("clicked", self.try_tor, None)
696 self.button_box.add(self.yes_button)
697 self.yes_button.show()
700 exit_image = gtk.Image()
701 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
702 self.exit_button = gtk.Button("Exit")
703 self.exit_button.set_image(exit_image)
704 self.exit_button.connect("clicked", self.destroy, None)
705 self.button_box.add(self.exit_button)
706 self.exit_button.show()
708 elif self.gui == 'task':
710 self.label = gtk.Label(self.gui_message)
711 self.label.set_line_wrap(True)
712 self.box.pack_start(self.label, True, True, 0)
716 self.progressbar = gtk.ProgressBar(adjustment=None)
717 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
718 self.progressbar.set_pulse_step(0.01)
719 self.box.pack_start(self.progressbar, True, True, 0)
722 self.button_box = gtk.HButtonBox()
723 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
724 self.box.pack_start(self.button_box, True, True, 0)
725 self.button_box.show()
728 start_image = gtk.Image()
729 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
730 self.start_button = gtk.Button(_("Start"))
731 self.start_button.set_image(start_image)
732 self.start_button.connect("clicked", self.start, None)
733 self.button_box.add(self.start_button)
734 if not self.gui_autostart:
735 self.start_button.show()
738 exit_image = gtk.Image()
739 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
740 self.exit_button = gtk.Button(_("Exit"))
741 self.exit_button.set_image(exit_image)
742 self.exit_button.connect("clicked", self.destroy, None)
743 self.button_box.add(self.exit_button)
744 self.exit_button.show()
749 if self.gui_autostart:
752 # start button clicked, begin tasks
753 def start(self, widget, data=None):
754 # disable the start button
755 if self.start_button:
756 self.start_button.set_sensitive(False)
758 # start running tasks
761 # run the next task in the task list
765 if self.gui_task_i >= len(self.gui_tasks):
769 task = self.gui_tasks[self.gui_task_i]
771 # get ready for the next task
774 print _('Running task: {0}'.format(task))
775 if task == 'download_update_check':
776 print _('Downloading'), self.common.paths['update_check_url']
777 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
779 if task == 'attempt_update':
780 print _('Checking to see if update is needed')
781 self.attempt_update()
783 elif task == 'download_sha256':
784 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
785 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
787 elif task == 'download_sha256_sig':
788 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
789 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
791 elif task == 'download_tarball':
792 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
793 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
795 elif task == 'verify':
796 print _('Verifying signature')
799 elif task == 'extract':
800 print _('Extracting'), self.common.paths['tarball_filename']
804 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
807 elif task == 'start_over':
808 print _('Starting download over again')
811 def response_received(self, response):
812 class FileDownloader(Protocol):
813 def __init__(self, common, file, total, progress, done_cb):
817 self.progress = progress
818 self.all_done = done_cb
820 if response.code != 200:
823 if response.code == 404:
824 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
828 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?"))
830 if common.settings['mirror'] != common.default_mirror:
831 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']))
833 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
835 def dataReceived(self, bytes):
836 self.file.write(bytes)
837 self.so_far += len(bytes)
838 percent = float(self.so_far) / float(self.total)
839 self.progress.set_fraction(percent)
840 amount = float(self.so_far)
842 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
845 amount = amount / float(size)
848 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
850 def connectionLost(self, reason):
851 print _('Finished receiving body:'), reason.getErrorMessage()
852 self.all_done(reason)
854 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
855 response.deliverBody(dl)
857 def response_finished(self, msg):
858 if msg.check(ResponseDone):
859 self.file_download.close()
860 delattr(self, 'current_download_path')
866 print "FINISHED", msg
867 ## FIXME handle errors
869 def download_error(self, f):
870 print _("Download error:"), f.value, type(f.value)
872 if isinstance(f.value, TryStableException):
873 f.trap(TryStableException)
874 self.set_gui('error_try_stable', str(f.value), [], False)
876 elif isinstance(f.value, TryDefaultMirrorException):
877 f.trap(TryDefaultMirrorException)
878 self.set_gui('error_try_default_mirror', str(f.value), [], False)
880 elif isinstance(f.value, DownloadErrorException):
881 f.trap(DownloadErrorException)
882 self.set_gui('error', str(f.value), [], False)
884 elif isinstance(f.value, DNSLookupError):
885 f.trap(DNSLookupError)
886 if common.settings['mirror'] != common.default_mirror:
887 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)
889 self.set_gui('error', str(f.value), [], False)
891 elif isinstance(f.value, ResponseFailed):
892 for reason in f.value.reasons:
893 if isinstance(reason.value, OpenSSL.SSL.Error):
894 # TODO: add the ability to report attack by posting bug to trac.torproject.org
895 if not self.common.settings['update_over_tor']:
896 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)
898 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
901 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
905 def download(self, name, url, path):
906 # keep track of current download
907 self.current_download_path = path
909 # initialize the progress bar
910 mirror_url = url.format(self.common.settings['mirror'])
911 self.progressbar.set_fraction(0)
912 self.progressbar.set_text(_('Downloading {0}').format(name))
913 self.progressbar.show()
916 # default mirror gets certificate pinning, only for requests that use the mirror
917 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
918 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
920 agent = Agent(reactor)
922 # actually, agent needs to follow redirect
923 agent = RedirectAgent(agent)
926 d = agent.request('GET', mirror_url,
927 Headers({'User-Agent': ['torbrowser-launcher']}),
930 self.file_download = open(path, 'w')
931 d.addCallback(self.response_received).addErrback(self.download_error)
933 if not reactor.running:
936 def try_stable(self, widget, data=None):
937 # change preferred to stable and relaunch TBL
938 self.common.settings['preferred'] = 'stable'
939 self.common.save_settings()
940 p = subprocess.Popen([self.common.paths['tbl_bin']])
943 def try_default_mirror(self, widget, data=None):
944 # change preferred to stable and relaunch TBL
945 self.common.settings['mirror'] = self.common.default_mirror
946 self.common.save_settings()
947 subprocess.Popen([self.common.paths['tbl_bin']])
950 def try_tor(self, widget, data=None):
951 # set update_over_tor to true and relaunch TBL
952 self.common.settings['update_over_tor'] = True
953 self.common.save_settings()
954 subprocess.Popen([self.common.paths['tbl_bin']])
957 def attempt_update(self):
958 # load the update check file
960 versions = json.load(open(self.common.paths['update_check_file']))
964 # filter linux versions
967 for version in versions:
968 if '-Linux' in version:
969 if 'alpha' in version or 'beta' in version or '-rc' in version:
970 valid_alphas.append(str(version))
972 valid_stables.append(str(version))
974 if len(valid_alphas):
975 latest_alpha = valid_alphas.pop()
977 if len(valid_stables):
978 latest_stable = valid_stables.pop()
980 if latest_stable or latest_alpha:
982 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
984 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
985 self.common.settings['last_update_check_timestamp'] = int(time.time())
986 self.common.settings['check_for_updates'] = False
987 self.common.save_settings()
988 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
989 self.start_launcher()
992 # failed to find the latest version
993 self.set_gui('error', _("Error checking for updates."), [], False)
996 # not a valid JSON object
997 self.set_gui('error', _("Error checking for updates."), [], False)
1004 # initialize the progress bar
1005 self.progressbar.set_fraction(0)
1006 self.progressbar.set_text(_('Verifying Signature'))
1007 self.progressbar.show()
1010 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1011 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1012 self.pulse_until_process_exits(p)
1013 if p.returncode == 0:
1014 # compare with sha256 of the tarball
1015 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1016 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1017 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1023 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1024 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)
1028 if not reactor.running:
1032 # initialize the progress bar
1033 self.progressbar.set_fraction(0)
1034 self.progressbar.set_text(_('Installing'))
1035 self.progressbar.show()
1040 if self.common.paths['tarball_file'][-2:] == 'xz':
1041 # if tarball is .tar.xz
1042 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1043 tf = tarfile.open(fileobj=xz)
1044 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1047 # if tarball is .tar.gz
1048 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1049 tf = tarfile.open(self.common.paths['tarball_file'])
1050 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1056 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1061 # installation is finished, so save installed_version
1062 self.common.settings['installed_version'] = self.common.settings['latest_version']
1063 self.common.save_settings()
1067 def run(self, run_next_task=True):
1068 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1071 if self.common.settings['modem_sound']:
1074 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1081 # make the progress bar pulse until process p (a Popen object) finishes
1082 def pulse_until_process_exits(self, p):
1083 while p.poll() is None:
1085 self.progressbar.pulse()
1088 # start over and download TBB again
1089 def start_over(self):
1090 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1091 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1096 def refresh_gtk(self):
1097 while gtk.events_pending():
1098 gtk.main_iteration(False)
1101 def delete_event(self, widget, event, data=None):
1104 def destroy(self, widget, data=None):
1105 if hasattr(self, 'file_download'):
1106 self.file_download.close()
1107 if hasattr(self, 'current_download_path'):
1108 os.remove(self.current_download_path)
1109 delattr(self, 'current_download_path')
1113 if __name__ == "__main__":
1114 with open(os.path.join(SHARE, 'torbrowser-launcher/version')) as buf:
1115 tor_browser_launcher_version = buf.read().strip()
1117 print _('Tor Browser Launcher')
1118 print _('By Micah Lee, licensed under GPLv3')
1119 print _('version {0}').format(tor_browser_launcher_version)
1120 print 'https://github.com/micahflee/torbrowser-launcher'
1122 common = TBLCommon(tor_browser_launcher_version)
1124 # is torbrowser-launcher already running?
1125 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1127 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1128 common.bring_window_to_front(tbl_pid)
1131 if '-settings' in sys.argv:
1133 app = TBLSettings(common)
1137 app = TBLLauncher(common)