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']
276 self.mkdir(self.paths['tbb']['alpha']['dir'])
278 # make sure settings file is up-to-date
279 for setting in default_settings:
280 if setting not in settings:
281 settings[setting] = default_settings[setting]
284 # make sure the version is current
285 if settings['tbl_version'] != self.tbl_version:
286 settings['tbl_version'] = self.tbl_version
289 self.settings = settings
294 self.settings = default_settings
298 def save_settings(self):
299 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
302 # get the process id of a program
303 def get_pid(self, bin_path, python = False):
306 for p in psutil.process_iter():
308 if p.pid != os.getpid():
311 if len(p.cmdline) > 1:
312 if 'python' in p.cmdline[0]:
315 if len(p.cmdline) > 0:
326 # bring program's x window to front
327 def bring_window_to_front(self, pid):
328 # figure out the window id
330 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
331 for line in p.stdout.readlines():
332 line_split = line.split()
333 cur_win_id = line_split[0]
334 cur_win_pid = int(line_split[2])
335 if cur_win_pid == pid:
340 subprocess.call(['wmctrl', '-i', '-a', win_id])
343 def __init__(self, common):
344 print _('Starting settings dialog')
348 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
349 self.window.set_title(_("Tor Browser Launcher Settings"))
350 self.window.set_icon_from_file(self.common.paths['icon_file'])
351 self.window.set_position(gtk.WIN_POS_CENTER)
352 self.window.set_border_width(10)
353 self.window.connect("delete_event", self.delete_event)
354 self.window.connect("destroy", self.destroy)
356 # build the rest of the UI
357 self.box = gtk.VBox(False, 10)
358 self.window.add(self.box)
361 self.hbox = gtk.HBox(False, 10)
362 self.box.pack_start(self.hbox, True, True, 0)
365 self.settings_box = gtk.VBox(False, 10)
366 self.hbox.pack_start(self.settings_box, True, True, 0)
367 self.settings_box.show()
369 self.labels_box = gtk.VBox(False, 10)
370 self.hbox.pack_start(self.labels_box, True, True, 0)
371 self.labels_box.show()
374 self.preferred_box = gtk.HBox(False, 10)
375 self.settings_box.pack_start(self.preferred_box, True, True, 0)
376 self.preferred_box.show()
378 self.preferred_label = gtk.Label(_('I prefer'))
379 self.preferred_label.set_line_wrap(True)
380 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
381 self.preferred_label.show()
383 self.preferred_options = []
384 for i in self.common.available_versions:
385 self.preferred_options.append(self.common.available_versions[i])
386 self.preferred_options.sort()
388 self.preferred = gtk.combo_box_new_text()
389 for option in self.preferred_options:
390 self.preferred.append_text(option)
391 if self.common.settings['preferred'] in self.common.available_versions:
392 self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
394 self.preferred.set_active(0)
395 self.preferred_box.pack_start(self.preferred, True, True, 0)
396 self.preferred.show()
399 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
401 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
402 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
403 if self.common.settings['update_over_tor']:
404 self.tor_update_checkbox.set_active(True)
406 self.tor_update_checkbox.set_active(False)
407 self.tor_update_checkbox.show()
411 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
412 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
413 if self.common.settings['check_for_updates']:
414 self.update_checkbox.set_active(True)
416 self.update_checkbox.set_active(False)
417 self.update_checkbox.show()
420 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
421 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
423 self.label1 = gtk.Label(_('Not installed'))
424 self.label1.set_line_wrap(True)
425 self.labels_box.pack_start(self.label1, True, True, 0)
428 if(self.common.settings['last_update_check_timestamp'] > 0):
429 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']))))
431 self.label1 = gtk.Label(_('Never checked for updates'))
432 self.label1.set_line_wrap(True)
433 self.labels_box.pack_start(self.label1, True, True, 0)
437 self.mirrors_box = gtk.HBox(False, 10)
438 self.box.pack_start(self.mirrors_box, True, True, 0)
439 self.mirrors_box.show()
441 self.mirrors_label = gtk.Label(_('Mirror'))
442 self.mirrors_label.set_line_wrap(True)
443 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
444 self.mirrors_label.show()
446 self.mirrors = gtk.combo_box_new_text()
447 for mirror in self.common.mirrors:
448 self.mirrors.append_text(mirror)
449 if self.common.settings['mirror'] in self.common.mirrors:
450 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
452 self.preferred.set_active(0)
453 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
457 self.button_box = gtk.HButtonBox()
458 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
459 self.box.pack_start(self.button_box, True, True, 0)
460 self.button_box.show()
462 # save and launch button
463 save_launch_image = gtk.Image()
464 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
465 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
466 self.save_launch_button.set_image(save_launch_image)
467 self.save_launch_button.connect("clicked", self.save_launch, None)
468 self.button_box.add(self.save_launch_button)
469 self.save_launch_button.show()
471 # save and exit button
472 save_exit_image = gtk.Image()
473 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
474 self.save_exit_button = gtk.Button(_("Save & Exit"))
475 self.save_exit_button.set_image(save_exit_image)
476 self.save_exit_button.connect("clicked", self.save_exit, None)
477 self.button_box.add(self.save_exit_button)
478 self.save_exit_button.show()
481 cancel_image = gtk.Image()
482 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
483 self.cancel_button = gtk.Button(_("Cancel"))
484 self.cancel_button.set_image(cancel_image)
485 self.cancel_button.connect("clicked", self.destroy, None)
486 self.button_box.add(self.cancel_button)
487 self.cancel_button.show()
496 def save_launch(self, widget, data=None):
498 p = subprocess.Popen([self.common.paths['tbl_bin']])
502 def save_exit(self, widget, data=None):
508 # figure out the selected preferred option
510 selected = self.preferred_options[self.preferred.get_active()]
511 for i in self.common.available_versions:
512 if self.common.available_versions[i] == selected:
515 self.common.settings['preferred'] = preferred
518 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
519 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
521 # figure out the selected mirror
522 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
525 self.common.save_settings()
528 def delete_event(self, widget, event, data=None):
530 def destroy(self, widget, data=None):
535 def __init__(self, common):
536 print _('Starting launcher dialog')
540 self.set_gui(None, '', [])
541 self.launch_gui = True
542 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
544 # is vidalia already running and we just need to open a new firefox?
545 if self.common.settings['installed_version']:
546 vidalia_pid = self.common.get_pid('./App/vidalia')
547 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
549 if vidalia_pid and not firefox_pid:
550 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
551 self.common.bring_window_to_front(vidalia_pid)
552 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']])
554 elif vidalia_pid and firefox_pid:
555 print _('Vidalia and Firefox are already open, bringing them to focus')
557 # bring firefox to front, then vidalia
558 self.common.bring_window_to_front(firefox_pid)
559 self.common.bring_window_to_front(vidalia_pid)
563 check_for_updates = False
564 if self.common.settings['check_for_updates']:
565 check_for_updates = True
567 if not check_for_updates:
568 # how long was it since the last update check?
569 # 86400 seconds = 24 hours
570 current_timestamp = int(time.time())
571 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
572 check_for_updates = True
574 if check_for_updates:
576 print 'Checking for update'
577 self.set_gui('task', _("Checking for Tor Browser update."),
578 ['download_update_check',
581 # no need to check for update
582 print _('Checked for update within 24 hours, skipping')
583 self.start_launcher()
587 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
588 self.window.set_title(_("Tor Browser"))
589 self.window.set_icon_from_file(self.common.paths['icon_file'])
590 self.window.set_position(gtk.WIN_POS_CENTER)
591 self.window.set_border_width(10)
592 self.window.connect("delete_event", self.delete_event)
593 self.window.connect("destroy", self.destroy)
595 # build the rest of the UI
598 # download or run TBB
599 def start_launcher(self):
600 # is TBB already installed?
601 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
602 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
604 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
605 if os.path.isfile(start) and os.access(start, os.X_OK):
606 if installed_version == latest_version:
607 print _('Latest version of TBB is installed, launching')
608 # current version of tbb is installed, launch it
610 self.launch_gui = False
611 elif installed_version < latest_version:
612 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
613 # there is a tbb upgrade available
614 if latest_version >= '3.':
615 self.set_gui('task', _("Your Tor Browser is out of date."),
617 'download_sha256_sig',
623 self.set_gui('task', _("Your Tor Browser is out of date."),
624 ['download_tarball_sig',
630 # for some reason the installed tbb is newer than the current version?
631 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
635 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
636 if latest_version >= '3.':
637 self.set_gui('task', _("Downloading and installing Tor Browser."),
639 'download_sha256_sig',
645 self.set_gui('task', _("Downloading and installing Tor Browser."),
646 ['download_tarball_sig',
652 # there are different GUIs that might appear, this sets which one we want
653 def set_gui(self, gui, message, tasks, autostart=True):
655 self.gui_message = message
656 self.gui_tasks = tasks
658 self.gui_autostart = autostart
660 # set all gtk variables to False
662 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
667 self.progressbar = False
668 self.button_box = False
669 self.start_button = False
670 self.exit_button = False
672 # build the application's UI
676 self.box = gtk.VBox(False, 20)
677 self.window.add(self.box)
679 if 'error' in self.gui:
681 self.label = gtk.Label( self.gui_message )
682 self.label.set_line_wrap(True)
683 self.box.pack_start(self.label, True, True, 0)
687 self.button_box = gtk.HButtonBox()
688 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
689 self.box.pack_start(self.button_box, True, True, 0)
690 self.button_box.show()
692 if self.gui != 'error':
694 yes_image = gtk.Image()
695 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
696 self.yes_button = gtk.Button("Yes")
697 self.yes_button.set_image(yes_image)
698 if self.gui == 'error_try_stable':
699 self.yes_button.connect("clicked", self.try_stable, None)
700 elif self.gui == 'error_try_default_mirror':
701 self.yes_button.connect("clicked", self.try_default_mirror, None)
702 elif self.gui == 'error_try_tor':
703 self.yes_button.connect("clicked", self.try_tor, None)
704 self.button_box.add(self.yes_button)
705 self.yes_button.show()
708 exit_image = gtk.Image()
709 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
710 self.exit_button = gtk.Button("Exit")
711 self.exit_button.set_image(exit_image)
712 self.exit_button.connect("clicked", self.destroy, None)
713 self.button_box.add(self.exit_button)
714 self.exit_button.show()
716 elif self.gui == 'task':
718 self.label = gtk.Label( self.gui_message )
719 self.label.set_line_wrap(True)
720 self.box.pack_start(self.label, True, True, 0)
724 self.progressbar = gtk.ProgressBar(adjustment=None)
725 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
726 self.progressbar.set_pulse_step(0.01)
727 self.box.pack_start(self.progressbar, True, True, 0)
730 self.button_box = gtk.HButtonBox()
731 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
732 self.box.pack_start(self.button_box, True, True, 0)
733 self.button_box.show()
736 start_image = gtk.Image()
737 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
738 self.start_button = gtk.Button(_("Start"))
739 self.start_button.set_image(start_image)
740 self.start_button.connect("clicked", self.start, None)
741 self.button_box.add(self.start_button)
742 if not self.gui_autostart:
743 self.start_button.show()
746 exit_image = gtk.Image()
747 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
748 self.exit_button = gtk.Button(_("Exit"))
749 self.exit_button.set_image(exit_image)
750 self.exit_button.connect("clicked", self.destroy, None)
751 self.button_box.add(self.exit_button)
752 self.exit_button.show()
757 if self.gui_autostart:
760 # start button clicked, begin tasks
761 def start(self, widget, data=None):
762 # disable the start button
763 if self.start_button:
764 self.start_button.set_sensitive(False)
766 # start running tasks
769 # run the next task in the task list
773 if self.gui_task_i >= len(self.gui_tasks):
777 task = self.gui_tasks[self.gui_task_i]
779 # get ready for the next task
782 print _('Running task: {0}'.format(task))
783 if task == 'download_update_check':
784 print _('Downloading'), self.common.paths['update_check_url']
785 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
787 if task == 'attempt_update':
788 print _('Checking to see if update is needed')
789 self.attempt_update()
791 elif task == 'download_sha256':
792 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
793 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
795 elif task == 'download_sha256_sig':
796 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
797 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
799 elif task == 'download_tarball_sig':
800 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
801 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
803 elif task == 'download_tarball':
804 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
805 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
807 elif task == 'verify':
808 print _('Verifying signature')
811 elif task == 'extract':
812 print _('Extracting'), self.common.paths['tarball_filename']
816 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
819 elif task == 'start_over':
820 print _('Starting download over again')
823 def response_received(self, response):
824 class FileDownloader(Protocol):
825 def __init__(self, common, file, total, progress, done_cb):
829 self.progress = progress
830 self.all_done = done_cb
832 if response.code != 200:
835 if response.code == 404:
836 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
840 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?"))
842 if common.settings['mirror'] != common.default_mirror:
843 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']))
845 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
847 def dataReceived(self, bytes):
848 self.file.write(bytes)
849 self.so_far += len(bytes)
850 percent = float(self.so_far) / float(self.total)
851 self.progress.set_fraction(percent)
852 amount = float(self.so_far)
854 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
857 amount = amount / float(size)
860 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
862 def connectionLost(self, reason):
863 print _('Finished receiving body:'), reason.getErrorMessage()
864 self.all_done(reason)
866 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
867 response.deliverBody(dl)
869 def response_finished(self, msg):
870 if msg.check(ResponseDone):
871 self.file_download.close()
872 delattr(self, 'current_download_path')
878 print "FINISHED", msg
879 ## FIXME handle errors
881 def download_error(self, f):
882 print _("Download error:"), f.value, type(f.value)
884 if isinstance(f.value, TryStableException):
885 f.trap(TryStableException)
886 self.set_gui('error_try_stable', str(f.value), [], False)
888 elif isinstance(f.value, TryDefaultMirrorException):
889 f.trap(TryDefaultMirrorException)
890 self.set_gui('error_try_default_mirror', str(f.value), [], False)
892 elif isinstance(f.value, DownloadErrorException):
893 f.trap(DownloadErrorException)
894 self.set_gui('error', str(f.value), [], False)
896 elif isinstance(f.value, DNSLookupError):
897 f.trap(DNSLookupError)
898 if common.settings['mirror'] != common.default_mirror:
899 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)
901 self.set_gui('error', str(f.value), [], False)
903 elif isinstance(f.value, ResponseFailed):
904 for reason in f.value.reasons:
905 if isinstance(reason.value, OpenSSL.SSL.Error):
906 # TODO: add the ability to report attack by posting bug to trac.torproject.org
907 if not self.common.settings['update_over_tor']:
908 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)
910 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
913 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
917 def download(self, name, url, path):
918 # keep track of current download
919 self.current_download_path = path
921 # initialize the progress bar
922 mirror_url = url.format(self.common.settings['mirror'])
923 self.progressbar.set_fraction(0)
924 self.progressbar.set_text(_('Downloading {0}').format(name))
925 self.progressbar.show()
928 # default mirror gets certificate pinning, only for requests that use the mirror
929 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
930 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
932 agent = Agent(reactor)
934 # actually, agent needs to follow redirect
935 agent = RedirectAgent(agent)
938 d = agent.request('GET', mirror_url,
939 Headers({'User-Agent': ['torbrowser-launcher']}),
942 self.file_download = open(path, 'w')
943 d.addCallback(self.response_received).addErrback(self.download_error)
945 if not reactor.running:
948 def try_stable(self, widget, data=None):
949 # change preferred to stable and relaunch TBL
950 self.common.settings['preferred'] = 'stable'
951 self.common.save_settings()
952 p = subprocess.Popen([self.common.paths['tbl_bin']])
955 def try_default_mirror(self, widget, data=None):
956 # change preferred to stable and relaunch TBL
957 self.common.settings['mirror'] = self.common.default_mirror
958 self.common.save_settings()
959 p = subprocess.Popen([self.common.paths['tbl_bin']])
962 def try_tor(self, widget, data=None):
963 # set update_over_tor to true and relaunch TBL
964 self.common.settings['update_over_tor'] = True
965 self.common.save_settings()
966 p = subprocess.Popen([self.common.paths['tbl_bin']])
969 def attempt_update(self):
970 # load the update check file
972 versions = json.load(open(self.common.paths['update_check_file']))
976 # filter linux versions
979 for version in versions:
980 if '-Linux' in version:
981 if 'alpha' in version or 'beta' in version or '-rc' in version:
982 valid_alphas.append(str(version))
984 valid_stables.append(str(version))
986 if len(valid_alphas):
987 latest_alpha = valid_alphas.pop()
989 if len(valid_stables):
990 latest_stable = valid_stables.pop()
992 if latest_stable or latest_alpha:
994 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
996 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
997 self.common.settings['last_update_check_timestamp'] = int(time.time())
998 self.common.settings['check_for_updates'] = False
999 self.common.save_settings()
1000 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
1001 self.start_launcher()
1004 # failed to find the latest version
1005 self.set_gui('error', _("Error checking for updates."), [], False)
1008 # not a valid JSON object
1009 self.set_gui('error', _("Error checking for updates."), [], False)
1016 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1018 # initialize the progress bar
1019 self.progressbar.set_fraction(0)
1020 self.progressbar.set_text(_('Verifying Signature'))
1021 self.progressbar.show()
1024 if latest_version >= '3.':
1025 # after 3.x we check the sha256 file's sig, and also take the sha256 of the tarball and compare
1026 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1027 self.pulse_until_process_exits(p)
1028 if p.returncode == 0:
1029 # compare with sha256 of the tarball
1030 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1031 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1032 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1036 # before 3.x we just check the tarball sig
1037 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
1038 self.pulse_until_process_exits(p)
1039 if p.returncode == 0:
1045 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1046 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)
1050 if not reactor.running:
1054 # initialize the progress bar
1055 self.progressbar.set_fraction(0)
1056 self.progressbar.set_text(_('Installing'))
1057 self.progressbar.show()
1062 if self.common.paths['tarball_file'][-2:] == 'xz':
1063 # if tarball is .tar.xz
1064 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1065 tf = tarfile.open(fileobj=xz)
1066 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1069 # if tarball is .tar.gz
1070 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1071 tf = tarfile.open(self.common.paths['tarball_file'])
1072 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1078 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1083 # installation is finished, so save installed_version
1084 self.common.settings['installed_version'] = self.common.settings['latest_version']
1085 self.common.save_settings()
1089 def run(self, run_next_task = True):
1090 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1094 # make the progress bar pulse until process p (a Popen object) finishes
1095 def pulse_until_process_exits(self, p):
1096 while p.poll() == None:
1098 self.progressbar.pulse()
1101 # start over and download TBB again
1102 def start_over(self):
1103 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1104 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1109 def refresh_gtk(self):
1110 while gtk.events_pending():
1111 gtk.main_iteration(False)
1114 def delete_event(self, widget, event, data=None):
1116 def destroy(self, widget, data=None):
1117 if hasattr(self, 'file_download'):
1118 self.file_download.close()
1119 if hasattr(self, 'current_download_path'):
1120 os.remove(self.current_download_path)
1121 delattr(self, 'current_download_path')
1125 if __name__ == "__main__":
1126 tor_browser_launcher_version = '0.0.4'
1128 print _('Tor Browser Launcher')
1129 print _('By Micah Lee, licensed under GPLv3')
1130 print _('version {0}').format(tor_browser_launcher_version)
1131 print 'https://github.com/micahflee/torbrowser-launcher'
1133 common = TBLCommon(tor_browser_launcher_version)
1135 # is torbrowser-launcher already running?
1136 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1138 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1139 common.bring_window_to_front(tbl_pid)
1142 if '-settings' in sys.argv:
1144 app = TBLSettings(common)
1148 app = TBLLauncher(common)