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').replace('-rc-', 'rc')
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-2013-09.asc',
188 'mirrors_txt': '/usr/share/torbrowser-launcher/mirrors.txt',
189 'modem_sound': '/usr/share/torbrowser-launcher/modem.ogg',
190 'data_dir': tbb_data,
191 'download_dir': tbb_data+'/download',
192 'gnupg_homedir': tbb_data+'/gnupg_homedir',
193 'settings_file': tbb_data+'/settings',
194 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
195 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
198 'dir': tbb_data+'/tbb/stable/'+self.architecture,
199 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
200 'vidalia_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
201 'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
202 'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
203 'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
206 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
207 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
208 'vidalia_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
209 'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
210 'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
211 'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
217 def mkdir(self, path):
219 if os.path.exists(path) == False:
220 os.makedirs(path, 0700)
223 print _("Cannot create directory {0}").format(path)
225 if not os.access(path, os.W_OK):
226 print _("{0} is not writable").format(path)
230 # if gnupg_homedir isn't set up, set it up
231 def init_gnupg(self):
232 if not os.path.exists(self.paths['gnupg_homedir']):
233 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
234 self.mkdir(self.paths['gnupg_homedir'])
238 def import_keys(self):
239 print _('Importing keys')
240 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()
243 def load_mirrors(self):
245 for mirror in open(self.paths['mirrors_txt'], 'r').readlines():
246 self.mirrors.append(mirror.strip())
249 def load_settings(self):
251 'tbl_version': self.tbl_version,
252 'preferred': 'stable',
253 'installed_version': {
261 'update_over_tor': True,
262 'check_for_updates': False,
263 'modem_sound': False,
264 'last_update_check_timestamp': 0,
265 'mirror': self.default_mirror
268 if os.path.isfile(self.paths['settings_file']):
269 settings = pickle.load(open(self.paths['settings_file']))
272 # settings migrations
273 if settings['tbl_version'] == '0.0.1':
274 print '0.0.1 migration'
275 self.settings = default_settings
276 self.settings['installed_version']['alpha'] = settings['installed_version']
280 self.mkdir(self.paths['tbb']['alpha']['dir'])
282 # make sure settings file is up-to-date
283 for setting in default_settings:
284 if setting not in settings:
285 settings[setting] = default_settings[setting]
288 # make sure the version is current
289 if settings['tbl_version'] != self.tbl_version:
290 settings['tbl_version'] = self.tbl_version
293 self.settings = settings
298 self.settings = default_settings
302 def save_settings(self):
303 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
306 # get the process id of a program
307 def get_pid(self, bin_path, python = False):
310 for p in psutil.process_iter():
312 if p.pid != os.getpid():
315 if len(p.cmdline) > 1:
316 if 'python' in p.cmdline[0]:
319 if len(p.cmdline) > 0:
330 # bring program's x window to front
331 def bring_window_to_front(self, pid):
332 # figure out the window id
334 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
335 for line in p.stdout.readlines():
336 line_split = line.split()
337 cur_win_id = line_split[0]
338 cur_win_pid = int(line_split[2])
339 if cur_win_pid == pid:
344 subprocess.call(['wmctrl', '-i', '-a', win_id])
347 def __init__(self, common):
348 print _('Starting settings dialog')
352 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
353 self.window.set_title(_("Tor Browser Launcher Settings"))
354 self.window.set_icon_from_file(self.common.paths['icon_file'])
355 self.window.set_position(gtk.WIN_POS_CENTER)
356 self.window.set_border_width(10)
357 self.window.connect("delete_event", self.delete_event)
358 self.window.connect("destroy", self.destroy)
360 # build the rest of the UI
361 self.box = gtk.VBox(False, 10)
362 self.window.add(self.box)
365 self.hbox = gtk.HBox(False, 10)
366 self.box.pack_start(self.hbox, True, True, 0)
369 self.settings_box = gtk.VBox(False, 10)
370 self.hbox.pack_start(self.settings_box, True, True, 0)
371 self.settings_box.show()
373 self.labels_box = gtk.VBox(False, 10)
374 self.hbox.pack_start(self.labels_box, True, True, 0)
375 self.labels_box.show()
378 self.preferred_box = gtk.HBox(False, 10)
379 self.settings_box.pack_start(self.preferred_box, True, True, 0)
380 self.preferred_box.show()
382 self.preferred_label = gtk.Label(_('I prefer'))
383 self.preferred_label.set_line_wrap(True)
384 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
385 self.preferred_label.show()
387 self.preferred_options = []
388 for i in self.common.available_versions:
389 self.preferred_options.append(self.common.available_versions[i])
390 self.preferred_options.sort()
392 self.preferred = gtk.combo_box_new_text()
393 for option in self.preferred_options:
394 self.preferred.append_text(option)
395 if self.common.settings['preferred'] in self.common.available_versions:
396 self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
398 self.preferred.set_active(0)
399 self.preferred_box.pack_start(self.preferred, True, True, 0)
400 self.preferred.show()
403 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
405 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
406 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
407 if self.common.settings['update_over_tor']:
408 self.tor_update_checkbox.set_active(True)
410 self.tor_update_checkbox.set_active(False)
411 self.tor_update_checkbox.show()
415 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
416 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
417 if self.common.settings['check_for_updates']:
418 self.update_checkbox.set_active(True)
420 self.update_checkbox.set_active(False)
421 self.update_checkbox.show()
424 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
425 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
426 if self.common.settings['modem_sound']:
427 self.modem_checkbox.set_active(True)
429 self.modem_checkbox.set_active(False)
430 self.modem_checkbox.show()
433 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
434 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
436 self.label1 = gtk.Label(_('Not installed'))
437 self.label1.set_line_wrap(True)
438 self.labels_box.pack_start(self.label1, True, True, 0)
441 if(self.common.settings['last_update_check_timestamp'] > 0):
442 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']))))
444 self.label1 = gtk.Label(_('Never checked for updates'))
445 self.label1.set_line_wrap(True)
446 self.labels_box.pack_start(self.label1, True, True, 0)
450 self.mirrors_box = gtk.HBox(False, 10)
451 self.box.pack_start(self.mirrors_box, True, True, 0)
452 self.mirrors_box.show()
454 self.mirrors_label = gtk.Label(_('Mirror'))
455 self.mirrors_label.set_line_wrap(True)
456 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
457 self.mirrors_label.show()
459 self.mirrors = gtk.combo_box_new_text()
460 for mirror in self.common.mirrors:
461 self.mirrors.append_text(mirror)
462 if self.common.settings['mirror'] in self.common.mirrors:
463 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
465 self.preferred.set_active(0)
466 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
470 self.button_box = gtk.HButtonBox()
471 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
472 self.box.pack_start(self.button_box, True, True, 0)
473 self.button_box.show()
475 # save and launch button
476 save_launch_image = gtk.Image()
477 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
478 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
479 self.save_launch_button.set_image(save_launch_image)
480 self.save_launch_button.connect("clicked", self.save_launch, None)
481 self.button_box.add(self.save_launch_button)
482 self.save_launch_button.show()
484 # save and exit button
485 save_exit_image = gtk.Image()
486 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
487 self.save_exit_button = gtk.Button(_("Save & Exit"))
488 self.save_exit_button.set_image(save_exit_image)
489 self.save_exit_button.connect("clicked", self.save_exit, None)
490 self.button_box.add(self.save_exit_button)
491 self.save_exit_button.show()
494 cancel_image = gtk.Image()
495 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
496 self.cancel_button = gtk.Button(_("Cancel"))
497 self.cancel_button.set_image(cancel_image)
498 self.cancel_button.connect("clicked", self.destroy, None)
499 self.button_box.add(self.cancel_button)
500 self.cancel_button.show()
509 def save_launch(self, widget, data=None):
511 p = subprocess.Popen([self.common.paths['tbl_bin']])
515 def save_exit(self, widget, data=None):
521 # figure out the selected preferred option
523 selected = self.preferred_options[self.preferred.get_active()]
524 for i in self.common.available_versions:
525 if self.common.available_versions[i] == selected:
528 self.common.settings['preferred'] = preferred
531 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
532 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
533 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
535 # figure out the selected mirror
536 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
539 self.common.save_settings()
542 def delete_event(self, widget, event, data=None):
544 def destroy(self, widget, data=None):
549 def __init__(self, common):
550 print _('Starting launcher dialog')
554 self.set_gui(None, '', [])
555 self.launch_gui = True
556 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
558 # is vidalia already running and we just need to open a new firefox?
559 if self.common.settings['installed_version']:
560 vidalia_pid = self.common.get_pid('./App/vidalia')
561 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
563 if vidalia_pid and not firefox_pid:
564 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
565 self.common.bring_window_to_front(vidalia_pid)
566 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']])
568 elif vidalia_pid and firefox_pid:
569 print _('Vidalia and Firefox are already open, bringing them to focus')
571 # bring firefox to front, then vidalia
572 self.common.bring_window_to_front(firefox_pid)
573 self.common.bring_window_to_front(vidalia_pid)
577 check_for_updates = False
578 if self.common.settings['check_for_updates']:
579 check_for_updates = True
581 if not check_for_updates:
582 # how long was it since the last update check?
583 # 86400 seconds = 24 hours
584 current_timestamp = int(time.time())
585 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
586 check_for_updates = True
588 if check_for_updates:
590 print 'Checking for update'
591 self.set_gui('task', _("Checking for Tor Browser update."),
592 ['download_update_check',
595 # no need to check for update
596 print _('Checked for update within 24 hours, skipping')
597 self.start_launcher()
601 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
602 self.window.set_title(_("Tor Browser"))
603 self.window.set_icon_from_file(self.common.paths['icon_file'])
604 self.window.set_position(gtk.WIN_POS_CENTER)
605 self.window.set_border_width(10)
606 self.window.connect("delete_event", self.delete_event)
607 self.window.connect("destroy", self.destroy)
609 # build the rest of the UI
612 # download or run TBB
613 def start_launcher(self):
614 # is TBB already installed?
615 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
616 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
618 # verify installed version for newer versions of TBB (#58)
619 if installed_version >= '3.0':
620 versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
621 if os.path.exists(versions_filename):
622 for line in open(versions_filename):
623 if 'TORBROWSER_VERSION' in line:
624 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
626 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
627 if os.path.isfile(start) and os.access(start, os.X_OK):
628 if installed_version == latest_version:
629 print _('Latest version of TBB is installed, launching')
630 # current version of tbb is installed, launch it
632 self.launch_gui = False
633 elif installed_version < latest_version:
634 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
635 # there is a tbb upgrade available
636 if latest_version >= '3.':
637 self.set_gui('task', _("Your Tor Browser is out of date."),
639 'download_sha256_sig',
645 self.set_gui('task', _("Your Tor Browser is out of date."),
646 ['download_tarball_sig',
652 # for some reason the installed tbb is newer than the current version?
653 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
657 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
658 if latest_version >= '3.':
659 self.set_gui('task', _("Downloading and installing Tor Browser."),
661 'download_sha256_sig',
667 self.set_gui('task', _("Downloading and installing Tor Browser."),
668 ['download_tarball_sig',
674 # there are different GUIs that might appear, this sets which one we want
675 def set_gui(self, gui, message, tasks, autostart=True):
677 self.gui_message = message
678 self.gui_tasks = tasks
680 self.gui_autostart = autostart
682 # set all gtk variables to False
684 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
689 self.progressbar = False
690 self.button_box = False
691 self.start_button = False
692 self.exit_button = False
694 # build the application's UI
698 self.box = gtk.VBox(False, 20)
699 self.window.add(self.box)
701 if 'error' in self.gui:
703 self.label = gtk.Label( self.gui_message )
704 self.label.set_line_wrap(True)
705 self.box.pack_start(self.label, True, True, 0)
709 self.button_box = gtk.HButtonBox()
710 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
711 self.box.pack_start(self.button_box, True, True, 0)
712 self.button_box.show()
714 if self.gui != 'error':
716 yes_image = gtk.Image()
717 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
718 self.yes_button = gtk.Button("Yes")
719 self.yes_button.set_image(yes_image)
720 if self.gui == 'error_try_stable':
721 self.yes_button.connect("clicked", self.try_stable, None)
722 elif self.gui == 'error_try_default_mirror':
723 self.yes_button.connect("clicked", self.try_default_mirror, None)
724 elif self.gui == 'error_try_tor':
725 self.yes_button.connect("clicked", self.try_tor, None)
726 self.button_box.add(self.yes_button)
727 self.yes_button.show()
730 exit_image = gtk.Image()
731 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
732 self.exit_button = gtk.Button("Exit")
733 self.exit_button.set_image(exit_image)
734 self.exit_button.connect("clicked", self.destroy, None)
735 self.button_box.add(self.exit_button)
736 self.exit_button.show()
738 elif self.gui == 'task':
740 self.label = gtk.Label( self.gui_message )
741 self.label.set_line_wrap(True)
742 self.box.pack_start(self.label, True, True, 0)
746 self.progressbar = gtk.ProgressBar(adjustment=None)
747 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
748 self.progressbar.set_pulse_step(0.01)
749 self.box.pack_start(self.progressbar, True, True, 0)
752 self.button_box = gtk.HButtonBox()
753 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
754 self.box.pack_start(self.button_box, True, True, 0)
755 self.button_box.show()
758 start_image = gtk.Image()
759 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
760 self.start_button = gtk.Button(_("Start"))
761 self.start_button.set_image(start_image)
762 self.start_button.connect("clicked", self.start, None)
763 self.button_box.add(self.start_button)
764 if not self.gui_autostart:
765 self.start_button.show()
768 exit_image = gtk.Image()
769 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
770 self.exit_button = gtk.Button(_("Exit"))
771 self.exit_button.set_image(exit_image)
772 self.exit_button.connect("clicked", self.destroy, None)
773 self.button_box.add(self.exit_button)
774 self.exit_button.show()
779 if self.gui_autostart:
782 # start button clicked, begin tasks
783 def start(self, widget, data=None):
784 # disable the start button
785 if self.start_button:
786 self.start_button.set_sensitive(False)
788 # start running tasks
791 # run the next task in the task list
795 if self.gui_task_i >= len(self.gui_tasks):
799 task = self.gui_tasks[self.gui_task_i]
801 # get ready for the next task
804 print _('Running task: {0}'.format(task))
805 if task == 'download_update_check':
806 print _('Downloading'), self.common.paths['update_check_url']
807 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
809 if task == 'attempt_update':
810 print _('Checking to see if update is needed')
811 self.attempt_update()
813 elif task == 'download_sha256':
814 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
815 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
817 elif task == 'download_sha256_sig':
818 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
819 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
821 elif task == 'download_tarball_sig':
822 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
823 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
825 elif task == 'download_tarball':
826 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
827 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
829 elif task == 'verify':
830 print _('Verifying signature')
833 elif task == 'extract':
834 print _('Extracting'), self.common.paths['tarball_filename']
838 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
841 elif task == 'start_over':
842 print _('Starting download over again')
845 def response_received(self, response):
846 class FileDownloader(Protocol):
847 def __init__(self, common, file, total, progress, done_cb):
851 self.progress = progress
852 self.all_done = done_cb
854 if response.code != 200:
857 if response.code == 404:
858 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
862 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?"))
864 if common.settings['mirror'] != common.default_mirror:
865 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']))
867 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
869 def dataReceived(self, bytes):
870 self.file.write(bytes)
871 self.so_far += len(bytes)
872 percent = float(self.so_far) / float(self.total)
873 self.progress.set_fraction(percent)
874 amount = float(self.so_far)
876 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
879 amount = amount / float(size)
882 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
884 def connectionLost(self, reason):
885 print _('Finished receiving body:'), reason.getErrorMessage()
886 self.all_done(reason)
888 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
889 response.deliverBody(dl)
891 def response_finished(self, msg):
892 if msg.check(ResponseDone):
893 self.file_download.close()
894 delattr(self, 'current_download_path')
900 print "FINISHED", msg
901 ## FIXME handle errors
903 def download_error(self, f):
904 print _("Download error:"), f.value, type(f.value)
906 if isinstance(f.value, TryStableException):
907 f.trap(TryStableException)
908 self.set_gui('error_try_stable', str(f.value), [], False)
910 elif isinstance(f.value, TryDefaultMirrorException):
911 f.trap(TryDefaultMirrorException)
912 self.set_gui('error_try_default_mirror', str(f.value), [], False)
914 elif isinstance(f.value, DownloadErrorException):
915 f.trap(DownloadErrorException)
916 self.set_gui('error', str(f.value), [], False)
918 elif isinstance(f.value, DNSLookupError):
919 f.trap(DNSLookupError)
920 if common.settings['mirror'] != common.default_mirror:
921 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)
923 self.set_gui('error', str(f.value), [], False)
925 elif isinstance(f.value, ResponseFailed):
926 for reason in f.value.reasons:
927 if isinstance(reason.value, OpenSSL.SSL.Error):
928 # TODO: add the ability to report attack by posting bug to trac.torproject.org
929 if not self.common.settings['update_over_tor']:
930 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)
932 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
935 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
939 def download(self, name, url, path):
940 # keep track of current download
941 self.current_download_path = path
943 # initialize the progress bar
944 mirror_url = url.format(self.common.settings['mirror'])
945 self.progressbar.set_fraction(0)
946 self.progressbar.set_text(_('Downloading {0}').format(name))
947 self.progressbar.show()
950 # default mirror gets certificate pinning, only for requests that use the mirror
951 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
952 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
954 agent = Agent(reactor)
956 # actually, agent needs to follow redirect
957 agent = RedirectAgent(agent)
960 d = agent.request('GET', mirror_url,
961 Headers({'User-Agent': ['torbrowser-launcher']}),
964 self.file_download = open(path, 'w')
965 d.addCallback(self.response_received).addErrback(self.download_error)
967 if not reactor.running:
970 def try_stable(self, widget, data=None):
971 # change preferred to stable and relaunch TBL
972 self.common.settings['preferred'] = 'stable'
973 self.common.save_settings()
974 p = subprocess.Popen([self.common.paths['tbl_bin']])
977 def try_default_mirror(self, widget, data=None):
978 # change preferred to stable and relaunch TBL
979 self.common.settings['mirror'] = self.common.default_mirror
980 self.common.save_settings()
981 p = subprocess.Popen([self.common.paths['tbl_bin']])
984 def try_tor(self, widget, data=None):
985 # set update_over_tor to true and relaunch TBL
986 self.common.settings['update_over_tor'] = True
987 self.common.save_settings()
988 p = subprocess.Popen([self.common.paths['tbl_bin']])
991 def attempt_update(self):
992 # load the update check file
994 versions = json.load(open(self.common.paths['update_check_file']))
998 # filter linux versions
1001 for version in versions:
1002 if '-Linux' in version:
1003 if 'alpha' in version or 'beta' in version or '-rc' in version:
1004 valid_alphas.append(str(version))
1006 valid_stables.append(str(version))
1008 if len(valid_alphas):
1009 latest_alpha = valid_alphas.pop()
1010 valid_stables.sort()
1011 if len(valid_stables):
1012 latest_stable = valid_stables.pop()
1014 if latest_stable or latest_alpha:
1016 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
1018 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
1019 self.common.settings['last_update_check_timestamp'] = int(time.time())
1020 self.common.settings['check_for_updates'] = False
1021 self.common.save_settings()
1022 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
1023 self.start_launcher()
1026 # failed to find the latest version
1027 self.set_gui('error', _("Error checking for updates."), [], False)
1030 # not a valid JSON object
1031 self.set_gui('error', _("Error checking for updates."), [], False)
1038 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1040 # initialize the progress bar
1041 self.progressbar.set_fraction(0)
1042 self.progressbar.set_text(_('Verifying Signature'))
1043 self.progressbar.show()
1046 if latest_version >= '3.':
1047 # after 3.x we check the sha256 file's sig, and also take the sha256 of the tarball and compare
1048 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1049 self.pulse_until_process_exits(p)
1050 if p.returncode == 0:
1051 # compare with sha256 of the tarball
1052 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1053 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1054 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1058 # before 3.x we just check the tarball sig
1059 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
1060 self.pulse_until_process_exits(p)
1061 if p.returncode == 0:
1067 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1068 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)
1072 if not reactor.running:
1076 # initialize the progress bar
1077 self.progressbar.set_fraction(0)
1078 self.progressbar.set_text(_('Installing'))
1079 self.progressbar.show()
1084 if self.common.paths['tarball_file'][-2:] == 'xz':
1085 # if tarball is .tar.xz
1086 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1087 tf = tarfile.open(fileobj=xz)
1088 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1091 # if tarball is .tar.gz
1092 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1093 tf = tarfile.open(self.common.paths['tarball_file'])
1094 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1100 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1105 # installation is finished, so save installed_version
1106 self.common.settings['installed_version'] = self.common.settings['latest_version']
1107 self.common.save_settings()
1111 def run(self, run_next_task = True):
1112 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1115 if self.common.settings['modem_sound']:
1118 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1125 # make the progress bar pulse until process p (a Popen object) finishes
1126 def pulse_until_process_exits(self, p):
1127 while p.poll() == None:
1129 self.progressbar.pulse()
1132 # start over and download TBB again
1133 def start_over(self):
1134 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1135 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1140 def refresh_gtk(self):
1141 while gtk.events_pending():
1142 gtk.main_iteration(False)
1145 def delete_event(self, widget, event, data=None):
1147 def destroy(self, widget, data=None):
1148 if hasattr(self, 'file_download'):
1149 self.file_download.close()
1150 if hasattr(self, 'current_download_path'):
1151 os.remove(self.current_download_path)
1152 delattr(self, 'current_download_path')
1156 if __name__ == "__main__":
1157 tor_browser_launcher_version = '0.0.4'
1159 print _('Tor Browser Launcher')
1160 print _('By Micah Lee, licensed under GPLv3')
1161 print _('version {0}').format(tor_browser_launcher_version)
1162 print 'https://github.com/micahflee/torbrowser-launcher'
1164 common = TBLCommon(tor_browser_launcher_version)
1166 # is torbrowser-launcher already running?
1167 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1169 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1170 common.bring_window_to_front(tbl_pid)
1173 if '-settings' in sys.argv:
1175 app = TBLSettings(common)
1179 app = TBLLauncher(common)