4 https://github.com/micahflee/torbrowser-launcher/
6 Copyright (c) 2013 Micah Lee <micahflee@riseup.net>
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
31 sys.path.append('/usr/share/torbrowser-launcher/lib/')
34 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
36 from twisted.internet import gtk2reactor
38 from twisted.internet import reactor
44 import os, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil, hashlib, lzma
46 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
47 from twisted.web.http_headers import Headers
48 from twisted.internet.protocol import Protocol
49 from twisted.internet.ssl import ClientContextFactory
50 from twisted.internet.endpoints import TCP4ClientEndpoint
51 from twisted.internet.error import DNSLookupError
53 from txsocksx.client import SOCKS5ClientEndpoint
57 class TryStableException(Exception):
59 class TryDefaultMirrorException(Exception):
61 class DownloadErrorException(Exception):
64 class VerifyTorProjectCert(ClientContextFactory):
66 def __init__(self, torproject_pem):
67 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
69 def getContext(self, host, port):
70 ctx = ClientContextFactory.getContext(self)
71 ctx.set_verify_depth(0)
72 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
75 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
76 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
80 def __init__(self, tbl_version):
81 print _('Initializing Tor Browser Launcher')
82 self.tbl_version = tbl_version
85 self.available_versions = {
86 'stable': _('Tor Browser Bundle - stable'),
87 'alpha': _('Tor Browser Bundle - alpha')
89 self.default_mirror = 'https://www.torproject.org/dist/'
91 self.discover_arch_lang()
93 self.mkdir(self.paths['data_dir'])
96 self.mkdir(self.paths['download_dir'])
97 self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
100 # allow buttons to have icons
102 gtk_settings = gtk.settings_get_default()
103 gtk_settings.props.gtk_button_images = True
107 # discover the architecture and language
108 def discover_arch_lang(self):
109 # figure out the architecture
110 (sysname, nodename, release, version, machine) = os.uname()
111 self.architecture = machine
113 # figure out the language
114 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
115 default_locale = locale.getdefaultlocale()[0]
116 if default_locale == None:
117 self.language = 'en-US'
119 self.language = default_locale.replace('_', '-')
120 if self.language not in available_languages:
121 self.language = self.language.split('-')[0]
122 if self.language not in available_languages:
123 for l in available_languages:
124 if l[0:2] == self.language:
126 # if language isn't available, default to english
127 if self.language not in available_languages:
128 self.language = 'en-US'
130 # build all relevant paths
131 def build_paths(self, tbb_version = None):
132 homedir = os.getenv('HOME')
134 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
135 if os.path.exists(homedir) == False:
137 os.mkdir(homedir, 0700)
139 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
140 if not os.access(homedir, os.W_OK):
141 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
143 tbb_data = '%s/.torbrowser' % homedir
146 if tbb_version >= '3.':
148 dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b')
149 if self.architecture == 'x86_64':
153 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
156 self.paths['tarball_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/'+tarball_filename
159 self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
160 self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
161 self.paths['sha256_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/sha256sums.txt'
162 self.paths['sha256_sig_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/sha256sums.txt.mp-asc'
165 tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
168 self.paths['tarball_url'] = '{0}torbrowser/linux/'+tarball_filename # {0} will be replaced with the mirror
171 self.paths['tarball_sig_file'] = tbb_data+'/download/'+tarball_filename+'.asc'
172 self.paths['tarball_sig_url'] = '{0}torbrowser/linux/'+tarball_filename+'.asc'
173 self.paths['tarball_sig_filename'] = tarball_filename+'.asc'
175 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
176 self.paths['tarball_filename'] = tarball_filename
180 'tbl_bin': '/usr/bin/torbrowser-launcher',
181 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
182 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
183 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
184 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
185 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
186 'mike_key': '/usr/share/torbrowser-launcher/mike.asc',
187 'mirrors_txt': '/usr/share/torbrowser-launcher/mirrors.txt',
188 'data_dir': tbb_data,
189 'download_dir': tbb_data+'/download',
190 'gnupg_homedir': tbb_data+'/gnupg_homedir',
191 'settings_file': tbb_data+'/settings',
192 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
193 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
196 'dir': tbb_data+'/tbb/stable/'+self.architecture,
197 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
198 'vidalia_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
199 'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
200 'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
203 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
204 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
205 'vidalia_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
206 'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
207 'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
213 def mkdir(self, path):
215 if os.path.exists(path) == False:
216 os.makedirs(path, 0700)
219 print _("Cannot create directory {0}").format(path)
221 if not os.access(path, os.W_OK):
222 print _("{0} is not writable").format(path)
226 # if gnupg_homedir isn't set up, set it up
227 def init_gnupg(self):
228 if not os.path.exists(self.paths['gnupg_homedir']):
229 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
230 if self.mkdir(self.paths['gnupg_homedir']):
234 def import_keys(self):
235 print _('Importing keys')
236 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key']]).wait()
237 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['sebastian_key']]).wait()
238 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['alexandre_key']]).wait()
239 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['mike_key']]).wait()
242 def load_mirrors(self):
244 for mirror in open(self.paths['mirrors_txt'], 'r').readlines():
245 self.mirrors.append(mirror.strip())
248 def load_settings(self):
250 'tbl_version': self.tbl_version,
251 'preferred': 'stable',
252 'installed_version': {
260 'update_over_tor': True,
261 'check_for_updates': False,
262 'last_update_check_timestamp': 0,
263 'mirror': self.default_mirror
266 if os.path.isfile(self.paths['settings_file']):
267 settings = pickle.load(open(self.paths['settings_file']))
269 # what version settings is this?
270 if not 'tbl_version' in settings:
271 settings['tbl_version'] = '0.0.1'
273 # sanity checks for current version
274 if settings['tbl_version'] == self.tbl_version:
276 if not 'preferred' in settings:
277 good_settings = False
278 if not 'installed_version' in settings:
279 good_settings = False
280 if not 'stable' in settings['installed_version']:
281 good_settings = False
282 if not 'alpha' in settings['installed_version']:
283 good_settings = False
284 if not 'latest_version' in settings:
285 good_settings = False
286 if not 'stable' in settings['latest_version']:
287 good_settings = False
288 if not 'alpha' in settings['latest_version']:
289 good_settings = False
290 if not 'update_over_tor' in settings:
291 good_settings = False
292 if not 'check_for_updates' in settings:
293 good_settings = False
294 if not 'last_update_check_timestamp' in settings:
295 good_settings = False
296 if not 'mirror' in settings:
297 good_settings = False
300 self.settings = settings
302 self.settings = default_settings
304 # settings migrations
305 if settings['tbl_version'] == '0.0.1':
306 self.settings = default_settings
307 self.settings['installed_version']['alpha'] = settings['installed_version']
308 self.settings['tbl_version'] = '0.0.2'
312 self.mkdir(self.paths['tbb']['alpha']['dir'])
313 # todo: move already-installed TBB alpha to new location
314 if os.path.exists(self.paths['data_dir']+'/tbb/x86_64'):
316 if os.path.exists(self.paths['data_dir']+'/tbb/i686'):
319 # 0.0.2 added Mike Perry's key
323 self.settings = default_settings
327 def save_settings(self):
328 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
331 # get the process id of a program
332 def get_pid(self, bin_path, python = False):
335 for p in psutil.process_iter():
337 if p.pid != os.getpid():
340 if len(p.cmdline) > 1:
341 if 'python' in p.cmdline[0]:
344 if len(p.cmdline) > 0:
355 # bring program's x window to front
356 def bring_window_to_front(self, pid):
357 # figure out the window id
359 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
360 for line in p.stdout.readlines():
361 line_split = line.split()
362 cur_win_id = line_split[0]
363 cur_win_pid = int(line_split[2])
364 if cur_win_pid == pid:
369 subprocess.call(['wmctrl', '-i', '-a', win_id])
372 def __init__(self, common):
373 print _('Starting settings dialog')
377 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
378 self.window.set_title(_("Tor Browser Launcher Settings"))
379 self.window.set_icon_from_file(self.common.paths['icon_file'])
380 self.window.set_position(gtk.WIN_POS_CENTER)
381 self.window.set_border_width(10)
382 self.window.connect("delete_event", self.delete_event)
383 self.window.connect("destroy", self.destroy)
385 # build the rest of the UI
386 self.box = gtk.VBox(False, 10)
387 self.window.add(self.box)
390 self.hbox = gtk.HBox(False, 10)
391 self.box.pack_start(self.hbox, True, True, 0)
394 self.settings_box = gtk.VBox(False, 10)
395 self.hbox.pack_start(self.settings_box, True, True, 0)
396 self.settings_box.show()
398 self.labels_box = gtk.VBox(False, 10)
399 self.hbox.pack_start(self.labels_box, True, True, 0)
400 self.labels_box.show()
403 self.preferred_box = gtk.HBox(False, 10)
404 self.settings_box.pack_start(self.preferred_box, True, True, 0)
405 self.preferred_box.show()
407 self.preferred_label = gtk.Label(_('I prefer'))
408 self.preferred_label.set_line_wrap(True)
409 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
410 self.preferred_label.show()
412 self.preferred_options = []
413 for i in self.common.available_versions:
414 self.preferred_options.append(self.common.available_versions[i])
415 self.preferred_options.sort()
417 self.preferred = gtk.combo_box_new_text()
418 for option in self.preferred_options:
419 self.preferred.append_text(option)
420 if self.common.settings['preferred'] in self.common.available_versions:
421 self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
423 self.preferred.set_active(0)
424 self.preferred_box.pack_start(self.preferred, True, True, 0)
425 self.preferred.show()
428 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
430 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
431 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
432 if self.common.settings['update_over_tor']:
433 self.tor_update_checkbox.set_active(True)
435 self.tor_update_checkbox.set_active(False)
436 self.tor_update_checkbox.show()
440 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
441 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
442 if self.common.settings['check_for_updates']:
443 self.update_checkbox.set_active(True)
445 self.update_checkbox.set_active(False)
446 self.update_checkbox.show()
449 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
450 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
452 self.label1 = gtk.Label(_('Not installed'))
453 self.label1.set_line_wrap(True)
454 self.labels_box.pack_start(self.label1, True, True, 0)
457 if(self.common.settings['last_update_check_timestamp'] > 0):
458 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']))))
460 self.label1 = gtk.Label(_('Never checked for updates'))
461 self.label1.set_line_wrap(True)
462 self.labels_box.pack_start(self.label1, True, True, 0)
466 self.mirrors_box = gtk.HBox(False, 10)
467 self.box.pack_start(self.mirrors_box, True, True, 0)
468 self.mirrors_box.show()
470 self.mirrors_label = gtk.Label(_('Mirror'))
471 self.mirrors_label.set_line_wrap(True)
472 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
473 self.mirrors_label.show()
475 self.mirrors = gtk.combo_box_new_text()
476 for mirror in self.common.mirrors:
477 self.mirrors.append_text(mirror)
478 if self.common.settings['mirror'] in self.common.mirrors:
479 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
481 self.preferred.set_active(0)
482 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
486 self.button_box = gtk.HButtonBox()
487 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
488 self.box.pack_start(self.button_box, True, True, 0)
489 self.button_box.show()
491 # save and launch button
492 save_launch_image = gtk.Image()
493 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
494 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
495 self.save_launch_button.set_image(save_launch_image)
496 self.save_launch_button.connect("clicked", self.save_launch, None)
497 self.button_box.add(self.save_launch_button)
498 self.save_launch_button.show()
500 # save and exit button
501 save_exit_image = gtk.Image()
502 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
503 self.save_exit_button = gtk.Button(_("Save & Exit"))
504 self.save_exit_button.set_image(save_exit_image)
505 self.save_exit_button.connect("clicked", self.save_exit, None)
506 self.button_box.add(self.save_exit_button)
507 self.save_exit_button.show()
510 cancel_image = gtk.Image()
511 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
512 self.cancel_button = gtk.Button(_("Cancel"))
513 self.cancel_button.set_image(cancel_image)
514 self.cancel_button.connect("clicked", self.destroy, None)
515 self.button_box.add(self.cancel_button)
516 self.cancel_button.show()
525 def save_launch(self, widget, data=None):
527 p = subprocess.Popen([self.common.paths['tbl_bin']])
531 def save_exit(self, widget, data=None):
537 # figure out the selected preferred option
539 selected = self.preferred_options[self.preferred.get_active()]
540 for i in self.common.available_versions:
541 if self.common.available_versions[i] == selected:
544 self.common.settings['preferred'] = preferred
547 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
548 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
550 # figure out the selected mirror
551 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
554 self.common.save_settings()
557 def delete_event(self, widget, event, data=None):
559 def destroy(self, widget, data=None):
564 def __init__(self, common):
565 print _('Starting launcher dialog')
569 self.set_gui(None, '', [])
570 self.launch_gui = True
571 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
573 # is vidalia already running and we just need to open a new firefox?
574 if self.common.settings['installed_version']:
575 vidalia_pid = self.common.get_pid('./App/vidalia')
576 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
578 if vidalia_pid and not firefox_pid:
579 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
580 self.common.bring_window_to_front(vidalia_pid)
581 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'], '-no-remote', '-profile', self.common.paths['tbb'][self.common.settings['preferred']]['firefox_profile']])
583 elif vidalia_pid and firefox_pid:
584 print _('Vidalia and Firefox are already open, bringing them to focus')
586 # bring firefox to front, then vidalia
587 self.common.bring_window_to_front(firefox_pid)
588 self.common.bring_window_to_front(vidalia_pid)
592 check_for_updates = False
593 if self.common.settings['check_for_updates']:
594 check_for_updates = True
596 if not check_for_updates:
597 # how long was it since the last update check?
598 # 86400 seconds = 24 hours
599 current_timestamp = int(time.time())
600 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
601 check_for_updates = True
603 if check_for_updates:
605 print 'Checking for update'
606 self.set_gui('task', _("Checking for Tor Browser update."),
607 ['download_update_check',
610 # no need to check for update
611 print _('Checked for update within 24 hours, skipping')
612 self.start_launcher()
616 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
617 self.window.set_title(_("Tor Browser"))
618 self.window.set_icon_from_file(self.common.paths['icon_file'])
619 self.window.set_position(gtk.WIN_POS_CENTER)
620 self.window.set_border_width(10)
621 self.window.connect("delete_event", self.delete_event)
622 self.window.connect("destroy", self.destroy)
624 # build the rest of the UI
627 # download or run TBB
628 def start_launcher(self):
629 # is TBB already installed?
630 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
631 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
633 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
634 if os.path.isfile(start) and os.access(start, os.X_OK):
635 if installed_version == latest_version:
636 print _('Latest version of TBB is installed, launching')
637 # current version of tbb is installed, launch it
639 self.launch_gui = False
640 elif installed_version < latest_version:
641 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
642 # there is a tbb upgrade available
643 if latest_version >= '3.':
644 self.set_gui('task', _("Your Tor Browser is out of date."),
646 'download_sha256_sig',
652 self.set_gui('task', _("Your Tor Browser is out of date."),
653 ['download_tarball_sig',
659 # for some reason the installed tbb is newer than the current version?
660 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
664 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
665 if latest_version >= '3.':
666 self.set_gui('task', _("Downloading and installing Tor Browser."),
668 'download_sha256_sig',
674 self.set_gui('task', _("Downloading and installing Tor Browser."),
675 ['download_tarball_sig',
681 # there are different GUIs that might appear, this sets which one we want
682 def set_gui(self, gui, message, tasks, autostart=True):
684 self.gui_message = message
685 self.gui_tasks = tasks
687 self.gui_autostart = autostart
689 # set all gtk variables to False
691 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
696 self.progressbar = False
697 self.button_box = False
698 self.start_button = False
699 self.exit_button = False
701 # build the application's UI
705 self.box = gtk.VBox(False, 20)
706 self.window.add(self.box)
708 if 'error' in self.gui:
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.button_box = gtk.HButtonBox()
717 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
718 self.box.pack_start(self.button_box, True, True, 0)
719 self.button_box.show()
721 if self.gui != 'error':
723 yes_image = gtk.Image()
724 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
725 self.yes_button = gtk.Button("Yes")
726 self.yes_button.set_image(yes_image)
727 if self.gui == 'error_try_stable':
728 self.yes_button.connect("clicked", self.try_stable, None)
729 elif self.gui == 'error_try_default_mirror':
730 self.yes_button.connect("clicked", self.try_default_mirror, None)
731 elif self.gui == 'error_try_tor':
732 self.yes_button.connect("clicked", self.try_tor, None)
733 self.button_box.add(self.yes_button)
734 self.yes_button.show()
737 exit_image = gtk.Image()
738 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
739 self.exit_button = gtk.Button("Exit")
740 self.exit_button.set_image(exit_image)
741 self.exit_button.connect("clicked", self.destroy, None)
742 self.button_box.add(self.exit_button)
743 self.exit_button.show()
745 elif self.gui == 'task':
747 self.label = gtk.Label( self.gui_message )
748 self.label.set_line_wrap(True)
749 self.box.pack_start(self.label, True, True, 0)
753 self.progressbar = gtk.ProgressBar(adjustment=None)
754 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
755 self.progressbar.set_pulse_step(0.01)
756 self.box.pack_start(self.progressbar, True, True, 0)
759 self.button_box = gtk.HButtonBox()
760 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
761 self.box.pack_start(self.button_box, True, True, 0)
762 self.button_box.show()
765 start_image = gtk.Image()
766 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
767 self.start_button = gtk.Button(_("Start"))
768 self.start_button.set_image(start_image)
769 self.start_button.connect("clicked", self.start, None)
770 self.button_box.add(self.start_button)
771 if not self.gui_autostart:
772 self.start_button.show()
775 exit_image = gtk.Image()
776 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
777 self.exit_button = gtk.Button(_("Exit"))
778 self.exit_button.set_image(exit_image)
779 self.exit_button.connect("clicked", self.destroy, None)
780 self.button_box.add(self.exit_button)
781 self.exit_button.show()
786 if self.gui_autostart:
789 # start button clicked, begin tasks
790 def start(self, widget, data=None):
791 # disable the start button
792 if self.start_button:
793 self.start_button.set_sensitive(False)
795 # start running tasks
798 # run the next task in the task list
802 if self.gui_task_i >= len(self.gui_tasks):
806 task = self.gui_tasks[self.gui_task_i]
808 # get ready for the next task
811 print _('Running task: {0}'.format(task))
812 if task == 'download_update_check':
813 print _('Downloading'), self.common.paths['update_check_url']
814 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
816 if task == 'attempt_update':
817 print _('Checking to see if update is needed')
818 self.attempt_update()
820 elif task == 'download_sha256':
821 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
822 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
824 elif task == 'download_sha256_sig':
825 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
826 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
828 elif task == 'download_tarball_sig':
829 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
830 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
832 elif task == 'download_tarball':
833 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
834 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
836 elif task == 'verify':
837 print _('Verifying signature')
840 elif task == 'extract':
841 print _('Extracting'), self.common.paths['tarball_filename']
845 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
848 elif task == 'start_over':
849 print _('Starting download over again')
852 def response_received(self, response):
853 class FileDownloader(Protocol):
854 def __init__(self, common, file, total, progress, done_cb):
858 self.progress = progress
859 self.all_done = done_cb
861 if response.code != 200:
864 if response.code == 404:
865 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
869 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?"))
871 if common.settings['mirror'] != common.default_mirror:
872 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']))
874 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
876 def dataReceived(self, bytes):
877 self.file.write(bytes)
878 self.so_far += len(bytes)
879 percent = float(self.so_far) / float(self.total)
880 self.progress.set_fraction(percent)
881 amount = float(self.so_far)
883 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
886 amount = amount / float(size)
889 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
891 def connectionLost(self, reason):
892 print _('Finished receiving body:'), reason.getErrorMessage()
893 self.all_done(reason)
895 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
896 response.deliverBody(dl)
898 def response_finished(self, msg):
899 if msg.check(ResponseDone):
900 self.file_download.close()
901 delattr(self, 'current_download_path')
907 print "FINISHED", msg
908 ## FIXME handle errors
910 def download_error(self, f):
911 print _("Download error:"), f.value, type(f.value)
913 if isinstance(f.value, TryStableException):
914 f.trap(TryStableException)
915 self.set_gui('error_try_stable', str(f.value), [], False)
917 elif isinstance(f.value, TryDefaultMirrorException):
918 f.trap(TryDefaultMirrorException)
919 self.set_gui('error_try_default_mirror', str(f.value), [], False)
921 elif isinstance(f.value, DownloadErrorException):
922 f.trap(DownloadErrorException)
923 self.set_gui('error', str(f.value), [], False)
925 elif isinstance(f.value, DNSLookupError):
926 f.trap(DNSLookupError)
927 if common.settings['mirror'] != common.default_mirror:
928 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)
930 self.set_gui('error', str(f.value), [], False)
932 elif isinstance(f.value, ResponseFailed):
933 for reason in f.value.reasons:
934 if isinstance(reason.value, OpenSSL.SSL.Error):
935 # TODO: add the ability to report attack by posting bug to trac.torproject.org
936 if not self.common.settings['update_over_tor']:
937 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)
939 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
942 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
946 def download(self, name, url, path):
947 # keep track of current download
948 self.current_download_path = path
950 # initialize the progress bar
951 mirror_url = url.format(self.common.settings['mirror'])
952 self.progressbar.set_fraction(0)
953 self.progressbar.set_text(_('Downloading {0}').format(name))
954 self.progressbar.show()
957 # default mirror gets certificate pinning, only for requests that use the mirror
958 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
959 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
961 agent = Agent(reactor)
963 # actually, agent needs to follow redirect
964 agent = RedirectAgent(agent)
967 d = agent.request('GET', mirror_url,
968 Headers({'User-Agent': ['torbrowser-launcher']}),
971 self.file_download = open(path, 'w')
972 d.addCallback(self.response_received).addErrback(self.download_error)
974 if not reactor.running:
977 def try_stable(self, widget, data=None):
978 # change preferred to stable and relaunch TBL
979 self.common.settings['preferred'] = 'stable'
980 self.common.save_settings()
981 p = subprocess.Popen([self.common.paths['tbl_bin']])
984 def try_default_mirror(self, widget, data=None):
985 # change preferred to stable and relaunch TBL
986 self.common.settings['mirror'] = self.common.default_mirror
987 self.common.save_settings()
988 p = subprocess.Popen([self.common.paths['tbl_bin']])
991 def try_tor(self, widget, data=None):
992 # set update_over_tor to true and relaunch TBL
993 self.common.settings['update_over_tor'] = True
994 self.common.save_settings()
995 p = subprocess.Popen([self.common.paths['tbl_bin']])
998 def attempt_update(self):
999 # load the update check file
1001 versions = json.load(open(self.common.paths['update_check_file']))
1002 latest_stable = None
1005 # filter linux versions
1008 for version in versions:
1009 if str(version).find('-Linux') != -1:
1010 if version.find('alpha') != -1 or version.find('beta') != -1:
1011 valid_alphas.append(str(version))
1013 valid_stables.append(str(version))
1015 if len(valid_alphas):
1016 latest_alpha = valid_alphas.pop()
1017 valid_stables.sort()
1018 if len(valid_stables):
1019 latest_stable = valid_stables.pop()
1021 if latest_stable or latest_alpha:
1023 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
1025 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
1026 self.common.settings['last_update_check_timestamp'] = int(time.time())
1027 self.common.settings['check_for_updates'] = False
1028 self.common.save_settings()
1029 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
1030 self.start_launcher()
1033 # failed to find the latest version
1034 self.set_gui('error', _("Error checking for updates."), [], False)
1037 # not a valid JSON object
1038 self.set_gui('error', _("Error checking for updates."), [], False)
1045 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1047 # initialize the progress bar
1048 self.progressbar.set_fraction(0)
1049 self.progressbar.set_text(_('Verifying Signature'))
1050 self.progressbar.show()
1053 if latest_version >= '3.':
1054 # after 3.x we check the sha256 file's sig, and also take the sha256 of the tarball and compare
1055 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1056 self.pulse_until_process_exits(p)
1057 if p.returncode == 0:
1058 # compare with sha256 of the tarball
1059 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1060 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1061 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1065 # before 3.x we just check the tarball sig
1066 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
1067 self.pulse_until_process_exits(p)
1068 if p.returncode == 0:
1074 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1075 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)
1079 if not reactor.running:
1083 # initialize the progress bar
1084 self.progressbar.set_fraction(0)
1085 self.progressbar.set_text(_('Installing'))
1086 self.progressbar.show()
1091 if self.common.paths['tarball_file'][-2:] == 'xz':
1092 # if tarball is .tar.xz
1093 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1094 tf = tarfile.open(fileobj=xz)
1095 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1098 # if tarball is .tar.gz
1099 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1100 tf = tarfile.open(self.common.paths['tarball_file'])
1101 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1107 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1112 # installation is finished, so save installed_version
1113 self.common.settings['installed_version'] = self.common.settings['latest_version']
1114 self.common.save_settings()
1118 def run(self, run_next_task = True):
1119 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1123 # make the progress bar pulse until process p (a Popen object) finishes
1124 def pulse_until_process_exits(self, p):
1125 while p.poll() == None:
1127 self.progressbar.pulse()
1130 # start over and download TBB again
1131 def start_over(self):
1132 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1133 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1138 def refresh_gtk(self):
1139 while gtk.events_pending():
1140 gtk.main_iteration(False)
1143 def delete_event(self, widget, event, data=None):
1145 def destroy(self, widget, data=None):
1146 if hasattr(self, 'file_download'):
1147 self.file_download.close()
1148 if hasattr(self, 'current_download_path'):
1149 os.remove(self.current_download_path)
1150 delattr(self, 'current_download_path')
1154 if __name__ == "__main__":
1155 tor_browser_launcher_version = '0.0.2'
1157 print _('Tor Browser Launcher')
1158 print _('By Micah Lee, licensed under GPLv3')
1159 print _('version {0}').format(tor_browser_launcher_version)
1160 print 'https://github.com/micahflee/torbrowser-launcher'
1162 common = TBLCommon(tor_browser_launcher_version)
1164 # is torbrowser-launcher already running?
1165 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1167 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1168 common.bring_window_to_front(tbl_pid)
1171 if '-settings' in sys.argv:
1173 app = TBLSettings(common)
1177 app = TBLLauncher(common)