4 https://github.com/micahflee/torbrowser-launcher/
6 Copyright (c) 2013-2014 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-mikeperry.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 'use_system_tor': False,
253 'modem_sound': False,
254 'last_update_check_timestamp': 0,
255 'mirror': self.default_mirror
258 if os.getenv("TOR_SKIP_LAUNCH") == "1":
259 default_settings['use_system_tor'] = True
261 if os.path.isfile(self.paths['settings_file']):
262 settings = pickle.load(open(self.paths['settings_file']))
265 # settings migrations
266 if settings['tbl_version'] == '0.0.1':
267 print '0.0.1 migration'
268 self.settings = default_settings
269 self.settings['installed_version']['alpha'] = settings['installed_version']
273 self.mkdir(self.paths['tbb']['alpha']['dir'])
275 # make sure settings file is up-to-date
276 for setting in default_settings:
277 if setting not in settings:
278 settings[setting] = default_settings[setting]
281 # make sure the version is current
282 if settings['tbl_version'] != self.tbl_version:
283 settings['tbl_version'] = self.tbl_version
286 self.settings = settings
291 self.settings = default_settings
295 def save_settings(self):
296 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
299 # get the process id of a program
301 def get_pid(bin_path, python=False):
304 for p in psutil.process_iter():
306 if p.pid != os.getpid():
309 if len(p.cmdline) > 1:
310 if 'python' in p.cmdline[0]:
313 if len(p.cmdline) > 0:
324 # bring program's x window to front
326 def bring_window_to_front(pid):
327 # figure out the window id
329 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
330 for line in p.stdout.readlines():
331 line_split = line.split()
332 cur_win_id = line_split[0]
333 cur_win_pid = int(line_split[2])
334 if cur_win_pid == pid:
339 subprocess.call(['wmctrl', '-i', '-a', win_id])
343 def __init__(self, common):
344 print _('Starting settings dialog')
348 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
349 self.window.set_title(_("Tor Browser Launcher Settings"))
350 self.window.set_icon_from_file(self.common.paths['icon_file'])
351 self.window.set_position(gtk.WIN_POS_CENTER)
352 self.window.set_border_width(10)
353 self.window.connect("delete_event", self.delete_event)
354 self.window.connect("destroy", self.destroy)
356 # build the rest of the UI
357 self.box = gtk.VBox(False, 10)
358 self.window.add(self.box)
361 self.hbox = gtk.HBox(False, 10)
362 self.box.pack_start(self.hbox, True, True, 0)
365 self.settings_box = gtk.VBox(False, 10)
366 self.hbox.pack_start(self.settings_box, True, True, 0)
367 self.settings_box.show()
369 self.labels_box = gtk.VBox(False, 10)
370 self.hbox.pack_start(self.labels_box, True, True, 0)
371 self.labels_box.show()
374 self.preferred_box = gtk.HBox(False, 10)
375 self.settings_box.pack_start(self.preferred_box, True, True, 0)
376 self.preferred_box.show()
378 self.preferred_label = gtk.Label(_('I prefer'))
379 self.preferred_label.set_line_wrap(True)
380 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
381 self.preferred_label.show()
383 self.preferred_options = []
384 for i in self.common.available_versions:
385 self.preferred_options.append(self.common.available_versions[i])
386 self.preferred_options.sort()
388 self.preferred = gtk.combo_box_new_text()
389 for option in self.preferred_options:
390 self.preferred.append_text(option)
391 if self.common.settings['preferred'] in self.common.available_versions:
392 self.preferred.set_active(self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]))
394 self.preferred.set_active(0)
395 self.preferred_box.pack_start(self.preferred, True, True, 0)
396 self.preferred.show()
399 self.system_tor_checkbox = gtk.CheckButton(_("Use the system's Tor installation"))
400 self.settings_box.pack_start(self.system_tor_checkbox, True, True, 0)
401 if self.common.settings['use_system_tor']:
402 self.system_tor_checkbox.set_active(True)
404 self.system_tor_checkbox.set_active(False)
405 self.system_tor_checkbox.show()
409 self.txsocks_found = True
411 self.txsocks_found = False
414 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
415 if self.txsocks_found:
416 self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
418 self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))
420 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
421 if self.common.settings['update_over_tor'] and self.txsocks_found:
422 self.tor_update_checkbox.set_active(True)
424 self.tor_update_checkbox.set_active(False)
426 if self.txsocks_found == False:
427 self.tor_update_checkbox.set_sensitive(False)
429 self.tor_update_checkbox.show()
431 # Set callback for system tor and update over tor
432 self.system_tor_checkbox.connect('clicked', self.on_system_tor_clicked)
433 self.on_system_tor_clicked(None)
436 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
437 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
438 if self.common.settings['check_for_updates']:
439 self.update_checkbox.set_active(True)
441 self.update_checkbox.set_active(False)
442 self.update_checkbox.show()
445 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
446 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
450 if self.common.settings['modem_sound']:
451 self.modem_checkbox.set_active(True)
453 self.modem_checkbox.set_active(False)
455 self.modem_checkbox.set_active(False)
456 self.modem_checkbox.set_sensitive(False)
457 self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
458 self.modem_checkbox.show()
461 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
462 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
464 self.label1 = gtk.Label(_('Not installed'))
465 self.label1.set_line_wrap(True)
466 self.labels_box.pack_start(self.label1, True, True, 0)
469 if(self.common.settings['last_update_check_timestamp'] > 0):
470 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']))))
472 self.label1 = gtk.Label(_('Never checked for updates'))
473 self.label1.set_line_wrap(True)
474 self.labels_box.pack_start(self.label1, True, True, 0)
478 self.mirrors_box = gtk.HBox(False, 10)
479 self.box.pack_start(self.mirrors_box, True, True, 0)
480 self.mirrors_box.show()
482 self.mirrors_label = gtk.Label(_('Mirror'))
483 self.mirrors_label.set_line_wrap(True)
484 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
485 self.mirrors_label.show()
487 self.mirrors = gtk.combo_box_new_text()
488 for mirror in self.common.mirrors:
489 self.mirrors.append_text(mirror)
490 if self.common.settings['mirror'] in self.common.mirrors:
491 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
493 self.preferred.set_active(0)
494 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
498 self.button_box = gtk.HButtonBox()
499 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
500 self.box.pack_start(self.button_box, True, True, 0)
501 self.button_box.show()
503 # save and launch button
504 save_launch_image = gtk.Image()
505 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
506 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
507 self.save_launch_button.set_image(save_launch_image)
508 self.save_launch_button.connect("clicked", self.save_launch, None)
509 self.button_box.add(self.save_launch_button)
510 self.save_launch_button.show()
512 # save and exit button
513 save_exit_image = gtk.Image()
514 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
515 self.save_exit_button = gtk.Button(_("Save & Exit"))
516 self.save_exit_button.set_image(save_exit_image)
517 self.save_exit_button.connect("clicked", self.save_exit, None)
518 self.button_box.add(self.save_exit_button)
519 self.save_exit_button.show()
522 cancel_image = gtk.Image()
523 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
524 self.cancel_button = gtk.Button(_("Cancel"))
525 self.cancel_button.set_image(cancel_image)
526 self.cancel_button.connect("clicked", self.destroy, None)
527 self.button_box.add(self.cancel_button)
528 self.cancel_button.show()
536 # UI Callback for update over tor/use system tor
537 def on_system_tor_clicked(self, event):
538 if self.txsocks_found:
539 value = self.system_tor_checkbox.get_active()
543 self.tor_update_checkbox.set_active(value)
544 self.tor_update_checkbox.set_sensitive(value)
547 def save_launch(self, widget, data=None):
549 subprocess.Popen([self.common.paths['tbl_bin']])
553 def save_exit(self, widget, data=None):
559 # figure out the selected preferred option
561 selected = self.preferred_options[self.preferred.get_active()]
562 for i in self.common.available_versions:
563 if self.common.available_versions[i] == selected:
566 self.common.settings['preferred'] = preferred
569 self.common.settings['use_system_tor'] = self.system_tor_checkbox.get_active()
570 self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
571 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
572 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
574 # figure out the selected mirror
575 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
578 self.common.save_settings()
581 def delete_event(self, widget, event, data=None):
584 def destroy(self, widget, data=None):
589 def __init__(self, common):
590 print _('Starting launcher dialog')
594 self.set_gui(None, '', [])
595 self.launch_gui = True
596 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
598 if self.common.settings['use_system_tor']:
599 os.putenv("TOR_SKIP_LAUNCH", "1")
600 os.putenv("TOR_SOCKS_HOST", "127.0.0.1")
601 os.putenv("TOR_SOCKS_PORT", "9050")
602 # need to manually set os.environ because os.putenv doesn't update it
603 # https://docs.python.org/2/library/os.html#os.putenv
604 os.environ["TOR_SKIP_LAUNCH"] = "1"
605 os.environ["TOR_SOCKS_HOST"] = "127.0.0.1"
606 os.environ["TOR_SOCKS_PORT"] = "9050"
611 self.common.settings['update_over_tor'] = True
613 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-txsocksx package is missing, downloads will not happen over tor"))
614 md.set_position(gtk.WIN_POS_CENTER)
617 self.common.settings['update_over_tor'] = False
619 # is firefox already running?
620 if self.common.settings['installed_version']:
621 firefox_pid = self.common.get_pid('./Browser/firefox')
623 print _('Firefox are is open, bringing to focus')
624 # bring firefox to front
625 self.common.bring_window_to_front(firefox_pid)
629 check_for_updates = False
630 if self.common.settings['check_for_updates']:
631 check_for_updates = True
633 if not check_for_updates:
634 # how long was it since the last update check?
635 # 86400 seconds = 24 hours
636 current_timestamp = int(time.time())
637 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
638 check_for_updates = True
640 if check_for_updates:
642 print 'Checking for update'
643 self.set_gui('task', _("Checking for Tor Browser update."),
644 ['download_update_check',
647 # no need to check for update
648 print _('Checked for update within 24 hours, skipping')
649 self.start_launcher()
653 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
654 self.window.set_title(_("Tor Browser"))
655 self.window.set_icon_from_file(self.common.paths['icon_file'])
656 self.window.set_position(gtk.WIN_POS_CENTER)
657 self.window.set_border_width(10)
658 self.window.connect("delete_event", self.delete_event)
659 self.window.connect("destroy", self.destroy)
661 # build the rest of the UI
664 # download or run TBB
665 def start_launcher(self):
666 # is TBB already installed?
667 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
668 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
670 # verify installed version for newer versions of TBB (#58)
671 if installed_version >= '3.0':
672 versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
673 if os.path.exists(versions_filename):
674 for line in open(versions_filename):
675 if 'TORBROWSER_VERSION' in line:
676 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
678 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
679 if os.path.isfile(start) and os.access(start, os.X_OK):
680 if installed_version == latest_version:
681 print _('Latest version of TBB is installed, launching')
682 # current version of tbb is installed, launch it
684 self.launch_gui = False
685 elif installed_version < latest_version:
686 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
687 # there is a tbb upgrade available
688 self.set_gui('task', _("Your Tor Browser is out of date."),
690 'download_sha256_sig',
696 # for some reason the installed tbb is newer than the current version?
697 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
701 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
702 self.set_gui('task', _("Downloading and installing Tor Browser."),
704 'download_sha256_sig',
710 # there are different GUIs that might appear, this sets which one we want
711 def set_gui(self, gui, message, tasks, autostart=True):
713 self.gui_message = message
714 self.gui_tasks = tasks
716 self.gui_autostart = autostart
718 # set all gtk variables to False
720 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
725 self.progressbar = False
726 self.button_box = False
727 self.start_button = False
728 self.exit_button = False
730 # build the application's UI
734 self.box = gtk.VBox(False, 20)
735 self.window.add(self.box)
737 if 'error' in self.gui:
739 self.label = gtk.Label(self.gui_message)
740 self.label.set_line_wrap(True)
741 self.box.pack_start(self.label, True, True, 0)
745 self.button_box = gtk.HButtonBox()
746 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
747 self.box.pack_start(self.button_box, True, True, 0)
748 self.button_box.show()
750 if self.gui != 'error':
752 yes_image = gtk.Image()
753 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
754 self.yes_button = gtk.Button("Yes")
755 self.yes_button.set_image(yes_image)
756 if self.gui == 'error_try_stable':
757 self.yes_button.connect("clicked", self.try_stable, None)
758 elif self.gui == 'error_try_default_mirror':
759 self.yes_button.connect("clicked", self.try_default_mirror, None)
760 elif self.gui == 'error_try_tor':
761 self.yes_button.connect("clicked", self.try_tor, None)
762 self.button_box.add(self.yes_button)
763 self.yes_button.show()
766 exit_image = gtk.Image()
767 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
768 self.exit_button = gtk.Button("Exit")
769 self.exit_button.set_image(exit_image)
770 self.exit_button.connect("clicked", self.destroy, None)
771 self.button_box.add(self.exit_button)
772 self.exit_button.show()
774 elif self.gui == 'task':
776 self.label = gtk.Label(self.gui_message)
777 self.label.set_line_wrap(True)
778 self.box.pack_start(self.label, True, True, 0)
782 self.progressbar = gtk.ProgressBar(adjustment=None)
783 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
784 self.progressbar.set_pulse_step(0.01)
785 self.box.pack_start(self.progressbar, True, True, 0)
788 self.button_box = gtk.HButtonBox()
789 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
790 self.box.pack_start(self.button_box, True, True, 0)
791 self.button_box.show()
794 start_image = gtk.Image()
795 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
796 self.start_button = gtk.Button(_("Start"))
797 self.start_button.set_image(start_image)
798 self.start_button.connect("clicked", self.start, None)
799 self.button_box.add(self.start_button)
800 if not self.gui_autostart:
801 self.start_button.show()
804 exit_image = gtk.Image()
805 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
806 self.exit_button = gtk.Button(_("Exit"))
807 self.exit_button.set_image(exit_image)
808 self.exit_button.connect("clicked", self.destroy, None)
809 self.button_box.add(self.exit_button)
810 self.exit_button.show()
815 if self.gui_autostart:
818 # start button clicked, begin tasks
819 def start(self, widget, data=None):
820 # disable the start button
821 if self.start_button:
822 self.start_button.set_sensitive(False)
824 # start running tasks
827 # run the next task in the task list
831 if self.gui_task_i >= len(self.gui_tasks):
835 task = self.gui_tasks[self.gui_task_i]
837 # get ready for the next task
840 print _('Running task: {0}'.format(task))
841 if task == 'download_update_check':
842 print _('Downloading'), self.common.paths['update_check_url']
843 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
845 if task == 'attempt_update':
846 print _('Checking to see if update is needed')
847 self.attempt_update()
849 elif task == 'download_sha256':
850 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
851 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
853 elif task == 'download_sha256_sig':
854 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
855 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
857 elif task == 'download_tarball':
858 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
859 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
861 elif task == 'verify':
862 print _('Verifying signature')
865 elif task == 'extract':
866 print _('Extracting'), self.common.paths['tarball_filename']
870 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
873 elif task == 'start_over':
874 print _('Starting download over again')
877 def response_received(self, response):
878 class FileDownloader(Protocol):
879 def __init__(self, common, file, total, progress, done_cb):
883 self.progress = progress
884 self.all_done = done_cb
886 if response.code != 200:
889 if response.code == 404:
890 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
894 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?"))
896 if common.settings['mirror'] != common.default_mirror:
897 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']))
899 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
901 def dataReceived(self, bytes):
902 self.file.write(bytes)
903 self.so_far += len(bytes)
904 percent = float(self.so_far) / float(self.total)
905 self.progress.set_fraction(percent)
906 amount = float(self.so_far)
908 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
911 amount = amount / float(size)
914 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
916 def connectionLost(self, reason):
917 print _('Finished receiving body:'), reason.getErrorMessage()
918 self.all_done(reason)
920 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
921 response.deliverBody(dl)
923 def response_finished(self, msg):
924 if msg.check(ResponseDone):
925 self.file_download.close()
926 delattr(self, 'current_download_path')
932 print "FINISHED", msg
933 ## FIXME handle errors
935 def download_error(self, f):
936 print _("Download error:"), f.value, type(f.value)
938 if isinstance(f.value, TryStableException):
939 f.trap(TryStableException)
940 self.set_gui('error_try_stable', str(f.value), [], False)
942 elif isinstance(f.value, TryDefaultMirrorException):
943 f.trap(TryDefaultMirrorException)
944 self.set_gui('error_try_default_mirror', str(f.value), [], False)
946 elif isinstance(f.value, DownloadErrorException):
947 f.trap(DownloadErrorException)
948 self.set_gui('error', str(f.value), [], False)
950 elif isinstance(f.value, DNSLookupError):
951 f.trap(DNSLookupError)
952 if common.settings['mirror'] != common.default_mirror:
953 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)
955 self.set_gui('error', str(f.value), [], False)
957 elif isinstance(f.value, ResponseFailed):
958 for reason in f.value.reasons:
959 if isinstance(reason.value, OpenSSL.SSL.Error):
960 # TODO: add the ability to report attack by posting bug to trac.torproject.org
961 if not self.common.settings['update_over_tor']:
962 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)
964 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
967 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
971 def download(self, name, url, path):
972 # keep track of current download
973 self.current_download_path = path
975 # initialize the progress bar
976 mirror_url = url.format(self.common.settings['mirror'])
977 self.progressbar.set_fraction(0)
978 self.progressbar.set_text(_('Downloading {0}').format(name))
979 self.progressbar.show()
982 if self.common.settings['use_system_tor'] and self.common.settings['update_over_tor']:
983 from twisted.internet.endpoints import TCP4ClientEndpoint
984 from txsocksx.http import SOCKS5Agent
986 torEndpoint = TCP4ClientEndpoint(reactor, os.getenv("TOR_SOCKS_HOST"), int(os.getenv("TOR_SOCKS_PORT")))
988 # default mirror gets certificate pinning, only for requests that use the mirror
989 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
990 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
992 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
994 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
995 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
997 agent = Agent(reactor)
999 # actually, agent needs to follow redirect
1000 agent = RedirectAgent(agent)
1003 d = agent.request('GET', mirror_url,
1004 Headers({'User-Agent': ['torbrowser-launcher']}),
1007 self.file_download = open(path, 'w')
1008 d.addCallback(self.response_received).addErrback(self.download_error)
1010 if not reactor.running:
1013 def try_stable(self, widget, data=None):
1014 # change preferred to stable and relaunch TBL
1015 self.common.settings['preferred'] = 'stable'
1016 self.common.save_settings()
1017 p = subprocess.Popen([self.common.paths['tbl_bin']])
1020 def try_default_mirror(self, widget, data=None):
1021 # change preferred to stable and relaunch TBL
1022 self.common.settings['mirror'] = self.common.default_mirror
1023 self.common.save_settings()
1024 subprocess.Popen([self.common.paths['tbl_bin']])
1027 def try_tor(self, widget, data=None):
1028 # set update_over_tor to true and relaunch TBL
1029 self.common.settings['update_over_tor'] = True
1030 self.common.save_settings()
1031 subprocess.Popen([self.common.paths['tbl_bin']])
1034 def attempt_update(self):
1035 # load the update check file
1037 versions = json.load(open(self.common.paths['update_check_file']))
1038 latest_stable = None
1041 # filter linux versions
1044 for version in versions:
1045 if '-Linux' in version:
1046 if 'alpha' in version or 'beta' in version or '-rc' in version:
1047 valid_alphas.append(str(version))
1049 valid_stables.append(str(version))
1051 if len(valid_alphas):
1052 latest_alpha = valid_alphas.pop()
1053 valid_stables.sort()
1054 if len(valid_stables):
1055 latest_stable = valid_stables.pop()
1057 if latest_stable or latest_alpha:
1059 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
1061 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
1062 self.common.settings['last_update_check_timestamp'] = int(time.time())
1063 self.common.settings['check_for_updates'] = False
1064 self.common.save_settings()
1065 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
1066 self.start_launcher()
1069 # failed to find the latest version
1070 self.set_gui('error', _("Error checking for updates."), [], False)
1073 # not a valid JSON object
1074 self.set_gui('error', _("Error checking for updates."), [], False)
1081 # initialize the progress bar
1082 self.progressbar.set_fraction(0)
1083 self.progressbar.set_text(_('Verifying Signature'))
1084 self.progressbar.show()
1087 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1088 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1089 self.pulse_until_process_exits(p)
1090 if p.returncode == 0:
1091 # compare with sha256 of the tarball
1092 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1093 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1094 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1100 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1101 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)
1105 if not reactor.running:
1109 # initialize the progress bar
1110 self.progressbar.set_fraction(0)
1111 self.progressbar.set_text(_('Installing'))
1112 self.progressbar.show()
1117 if self.common.paths['tarball_file'][-2:] == 'xz':
1118 # if tarball is .tar.xz
1119 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1120 tf = tarfile.open(fileobj=xz)
1121 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1124 # if tarball is .tar.gz
1125 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1126 tf = tarfile.open(self.common.paths['tarball_file'])
1127 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1133 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1138 # installation is finished, so save installed_version
1139 self.common.settings['installed_version'] = self.common.settings['latest_version']
1140 self.common.save_settings()
1144 def run(self, run_next_task=True):
1145 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1148 if self.common.settings['modem_sound']:
1152 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1156 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-pygame package is missing, the modem sound is unavailable."))
1157 md.set_position(gtk.WIN_POS_CENTER)
1165 # make the progress bar pulse until process p (a Popen object) finishes
1166 def pulse_until_process_exits(self, p):
1167 while p.poll() is None:
1169 self.progressbar.pulse()
1172 # start over and download TBB again
1173 def start_over(self):
1174 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1175 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1180 def refresh_gtk(self):
1181 while gtk.events_pending():
1182 gtk.main_iteration(False)
1185 def delete_event(self, widget, event, data=None):
1188 def destroy(self, widget, data=None):
1189 if hasattr(self, 'file_download'):
1190 self.file_download.close()
1191 if hasattr(self, 'current_download_path'):
1192 os.remove(self.current_download_path)
1193 delattr(self, 'current_download_path')
1197 if __name__ == "__main__":
1198 tor_browser_launcher_version = open('/usr/share/torbrowser-launcher/version').read().strip()
1200 print _('Tor Browser Launcher')
1201 print _('By Micah Lee, licensed under GPLv3')
1202 print _('version {0}').format(tor_browser_launcher_version)
1203 print 'https://github.com/micahflee/torbrowser-launcher'
1205 common = TBLCommon(tor_browser_launcher_version)
1207 # is torbrowser-launcher already running?
1208 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1210 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1211 common.bring_window_to_front(tbl_pid)
1214 if '-settings' in sys.argv:
1216 app = TBLSettings(common)
1220 app = TBLLauncher(common)