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.
31 SHARE = os.getenv('TBL_SHARE', '/usr/share')
36 gettext.install('torbrowser-launcher', os.path.join(SHARE, 'torbrowser-launcher/locale'))
38 from twisted.internet import gtk2reactor
40 from twisted.internet import reactor
46 import subprocess, locale, time, pickle, json, tarfile, psutil, hashlib, lzma
48 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
49 from twisted.web.http_headers import Headers
50 from twisted.internet.protocol import Protocol
51 from twisted.internet.ssl import ClientContextFactory
52 from twisted.internet.error import DNSLookupError
57 class TryStableException(Exception):
61 class TryDefaultMirrorException(Exception):
65 class DownloadErrorException(Exception):
69 class VerifyTorProjectCert(ClientContextFactory):
71 def __init__(self, torproject_pem):
72 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
74 def getContext(self, host, port):
75 ctx = ClientContextFactory.getContext(self)
76 ctx.set_verify_depth(0)
77 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
80 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
81 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
86 def __init__(self, tbl_version):
87 print _('Initializing Tor Browser Launcher')
88 self.tbl_version = tbl_version
91 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']['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 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
113 # figure out the language
114 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
115 default_locale = locale.getdefaultlocale()[0]
116 if default_locale is None:
117 self.language = 'en-US'
119 self.language = default_locale.replace('_', '-')
120 if self.language not in available_languages:
121 self.language = self.language.split('-')[0]
122 if self.language not in available_languages:
123 for l in available_languages:
124 if l[0:2] == self.language:
126 # if language isn't available, default to english
127 if self.language not in available_languages:
128 self.language = 'en-US'
130 # build all relevant paths
131 def build_paths(self, tbb_version=None):
132 homedir = os.getenv('HOME')
134 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
135 if not os.path.exists(homedir):
137 os.mkdir(homedir, 0700)
139 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
140 if not os.access(homedir, os.W_OK):
141 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
143 tbb_data = '%s/.torbrowser' % homedir
147 if self.architecture == 'x86_64':
151 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
154 self.paths['tarball_url'] = '{0}torbrowser/'+tbb_version+'/'+tarball_filename
155 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
156 self.paths['tarball_filename'] = tarball_filename
159 self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
160 self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
161 self.paths['sha256_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt'
162 self.paths['sha256_sig_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt-mikeperry.asc'
165 'tbl_bin': '/usr/bin/torbrowser-launcher',
166 'icon_file': os.path.join(SHARE, 'pixmaps/torbrowser80.xpm'),
167 'torproject_pem': os.path.join(SHARE, 'torbrowser-launcher/torproject.pem'),
168 'erinn_key': os.path.join(SHARE, 'torbrowser-launcher/erinn.asc'),
169 'sebastian_key': os.path.join(SHARE, 'torbrowser-launcher/sebastian.asc'),
170 'alexandre_key': os.path.join(SHARE, 'torbrowser-launcher/alexandre.asc'),
171 'mike_key': os.path.join(SHARE, 'torbrowser-launcher/mike-2013-09.asc'),
172 'mirrors_txt': [os.path.join(SHARE, 'torbrowser-launcher/mirrors.txt'),
173 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
174 'modem_sound': os.path.join(SHARE, 'torbrowser-launcher/modem.ogg'),
175 'data_dir': tbb_data,
176 'download_dir': tbb_data+'/download',
177 'gnupg_homedir': tbb_data+'/gnupg_homedir',
178 'settings_file': tbb_data+'/settings',
179 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
180 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
182 'dir': tbb_data+'/tbb/'+self.architecture,
183 'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
184 'versions': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
192 if not os.path.exists(path):
193 os.makedirs(path, 0700)
196 print _("Cannot create directory {0}").format(path)
198 if not os.access(path, os.W_OK):
199 print _("{0} is not writable").format(path)
203 # if gnupg_homedir isn't set up, set it up
204 def init_gnupg(self):
205 if not os.path.exists(self.paths['gnupg_homedir']):
206 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
207 self.mkdir(self.paths['gnupg_homedir'])
211 def import_keys(self):
212 print _('Importing keys')
213 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()
216 def load_mirrors(self):
218 for srcfile in self.paths['mirrors_txt']:
219 if os.path.exists(srcfile):
220 print "Successfully loaded mirrors from %s" % srcfile
221 elif not os.path.exists(srcfile):
222 print "Warning: can't load mirrors from %s" % srcfile
224 for mirror in open(srcfile, 'r').readlines():
225 if mirror.strip() not in self.mirrors:
226 self.mirrors.append(mirror.strip())
229 def load_settings(self):
231 'tbl_version': self.tbl_version,
232 'installed_version': False,
233 'latest_version': '0',
234 'update_over_tor': True,
235 'check_for_updates': False,
236 'modem_sound': False,
237 'last_update_check_timestamp': 0,
238 'mirror': self.default_mirror
241 if os.path.isfile(self.paths['settings_file']):
242 settings = pickle.load(open(self.paths['settings_file']))
245 # settings migrations
246 if settings['tbl_version'] <= '0.1.0':
247 print '0.1.0 migration'
248 settings['installed_version'] = settings['installed_version']['stable']
249 settings['latest_version'] = settings['latest_version']['stable']
252 # make new tbb folder
253 self.mkdir(self.paths['tbb']['dir'])
254 old_tbb_dir = self.paths['data_dir']+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language
255 new_tbb_dir = self.paths['tbb']['dir']+'/tor-browser_'+self.language
256 if os.path.isdir(old_tbb_dir):
257 os.rename(old_tbb_dir, new_tbb_dir)
259 # make sure settings file is up-to-date
260 for setting in default_settings:
261 if setting not in settings:
262 settings[setting] = default_settings[setting]
265 # make sure the version is current
266 if settings['tbl_version'] != self.tbl_version:
267 settings['tbl_version'] = self.tbl_version
270 self.settings = settings
275 self.settings = default_settings
279 def save_settings(self):
280 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
283 # get the process id of a program
285 def get_pid(bin_path, python=False):
288 for p in psutil.process_iter():
290 if p.pid != os.getpid():
293 if len(p.cmdline) > 1:
294 if 'python' in p.cmdline[0]:
297 if len(p.cmdline) > 0:
308 # bring program's x window to front
310 def bring_window_to_front(pid):
311 # figure out the window id
313 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
314 for line in p.stdout.readlines():
315 line_split = line.split()
316 cur_win_id = line_split[0]
317 cur_win_pid = int(line_split[2])
318 if cur_win_pid == pid:
323 subprocess.call(['wmctrl', '-i', '-a', win_id])
327 def __init__(self, common):
328 print _('Starting settings dialog')
332 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
333 self.window.set_title(_("Tor Browser Launcher Settings"))
334 self.window.set_icon_from_file(self.common.paths['icon_file'])
335 self.window.set_position(gtk.WIN_POS_CENTER)
336 self.window.set_border_width(10)
337 self.window.connect("delete_event", self.delete_event)
338 self.window.connect("destroy", self.destroy)
340 # build the rest of the UI
341 self.box = gtk.VBox(False, 10)
342 self.window.add(self.box)
345 self.hbox = gtk.HBox(False, 10)
346 self.box.pack_start(self.hbox, True, True, 0)
349 self.settings_box = gtk.VBox(False, 10)
350 self.hbox.pack_start(self.settings_box, True, True, 0)
351 self.settings_box.show()
353 self.labels_box = gtk.VBox(False, 10)
354 self.hbox.pack_start(self.labels_box, True, True, 0)
355 self.labels_box.show()
360 self.txsocks_found = True
362 self.txsocks_found = False
363 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
364 if self.txsocks_found:
365 self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
367 self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))
369 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
370 if self.common.settings['update_over_tor'] and self.txsocks_found:
371 self.tor_update_checkbox.set_active(True)
373 self.tor_update_checkbox.set_active(False)
375 if self.txsocks_found == False:
376 self.tor_update_checkbox.set_sensitive(False)
378 self.tor_update_checkbox.show()
381 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
382 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
383 if self.common.settings['check_for_updates']:
384 self.update_checkbox.set_active(True)
386 self.update_checkbox.set_active(False)
387 self.update_checkbox.show()
390 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
391 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
395 if self.common.settings['modem_sound']:
396 self.modem_checkbox.set_active(True)
398 self.modem_checkbox.set_active(False)
400 self.modem_checkbox.set_active(False)
401 self.modem_checkbox.set_sensitive(False)
402 self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
403 self.modem_checkbox.show()
406 if(self.common.settings['installed_version']):
407 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version']))
409 self.label1 = gtk.Label(_('Not installed'))
410 self.label1.set_line_wrap(True)
411 self.labels_box.pack_start(self.label1, True, True, 0)
414 if(self.common.settings['last_update_check_timestamp'] > 0):
415 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']))))
417 self.label1 = gtk.Label(_('Never checked for updates'))
418 self.label1.set_line_wrap(True)
419 self.labels_box.pack_start(self.label1, True, True, 0)
423 self.mirrors_box = gtk.HBox(False, 10)
424 self.box.pack_start(self.mirrors_box, True, True, 0)
425 self.mirrors_box.show()
427 self.mirrors_label = gtk.Label(_('Mirror'))
428 self.mirrors_label.set_line_wrap(True)
429 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
430 self.mirrors_label.show()
432 self.mirrors = gtk.combo_box_new_text()
433 for mirror in self.common.mirrors:
434 self.mirrors.append_text(mirror)
435 if self.common.settings['mirror'] in self.common.mirrors:
436 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
438 self.mirrors.set_active(0)
439 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
443 self.button_box = gtk.HButtonBox()
444 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
445 self.box.pack_start(self.button_box, True, True, 0)
446 self.button_box.show()
448 # save and launch button
449 save_launch_image = gtk.Image()
450 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
451 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
452 self.save_launch_button.set_image(save_launch_image)
453 self.save_launch_button.connect("clicked", self.save_launch, None)
454 self.button_box.add(self.save_launch_button)
455 self.save_launch_button.show()
457 # save and exit button
458 save_exit_image = gtk.Image()
459 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
460 self.save_exit_button = gtk.Button(_("Save & Exit"))
461 self.save_exit_button.set_image(save_exit_image)
462 self.save_exit_button.connect("clicked", self.save_exit, None)
463 self.button_box.add(self.save_exit_button)
464 self.save_exit_button.show()
467 cancel_image = gtk.Image()
468 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
469 self.cancel_button = gtk.Button(_("Cancel"))
470 self.cancel_button.set_image(cancel_image)
471 self.cancel_button.connect("clicked", self.destroy, None)
472 self.button_box.add(self.cancel_button)
473 self.cancel_button.show()
481 # UI Callback for update over tor/use system tor
482 def on_system_tor_clicked(self, event):
483 if self.txsocks_found:
484 value = self.system_tor_checkbox.get_active()
488 self.tor_update_checkbox.set_active(value)
489 self.tor_update_checkbox.set_sensitive(value)
492 def save_launch(self, widget, data=None):
494 subprocess.Popen([self.common.paths['tbl_bin']])
498 def save_exit(self, widget, data=None):
505 self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
506 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
507 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
509 # figure out the selected mirror
510 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
513 self.common.save_settings()
516 def delete_event(self, widget, event, data=None):
519 def destroy(self, widget, data=None):
524 def __init__(self, common):
525 print _('Starting launcher dialog')
529 self.set_gui(None, '', [])
530 self.launch_gui = True
531 print "LATEST VERSION", self.common.settings['latest_version']
532 self.common.build_paths(self.common.settings['latest_version'])
534 if self.common.settings['update_over_tor']:
538 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"))
539 md.set_position(gtk.WIN_POS_CENTER)
542 self.common.settings['update_over_tor'] = False
543 self.common.save_settings()
545 # is firefox already running?
546 if self.common.settings['installed_version']:
547 firefox_pid = self.common.get_pid('./Browser/firefox')
549 print _('Firefox are is open, bringing to focus')
550 # bring firefox to front
551 self.common.bring_window_to_front(firefox_pid)
555 check_for_updates = False
556 if self.common.settings['check_for_updates']:
557 check_for_updates = True
559 if not check_for_updates:
560 # how long was it since the last update check?
561 # 86400 seconds = 24 hours
562 current_timestamp = int(time.time())
563 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
564 check_for_updates = True
566 if check_for_updates:
568 print 'Checking for update'
569 self.set_gui('task', _("Checking for Tor Browser update."),
570 ['download_update_check',
573 # no need to check for update
574 print _('Checked for update within 24 hours, skipping')
575 self.start_launcher()
579 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
580 self.window.set_title(_("Tor Browser"))
581 self.window.set_icon_from_file(self.common.paths['icon_file'])
582 self.window.set_position(gtk.WIN_POS_CENTER)
583 self.window.set_border_width(10)
584 self.window.connect("delete_event", self.delete_event)
585 self.window.connect("destroy", self.destroy)
587 # build the rest of the UI
590 # download or run TBB
591 def start_launcher(self):
592 # is TBB already installed?
593 latest_version = self.common.settings['latest_version']
594 installed_version = self.common.settings['installed_version']
596 # verify installed version for newer versions of TBB (#58)
597 if installed_version >= '3.0':
598 versions_filename = self.common.paths['tbb']['versions']
599 if os.path.exists(versions_filename):
600 for line in open(versions_filename):
601 if 'TORBROWSER_VERSION' in line:
602 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
604 start = self.common.paths['tbb']['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 self.set_gui('task', _("Your Tor Browser is out of date."),
616 'download_sha256_sig',
622 # for some reason the installed tbb is newer than the current version?
623 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
627 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
628 self.set_gui('task', _("Downloading and installing Tor Browser."),
630 'download_sha256_sig',
636 # there are different GUIs that might appear, this sets which one we want
637 def set_gui(self, gui, message, tasks, autostart=True):
639 self.gui_message = message
640 self.gui_tasks = tasks
642 self.gui_autostart = autostart
644 # set all gtk variables to False
646 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
651 self.progressbar = False
652 self.button_box = False
653 self.start_button = False
654 self.exit_button = False
656 # build the application's UI
660 self.box = gtk.VBox(False, 20)
661 self.window.add(self.box)
663 if 'error' in self.gui:
665 self.label = gtk.Label(self.gui_message)
666 self.label.set_line_wrap(True)
667 self.box.pack_start(self.label, True, True, 0)
671 self.button_box = gtk.HButtonBox()
672 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
673 self.box.pack_start(self.button_box, True, True, 0)
674 self.button_box.show()
676 if self.gui != 'error':
678 yes_image = gtk.Image()
679 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
680 self.yes_button = gtk.Button("Yes")
681 self.yes_button.set_image(yes_image)
682 if self.gui == 'error_try_stable':
683 self.yes_button.connect("clicked", self.try_stable, None)
684 elif self.gui == 'error_try_default_mirror':
685 self.yes_button.connect("clicked", self.try_default_mirror, None)
686 elif self.gui == 'error_try_tor':
687 self.yes_button.connect("clicked", self.try_tor, None)
688 self.button_box.add(self.yes_button)
689 self.yes_button.show()
692 exit_image = gtk.Image()
693 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
694 self.exit_button = gtk.Button("Exit")
695 self.exit_button.set_image(exit_image)
696 self.exit_button.connect("clicked", self.destroy, None)
697 self.button_box.add(self.exit_button)
698 self.exit_button.show()
700 elif self.gui == 'task':
702 self.label = gtk.Label(self.gui_message)
703 self.label.set_line_wrap(True)
704 self.box.pack_start(self.label, True, True, 0)
708 self.progressbar = gtk.ProgressBar(adjustment=None)
709 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
710 self.progressbar.set_pulse_step(0.01)
711 self.box.pack_start(self.progressbar, True, True, 0)
714 self.button_box = gtk.HButtonBox()
715 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
716 self.box.pack_start(self.button_box, True, True, 0)
717 self.button_box.show()
720 start_image = gtk.Image()
721 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
722 self.start_button = gtk.Button(_("Start"))
723 self.start_button.set_image(start_image)
724 self.start_button.connect("clicked", self.start, None)
725 self.button_box.add(self.start_button)
726 if not self.gui_autostart:
727 self.start_button.show()
730 exit_image = gtk.Image()
731 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
732 self.exit_button = gtk.Button(_("Exit"))
733 self.exit_button.set_image(exit_image)
734 self.exit_button.connect("clicked", self.destroy, None)
735 self.button_box.add(self.exit_button)
736 self.exit_button.show()
741 if self.gui_autostart:
744 # start button clicked, begin tasks
745 def start(self, widget, data=None):
746 # disable the start button
747 if self.start_button:
748 self.start_button.set_sensitive(False)
750 # start running tasks
753 # run the next task in the task list
757 if self.gui_task_i >= len(self.gui_tasks):
761 task = self.gui_tasks[self.gui_task_i]
763 # get ready for the next task
766 print _('Running task: {0}'.format(task))
767 if task == 'download_update_check':
768 print _('Downloading'), self.common.paths['update_check_url']
769 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
771 if task == 'attempt_update':
772 print _('Checking to see if update is needed')
773 self.attempt_update()
775 elif task == 'download_sha256':
776 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
777 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
779 elif task == 'download_sha256_sig':
780 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
781 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
783 elif task == 'download_tarball':
784 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
785 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
787 elif task == 'verify':
788 print _('Verifying signature')
791 elif task == 'extract':
792 print _('Extracting'), self.common.paths['tarball_filename']
796 print _('Running'), self.common.paths['tbb']['start']
799 elif task == 'start_over':
800 print _('Starting download over again')
803 def response_received(self, response):
804 class FileDownloader(Protocol):
805 def __init__(self, common, file, total, progress, done_cb):
809 self.progress = progress
810 self.all_done = done_cb
812 if response.code != 200:
813 if common.settings['mirror'] != common.default_mirror:
814 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']))
816 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
818 def dataReceived(self, bytes):
819 self.file.write(bytes)
820 self.so_far += len(bytes)
821 percent = float(self.so_far) / float(self.total)
822 self.progress.set_fraction(percent)
823 amount = float(self.so_far)
825 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
828 amount = amount / float(size)
831 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
833 def connectionLost(self, reason):
834 print _('Finished receiving body:'), reason.getErrorMessage()
835 self.all_done(reason)
837 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
838 response.deliverBody(dl)
840 def response_finished(self, msg):
841 if msg.check(ResponseDone):
842 self.file_download.close()
843 delattr(self, 'current_download_path')
849 print "FINISHED", msg
850 ## FIXME handle errors
852 def download_error(self, f):
853 print _("Download error:"), f.value, type(f.value)
855 if isinstance(f.value, TryStableException):
856 f.trap(TryStableException)
857 self.set_gui('error_try_stable', str(f.value), [], False)
859 elif isinstance(f.value, TryDefaultMirrorException):
860 f.trap(TryDefaultMirrorException)
861 self.set_gui('error_try_default_mirror', str(f.value), [], False)
863 elif isinstance(f.value, DownloadErrorException):
864 f.trap(DownloadErrorException)
865 self.set_gui('error', str(f.value), [], False)
867 elif isinstance(f.value, DNSLookupError):
868 f.trap(DNSLookupError)
869 if common.settings['mirror'] != common.default_mirror:
870 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)
872 self.set_gui('error', str(f.value), [], False)
874 elif isinstance(f.value, ResponseFailed):
875 for reason in f.value.reasons:
876 if isinstance(reason.value, OpenSSL.SSL.Error):
877 # TODO: add the ability to report attack by posting bug to trac.torproject.org
878 if not self.common.settings['update_over_tor']:
879 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)
881 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
884 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
888 def download(self, name, url, path):
889 # keep track of current download
890 self.current_download_path = path
892 # initialize the progress bar
893 mirror_url = url.format(self.common.settings['mirror'])
894 self.progressbar.set_fraction(0)
895 self.progressbar.set_text(_('Downloading {0}').format(name))
896 self.progressbar.show()
899 if self.common.settings['update_over_tor']:
900 print _('Updating over Tor')
901 from twisted.internet.endpoints import TCP4ClientEndpoint
902 from txsocksx.http import SOCKS5Agent
904 torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
906 # default mirror gets certificate pinning, only for requests that use the mirror
907 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
908 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
910 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
912 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
913 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
915 agent = Agent(reactor)
917 # actually, agent needs to follow redirect
918 agent = RedirectAgent(agent)
921 d = agent.request('GET', mirror_url,
922 Headers({'User-Agent': ['torbrowser-launcher']}),
925 self.file_download = open(path, 'w')
926 d.addCallback(self.response_received).addErrback(self.download_error)
928 if not reactor.running:
931 def try_default_mirror(self, widget, data=None):
932 # change mirror to default and relaunch TBL
933 self.common.settings['mirror'] = self.common.default_mirror
934 self.common.save_settings()
935 subprocess.Popen([self.common.paths['tbl_bin']])
938 def try_tor(self, widget, data=None):
939 # set update_over_tor to true and relaunch TBL
940 self.common.settings['update_over_tor'] = True
941 self.common.save_settings()
942 subprocess.Popen([self.common.paths['tbl_bin']])
945 def attempt_update(self):
946 # load the update check file
948 versions = json.load(open(self.common.paths['update_check_file']))
951 # filter linux versions
953 for version in versions:
954 if '-Linux' in version:
955 valid.append(str(version))
961 self.common.settings['latest_version'] = latest[:-len('-Linux')]
962 self.common.settings['last_update_check_timestamp'] = int(time.time())
963 self.common.settings['check_for_updates'] = False
964 self.common.save_settings()
965 self.common.build_paths(self.common.settings['latest_version'])
966 self.start_launcher()
969 # failed to find the latest version
970 self.set_gui('error', _("Error checking for updates."), [], False)
973 # not a valid JSON object
974 self.set_gui('error', _("Error checking for updates."), [], False)
981 # initialize the progress bar
982 self.progressbar.set_fraction(0)
983 self.progressbar.set_text(_('Verifying Signature'))
984 self.progressbar.show()
987 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
988 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
989 self.pulse_until_process_exits(p)
990 if p.returncode == 0:
991 # compare with sha256 of the tarball
992 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
993 for line in open(self.common.paths['sha256_file'], 'r').readlines():
994 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1000 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1001 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)
1005 if not reactor.running:
1009 # initialize the progress bar
1010 self.progressbar.set_fraction(0)
1011 self.progressbar.set_text(_('Installing'))
1012 self.progressbar.show()
1017 if self.common.paths['tarball_file'][-2:] == 'xz':
1018 # if tarball is .tar.xz
1019 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1020 tf = tarfile.open(fileobj=xz)
1021 tf.extractall(self.common.paths['tbb']['dir'])
1024 # if tarball is .tar.gz
1025 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1026 tf = tarfile.open(self.common.paths['tarball_file'])
1027 tf.extractall(self.common.paths['tbb']['dir'])
1033 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1038 # installation is finished, so save installed_version
1039 self.common.settings['installed_version'] = self.common.settings['latest_version']
1040 self.common.save_settings()
1044 def run(self, run_next_task=True):
1045 subprocess.Popen([self.common.paths['tbb']['start']])
1048 if self.common.settings['modem_sound']:
1052 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1056 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."))
1057 md.set_position(gtk.WIN_POS_CENTER)
1065 # make the progress bar pulse until process p (a Popen object) finishes
1066 def pulse_until_process_exits(self, p):
1067 while p.poll() is None:
1069 self.progressbar.pulse()
1072 # start over and download TBB again
1073 def start_over(self):
1074 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1075 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1080 def refresh_gtk(self):
1081 while gtk.events_pending():
1082 gtk.main_iteration(False)
1085 def delete_event(self, widget, event, data=None):
1088 def destroy(self, widget, data=None):
1089 if hasattr(self, 'file_download'):
1090 self.file_download.close()
1091 if hasattr(self, 'current_download_path'):
1092 os.remove(self.current_download_path)
1093 delattr(self, 'current_download_path')
1097 if __name__ == "__main__":
1098 with open(os.path.join(SHARE, 'torbrowser-launcher/version')) as buf:
1099 tor_browser_launcher_version = buf.read().strip()
1101 print _('Tor Browser Launcher')
1102 print _('By Micah Lee, licensed under GPLv3')
1103 print _('version {0}').format(tor_browser_launcher_version)
1104 print 'https://github.com/micahflee/torbrowser-launcher'
1106 common = TBLCommon(tor_browser_launcher_version)
1108 # is torbrowser-launcher already running?
1109 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1111 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1112 common.bring_window_to_front(tbl_pid)
1115 if '-settings' in sys.argv:
1117 app = TBLSettings(common)
1121 app = TBLLauncher(common)