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',
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']))
268 # settings migrations
269 if settings['tbl_version'] == '0.0.1':
270 print '0.0.1 migration'
271 self.settings = default_settings
272 self.settings['installed_version']['alpha'] = settings['installed_version']
273 self.settings['tbl_version'] = self.tbl_version
277 self.mkdir(self.paths['tbb']['alpha']['dir'])
279 # make sure settings file is up-to-date
280 for setting in default_settings:
281 if setting not in settings:
282 settings[setting] = default_settings[setting]
285 self.settings = settings
290 self.settings = default_settings
294 def save_settings(self):
295 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
298 # get the process id of a program
299 def get_pid(self, bin_path, python = False):
302 for p in psutil.process_iter():
304 if p.pid != os.getpid():
307 if len(p.cmdline) > 1:
308 if 'python' in p.cmdline[0]:
311 if len(p.cmdline) > 0:
322 # bring program's x window to front
323 def bring_window_to_front(self, pid):
324 # figure out the window id
326 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
327 for line in p.stdout.readlines():
328 line_split = line.split()
329 cur_win_id = line_split[0]
330 cur_win_pid = int(line_split[2])
331 if cur_win_pid == pid:
336 subprocess.call(['wmctrl', '-i', '-a', win_id])
339 def __init__(self, common):
340 print _('Starting settings dialog')
344 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
345 self.window.set_title(_("Tor Browser Launcher Settings"))
346 self.window.set_icon_from_file(self.common.paths['icon_file'])
347 self.window.set_position(gtk.WIN_POS_CENTER)
348 self.window.set_border_width(10)
349 self.window.connect("delete_event", self.delete_event)
350 self.window.connect("destroy", self.destroy)
352 # build the rest of the UI
353 self.box = gtk.VBox(False, 10)
354 self.window.add(self.box)
357 self.hbox = gtk.HBox(False, 10)
358 self.box.pack_start(self.hbox, True, True, 0)
361 self.settings_box = gtk.VBox(False, 10)
362 self.hbox.pack_start(self.settings_box, True, True, 0)
363 self.settings_box.show()
365 self.labels_box = gtk.VBox(False, 10)
366 self.hbox.pack_start(self.labels_box, True, True, 0)
367 self.labels_box.show()
370 self.preferred_box = gtk.HBox(False, 10)
371 self.settings_box.pack_start(self.preferred_box, True, True, 0)
372 self.preferred_box.show()
374 self.preferred_label = gtk.Label(_('I prefer'))
375 self.preferred_label.set_line_wrap(True)
376 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
377 self.preferred_label.show()
379 self.preferred_options = []
380 for i in self.common.available_versions:
381 self.preferred_options.append(self.common.available_versions[i])
382 self.preferred_options.sort()
384 self.preferred = gtk.combo_box_new_text()
385 for option in self.preferred_options:
386 self.preferred.append_text(option)
387 if self.common.settings['preferred'] in self.common.available_versions:
388 self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
390 self.preferred.set_active(0)
391 self.preferred_box.pack_start(self.preferred, True, True, 0)
392 self.preferred.show()
395 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
397 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
398 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
399 if self.common.settings['update_over_tor']:
400 self.tor_update_checkbox.set_active(True)
402 self.tor_update_checkbox.set_active(False)
403 self.tor_update_checkbox.show()
407 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
408 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
409 if self.common.settings['check_for_updates']:
410 self.update_checkbox.set_active(True)
412 self.update_checkbox.set_active(False)
413 self.update_checkbox.show()
416 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
417 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
419 self.label1 = gtk.Label(_('Not installed'))
420 self.label1.set_line_wrap(True)
421 self.labels_box.pack_start(self.label1, True, True, 0)
424 if(self.common.settings['last_update_check_timestamp'] > 0):
425 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']))))
427 self.label1 = gtk.Label(_('Never checked for updates'))
428 self.label1.set_line_wrap(True)
429 self.labels_box.pack_start(self.label1, True, True, 0)
433 self.mirrors_box = gtk.HBox(False, 10)
434 self.box.pack_start(self.mirrors_box, True, True, 0)
435 self.mirrors_box.show()
437 self.mirrors_label = gtk.Label(_('Mirror'))
438 self.mirrors_label.set_line_wrap(True)
439 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
440 self.mirrors_label.show()
442 self.mirrors = gtk.combo_box_new_text()
443 for mirror in self.common.mirrors:
444 self.mirrors.append_text(mirror)
445 if self.common.settings['mirror'] in self.common.mirrors:
446 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
448 self.preferred.set_active(0)
449 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
453 self.button_box = gtk.HButtonBox()
454 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
455 self.box.pack_start(self.button_box, True, True, 0)
456 self.button_box.show()
458 # save and launch button
459 save_launch_image = gtk.Image()
460 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
461 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
462 self.save_launch_button.set_image(save_launch_image)
463 self.save_launch_button.connect("clicked", self.save_launch, None)
464 self.button_box.add(self.save_launch_button)
465 self.save_launch_button.show()
467 # save and exit button
468 save_exit_image = gtk.Image()
469 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
470 self.save_exit_button = gtk.Button(_("Save & Exit"))
471 self.save_exit_button.set_image(save_exit_image)
472 self.save_exit_button.connect("clicked", self.save_exit, None)
473 self.button_box.add(self.save_exit_button)
474 self.save_exit_button.show()
477 cancel_image = gtk.Image()
478 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
479 self.cancel_button = gtk.Button(_("Cancel"))
480 self.cancel_button.set_image(cancel_image)
481 self.cancel_button.connect("clicked", self.destroy, None)
482 self.button_box.add(self.cancel_button)
483 self.cancel_button.show()
492 def save_launch(self, widget, data=None):
494 p = subprocess.Popen([self.common.paths['tbl_bin']])
498 def save_exit(self, widget, data=None):
504 # figure out the selected preferred option
506 selected = self.preferred_options[self.preferred.get_active()]
507 for i in self.common.available_versions:
508 if self.common.available_versions[i] == selected:
511 self.common.settings['preferred'] = preferred
514 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
515 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
517 # figure out the selected mirror
518 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
521 self.common.save_settings()
524 def delete_event(self, widget, event, data=None):
526 def destroy(self, widget, data=None):
531 def __init__(self, common):
532 print _('Starting launcher dialog')
536 self.set_gui(None, '', [])
537 self.launch_gui = True
538 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
540 # is vidalia already running and we just need to open a new firefox?
541 if self.common.settings['installed_version']:
542 vidalia_pid = self.common.get_pid('./App/vidalia')
543 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
545 if vidalia_pid and not firefox_pid:
546 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
547 self.common.bring_window_to_front(vidalia_pid)
548 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']])
550 elif vidalia_pid and firefox_pid:
551 print _('Vidalia and Firefox are already open, bringing them to focus')
553 # bring firefox to front, then vidalia
554 self.common.bring_window_to_front(firefox_pid)
555 self.common.bring_window_to_front(vidalia_pid)
559 check_for_updates = False
560 if self.common.settings['check_for_updates']:
561 check_for_updates = True
563 if not check_for_updates:
564 # how long was it since the last update check?
565 # 86400 seconds = 24 hours
566 current_timestamp = int(time.time())
567 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
568 check_for_updates = True
570 if check_for_updates:
572 print 'Checking for update'
573 self.set_gui('task', _("Checking for Tor Browser update."),
574 ['download_update_check',
577 # no need to check for update
578 print _('Checked for update within 24 hours, skipping')
579 self.start_launcher()
583 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
584 self.window.set_title(_("Tor Browser"))
585 self.window.set_icon_from_file(self.common.paths['icon_file'])
586 self.window.set_position(gtk.WIN_POS_CENTER)
587 self.window.set_border_width(10)
588 self.window.connect("delete_event", self.delete_event)
589 self.window.connect("destroy", self.destroy)
591 # build the rest of the UI
594 # download or run TBB
595 def start_launcher(self):
596 # is TBB already installed?
597 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
598 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
600 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
601 if os.path.isfile(start) and os.access(start, os.X_OK):
602 if installed_version == latest_version:
603 print _('Latest version of TBB is installed, launching')
604 # current version of tbb is installed, launch it
606 self.launch_gui = False
607 elif installed_version < latest_version:
608 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
609 # there is a tbb upgrade available
610 if latest_version >= '3.':
611 self.set_gui('task', _("Your Tor Browser is out of date."),
613 'download_sha256_sig',
619 self.set_gui('task', _("Your Tor Browser is out of date."),
620 ['download_tarball_sig',
626 # for some reason the installed tbb is newer than the current version?
627 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
631 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
632 if latest_version >= '3.':
633 self.set_gui('task', _("Downloading and installing Tor Browser."),
635 'download_sha256_sig',
641 self.set_gui('task', _("Downloading and installing Tor Browser."),
642 ['download_tarball_sig',
648 # there are different GUIs that might appear, this sets which one we want
649 def set_gui(self, gui, message, tasks, autostart=True):
651 self.gui_message = message
652 self.gui_tasks = tasks
654 self.gui_autostart = autostart
656 # set all gtk variables to False
658 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
663 self.progressbar = False
664 self.button_box = False
665 self.start_button = False
666 self.exit_button = False
668 # build the application's UI
672 self.box = gtk.VBox(False, 20)
673 self.window.add(self.box)
675 if 'error' in self.gui:
677 self.label = gtk.Label( self.gui_message )
678 self.label.set_line_wrap(True)
679 self.box.pack_start(self.label, True, True, 0)
683 self.button_box = gtk.HButtonBox()
684 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
685 self.box.pack_start(self.button_box, True, True, 0)
686 self.button_box.show()
688 if self.gui != 'error':
690 yes_image = gtk.Image()
691 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
692 self.yes_button = gtk.Button("Yes")
693 self.yes_button.set_image(yes_image)
694 if self.gui == 'error_try_stable':
695 self.yes_button.connect("clicked", self.try_stable, None)
696 elif self.gui == 'error_try_default_mirror':
697 self.yes_button.connect("clicked", self.try_default_mirror, None)
698 elif self.gui == 'error_try_tor':
699 self.yes_button.connect("clicked", self.try_tor, None)
700 self.button_box.add(self.yes_button)
701 self.yes_button.show()
704 exit_image = gtk.Image()
705 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
706 self.exit_button = gtk.Button("Exit")
707 self.exit_button.set_image(exit_image)
708 self.exit_button.connect("clicked", self.destroy, None)
709 self.button_box.add(self.exit_button)
710 self.exit_button.show()
712 elif self.gui == 'task':
714 self.label = gtk.Label( self.gui_message )
715 self.label.set_line_wrap(True)
716 self.box.pack_start(self.label, True, True, 0)
720 self.progressbar = gtk.ProgressBar(adjustment=None)
721 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
722 self.progressbar.set_pulse_step(0.01)
723 self.box.pack_start(self.progressbar, True, True, 0)
726 self.button_box = gtk.HButtonBox()
727 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
728 self.box.pack_start(self.button_box, True, True, 0)
729 self.button_box.show()
732 start_image = gtk.Image()
733 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
734 self.start_button = gtk.Button(_("Start"))
735 self.start_button.set_image(start_image)
736 self.start_button.connect("clicked", self.start, None)
737 self.button_box.add(self.start_button)
738 if not self.gui_autostart:
739 self.start_button.show()
742 exit_image = gtk.Image()
743 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
744 self.exit_button = gtk.Button(_("Exit"))
745 self.exit_button.set_image(exit_image)
746 self.exit_button.connect("clicked", self.destroy, None)
747 self.button_box.add(self.exit_button)
748 self.exit_button.show()
753 if self.gui_autostart:
756 # start button clicked, begin tasks
757 def start(self, widget, data=None):
758 # disable the start button
759 if self.start_button:
760 self.start_button.set_sensitive(False)
762 # start running tasks
765 # run the next task in the task list
769 if self.gui_task_i >= len(self.gui_tasks):
773 task = self.gui_tasks[self.gui_task_i]
775 # get ready for the next task
778 print _('Running task: {0}'.format(task))
779 if task == 'download_update_check':
780 print _('Downloading'), self.common.paths['update_check_url']
781 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
783 if task == 'attempt_update':
784 print _('Checking to see if update is needed')
785 self.attempt_update()
787 elif task == 'download_sha256':
788 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
789 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
791 elif task == 'download_sha256_sig':
792 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
793 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
795 elif task == 'download_tarball_sig':
796 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
797 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
799 elif task == 'download_tarball':
800 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
801 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
803 elif task == 'verify':
804 print _('Verifying signature')
807 elif task == 'extract':
808 print _('Extracting'), self.common.paths['tarball_filename']
812 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
815 elif task == 'start_over':
816 print _('Starting download over again')
819 def response_received(self, response):
820 class FileDownloader(Protocol):
821 def __init__(self, common, file, total, progress, done_cb):
825 self.progress = progress
826 self.all_done = done_cb
828 if response.code != 200:
831 if response.code == 404:
832 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
836 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?"))
838 if common.settings['mirror'] != common.default_mirror:
839 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']))
841 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
843 def dataReceived(self, bytes):
844 self.file.write(bytes)
845 self.so_far += len(bytes)
846 percent = float(self.so_far) / float(self.total)
847 self.progress.set_fraction(percent)
848 amount = float(self.so_far)
850 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
853 amount = amount / float(size)
856 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
858 def connectionLost(self, reason):
859 print _('Finished receiving body:'), reason.getErrorMessage()
860 self.all_done(reason)
862 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
863 response.deliverBody(dl)
865 def response_finished(self, msg):
866 if msg.check(ResponseDone):
867 self.file_download.close()
868 delattr(self, 'current_download_path')
874 print "FINISHED", msg
875 ## FIXME handle errors
877 def download_error(self, f):
878 print _("Download error:"), f.value, type(f.value)
880 if isinstance(f.value, TryStableException):
881 f.trap(TryStableException)
882 self.set_gui('error_try_stable', str(f.value), [], False)
884 elif isinstance(f.value, TryDefaultMirrorException):
885 f.trap(TryDefaultMirrorException)
886 self.set_gui('error_try_default_mirror', str(f.value), [], False)
888 elif isinstance(f.value, DownloadErrorException):
889 f.trap(DownloadErrorException)
890 self.set_gui('error', str(f.value), [], False)
892 elif isinstance(f.value, DNSLookupError):
893 f.trap(DNSLookupError)
894 if common.settings['mirror'] != common.default_mirror:
895 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)
897 self.set_gui('error', str(f.value), [], False)
899 elif isinstance(f.value, ResponseFailed):
900 for reason in f.value.reasons:
901 if isinstance(reason.value, OpenSSL.SSL.Error):
902 # TODO: add the ability to report attack by posting bug to trac.torproject.org
903 if not self.common.settings['update_over_tor']:
904 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)
906 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
909 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
913 def download(self, name, url, path):
914 # keep track of current download
915 self.current_download_path = path
917 # initialize the progress bar
918 mirror_url = url.format(self.common.settings['mirror'])
919 self.progressbar.set_fraction(0)
920 self.progressbar.set_text(_('Downloading {0}').format(name))
921 self.progressbar.show()
924 # default mirror gets certificate pinning, only for requests that use the mirror
925 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
926 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
928 agent = Agent(reactor)
930 # actually, agent needs to follow redirect
931 agent = RedirectAgent(agent)
934 d = agent.request('GET', mirror_url,
935 Headers({'User-Agent': ['torbrowser-launcher']}),
938 self.file_download = open(path, 'w')
939 d.addCallback(self.response_received).addErrback(self.download_error)
941 if not reactor.running:
944 def try_stable(self, widget, data=None):
945 # change preferred to stable and relaunch TBL
946 self.common.settings['preferred'] = 'stable'
947 self.common.save_settings()
948 p = subprocess.Popen([self.common.paths['tbl_bin']])
951 def try_default_mirror(self, widget, data=None):
952 # change preferred to stable and relaunch TBL
953 self.common.settings['mirror'] = self.common.default_mirror
954 self.common.save_settings()
955 p = subprocess.Popen([self.common.paths['tbl_bin']])
958 def try_tor(self, widget, data=None):
959 # set update_over_tor to true and relaunch TBL
960 self.common.settings['update_over_tor'] = True
961 self.common.save_settings()
962 p = subprocess.Popen([self.common.paths['tbl_bin']])
965 def attempt_update(self):
966 # load the update check file
968 versions = json.load(open(self.common.paths['update_check_file']))
972 # filter linux versions
975 for version in versions:
976 if '-Linux' in version:
977 if 'alpha' in version or 'beta' in version or '-rc' in version:
978 valid_alphas.append(str(version))
980 valid_stables.append(str(version))
982 if len(valid_alphas):
983 latest_alpha = valid_alphas.pop()
985 if len(valid_stables):
986 latest_stable = valid_stables.pop()
988 if latest_stable or latest_alpha:
990 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
992 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
993 self.common.settings['last_update_check_timestamp'] = int(time.time())
994 self.common.settings['check_for_updates'] = False
995 self.common.save_settings()
996 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
997 self.start_launcher()
1000 # failed to find the latest version
1001 self.set_gui('error', _("Error checking for updates."), [], False)
1004 # not a valid JSON object
1005 self.set_gui('error', _("Error checking for updates."), [], False)
1012 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1014 # initialize the progress bar
1015 self.progressbar.set_fraction(0)
1016 self.progressbar.set_text(_('Verifying Signature'))
1017 self.progressbar.show()
1020 if latest_version >= '3.':
1021 # after 3.x we check the sha256 file's sig, and also take the sha256 of the tarball and compare
1022 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1023 self.pulse_until_process_exits(p)
1024 if p.returncode == 0:
1025 # compare with sha256 of the tarball
1026 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1027 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1028 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1032 # before 3.x we just check the tarball sig
1033 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
1034 self.pulse_until_process_exits(p)
1035 if p.returncode == 0:
1041 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1042 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)
1046 if not reactor.running:
1050 # initialize the progress bar
1051 self.progressbar.set_fraction(0)
1052 self.progressbar.set_text(_('Installing'))
1053 self.progressbar.show()
1058 if self.common.paths['tarball_file'][-2:] == 'xz':
1059 # if tarball is .tar.xz
1060 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1061 tf = tarfile.open(fileobj=xz)
1062 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1065 # if tarball is .tar.gz
1066 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1067 tf = tarfile.open(self.common.paths['tarball_file'])
1068 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1074 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1079 # installation is finished, so save installed_version
1080 self.common.settings['installed_version'] = self.common.settings['latest_version']
1081 self.common.save_settings()
1085 def run(self, run_next_task = True):
1086 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1090 # make the progress bar pulse until process p (a Popen object) finishes
1091 def pulse_until_process_exits(self, p):
1092 while p.poll() == None:
1094 self.progressbar.pulse()
1097 # start over and download TBB again
1098 def start_over(self):
1099 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1100 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1105 def refresh_gtk(self):
1106 while gtk.events_pending():
1107 gtk.main_iteration(False)
1110 def delete_event(self, widget, event, data=None):
1112 def destroy(self, widget, data=None):
1113 if hasattr(self, 'file_download'):
1114 self.file_download.close()
1115 if hasattr(self, 'current_download_path'):
1116 os.remove(self.current_download_path)
1117 delattr(self, 'current_download_path')
1121 if __name__ == "__main__":
1122 tor_browser_launcher_version = '0.0.4'
1124 print _('Tor Browser Launcher')
1125 print _('By Micah Lee, licensed under GPLv3')
1126 print _('version {0}').format(tor_browser_launcher_version)
1127 print 'https://github.com/micahflee/torbrowser-launcher'
1129 common = TBLCommon(tor_browser_launcher_version)
1131 # is torbrowser-launcher already running?
1132 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1134 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1135 common.bring_window_to_front(tbl_pid)
1138 if '-settings' in sys.argv:
1140 app = TBLSettings(common)
1144 app = TBLLauncher(common)