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 sys.path.append('/usr/share/torbrowser-launcher/lib/')
35 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
37 from twisted.internet import gtk2reactor
39 from twisted.internet import reactor
45 import os, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil, hashlib, lzma
47 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
48 from twisted.web.http_headers import Headers
49 from twisted.internet.protocol import Protocol
50 from twisted.internet.ssl import ClientContextFactory
51 from twisted.internet.endpoints import TCP4ClientEndpoint
52 from twisted.internet.error import DNSLookupError
54 from txsocksx.client import SOCKS5ClientEndpoint
58 class TryStableException(Exception):
60 class TryDefaultMirrorException(Exception):
62 class DownloadErrorException(Exception):
65 class VerifyTorProjectCert(ClientContextFactory):
67 def __init__(self, torproject_pem):
68 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
70 def getContext(self, host, port):
71 ctx = ClientContextFactory.getContext(self)
72 ctx.set_verify_depth(0)
73 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
76 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
77 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
81 def __init__(self, tbl_version):
82 print _('Initializing Tor Browser Launcher')
83 self.tbl_version = tbl_version
86 self.available_versions = {
87 'stable': _('Tor Browser Bundle - stable'),
88 'alpha': _('Tor Browser Bundle - alpha')
90 self.default_mirror = 'https://www.torproject.org/dist/'
92 self.discover_arch_lang()
94 self.mkdir(self.paths['data_dir'])
97 self.mkdir(self.paths['download_dir'])
98 self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
101 # allow buttons to have icons
103 gtk_settings = gtk.settings_get_default()
104 gtk_settings.props.gtk_button_images = True
108 # discover the architecture and language
109 def discover_arch_lang(self):
110 # figure out the architecture
111 (sysname, nodename, release, version, machine) = os.uname()
112 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
114 # figure out the language
115 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
116 default_locale = locale.getdefaultlocale()[0]
117 if default_locale == None:
118 self.language = 'en-US'
120 self.language = default_locale.replace('_', '-')
121 if self.language not in available_languages:
122 self.language = self.language.split('-')[0]
123 if self.language not in available_languages:
124 for l in available_languages:
125 if l[0:2] == self.language:
127 # if language isn't available, default to english
128 if self.language not in available_languages:
129 self.language = 'en-US'
131 # build all relevant paths
132 def build_paths(self, tbb_version = None):
133 homedir = os.getenv('HOME')
135 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
136 if os.path.exists(homedir) == False:
138 os.mkdir(homedir, 0700)
140 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
141 if not os.access(homedir, os.W_OK):
142 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
144 tbb_data = '%s/.torbrowser' % homedir
147 if tbb_version >= '3.':
149 dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b')
150 if self.architecture == 'x86_64':
154 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
157 self.paths['tarball_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/'+tarball_filename
160 self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
161 self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
162 self.paths['sha256_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/sha256sums.txt'
163 self.paths['sha256_sig_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/sha256sums.txt.mp-asc'
166 tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
169 self.paths['tarball_url'] = '{0}torbrowser/linux/'+tarball_filename # {0} will be replaced with the mirror
172 self.paths['tarball_sig_file'] = tbb_data+'/download/'+tarball_filename+'.asc'
173 self.paths['tarball_sig_url'] = '{0}torbrowser/linux/'+tarball_filename+'.asc'
174 self.paths['tarball_sig_filename'] = tarball_filename+'.asc'
176 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
177 self.paths['tarball_filename'] = tarball_filename
181 'tbl_bin': '/usr/bin/torbrowser-launcher',
182 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
183 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
184 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
185 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
186 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
187 'mike_key': '/usr/share/torbrowser-launcher/mike.asc',
188 'mirrors_txt': '/usr/share/torbrowser-launcher/mirrors.txt',
189 'data_dir': tbb_data,
190 'download_dir': tbb_data+'/download',
191 'gnupg_homedir': tbb_data+'/gnupg_homedir',
192 'settings_file': tbb_data+'/settings',
193 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
194 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
197 'dir': tbb_data+'/tbb/stable/'+self.architecture,
198 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
199 'vidalia_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
200 'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
201 'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
204 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
205 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
206 'vidalia_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
207 'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
208 'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
214 def mkdir(self, path):
216 if os.path.exists(path) == False:
217 os.makedirs(path, 0700)
220 print _("Cannot create directory {0}").format(path)
222 if not os.access(path, os.W_OK):
223 print _("{0} is not writable").format(path)
227 # if gnupg_homedir isn't set up, set it up
228 def init_gnupg(self):
229 if not os.path.exists(self.paths['gnupg_homedir']):
230 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
231 self.mkdir(self.paths['gnupg_homedir'])
235 def import_keys(self):
236 print _('Importing keys')
237 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()
240 def load_mirrors(self):
242 for mirror in open(self.paths['mirrors_txt'], 'r').readlines():
243 self.mirrors.append(mirror.strip())
246 def load_settings(self):
248 'tbl_version': self.tbl_version,
249 'preferred': 'stable',
250 'installed_version': {
258 'update_over_tor': True,
259 'check_for_updates': False,
260 'last_update_check_timestamp': 0,
261 'mirror': self.default_mirror
264 if os.path.isfile(self.paths['settings_file']):
265 settings = pickle.load(open(self.paths['settings_file']))
267 # what version settings is this?
268 if not 'tbl_version' in settings:
269 settings['tbl_version'] = '0.0.1'
271 # sanity checks for current version
272 if settings['tbl_version'] == self.tbl_version:
274 if not 'preferred' in settings:
275 good_settings = False
276 if not 'installed_version' in settings:
277 good_settings = False
278 if not 'stable' in settings['installed_version']:
279 good_settings = False
280 if not 'alpha' in settings['installed_version']:
281 good_settings = False
282 if not 'latest_version' in settings:
283 good_settings = False
284 if not 'stable' in settings['latest_version']:
285 good_settings = False
286 if not 'alpha' in settings['latest_version']:
287 good_settings = False
288 if not 'update_over_tor' in settings:
289 good_settings = False
290 if not 'check_for_updates' in settings:
291 good_settings = False
292 if not 'last_update_check_timestamp' in settings:
293 good_settings = False
294 if not 'mirror' in settings:
295 good_settings = False
298 self.settings = settings
300 self.settings = default_settings
302 # settings migrations
303 if settings['tbl_version'] == '0.0.1':
304 self.settings = default_settings
305 self.settings['installed_version']['alpha'] = settings['installed_version']
306 self.settings['tbl_version'] = '0.0.2'
310 self.mkdir(self.paths['tbb']['alpha']['dir'])
311 # todo: move already-installed TBB alpha to new location
312 if os.path.exists(self.paths['data_dir']+'/tbb/x86_64'):
314 if os.path.exists(self.paths['data_dir']+'/tbb/i686'):
318 self.settings = default_settings
322 def save_settings(self):
323 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
326 # get the process id of a program
327 def get_pid(self, bin_path, python = False):
330 for p in psutil.process_iter():
332 if p.pid != os.getpid():
335 if len(p.cmdline) > 1:
336 if 'python' in p.cmdline[0]:
339 if len(p.cmdline) > 0:
350 # bring program's x window to front
351 def bring_window_to_front(self, pid):
352 # figure out the window id
354 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
355 for line in p.stdout.readlines():
356 line_split = line.split()
357 cur_win_id = line_split[0]
358 cur_win_pid = int(line_split[2])
359 if cur_win_pid == pid:
364 subprocess.call(['wmctrl', '-i', '-a', win_id])
367 def __init__(self, common):
368 print _('Starting settings dialog')
372 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
373 self.window.set_title(_("Tor Browser Launcher Settings"))
374 self.window.set_icon_from_file(self.common.paths['icon_file'])
375 self.window.set_position(gtk.WIN_POS_CENTER)
376 self.window.set_border_width(10)
377 self.window.connect("delete_event", self.delete_event)
378 self.window.connect("destroy", self.destroy)
380 # build the rest of the UI
381 self.box = gtk.VBox(False, 10)
382 self.window.add(self.box)
385 self.hbox = gtk.HBox(False, 10)
386 self.box.pack_start(self.hbox, True, True, 0)
389 self.settings_box = gtk.VBox(False, 10)
390 self.hbox.pack_start(self.settings_box, True, True, 0)
391 self.settings_box.show()
393 self.labels_box = gtk.VBox(False, 10)
394 self.hbox.pack_start(self.labels_box, True, True, 0)
395 self.labels_box.show()
398 self.preferred_box = gtk.HBox(False, 10)
399 self.settings_box.pack_start(self.preferred_box, True, True, 0)
400 self.preferred_box.show()
402 self.preferred_label = gtk.Label(_('I prefer'))
403 self.preferred_label.set_line_wrap(True)
404 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
405 self.preferred_label.show()
407 self.preferred_options = []
408 for i in self.common.available_versions:
409 self.preferred_options.append(self.common.available_versions[i])
410 self.preferred_options.sort()
412 self.preferred = gtk.combo_box_new_text()
413 for option in self.preferred_options:
414 self.preferred.append_text(option)
415 if self.common.settings['preferred'] in self.common.available_versions:
416 self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
418 self.preferred.set_active(0)
419 self.preferred_box.pack_start(self.preferred, True, True, 0)
420 self.preferred.show()
423 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
425 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
426 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
427 if self.common.settings['update_over_tor']:
428 self.tor_update_checkbox.set_active(True)
430 self.tor_update_checkbox.set_active(False)
431 self.tor_update_checkbox.show()
435 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
436 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
437 if self.common.settings['check_for_updates']:
438 self.update_checkbox.set_active(True)
440 self.update_checkbox.set_active(False)
441 self.update_checkbox.show()
444 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
445 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
447 self.label1 = gtk.Label(_('Not installed'))
448 self.label1.set_line_wrap(True)
449 self.labels_box.pack_start(self.label1, True, True, 0)
452 if(self.common.settings['last_update_check_timestamp'] > 0):
453 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']))))
455 self.label1 = gtk.Label(_('Never checked for updates'))
456 self.label1.set_line_wrap(True)
457 self.labels_box.pack_start(self.label1, True, True, 0)
461 self.mirrors_box = gtk.HBox(False, 10)
462 self.box.pack_start(self.mirrors_box, True, True, 0)
463 self.mirrors_box.show()
465 self.mirrors_label = gtk.Label(_('Mirror'))
466 self.mirrors_label.set_line_wrap(True)
467 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
468 self.mirrors_label.show()
470 self.mirrors = gtk.combo_box_new_text()
471 for mirror in self.common.mirrors:
472 self.mirrors.append_text(mirror)
473 if self.common.settings['mirror'] in self.common.mirrors:
474 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
476 self.preferred.set_active(0)
477 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
481 self.button_box = gtk.HButtonBox()
482 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
483 self.box.pack_start(self.button_box, True, True, 0)
484 self.button_box.show()
486 # save and launch button
487 save_launch_image = gtk.Image()
488 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
489 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
490 self.save_launch_button.set_image(save_launch_image)
491 self.save_launch_button.connect("clicked", self.save_launch, None)
492 self.button_box.add(self.save_launch_button)
493 self.save_launch_button.show()
495 # save and exit button
496 save_exit_image = gtk.Image()
497 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
498 self.save_exit_button = gtk.Button(_("Save & Exit"))
499 self.save_exit_button.set_image(save_exit_image)
500 self.save_exit_button.connect("clicked", self.save_exit, None)
501 self.button_box.add(self.save_exit_button)
502 self.save_exit_button.show()
505 cancel_image = gtk.Image()
506 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
507 self.cancel_button = gtk.Button(_("Cancel"))
508 self.cancel_button.set_image(cancel_image)
509 self.cancel_button.connect("clicked", self.destroy, None)
510 self.button_box.add(self.cancel_button)
511 self.cancel_button.show()
520 def save_launch(self, widget, data=None):
522 p = subprocess.Popen([self.common.paths['tbl_bin']])
526 def save_exit(self, widget, data=None):
532 # figure out the selected preferred option
534 selected = self.preferred_options[self.preferred.get_active()]
535 for i in self.common.available_versions:
536 if self.common.available_versions[i] == selected:
539 self.common.settings['preferred'] = preferred
542 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
543 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
545 # figure out the selected mirror
546 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
549 self.common.save_settings()
552 def delete_event(self, widget, event, data=None):
554 def destroy(self, widget, data=None):
559 def __init__(self, common):
560 print _('Starting launcher dialog')
564 self.set_gui(None, '', [])
565 self.launch_gui = True
566 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
568 # is vidalia already running and we just need to open a new firefox?
569 if self.common.settings['installed_version']:
570 vidalia_pid = self.common.get_pid('./App/vidalia')
571 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
573 if vidalia_pid and not firefox_pid:
574 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
575 self.common.bring_window_to_front(vidalia_pid)
576 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']])
578 elif vidalia_pid and firefox_pid:
579 print _('Vidalia and Firefox are already open, bringing them to focus')
581 # bring firefox to front, then vidalia
582 self.common.bring_window_to_front(firefox_pid)
583 self.common.bring_window_to_front(vidalia_pid)
587 check_for_updates = False
588 if self.common.settings['check_for_updates']:
589 check_for_updates = True
591 if not check_for_updates:
592 # how long was it since the last update check?
593 # 86400 seconds = 24 hours
594 current_timestamp = int(time.time())
595 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
596 check_for_updates = True
598 if check_for_updates:
600 print 'Checking for update'
601 self.set_gui('task', _("Checking for Tor Browser update."),
602 ['download_update_check',
605 # no need to check for update
606 print _('Checked for update within 24 hours, skipping')
607 self.start_launcher()
611 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
612 self.window.set_title(_("Tor Browser"))
613 self.window.set_icon_from_file(self.common.paths['icon_file'])
614 self.window.set_position(gtk.WIN_POS_CENTER)
615 self.window.set_border_width(10)
616 self.window.connect("delete_event", self.delete_event)
617 self.window.connect("destroy", self.destroy)
619 # build the rest of the UI
622 # download or run TBB
623 def start_launcher(self):
624 # is TBB already installed?
625 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
626 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
628 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
629 if os.path.isfile(start) and os.access(start, os.X_OK):
630 if installed_version == latest_version:
631 print _('Latest version of TBB is installed, launching')
632 # current version of tbb is installed, launch it
634 self.launch_gui = False
635 elif installed_version < latest_version:
636 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
637 # there is a tbb upgrade available
638 if latest_version >= '3.':
639 self.set_gui('task', _("Your Tor Browser is out of date."),
641 'download_sha256_sig',
647 self.set_gui('task', _("Your Tor Browser is out of date."),
648 ['download_tarball_sig',
654 # for some reason the installed tbb is newer than the current version?
655 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
659 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
660 if latest_version >= '3.':
661 self.set_gui('task', _("Downloading and installing Tor Browser."),
663 'download_sha256_sig',
669 self.set_gui('task', _("Downloading and installing Tor Browser."),
670 ['download_tarball_sig',
676 # there are different GUIs that might appear, this sets which one we want
677 def set_gui(self, gui, message, tasks, autostart=True):
679 self.gui_message = message
680 self.gui_tasks = tasks
682 self.gui_autostart = autostart
684 # set all gtk variables to False
686 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
691 self.progressbar = False
692 self.button_box = False
693 self.start_button = False
694 self.exit_button = False
696 # build the application's UI
700 self.box = gtk.VBox(False, 20)
701 self.window.add(self.box)
703 if 'error' in self.gui:
705 self.label = gtk.Label( self.gui_message )
706 self.label.set_line_wrap(True)
707 self.box.pack_start(self.label, True, True, 0)
711 self.button_box = gtk.HButtonBox()
712 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
713 self.box.pack_start(self.button_box, True, True, 0)
714 self.button_box.show()
716 if self.gui != 'error':
718 yes_image = gtk.Image()
719 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
720 self.yes_button = gtk.Button("Yes")
721 self.yes_button.set_image(yes_image)
722 if self.gui == 'error_try_stable':
723 self.yes_button.connect("clicked", self.try_stable, None)
724 elif self.gui == 'error_try_default_mirror':
725 self.yes_button.connect("clicked", self.try_default_mirror, None)
726 elif self.gui == 'error_try_tor':
727 self.yes_button.connect("clicked", self.try_tor, None)
728 self.button_box.add(self.yes_button)
729 self.yes_button.show()
732 exit_image = gtk.Image()
733 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
734 self.exit_button = gtk.Button("Exit")
735 self.exit_button.set_image(exit_image)
736 self.exit_button.connect("clicked", self.destroy, None)
737 self.button_box.add(self.exit_button)
738 self.exit_button.show()
740 elif self.gui == 'task':
742 self.label = gtk.Label( self.gui_message )
743 self.label.set_line_wrap(True)
744 self.box.pack_start(self.label, True, True, 0)
748 self.progressbar = gtk.ProgressBar(adjustment=None)
749 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
750 self.progressbar.set_pulse_step(0.01)
751 self.box.pack_start(self.progressbar, True, True, 0)
754 self.button_box = gtk.HButtonBox()
755 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
756 self.box.pack_start(self.button_box, True, True, 0)
757 self.button_box.show()
760 start_image = gtk.Image()
761 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
762 self.start_button = gtk.Button(_("Start"))
763 self.start_button.set_image(start_image)
764 self.start_button.connect("clicked", self.start, None)
765 self.button_box.add(self.start_button)
766 if not self.gui_autostart:
767 self.start_button.show()
770 exit_image = gtk.Image()
771 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
772 self.exit_button = gtk.Button(_("Exit"))
773 self.exit_button.set_image(exit_image)
774 self.exit_button.connect("clicked", self.destroy, None)
775 self.button_box.add(self.exit_button)
776 self.exit_button.show()
781 if self.gui_autostart:
784 # start button clicked, begin tasks
785 def start(self, widget, data=None):
786 # disable the start button
787 if self.start_button:
788 self.start_button.set_sensitive(False)
790 # start running tasks
793 # run the next task in the task list
797 if self.gui_task_i >= len(self.gui_tasks):
801 task = self.gui_tasks[self.gui_task_i]
803 # get ready for the next task
806 print _('Running task: {0}'.format(task))
807 if task == 'download_update_check':
808 print _('Downloading'), self.common.paths['update_check_url']
809 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
811 if task == 'attempt_update':
812 print _('Checking to see if update is needed')
813 self.attempt_update()
815 elif task == 'download_sha256':
816 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
817 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
819 elif task == 'download_sha256_sig':
820 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
821 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
823 elif task == 'download_tarball_sig':
824 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
825 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
827 elif task == 'download_tarball':
828 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
829 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
831 elif task == 'verify':
832 print _('Verifying signature')
835 elif task == 'extract':
836 print _('Extracting'), self.common.paths['tarball_filename']
840 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
843 elif task == 'start_over':
844 print _('Starting download over again')
847 def response_received(self, response):
848 class FileDownloader(Protocol):
849 def __init__(self, common, file, total, progress, done_cb):
853 self.progress = progress
854 self.all_done = done_cb
856 if response.code != 200:
859 if response.code == 404:
860 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
864 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?"))
866 if common.settings['mirror'] != common.default_mirror:
867 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']))
869 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
871 def dataReceived(self, bytes):
872 self.file.write(bytes)
873 self.so_far += len(bytes)
874 percent = float(self.so_far) / float(self.total)
875 self.progress.set_fraction(percent)
876 amount = float(self.so_far)
878 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
881 amount = amount / float(size)
884 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
886 def connectionLost(self, reason):
887 print _('Finished receiving body:'), reason.getErrorMessage()
888 self.all_done(reason)
890 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
891 response.deliverBody(dl)
893 def response_finished(self, msg):
894 if msg.check(ResponseDone):
895 self.file_download.close()
896 delattr(self, 'current_download_path')
902 print "FINISHED", msg
903 ## FIXME handle errors
905 def download_error(self, f):
906 print _("Download error:"), f.value, type(f.value)
908 if isinstance(f.value, TryStableException):
909 f.trap(TryStableException)
910 self.set_gui('error_try_stable', str(f.value), [], False)
912 elif isinstance(f.value, TryDefaultMirrorException):
913 f.trap(TryDefaultMirrorException)
914 self.set_gui('error_try_default_mirror', str(f.value), [], False)
916 elif isinstance(f.value, DownloadErrorException):
917 f.trap(DownloadErrorException)
918 self.set_gui('error', str(f.value), [], False)
920 elif isinstance(f.value, DNSLookupError):
921 f.trap(DNSLookupError)
922 if common.settings['mirror'] != common.default_mirror:
923 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)
925 self.set_gui('error', str(f.value), [], False)
927 elif isinstance(f.value, ResponseFailed):
928 for reason in f.value.reasons:
929 if isinstance(reason.value, OpenSSL.SSL.Error):
930 # TODO: add the ability to report attack by posting bug to trac.torproject.org
931 if not self.common.settings['update_over_tor']:
932 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)
934 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
937 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
941 def download(self, name, url, path):
942 # keep track of current download
943 self.current_download_path = path
945 # initialize the progress bar
946 mirror_url = url.format(self.common.settings['mirror'])
947 self.progressbar.set_fraction(0)
948 self.progressbar.set_text(_('Downloading {0}').format(name))
949 self.progressbar.show()
952 # default mirror gets certificate pinning, only for requests that use the mirror
953 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
954 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
956 agent = Agent(reactor)
958 # actually, agent needs to follow redirect
959 agent = RedirectAgent(agent)
962 d = agent.request('GET', mirror_url,
963 Headers({'User-Agent': ['torbrowser-launcher']}),
966 self.file_download = open(path, 'w')
967 d.addCallback(self.response_received).addErrback(self.download_error)
969 if not reactor.running:
972 def try_stable(self, widget, data=None):
973 # change preferred to stable and relaunch TBL
974 self.common.settings['preferred'] = 'stable'
975 self.common.save_settings()
976 p = subprocess.Popen([self.common.paths['tbl_bin']])
979 def try_default_mirror(self, widget, data=None):
980 # change preferred to stable and relaunch TBL
981 self.common.settings['mirror'] = self.common.default_mirror
982 self.common.save_settings()
983 p = subprocess.Popen([self.common.paths['tbl_bin']])
986 def try_tor(self, widget, data=None):
987 # set update_over_tor to true and relaunch TBL
988 self.common.settings['update_over_tor'] = True
989 self.common.save_settings()
990 p = subprocess.Popen([self.common.paths['tbl_bin']])
993 def attempt_update(self):
994 # load the update check file
996 versions = json.load(open(self.common.paths['update_check_file']))
1000 # filter linux versions
1003 for version in versions:
1004 if str(version).find('-Linux') != -1:
1005 if version.find('alpha') != -1 or version.find('beta') != -1:
1006 valid_alphas.append(str(version))
1008 valid_stables.append(str(version))
1010 if len(valid_alphas):
1011 latest_alpha = valid_alphas.pop()
1012 valid_stables.sort()
1013 if len(valid_stables):
1014 latest_stable = valid_stables.pop()
1016 if latest_stable or latest_alpha:
1018 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
1020 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
1021 self.common.settings['last_update_check_timestamp'] = int(time.time())
1022 self.common.settings['check_for_updates'] = False
1023 self.common.save_settings()
1024 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
1025 self.start_launcher()
1028 # failed to find the latest version
1029 self.set_gui('error', _("Error checking for updates."), [], False)
1032 # not a valid JSON object
1033 self.set_gui('error', _("Error checking for updates."), [], False)
1040 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1042 # initialize the progress bar
1043 self.progressbar.set_fraction(0)
1044 self.progressbar.set_text(_('Verifying Signature'))
1045 self.progressbar.show()
1048 if latest_version >= '3.':
1049 # after 3.x we check the sha256 file's sig, and also take the sha256 of the tarball and compare
1050 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1051 self.pulse_until_process_exits(p)
1052 if p.returncode == 0:
1053 # compare with sha256 of the tarball
1054 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1055 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1056 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1060 # before 3.x we just check the tarball sig
1061 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
1062 self.pulse_until_process_exits(p)
1063 if p.returncode == 0:
1069 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1070 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)
1074 if not reactor.running:
1078 # initialize the progress bar
1079 self.progressbar.set_fraction(0)
1080 self.progressbar.set_text(_('Installing'))
1081 self.progressbar.show()
1086 if self.common.paths['tarball_file'][-2:] == 'xz':
1087 # if tarball is .tar.xz
1088 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1089 tf = tarfile.open(fileobj=xz)
1090 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1093 # if tarball is .tar.gz
1094 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1095 tf = tarfile.open(self.common.paths['tarball_file'])
1096 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1102 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1107 # installation is finished, so save installed_version
1108 self.common.settings['installed_version'] = self.common.settings['latest_version']
1109 self.common.save_settings()
1113 def run(self, run_next_task = True):
1114 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1118 # make the progress bar pulse until process p (a Popen object) finishes
1119 def pulse_until_process_exits(self, p):
1120 while p.poll() == None:
1122 self.progressbar.pulse()
1125 # start over and download TBB again
1126 def start_over(self):
1127 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1128 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1133 def refresh_gtk(self):
1134 while gtk.events_pending():
1135 gtk.main_iteration(False)
1138 def delete_event(self, widget, event, data=None):
1140 def destroy(self, widget, data=None):
1141 if hasattr(self, 'file_download'):
1142 self.file_download.close()
1143 if hasattr(self, 'current_download_path'):
1144 os.remove(self.current_download_path)
1145 delattr(self, 'current_download_path')
1149 if __name__ == "__main__":
1150 tor_browser_launcher_version = '0.0.2'
1152 print _('Tor Browser Launcher')
1153 print _('By Micah Lee, licensed under GPLv3')
1154 print _('version {0}').format(tor_browser_launcher_version)
1155 print 'https://github.com/micahflee/torbrowser-launcher'
1157 common = TBLCommon(tor_browser_launcher_version)
1159 # is torbrowser-launcher already running?
1160 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1162 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1163 common.bring_window_to_front(tbl_pid)
1166 if '-settings' in sys.argv:
1168 app = TBLSettings(common)
1172 app = TBLLauncher(common)