4 https://github.com/micahflee/torbrowser-launcher/
6 Copyright (c) 2013-2014 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.
32 SHARE = os.getenv('TBL_SHARE', sys.prefix+'/share/torbrowser-launcher')
37 gettext.install('torbrowser-launcher', os.path.join(SHARE, 'locale'))
39 from twisted.internet import gtk2reactor
41 from twisted.internet import reactor
47 import subprocess, locale, time, pickle, json, tarfile, psutil, hashlib, lzma
49 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
50 from twisted.web.http_headers import Headers
51 from twisted.internet.protocol import Protocol
52 from twisted.internet.ssl import ClientContextFactory
53 from twisted.internet.error import DNSLookupError
58 class TryStableException(Exception):
62 class TryDefaultMirrorException(Exception):
66 class DownloadErrorException(Exception):
70 class VerifyTorProjectCert(ClientContextFactory):
72 def __init__(self, torproject_pem):
73 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
75 def getContext(self, host, port):
76 ctx = ClientContextFactory.getContext(self)
77 ctx.set_verify_depth(0)
78 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
81 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
82 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
87 def __init__(self, tbl_version):
88 print _('Initializing Tor Browser Launcher')
89 self.tbl_version = tbl_version
92 self.default_mirror = 'https://www.torproject.org/dist/'
93 self.discover_arch_lang()
95 for d in self.paths['dirs']:
96 self.mkdir(self.paths['dirs'][d])
99 self.mkdir(self.paths['download_dir'])
100 self.mkdir(self.paths['tbb']['dir'])
103 # allow buttons to have icons
105 gtk_settings = gtk.settings_get_default()
106 gtk_settings.props.gtk_button_images = True
110 # discover the architecture and language
111 def discover_arch_lang(self):
112 # figure out the architecture
113 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
115 # figure out the language
116 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
117 default_locale = locale.getdefaultlocale()[0]
118 if default_locale is None:
119 self.language = 'en-US'
121 self.language = default_locale.replace('_', '-')
122 if self.language not in available_languages:
123 self.language = self.language.split('-')[0]
124 if self.language not in available_languages:
125 for l in available_languages:
126 if l[0:2] == self.language:
128 # if language isn't available, default to english
129 if self.language not in available_languages:
130 self.language = 'en-US'
132 # build all relevant paths
133 def build_paths(self, tbb_version=None):
134 homedir = os.getenv('HOME')
136 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
137 if not os.path.exists(homedir):
139 os.mkdir(homedir, 0700)
141 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
142 if not os.access(homedir, os.W_OK):
143 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
145 tbb_config = '{0}/.config/torbrowser'.format(homedir)
146 tbb_cache = '{0}/.cache/torbrowser'.format(homedir)
147 tbb_local = '{0}/.local/share/torbrowser'.format(homedir)
148 old_tbb_data = '{0}/.torbrowser'.format(homedir)
152 if self.architecture == 'x86_64':
156 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
159 self.paths['tarball_url'] = '{0}torbrowser/'+tbb_version+'/'+tarball_filename
160 self.paths['tarball_file'] = tbb_cache+'/download/'+tarball_filename
161 self.paths['tarball_filename'] = tarball_filename
164 self.paths['sha256_file'] = tbb_cache+'/download/sha256sums.txt'
165 self.paths['sha256_sig_file'] = tbb_cache+'/download/sha256sums.txt.asc'
166 self.paths['sha256_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt'
167 self.paths['sha256_sig_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt.asc'
171 'config': tbb_config,
175 'old_data_dir': old_tbb_data,
177 'icon_file': os.path.join(os.path.dirname(SHARE), 'pixmaps/torbrowser80.xpm'),
178 'torproject_pem': os.path.join(SHARE, 'torproject.pem'),
179 'erinn_key': os.path.join(SHARE, 'erinn.asc'),
180 'mirrors_txt': [os.path.join(SHARE, 'mirrors.txt'),
181 tbb_config+'/mirrors.txt'],
182 'modem_sound': os.path.join(SHARE, 'modem.ogg'),
183 'download_dir': tbb_cache+'/download',
184 'gnupg_homedir': tbb_local+'/gnupg_homedir',
185 'settings_file': tbb_config+'/settings',
186 'update_check_url': 'https://www.torproject.org/projects/torbrowser/RecommendedTBBVersions',
187 'update_check_file': tbb_cache+'/download/RecommendedTBBVersions',
189 'dir': tbb_local+'/tbb/'+self.architecture,
190 'start': tbb_local+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
191 'versions': tbb_local+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
199 if not os.path.exists(path):
200 os.makedirs(path, 0700)
203 print _("Cannot create directory {0}").format(path)
205 if not os.access(path, os.W_OK):
206 print _("{0} is not writable").format(path)
210 # if gnupg_homedir isn't set up, set it up
211 def init_gnupg(self):
212 if not os.path.exists(self.paths['gnupg_homedir']):
213 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
214 self.mkdir(self.paths['gnupg_homedir'])
218 def import_keys(self):
219 print _('Importing keys')
220 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key']]).wait()
223 def load_mirrors(self):
225 for srcfile in self.paths['mirrors_txt']:
226 if os.path.exists(srcfile):
227 print "Successfully loaded mirrors from %s" % srcfile
228 elif not os.path.exists(srcfile):
229 print "Warning: can't load mirrors from %s" % srcfile
231 for mirror in open(srcfile, 'r').readlines():
232 if mirror.strip() not in self.mirrors:
233 self.mirrors.append(mirror.strip())
236 def load_settings(self):
238 'tbl_version': self.tbl_version,
239 'installed_version': False,
240 'latest_version': '0',
241 'update_over_tor': True,
242 'check_for_updates': False,
243 'modem_sound': False,
244 'last_update_check_timestamp': 0,
245 'mirror': self.default_mirror
248 if os.path.isfile(self.paths['settings_file']):
249 settings = pickle.load(open(self.paths['settings_file']))
252 # settings migrations
253 if settings['tbl_version'] <= '0.1.0':
254 print '0.1.0 migration'
255 settings['installed_version'] = settings['installed_version']['stable']
256 settings['latest_version'] = settings['latest_version']['stable']
259 # make new tbb folder
260 self.mkdir(self.paths['tbb']['dir'])
261 old_tbb_dir = self.paths['old_data_dir']+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language
262 new_tbb_dir = self.paths['tbb']['dir']+'/tor-browser_'+self.language
263 if os.path.isdir(old_tbb_dir):
264 os.rename(old_tbb_dir, new_tbb_dir)
266 # make sure settings file is up-to-date
267 for setting in default_settings:
268 if setting not in settings:
269 settings[setting] = default_settings[setting]
272 # make sure the version is current
273 if settings['tbl_version'] != self.tbl_version:
274 settings['tbl_version'] = self.tbl_version
277 self.settings = settings
282 self.settings = default_settings
286 def save_settings(self):
287 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
290 # get the process id of a program
292 def get_pid(bin_path, python=False):
295 for p in psutil.process_iter():
297 if p.pid != os.getpid():
300 if len(p.cmdline) > 1:
301 if 'python' in p.cmdline[0]:
304 if len(p.cmdline) > 0:
315 # bring program's x window to front
317 def bring_window_to_front(pid):
318 # figure out the window id
320 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
321 for line in p.stdout.readlines():
322 line_split = line.split()
323 cur_win_id = line_split[0]
324 cur_win_pid = int(line_split[2])
325 if cur_win_pid == pid:
330 subprocess.call(['wmctrl', '-i', '-a', win_id])
334 def __init__(self, common):
335 print _('Starting settings dialog')
339 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
340 self.window.set_title(_("Tor Browser Launcher Settings"))
341 self.window.set_icon_from_file(self.common.paths['icon_file'])
342 self.window.set_position(gtk.WIN_POS_CENTER)
343 self.window.set_border_width(10)
344 self.window.connect("delete_event", self.delete_event)
345 self.window.connect("destroy", self.destroy)
347 # build the rest of the UI
348 self.box = gtk.VBox(False, 10)
349 self.window.add(self.box)
352 self.hbox = gtk.HBox(False, 10)
353 self.box.pack_start(self.hbox, True, True, 0)
356 self.settings_box = gtk.VBox(False, 10)
357 self.hbox.pack_start(self.settings_box, True, True, 0)
358 self.settings_box.show()
360 self.labels_box = gtk.VBox(False, 10)
361 self.hbox.pack_start(self.labels_box, True, True, 0)
362 self.labels_box.show()
367 self.txsocks_found = True
369 self.txsocks_found = False
370 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
371 if self.txsocks_found:
372 self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
374 self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))
376 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
377 if self.common.settings['update_over_tor'] and self.txsocks_found:
378 self.tor_update_checkbox.set_active(True)
380 self.tor_update_checkbox.set_active(False)
382 if self.txsocks_found == False:
383 self.tor_update_checkbox.set_sensitive(False)
385 self.tor_update_checkbox.show()
388 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
389 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
390 if self.common.settings['check_for_updates']:
391 self.update_checkbox.set_active(True)
393 self.update_checkbox.set_active(False)
394 self.update_checkbox.show()
397 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
398 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
402 if self.common.settings['modem_sound']:
403 self.modem_checkbox.set_active(True)
405 self.modem_checkbox.set_active(False)
407 self.modem_checkbox.set_active(False)
408 self.modem_checkbox.set_sensitive(False)
409 self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
410 self.modem_checkbox.show()
413 if(self.common.settings['installed_version']):
414 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version']))
416 self.label1 = gtk.Label(_('Not installed'))
417 self.label1.set_line_wrap(True)
418 self.labels_box.pack_start(self.label1, True, True, 0)
421 if(self.common.settings['last_update_check_timestamp'] > 0):
422 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']))))
424 self.label1 = gtk.Label(_('Never checked for updates'))
425 self.label1.set_line_wrap(True)
426 self.labels_box.pack_start(self.label1, True, True, 0)
430 self.mirrors_box = gtk.HBox(False, 10)
431 self.box.pack_start(self.mirrors_box, True, True, 0)
432 self.mirrors_box.show()
434 self.mirrors_label = gtk.Label(_('Mirror'))
435 self.mirrors_label.set_line_wrap(True)
436 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
437 self.mirrors_label.show()
439 self.mirrors = gtk.combo_box_new_text()
440 for mirror in self.common.mirrors:
441 self.mirrors.append_text(mirror)
442 if self.common.settings['mirror'] in self.common.mirrors:
443 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
445 self.mirrors.set_active(0)
446 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
450 self.button_box = gtk.HButtonBox()
451 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
452 self.box.pack_start(self.button_box, True, True, 0)
453 self.button_box.show()
455 # save and launch button
456 save_launch_image = gtk.Image()
457 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
458 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
459 self.save_launch_button.set_image(save_launch_image)
460 self.save_launch_button.connect("clicked", self.save_launch, None)
461 self.button_box.add(self.save_launch_button)
462 self.save_launch_button.show()
464 # save and exit button
465 save_exit_image = gtk.Image()
466 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
467 self.save_exit_button = gtk.Button(_("Save & Exit"))
468 self.save_exit_button.set_image(save_exit_image)
469 self.save_exit_button.connect("clicked", self.save_exit, None)
470 self.button_box.add(self.save_exit_button)
471 self.save_exit_button.show()
474 cancel_image = gtk.Image()
475 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
476 self.cancel_button = gtk.Button(_("Cancel"))
477 self.cancel_button.set_image(cancel_image)
478 self.cancel_button.connect("clicked", self.destroy, None)
479 self.button_box.add(self.cancel_button)
480 self.cancel_button.show()
488 # UI Callback for update over tor/use system tor
489 def on_system_tor_clicked(self, event):
490 if self.txsocks_found:
491 value = self.system_tor_checkbox.get_active()
495 self.tor_update_checkbox.set_active(value)
496 self.tor_update_checkbox.set_sensitive(value)
499 def save_launch(self, widget, data=None):
501 subprocess.Popen([self.common.paths['tbl_bin']])
505 def save_exit(self, widget, data=None):
512 self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
513 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
514 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
516 # figure out the selected mirror
517 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
520 self.common.save_settings()
523 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 print "LATEST VERSION", self.common.settings['latest_version']
539 self.common.build_paths(self.common.settings['latest_version'])
541 if self.common.settings['update_over_tor']:
545 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-txsocksx package is missing, downloads will not happen over tor"))
546 md.set_position(gtk.WIN_POS_CENTER)
549 self.common.settings['update_over_tor'] = False
550 self.common.save_settings()
552 # is firefox already running?
553 if self.common.settings['installed_version']:
554 firefox_pid = self.common.get_pid('./Browser/firefox')
556 print _('Firefox is open, bringing to focus')
557 # bring firefox to front
558 self.common.bring_window_to_front(firefox_pid)
562 check_for_updates = False
563 if self.common.settings['check_for_updates']:
564 check_for_updates = True
566 if not check_for_updates:
567 # how long was it since the last update check?
568 # 86400 seconds = 24 hours
569 current_timestamp = int(time.time())
570 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
571 check_for_updates = True
573 if check_for_updates:
575 print 'Checking for update'
576 self.set_gui('task', _("Checking for Tor Browser update."),
577 ['download_update_check',
580 # no need to check for update
581 print _('Checked for update within 24 hours, skipping')
582 self.start_launcher()
586 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
587 self.window.set_title(_("Tor Browser"))
588 self.window.set_icon_from_file(self.common.paths['icon_file'])
589 self.window.set_position(gtk.WIN_POS_CENTER)
590 self.window.set_border_width(10)
591 self.window.connect("delete_event", self.delete_event)
592 self.window.connect("destroy", self.destroy)
594 # build the rest of the UI
597 # download or run TBB
598 def start_launcher(self):
599 # is TBB already installed?
600 latest_version = self.common.settings['latest_version']
601 installed_version = self.common.settings['installed_version']
603 # verify installed version for newer versions of TBB (#58)
604 if installed_version >= '3.0':
605 versions_filename = self.common.paths['tbb']['versions']
606 if os.path.exists(versions_filename):
607 for line in open(versions_filename):
608 if 'TORBROWSER_VERSION' in line:
609 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
611 start = self.common.paths['tbb']['start']
612 if os.path.isfile(start) and os.access(start, os.X_OK):
613 if installed_version == latest_version:
614 print _('Latest version of TBB is installed, launching')
615 # current version of tbb is installed, launch it
617 self.launch_gui = False
618 elif installed_version < latest_version:
619 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
620 # there is a tbb upgrade available
621 self.set_gui('task', _("Your Tor Browser is out of date."),
623 'download_sha256_sig',
629 # for some reason the installed tbb is newer than the current version?
630 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
634 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
635 self.set_gui('task', _("Downloading and installing Tor Browser."),
637 'download_sha256_sig',
643 # there are different GUIs that might appear, this sets which one we want
644 def set_gui(self, gui, message, tasks, autostart=True):
646 self.gui_message = message
647 self.gui_tasks = tasks
649 self.gui_autostart = autostart
651 # set all gtk variables to False
653 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
658 self.progressbar = False
659 self.button_box = False
660 self.start_button = False
661 self.exit_button = False
663 # build the application's UI
667 self.box = gtk.VBox(False, 20)
668 self.window.add(self.box)
670 if 'error' in self.gui:
672 self.label = gtk.Label(self.gui_message)
673 self.label.set_line_wrap(True)
674 self.box.pack_start(self.label, True, True, 0)
678 self.button_box = gtk.HButtonBox()
679 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
680 self.box.pack_start(self.button_box, True, True, 0)
681 self.button_box.show()
683 if self.gui != 'error':
685 yes_image = gtk.Image()
686 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
687 self.yes_button = gtk.Button("Yes")
688 self.yes_button.set_image(yes_image)
689 if self.gui == 'error_try_stable':
690 self.yes_button.connect("clicked", self.try_stable, None)
691 elif self.gui == 'error_try_default_mirror':
692 self.yes_button.connect("clicked", self.try_default_mirror, None)
693 elif self.gui == 'error_try_tor':
694 self.yes_button.connect("clicked", self.try_tor, None)
695 self.button_box.add(self.yes_button)
696 self.yes_button.show()
699 exit_image = gtk.Image()
700 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
701 self.exit_button = gtk.Button("Exit")
702 self.exit_button.set_image(exit_image)
703 self.exit_button.connect("clicked", self.destroy, None)
704 self.button_box.add(self.exit_button)
705 self.exit_button.show()
707 elif self.gui == 'task':
709 self.label = gtk.Label(self.gui_message)
710 self.label.set_line_wrap(True)
711 self.box.pack_start(self.label, True, True, 0)
715 self.progressbar = gtk.ProgressBar(adjustment=None)
716 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
717 self.progressbar.set_pulse_step(0.01)
718 self.box.pack_start(self.progressbar, True, True, 0)
721 self.button_box = gtk.HButtonBox()
722 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
723 self.box.pack_start(self.button_box, True, True, 0)
724 self.button_box.show()
727 start_image = gtk.Image()
728 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
729 self.start_button = gtk.Button(_("Start"))
730 self.start_button.set_image(start_image)
731 self.start_button.connect("clicked", self.start, None)
732 self.button_box.add(self.start_button)
733 if not self.gui_autostart:
734 self.start_button.show()
737 exit_image = gtk.Image()
738 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
739 self.exit_button = gtk.Button(_("Exit"))
740 self.exit_button.set_image(exit_image)
741 self.exit_button.connect("clicked", self.destroy, None)
742 self.button_box.add(self.exit_button)
743 self.exit_button.show()
748 if self.gui_autostart:
751 # start button clicked, begin tasks
752 def start(self, widget, data=None):
753 # disable the start button
754 if self.start_button:
755 self.start_button.set_sensitive(False)
757 # start running tasks
760 # run the next task in the task list
764 if self.gui_task_i >= len(self.gui_tasks):
768 task = self.gui_tasks[self.gui_task_i]
770 # get ready for the next task
773 print _('Running task: {0}'.format(task))
774 if task == 'download_update_check':
775 print _('Downloading'), self.common.paths['update_check_url']
776 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
778 if task == 'attempt_update':
779 print _('Checking to see if update is needed')
780 self.attempt_update()
782 elif task == 'download_sha256':
783 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
784 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
786 elif task == 'download_sha256_sig':
787 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
788 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
790 elif task == 'download_tarball':
791 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
792 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
794 elif task == 'verify':
795 print _('Verifying signature')
798 elif task == 'extract':
799 print _('Extracting'), self.common.paths['tarball_filename']
803 print _('Running'), self.common.paths['tbb']['start']
806 elif task == 'start_over':
807 print _('Starting download over again')
810 def response_received(self, response):
811 class FileDownloader(Protocol):
812 def __init__(self, common, file, url, total, progress, done_cb):
816 self.progress = progress
817 self.all_done = done_cb
819 if response.code != 200:
820 if common.settings['mirror'] != common.default_mirror:
821 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']))
823 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
825 def dataReceived(self, bytes):
826 self.file.write(bytes)
827 self.so_far += len(bytes)
828 percent = float(self.so_far) / float(self.total)
829 self.progress.set_fraction(percent)
830 amount = float(self.so_far)
832 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
835 amount = amount / float(size)
838 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
840 def connectionLost(self, reason):
841 print _('Finished receiving body:'), reason.getErrorMessage()
842 self.all_done(reason)
844 if hasattr(self, 'current_download_url'):
845 url = self.current_download_url
849 dl = FileDownloader(self.common, self.file_download, url, response.length, self.progressbar, self.response_finished)
850 response.deliverBody(dl)
852 def response_finished(self, msg):
853 if msg.check(ResponseDone):
854 self.file_download.close()
855 delattr(self, 'current_download_path')
856 delattr(self, 'current_download_url')
862 print "FINISHED", msg
863 ## FIXME handle errors
865 def download_error(self, f):
866 print _("Download error:"), f.value, type(f.value)
868 if isinstance(f.value, TryStableException):
869 f.trap(TryStableException)
870 self.set_gui('error_try_stable', str(f.value), [], False)
872 elif isinstance(f.value, TryDefaultMirrorException):
873 f.trap(TryDefaultMirrorException)
874 self.set_gui('error_try_default_mirror', str(f.value), [], False)
876 elif isinstance(f.value, DownloadErrorException):
877 f.trap(DownloadErrorException)
878 self.set_gui('error', str(f.value), [], False)
880 elif isinstance(f.value, DNSLookupError):
881 f.trap(DNSLookupError)
882 if common.settings['mirror'] != common.default_mirror:
883 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)
885 self.set_gui('error', str(f.value), [], False)
887 elif isinstance(f.value, ResponseFailed):
888 for reason in f.value.reasons:
889 if isinstance(reason.value, OpenSSL.SSL.Error):
890 # TODO: add the ability to report attack by posting bug to trac.torproject.org
891 if not self.common.settings['update_over_tor']:
892 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)
894 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
897 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
901 def download(self, name, url, path):
902 # keep track of current download
903 self.current_download_path = path
904 self.current_download_url = url
906 # initialize the progress bar
907 mirror_url = url.format(self.common.settings['mirror'])
908 self.progressbar.set_fraction(0)
909 self.progressbar.set_text(_('Downloading {0}').format(name))
910 self.progressbar.show()
913 if self.common.settings['update_over_tor']:
914 print _('Updating over Tor')
915 from twisted.internet.endpoints import TCP4ClientEndpoint
916 from txsocksx.http import SOCKS5Agent
918 torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
920 # default mirror gets certificate pinning, only for requests that use the mirror
921 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
922 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
924 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
926 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
927 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
929 agent = Agent(reactor)
931 # actually, agent needs to follow redirect
932 agent = RedirectAgent(agent)
935 d = agent.request('GET', mirror_url,
936 Headers({'User-Agent': ['torbrowser-launcher']}),
939 self.file_download = open(path, 'w')
940 d.addCallback(self.response_received).addErrback(self.download_error)
942 if not reactor.running:
945 def try_default_mirror(self, widget, data=None):
946 # change mirror to default and relaunch TBL
947 self.common.settings['mirror'] = self.common.default_mirror
948 self.common.save_settings()
949 subprocess.Popen([self.common.paths['tbl_bin']])
952 def try_tor(self, widget, data=None):
953 # set update_over_tor to true and relaunch TBL
954 self.common.settings['update_over_tor'] = True
955 self.common.save_settings()
956 subprocess.Popen([self.common.paths['tbl_bin']])
959 def attempt_update(self):
960 # load the update check file
962 versions = json.load(open(self.common.paths['update_check_file']))
965 # filter linux versions
967 for version in versions:
968 if '-Linux' in version:
969 valid.append(str(version))
976 # remove alphas/betas
977 for version in valid:
978 if '-alpha-' not in version and '-beta-' not in version:
979 stable.append(version)
981 latest = stable.pop()
986 self.common.settings['latest_version'] = latest[:-len('-Linux')]
987 self.common.settings['last_update_check_timestamp'] = int(time.time())
988 self.common.settings['check_for_updates'] = False
989 self.common.save_settings()
990 self.common.build_paths(self.common.settings['latest_version'])
991 self.start_launcher()
994 # failed to find the latest version
995 self.set_gui('error', _("Error checking for updates."), [], False)
998 # not a valid JSON object
999 self.set_gui('error', _("Error checking for updates."), [], False)
1006 # initialize the progress bar
1007 self.progressbar.set_fraction(0)
1008 self.progressbar.set_text(_('Verifying Signature'))
1009 self.progressbar.show()
1012 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1013 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1014 self.pulse_until_process_exits(p)
1015 if p.returncode == 0:
1016 # compare with sha256 of the tarball
1017 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1018 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1019 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1025 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1026 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)
1030 if not reactor.running:
1034 # initialize the progress bar
1035 self.progressbar.set_fraction(0)
1036 self.progressbar.set_text(_('Installing'))
1037 self.progressbar.show()
1042 if self.common.paths['tarball_file'][-2:] == 'xz':
1043 # if tarball is .tar.xz
1044 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1045 tf = tarfile.open(fileobj=xz)
1046 tf.extractall(self.common.paths['tbb']['dir'])
1049 # if tarball is .tar.gz
1050 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1051 tf = tarfile.open(self.common.paths['tarball_file'])
1052 tf.extractall(self.common.paths['tbb']['dir'])
1058 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1063 # installation is finished, so save installed_version
1064 self.common.settings['installed_version'] = self.common.settings['latest_version']
1065 self.common.save_settings()
1069 def run(self, run_next_task=True):
1070 devnull = open('/dev/null', 'w')
1071 subprocess.Popen([self.common.paths['tbb']['start']], stdout=devnull, stderr=devnull)
1074 if self.common.settings['modem_sound']:
1078 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1082 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-pygame package is missing, the modem sound is unavailable."))
1083 md.set_position(gtk.WIN_POS_CENTER)
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() is 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', '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):
1113 def destroy(self, widget, data=None):
1114 if hasattr(self, 'file_download'):
1115 self.file_download.close()
1116 if hasattr(self, 'current_download_path'):
1117 os.remove(self.current_download_path)
1118 delattr(self, 'current_download_path')
1119 delattr(self, 'current_download_url')
1123 if __name__ == "__main__":
1124 with open(os.path.join(SHARE, 'version')) as buf:
1125 tor_browser_launcher_version = buf.read().strip()
1127 print _('Tor Browser Launcher')
1128 print _('By Micah Lee, licensed under GPLv3')
1129 print _('version {0}').format(tor_browser_launcher_version)
1130 print 'https://github.com/micahflee/torbrowser-launcher'
1132 common = TBLCommon(tor_browser_launcher_version)
1134 # is torbrowser-launcher already running?
1135 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1137 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1138 common.bring_window_to_front(tbl_pid)
1141 if '-settings' in sys.argv:
1143 app = TBLSettings(common)
1147 app = TBLLauncher(common)