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.asc-mp'
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-2013-09.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',
202 'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/sources/versions',
205 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
206 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
207 'vidalia_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
208 'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
209 'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
210 'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/sources/versions',
216 def mkdir(self, path):
218 if os.path.exists(path) == False:
219 os.makedirs(path, 0700)
222 print _("Cannot create directory {0}").format(path)
224 if not os.access(path, os.W_OK):
225 print _("{0} is not writable").format(path)
229 # if gnupg_homedir isn't set up, set it up
230 def init_gnupg(self):
231 if not os.path.exists(self.paths['gnupg_homedir']):
232 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
233 self.mkdir(self.paths['gnupg_homedir'])
237 def import_keys(self):
238 print _('Importing keys')
239 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()
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']))
270 # settings migrations
271 if settings['tbl_version'] == '0.0.1':
272 print '0.0.1 migration'
273 self.settings = default_settings
274 self.settings['installed_version']['alpha'] = settings['installed_version']
278 self.mkdir(self.paths['tbb']['alpha']['dir'])
280 # make sure settings file is up-to-date
281 for setting in default_settings:
282 if setting not in settings:
283 settings[setting] = default_settings[setting]
286 # make sure the version is current
287 if settings['tbl_version'] != self.tbl_version:
288 settings['tbl_version'] = self.tbl_version
291 self.settings = settings
296 self.settings = default_settings
300 def save_settings(self):
301 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
304 # get the process id of a program
305 def get_pid(self, bin_path, python = False):
308 for p in psutil.process_iter():
310 if p.pid != os.getpid():
313 if len(p.cmdline) > 1:
314 if 'python' in p.cmdline[0]:
317 if len(p.cmdline) > 0:
328 # bring program's x window to front
329 def bring_window_to_front(self, pid):
330 # figure out the window id
332 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
333 for line in p.stdout.readlines():
334 line_split = line.split()
335 cur_win_id = line_split[0]
336 cur_win_pid = int(line_split[2])
337 if cur_win_pid == pid:
342 subprocess.call(['wmctrl', '-i', '-a', win_id])
345 def __init__(self, common):
346 print _('Starting settings dialog')
350 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
351 self.window.set_title(_("Tor Browser Launcher Settings"))
352 self.window.set_icon_from_file(self.common.paths['icon_file'])
353 self.window.set_position(gtk.WIN_POS_CENTER)
354 self.window.set_border_width(10)
355 self.window.connect("delete_event", self.delete_event)
356 self.window.connect("destroy", self.destroy)
358 # build the rest of the UI
359 self.box = gtk.VBox(False, 10)
360 self.window.add(self.box)
363 self.hbox = gtk.HBox(False, 10)
364 self.box.pack_start(self.hbox, True, True, 0)
367 self.settings_box = gtk.VBox(False, 10)
368 self.hbox.pack_start(self.settings_box, True, True, 0)
369 self.settings_box.show()
371 self.labels_box = gtk.VBox(False, 10)
372 self.hbox.pack_start(self.labels_box, True, True, 0)
373 self.labels_box.show()
376 self.preferred_box = gtk.HBox(False, 10)
377 self.settings_box.pack_start(self.preferred_box, True, True, 0)
378 self.preferred_box.show()
380 self.preferred_label = gtk.Label(_('I prefer'))
381 self.preferred_label.set_line_wrap(True)
382 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
383 self.preferred_label.show()
385 self.preferred_options = []
386 for i in self.common.available_versions:
387 self.preferred_options.append(self.common.available_versions[i])
388 self.preferred_options.sort()
390 self.preferred = gtk.combo_box_new_text()
391 for option in self.preferred_options:
392 self.preferred.append_text(option)
393 if self.common.settings['preferred'] in self.common.available_versions:
394 self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
396 self.preferred.set_active(0)
397 self.preferred_box.pack_start(self.preferred, True, True, 0)
398 self.preferred.show()
401 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
403 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
404 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
405 if self.common.settings['update_over_tor']:
406 self.tor_update_checkbox.set_active(True)
408 self.tor_update_checkbox.set_active(False)
409 self.tor_update_checkbox.show()
413 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
414 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
415 if self.common.settings['check_for_updates']:
416 self.update_checkbox.set_active(True)
418 self.update_checkbox.set_active(False)
419 self.update_checkbox.show()
422 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
423 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
425 self.label1 = gtk.Label(_('Not installed'))
426 self.label1.set_line_wrap(True)
427 self.labels_box.pack_start(self.label1, True, True, 0)
430 if(self.common.settings['last_update_check_timestamp'] > 0):
431 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']))))
433 self.label1 = gtk.Label(_('Never checked for updates'))
434 self.label1.set_line_wrap(True)
435 self.labels_box.pack_start(self.label1, True, True, 0)
439 self.mirrors_box = gtk.HBox(False, 10)
440 self.box.pack_start(self.mirrors_box, True, True, 0)
441 self.mirrors_box.show()
443 self.mirrors_label = gtk.Label(_('Mirror'))
444 self.mirrors_label.set_line_wrap(True)
445 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
446 self.mirrors_label.show()
448 self.mirrors = gtk.combo_box_new_text()
449 for mirror in self.common.mirrors:
450 self.mirrors.append_text(mirror)
451 if self.common.settings['mirror'] in self.common.mirrors:
452 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
454 self.preferred.set_active(0)
455 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
459 self.button_box = gtk.HButtonBox()
460 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
461 self.box.pack_start(self.button_box, True, True, 0)
462 self.button_box.show()
464 # save and launch button
465 save_launch_image = gtk.Image()
466 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
467 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
468 self.save_launch_button.set_image(save_launch_image)
469 self.save_launch_button.connect("clicked", self.save_launch, None)
470 self.button_box.add(self.save_launch_button)
471 self.save_launch_button.show()
473 # save and exit button
474 save_exit_image = gtk.Image()
475 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
476 self.save_exit_button = gtk.Button(_("Save & Exit"))
477 self.save_exit_button.set_image(save_exit_image)
478 self.save_exit_button.connect("clicked", self.save_exit, None)
479 self.button_box.add(self.save_exit_button)
480 self.save_exit_button.show()
483 cancel_image = gtk.Image()
484 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
485 self.cancel_button = gtk.Button(_("Cancel"))
486 self.cancel_button.set_image(cancel_image)
487 self.cancel_button.connect("clicked", self.destroy, None)
488 self.button_box.add(self.cancel_button)
489 self.cancel_button.show()
498 def save_launch(self, widget, data=None):
500 p = subprocess.Popen([self.common.paths['tbl_bin']])
504 def save_exit(self, widget, data=None):
510 # figure out the selected preferred option
512 selected = self.preferred_options[self.preferred.get_active()]
513 for i in self.common.available_versions:
514 if self.common.available_versions[i] == selected:
517 self.common.settings['preferred'] = preferred
520 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
521 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
523 # figure out the selected mirror
524 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
527 self.common.save_settings()
530 def delete_event(self, widget, event, data=None):
532 def destroy(self, widget, data=None):
537 def __init__(self, common):
538 print _('Starting launcher dialog')
542 self.set_gui(None, '', [])
543 self.launch_gui = True
544 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
546 # is vidalia already running and we just need to open a new firefox?
547 if self.common.settings['installed_version']:
548 vidalia_pid = self.common.get_pid('./App/vidalia')
549 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
551 if vidalia_pid and not firefox_pid:
552 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
553 self.common.bring_window_to_front(vidalia_pid)
554 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']])
556 elif vidalia_pid and firefox_pid:
557 print _('Vidalia and Firefox are already open, bringing them to focus')
559 # bring firefox to front, then vidalia
560 self.common.bring_window_to_front(firefox_pid)
561 self.common.bring_window_to_front(vidalia_pid)
565 check_for_updates = False
566 if self.common.settings['check_for_updates']:
567 check_for_updates = True
569 if not check_for_updates:
570 # how long was it since the last update check?
571 # 86400 seconds = 24 hours
572 current_timestamp = int(time.time())
573 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
574 check_for_updates = True
576 if check_for_updates:
578 print 'Checking for update'
579 self.set_gui('task', _("Checking for Tor Browser update."),
580 ['download_update_check',
583 # no need to check for update
584 print _('Checked for update within 24 hours, skipping')
585 self.start_launcher()
589 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
590 self.window.set_title(_("Tor Browser"))
591 self.window.set_icon_from_file(self.common.paths['icon_file'])
592 self.window.set_position(gtk.WIN_POS_CENTER)
593 self.window.set_border_width(10)
594 self.window.connect("delete_event", self.delete_event)
595 self.window.connect("destroy", self.destroy)
597 # build the rest of the UI
600 # download or run TBB
601 def start_launcher(self):
602 # is TBB already installed?
603 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
604 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
606 # verify installed version for newer versions of TBB (#58)
607 if installed_version >= '3.0':
608 versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
609 if os.path.exists(versions_filename):
610 for line in open(versions_filename):
611 if 'TORBROWSER_VERSION' in line:
612 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
614 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
615 if os.path.isfile(start) and os.access(start, os.X_OK):
616 if installed_version == latest_version:
617 print _('Latest version of TBB is installed, launching')
618 # current version of tbb is installed, launch it
620 self.launch_gui = False
621 elif installed_version < latest_version:
622 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
623 # there is a tbb upgrade available
624 if latest_version >= '3.':
625 self.set_gui('task', _("Your Tor Browser is out of date."),
627 'download_sha256_sig',
633 self.set_gui('task', _("Your Tor Browser is out of date."),
634 ['download_tarball_sig',
640 # for some reason the installed tbb is newer than the current version?
641 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
645 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
646 if latest_version >= '3.':
647 self.set_gui('task', _("Downloading and installing Tor Browser."),
649 'download_sha256_sig',
655 self.set_gui('task', _("Downloading and installing Tor Browser."),
656 ['download_tarball_sig',
662 # there are different GUIs that might appear, this sets which one we want
663 def set_gui(self, gui, message, tasks, autostart=True):
665 self.gui_message = message
666 self.gui_tasks = tasks
668 self.gui_autostart = autostart
670 # set all gtk variables to False
672 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
677 self.progressbar = False
678 self.button_box = False
679 self.start_button = False
680 self.exit_button = False
682 # build the application's UI
686 self.box = gtk.VBox(False, 20)
687 self.window.add(self.box)
689 if 'error' in self.gui:
691 self.label = gtk.Label( self.gui_message )
692 self.label.set_line_wrap(True)
693 self.box.pack_start(self.label, True, True, 0)
697 self.button_box = gtk.HButtonBox()
698 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
699 self.box.pack_start(self.button_box, True, True, 0)
700 self.button_box.show()
702 if self.gui != 'error':
704 yes_image = gtk.Image()
705 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
706 self.yes_button = gtk.Button("Yes")
707 self.yes_button.set_image(yes_image)
708 if self.gui == 'error_try_stable':
709 self.yes_button.connect("clicked", self.try_stable, None)
710 elif self.gui == 'error_try_default_mirror':
711 self.yes_button.connect("clicked", self.try_default_mirror, None)
712 elif self.gui == 'error_try_tor':
713 self.yes_button.connect("clicked", self.try_tor, None)
714 self.button_box.add(self.yes_button)
715 self.yes_button.show()
718 exit_image = gtk.Image()
719 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
720 self.exit_button = gtk.Button("Exit")
721 self.exit_button.set_image(exit_image)
722 self.exit_button.connect("clicked", self.destroy, None)
723 self.button_box.add(self.exit_button)
724 self.exit_button.show()
726 elif self.gui == 'task':
728 self.label = gtk.Label( self.gui_message )
729 self.label.set_line_wrap(True)
730 self.box.pack_start(self.label, True, True, 0)
734 self.progressbar = gtk.ProgressBar(adjustment=None)
735 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
736 self.progressbar.set_pulse_step(0.01)
737 self.box.pack_start(self.progressbar, True, True, 0)
740 self.button_box = gtk.HButtonBox()
741 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
742 self.box.pack_start(self.button_box, True, True, 0)
743 self.button_box.show()
746 start_image = gtk.Image()
747 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
748 self.start_button = gtk.Button(_("Start"))
749 self.start_button.set_image(start_image)
750 self.start_button.connect("clicked", self.start, None)
751 self.button_box.add(self.start_button)
752 if not self.gui_autostart:
753 self.start_button.show()
756 exit_image = gtk.Image()
757 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
758 self.exit_button = gtk.Button(_("Exit"))
759 self.exit_button.set_image(exit_image)
760 self.exit_button.connect("clicked", self.destroy, None)
761 self.button_box.add(self.exit_button)
762 self.exit_button.show()
767 if self.gui_autostart:
770 # start button clicked, begin tasks
771 def start(self, widget, data=None):
772 # disable the start button
773 if self.start_button:
774 self.start_button.set_sensitive(False)
776 # start running tasks
779 # run the next task in the task list
783 if self.gui_task_i >= len(self.gui_tasks):
787 task = self.gui_tasks[self.gui_task_i]
789 # get ready for the next task
792 print _('Running task: {0}'.format(task))
793 if task == 'download_update_check':
794 print _('Downloading'), self.common.paths['update_check_url']
795 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
797 if task == 'attempt_update':
798 print _('Checking to see if update is needed')
799 self.attempt_update()
801 elif task == 'download_sha256':
802 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
803 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
805 elif task == 'download_sha256_sig':
806 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
807 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
809 elif task == 'download_tarball_sig':
810 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
811 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
813 elif task == 'download_tarball':
814 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
815 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
817 elif task == 'verify':
818 print _('Verifying signature')
821 elif task == 'extract':
822 print _('Extracting'), self.common.paths['tarball_filename']
826 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
829 elif task == 'start_over':
830 print _('Starting download over again')
833 def response_received(self, response):
834 class FileDownloader(Protocol):
835 def __init__(self, common, file, total, progress, done_cb):
839 self.progress = progress
840 self.all_done = done_cb
842 if response.code != 200:
845 if response.code == 404:
846 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
850 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?"))
852 if common.settings['mirror'] != common.default_mirror:
853 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']))
855 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
857 def dataReceived(self, bytes):
858 self.file.write(bytes)
859 self.so_far += len(bytes)
860 percent = float(self.so_far) / float(self.total)
861 self.progress.set_fraction(percent)
862 amount = float(self.so_far)
864 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
867 amount = amount / float(size)
870 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
872 def connectionLost(self, reason):
873 print _('Finished receiving body:'), reason.getErrorMessage()
874 self.all_done(reason)
876 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
877 response.deliverBody(dl)
879 def response_finished(self, msg):
880 if msg.check(ResponseDone):
881 self.file_download.close()
882 delattr(self, 'current_download_path')
888 print "FINISHED", msg
889 ## FIXME handle errors
891 def download_error(self, f):
892 print _("Download error:"), f.value, type(f.value)
894 if isinstance(f.value, TryStableException):
895 f.trap(TryStableException)
896 self.set_gui('error_try_stable', str(f.value), [], False)
898 elif isinstance(f.value, TryDefaultMirrorException):
899 f.trap(TryDefaultMirrorException)
900 self.set_gui('error_try_default_mirror', str(f.value), [], False)
902 elif isinstance(f.value, DownloadErrorException):
903 f.trap(DownloadErrorException)
904 self.set_gui('error', str(f.value), [], False)
906 elif isinstance(f.value, DNSLookupError):
907 f.trap(DNSLookupError)
908 if common.settings['mirror'] != common.default_mirror:
909 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)
911 self.set_gui('error', str(f.value), [], False)
913 elif isinstance(f.value, ResponseFailed):
914 for reason in f.value.reasons:
915 if isinstance(reason.value, OpenSSL.SSL.Error):
916 # TODO: add the ability to report attack by posting bug to trac.torproject.org
917 if not self.common.settings['update_over_tor']:
918 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)
920 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
923 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
927 def download(self, name, url, path):
928 # keep track of current download
929 self.current_download_path = path
931 # initialize the progress bar
932 mirror_url = url.format(self.common.settings['mirror'])
933 self.progressbar.set_fraction(0)
934 self.progressbar.set_text(_('Downloading {0}').format(name))
935 self.progressbar.show()
938 # default mirror gets certificate pinning, only for requests that use the mirror
939 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
940 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
942 agent = Agent(reactor)
944 # actually, agent needs to follow redirect
945 agent = RedirectAgent(agent)
948 d = agent.request('GET', mirror_url,
949 Headers({'User-Agent': ['torbrowser-launcher']}),
952 self.file_download = open(path, 'w')
953 d.addCallback(self.response_received).addErrback(self.download_error)
955 if not reactor.running:
958 def try_stable(self, widget, data=None):
959 # change preferred to stable and relaunch TBL
960 self.common.settings['preferred'] = 'stable'
961 self.common.save_settings()
962 p = subprocess.Popen([self.common.paths['tbl_bin']])
965 def try_default_mirror(self, widget, data=None):
966 # change preferred to stable and relaunch TBL
967 self.common.settings['mirror'] = self.common.default_mirror
968 self.common.save_settings()
969 p = subprocess.Popen([self.common.paths['tbl_bin']])
972 def try_tor(self, widget, data=None):
973 # set update_over_tor to true and relaunch TBL
974 self.common.settings['update_over_tor'] = True
975 self.common.save_settings()
976 p = subprocess.Popen([self.common.paths['tbl_bin']])
979 def attempt_update(self):
980 # load the update check file
982 versions = json.load(open(self.common.paths['update_check_file']))
986 # filter linux versions
989 for version in versions:
990 if '-Linux' in version:
991 if 'alpha' in version or 'beta' in version or '-rc' in version:
992 valid_alphas.append(str(version))
994 valid_stables.append(str(version))
996 if len(valid_alphas):
997 latest_alpha = valid_alphas.pop()
999 if len(valid_stables):
1000 latest_stable = valid_stables.pop()
1002 if latest_stable or latest_alpha:
1004 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
1006 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
1007 self.common.settings['last_update_check_timestamp'] = int(time.time())
1008 self.common.settings['check_for_updates'] = False
1009 self.common.save_settings()
1010 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
1011 self.start_launcher()
1014 # failed to find the latest version
1015 self.set_gui('error', _("Error checking for updates."), [], False)
1018 # not a valid JSON object
1019 self.set_gui('error', _("Error checking for updates."), [], False)
1026 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1028 # initialize the progress bar
1029 self.progressbar.set_fraction(0)
1030 self.progressbar.set_text(_('Verifying Signature'))
1031 self.progressbar.show()
1034 if latest_version >= '3.':
1035 # after 3.x we check the sha256 file's sig, and also take the sha256 of the tarball and compare
1036 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1037 self.pulse_until_process_exits(p)
1038 if p.returncode == 0:
1039 # compare with sha256 of the tarball
1040 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1041 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1042 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1046 # before 3.x we just check the tarball sig
1047 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
1048 self.pulse_until_process_exits(p)
1049 if p.returncode == 0:
1055 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1056 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)
1060 if not reactor.running:
1064 # initialize the progress bar
1065 self.progressbar.set_fraction(0)
1066 self.progressbar.set_text(_('Installing'))
1067 self.progressbar.show()
1072 if self.common.paths['tarball_file'][-2:] == 'xz':
1073 # if tarball is .tar.xz
1074 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1075 tf = tarfile.open(fileobj=xz)
1076 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1079 # if tarball is .tar.gz
1080 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1081 tf = tarfile.open(self.common.paths['tarball_file'])
1082 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1088 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1093 # installation is finished, so save installed_version
1094 self.common.settings['installed_version'] = self.common.settings['latest_version']
1095 self.common.save_settings()
1099 def run(self, run_next_task = True):
1100 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1104 # make the progress bar pulse until process p (a Popen object) finishes
1105 def pulse_until_process_exits(self, p):
1106 while p.poll() == None:
1108 self.progressbar.pulse()
1111 # start over and download TBB again
1112 def start_over(self):
1113 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1114 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1119 def refresh_gtk(self):
1120 while gtk.events_pending():
1121 gtk.main_iteration(False)
1124 def delete_event(self, widget, event, data=None):
1126 def destroy(self, widget, data=None):
1127 if hasattr(self, 'file_download'):
1128 self.file_download.close()
1129 if hasattr(self, 'current_download_path'):
1130 os.remove(self.current_download_path)
1131 delattr(self, 'current_download_path')
1135 if __name__ == "__main__":
1136 tor_browser_launcher_version = '0.0.4'
1138 print _('Tor Browser Launcher')
1139 print _('By Micah Lee, licensed under GPLv3')
1140 print _('version {0}').format(tor_browser_launcher_version)
1141 print 'https://github.com/micahflee/torbrowser-launcher'
1143 common = TBLCommon(tor_browser_launcher_version)
1145 # is torbrowser-launcher already running?
1146 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1148 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1149 common.bring_window_to_front(tbl_pid)
1152 if '-settings' in sys.argv:
1154 app = TBLSettings(common)
1158 app = TBLLauncher(common)