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 'mike_key': os.path.join(SHARE, 'torbrowser-launcher/mike-2013-09.asc'),
169 'mirrors_txt': [os.path.join(SHARE, 'torbrowser-launcher/mirrors.txt'),
170 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
171 'modem_sound': os.path.join(SHARE, 'torbrowser-launcher/modem.ogg'),
172 'data_dir': tbb_data,
173 'download_dir': tbb_data+'/download',
174 'gnupg_homedir': tbb_data+'/gnupg_homedir',
175 'settings_file': tbb_data+'/settings',
176 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
177 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
179 'dir': tbb_data+'/tbb/'+self.architecture,
180 'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
181 'versions': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
189 if not os.path.exists(path):
190 os.makedirs(path, 0700)
193 print _("Cannot create directory {0}").format(path)
195 if not os.access(path, os.W_OK):
196 print _("{0} is not writable").format(path)
200 # if gnupg_homedir isn't set up, set it up
201 def init_gnupg(self):
202 if not os.path.exists(self.paths['gnupg_homedir']):
203 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
204 self.mkdir(self.paths['gnupg_homedir'])
208 def import_keys(self):
209 print _('Importing keys')
210 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['mike_key']]).wait()
213 def load_mirrors(self):
215 for srcfile in self.paths['mirrors_txt']:
216 if os.path.exists(srcfile):
217 print "Successfully loaded mirrors from %s" % srcfile
218 elif not os.path.exists(srcfile):
219 print "Warning: can't load mirrors from %s" % srcfile
221 for mirror in open(srcfile, 'r').readlines():
222 if mirror.strip() not in self.mirrors:
223 self.mirrors.append(mirror.strip())
226 def load_settings(self):
228 'tbl_version': self.tbl_version,
229 'installed_version': False,
230 'latest_version': '0',
231 'update_over_tor': True,
232 'check_for_updates': False,
233 'modem_sound': False,
234 'last_update_check_timestamp': 0,
235 'mirror': self.default_mirror
238 if os.path.isfile(self.paths['settings_file']):
239 settings = pickle.load(open(self.paths['settings_file']))
242 # settings migrations
243 if settings['tbl_version'] <= '0.1.0':
244 print '0.1.0 migration'
245 settings['installed_version'] = settings['installed_version']['stable']
246 settings['latest_version'] = settings['latest_version']['stable']
249 # make new tbb folder
250 self.mkdir(self.paths['tbb']['dir'])
251 old_tbb_dir = self.paths['data_dir']+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language
252 new_tbb_dir = self.paths['tbb']['dir']+'/tor-browser_'+self.language
253 if os.path.isdir(old_tbb_dir):
254 os.rename(old_tbb_dir, new_tbb_dir)
256 # make sure settings file is up-to-date
257 for setting in default_settings:
258 if setting not in settings:
259 settings[setting] = default_settings[setting]
262 # make sure the version is current
263 if settings['tbl_version'] != self.tbl_version:
264 settings['tbl_version'] = self.tbl_version
267 self.settings = settings
272 self.settings = default_settings
276 def save_settings(self):
277 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
280 # get the process id of a program
282 def get_pid(bin_path, python=False):
285 for p in psutil.process_iter():
287 if p.pid != os.getpid():
290 if len(p.cmdline) > 1:
291 if 'python' in p.cmdline[0]:
294 if len(p.cmdline) > 0:
305 # bring program's x window to front
307 def bring_window_to_front(pid):
308 # figure out the window id
310 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
311 for line in p.stdout.readlines():
312 line_split = line.split()
313 cur_win_id = line_split[0]
314 cur_win_pid = int(line_split[2])
315 if cur_win_pid == pid:
320 subprocess.call(['wmctrl', '-i', '-a', win_id])
324 def __init__(self, common):
325 print _('Starting settings dialog')
329 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
330 self.window.set_title(_("Tor Browser Launcher Settings"))
331 self.window.set_icon_from_file(self.common.paths['icon_file'])
332 self.window.set_position(gtk.WIN_POS_CENTER)
333 self.window.set_border_width(10)
334 self.window.connect("delete_event", self.delete_event)
335 self.window.connect("destroy", self.destroy)
337 # build the rest of the UI
338 self.box = gtk.VBox(False, 10)
339 self.window.add(self.box)
342 self.hbox = gtk.HBox(False, 10)
343 self.box.pack_start(self.hbox, True, True, 0)
346 self.settings_box = gtk.VBox(False, 10)
347 self.hbox.pack_start(self.settings_box, True, True, 0)
348 self.settings_box.show()
350 self.labels_box = gtk.VBox(False, 10)
351 self.hbox.pack_start(self.labels_box, True, True, 0)
352 self.labels_box.show()
357 self.txsocks_found = True
359 self.txsocks_found = False
360 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
361 if self.txsocks_found:
362 self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
364 self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))
366 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
367 if self.common.settings['update_over_tor'] and self.txsocks_found:
368 self.tor_update_checkbox.set_active(True)
370 self.tor_update_checkbox.set_active(False)
372 if self.txsocks_found == False:
373 self.tor_update_checkbox.set_sensitive(False)
375 self.tor_update_checkbox.show()
378 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
379 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
380 if self.common.settings['check_for_updates']:
381 self.update_checkbox.set_active(True)
383 self.update_checkbox.set_active(False)
384 self.update_checkbox.show()
387 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
388 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
392 if self.common.settings['modem_sound']:
393 self.modem_checkbox.set_active(True)
395 self.modem_checkbox.set_active(False)
397 self.modem_checkbox.set_active(False)
398 self.modem_checkbox.set_sensitive(False)
399 self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
400 self.modem_checkbox.show()
403 if(self.common.settings['installed_version']):
404 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version']))
406 self.label1 = gtk.Label(_('Not installed'))
407 self.label1.set_line_wrap(True)
408 self.labels_box.pack_start(self.label1, True, True, 0)
411 if(self.common.settings['last_update_check_timestamp'] > 0):
412 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']))))
414 self.label1 = gtk.Label(_('Never checked for updates'))
415 self.label1.set_line_wrap(True)
416 self.labels_box.pack_start(self.label1, True, True, 0)
420 self.mirrors_box = gtk.HBox(False, 10)
421 self.box.pack_start(self.mirrors_box, True, True, 0)
422 self.mirrors_box.show()
424 self.mirrors_label = gtk.Label(_('Mirror'))
425 self.mirrors_label.set_line_wrap(True)
426 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
427 self.mirrors_label.show()
429 self.mirrors = gtk.combo_box_new_text()
430 for mirror in self.common.mirrors:
431 self.mirrors.append_text(mirror)
432 if self.common.settings['mirror'] in self.common.mirrors:
433 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
435 self.mirrors.set_active(0)
436 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
440 self.button_box = gtk.HButtonBox()
441 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
442 self.box.pack_start(self.button_box, True, True, 0)
443 self.button_box.show()
445 # save and launch button
446 save_launch_image = gtk.Image()
447 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
448 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
449 self.save_launch_button.set_image(save_launch_image)
450 self.save_launch_button.connect("clicked", self.save_launch, None)
451 self.button_box.add(self.save_launch_button)
452 self.save_launch_button.show()
454 # save and exit button
455 save_exit_image = gtk.Image()
456 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
457 self.save_exit_button = gtk.Button(_("Save & Exit"))
458 self.save_exit_button.set_image(save_exit_image)
459 self.save_exit_button.connect("clicked", self.save_exit, None)
460 self.button_box.add(self.save_exit_button)
461 self.save_exit_button.show()
464 cancel_image = gtk.Image()
465 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
466 self.cancel_button = gtk.Button(_("Cancel"))
467 self.cancel_button.set_image(cancel_image)
468 self.cancel_button.connect("clicked", self.destroy, None)
469 self.button_box.add(self.cancel_button)
470 self.cancel_button.show()
478 # UI Callback for update over tor/use system tor
479 def on_system_tor_clicked(self, event):
480 if self.txsocks_found:
481 value = self.system_tor_checkbox.get_active()
485 self.tor_update_checkbox.set_active(value)
486 self.tor_update_checkbox.set_sensitive(value)
489 def save_launch(self, widget, data=None):
491 subprocess.Popen([self.common.paths['tbl_bin']])
495 def save_exit(self, widget, data=None):
502 self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
503 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
504 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
506 # figure out the selected mirror
507 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
510 self.common.save_settings()
513 def delete_event(self, widget, event, data=None):
516 def destroy(self, widget, data=None):
521 def __init__(self, common):
522 print _('Starting launcher dialog')
526 self.set_gui(None, '', [])
527 self.launch_gui = True
528 print "LATEST VERSION", self.common.settings['latest_version']
529 self.common.build_paths(self.common.settings['latest_version'])
531 if self.common.settings['update_over_tor']:
535 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"))
536 md.set_position(gtk.WIN_POS_CENTER)
539 self.common.settings['update_over_tor'] = False
540 self.common.save_settings()
542 # is firefox already running?
543 if self.common.settings['installed_version']:
544 firefox_pid = self.common.get_pid('./Browser/firefox')
546 print _('Firefox are is open, bringing to focus')
547 # bring firefox to front
548 self.common.bring_window_to_front(firefox_pid)
552 check_for_updates = False
553 if self.common.settings['check_for_updates']:
554 check_for_updates = True
556 if not check_for_updates:
557 # how long was it since the last update check?
558 # 86400 seconds = 24 hours
559 current_timestamp = int(time.time())
560 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
561 check_for_updates = True
563 if check_for_updates:
565 print 'Checking for update'
566 self.set_gui('task', _("Checking for Tor Browser update."),
567 ['download_update_check',
570 # no need to check for update
571 print _('Checked for update within 24 hours, skipping')
572 self.start_launcher()
576 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
577 self.window.set_title(_("Tor Browser"))
578 self.window.set_icon_from_file(self.common.paths['icon_file'])
579 self.window.set_position(gtk.WIN_POS_CENTER)
580 self.window.set_border_width(10)
581 self.window.connect("delete_event", self.delete_event)
582 self.window.connect("destroy", self.destroy)
584 # build the rest of the UI
587 # download or run TBB
588 def start_launcher(self):
589 # is TBB already installed?
590 latest_version = self.common.settings['latest_version']
591 installed_version = self.common.settings['installed_version']
593 # verify installed version for newer versions of TBB (#58)
594 if installed_version >= '3.0':
595 versions_filename = self.common.paths['tbb']['versions']
596 if os.path.exists(versions_filename):
597 for line in open(versions_filename):
598 if 'TORBROWSER_VERSION' in line:
599 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
601 start = self.common.paths['tbb']['start']
602 if os.path.isfile(start) and os.access(start, os.X_OK):
603 if installed_version == latest_version:
604 print _('Latest version of TBB is installed, launching')
605 # current version of tbb is installed, launch it
607 self.launch_gui = False
608 elif installed_version < latest_version:
609 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
610 # there is a tbb upgrade available
611 self.set_gui('task', _("Your Tor Browser is out of date."),
613 'download_sha256_sig',
619 # for some reason the installed tbb is newer than the current version?
620 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
624 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
625 self.set_gui('task', _("Downloading and installing Tor Browser."),
627 'download_sha256_sig',
633 # there are different GUIs that might appear, this sets which one we want
634 def set_gui(self, gui, message, tasks, autostart=True):
636 self.gui_message = message
637 self.gui_tasks = tasks
639 self.gui_autostart = autostart
641 # set all gtk variables to False
643 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
648 self.progressbar = False
649 self.button_box = False
650 self.start_button = False
651 self.exit_button = False
653 # build the application's UI
657 self.box = gtk.VBox(False, 20)
658 self.window.add(self.box)
660 if 'error' in self.gui:
662 self.label = gtk.Label(self.gui_message)
663 self.label.set_line_wrap(True)
664 self.box.pack_start(self.label, True, True, 0)
668 self.button_box = gtk.HButtonBox()
669 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
670 self.box.pack_start(self.button_box, True, True, 0)
671 self.button_box.show()
673 if self.gui != 'error':
675 yes_image = gtk.Image()
676 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
677 self.yes_button = gtk.Button("Yes")
678 self.yes_button.set_image(yes_image)
679 if self.gui == 'error_try_stable':
680 self.yes_button.connect("clicked", self.try_stable, None)
681 elif self.gui == 'error_try_default_mirror':
682 self.yes_button.connect("clicked", self.try_default_mirror, None)
683 elif self.gui == 'error_try_tor':
684 self.yes_button.connect("clicked", self.try_tor, None)
685 self.button_box.add(self.yes_button)
686 self.yes_button.show()
689 exit_image = gtk.Image()
690 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
691 self.exit_button = gtk.Button("Exit")
692 self.exit_button.set_image(exit_image)
693 self.exit_button.connect("clicked", self.destroy, None)
694 self.button_box.add(self.exit_button)
695 self.exit_button.show()
697 elif self.gui == 'task':
699 self.label = gtk.Label(self.gui_message)
700 self.label.set_line_wrap(True)
701 self.box.pack_start(self.label, True, True, 0)
705 self.progressbar = gtk.ProgressBar(adjustment=None)
706 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
707 self.progressbar.set_pulse_step(0.01)
708 self.box.pack_start(self.progressbar, True, True, 0)
711 self.button_box = gtk.HButtonBox()
712 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
713 self.box.pack_start(self.button_box, True, True, 0)
714 self.button_box.show()
717 start_image = gtk.Image()
718 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
719 self.start_button = gtk.Button(_("Start"))
720 self.start_button.set_image(start_image)
721 self.start_button.connect("clicked", self.start, None)
722 self.button_box.add(self.start_button)
723 if not self.gui_autostart:
724 self.start_button.show()
727 exit_image = gtk.Image()
728 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
729 self.exit_button = gtk.Button(_("Exit"))
730 self.exit_button.set_image(exit_image)
731 self.exit_button.connect("clicked", self.destroy, None)
732 self.button_box.add(self.exit_button)
733 self.exit_button.show()
738 if self.gui_autostart:
741 # start button clicked, begin tasks
742 def start(self, widget, data=None):
743 # disable the start button
744 if self.start_button:
745 self.start_button.set_sensitive(False)
747 # start running tasks
750 # run the next task in the task list
754 if self.gui_task_i >= len(self.gui_tasks):
758 task = self.gui_tasks[self.gui_task_i]
760 # get ready for the next task
763 print _('Running task: {0}'.format(task))
764 if task == 'download_update_check':
765 print _('Downloading'), self.common.paths['update_check_url']
766 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
768 if task == 'attempt_update':
769 print _('Checking to see if update is needed')
770 self.attempt_update()
772 elif task == 'download_sha256':
773 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
774 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
776 elif task == 'download_sha256_sig':
777 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
778 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
780 elif task == 'download_tarball':
781 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
782 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
784 elif task == 'verify':
785 print _('Verifying signature')
788 elif task == 'extract':
789 print _('Extracting'), self.common.paths['tarball_filename']
793 print _('Running'), self.common.paths['tbb']['start']
796 elif task == 'start_over':
797 print _('Starting download over again')
800 def response_received(self, response):
801 class FileDownloader(Protocol):
802 def __init__(self, common, file, total, progress, done_cb):
806 self.progress = progress
807 self.all_done = done_cb
809 if response.code != 200:
810 if common.settings['mirror'] != common.default_mirror:
811 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']))
813 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
815 def dataReceived(self, bytes):
816 self.file.write(bytes)
817 self.so_far += len(bytes)
818 percent = float(self.so_far) / float(self.total)
819 self.progress.set_fraction(percent)
820 amount = float(self.so_far)
822 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
825 amount = amount / float(size)
828 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
830 def connectionLost(self, reason):
831 print _('Finished receiving body:'), reason.getErrorMessage()
832 self.all_done(reason)
834 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
835 response.deliverBody(dl)
837 def response_finished(self, msg):
838 if msg.check(ResponseDone):
839 self.file_download.close()
840 delattr(self, 'current_download_path')
846 print "FINISHED", msg
847 ## FIXME handle errors
849 def download_error(self, f):
850 print _("Download error:"), f.value, type(f.value)
852 if isinstance(f.value, TryStableException):
853 f.trap(TryStableException)
854 self.set_gui('error_try_stable', str(f.value), [], False)
856 elif isinstance(f.value, TryDefaultMirrorException):
857 f.trap(TryDefaultMirrorException)
858 self.set_gui('error_try_default_mirror', str(f.value), [], False)
860 elif isinstance(f.value, DownloadErrorException):
861 f.trap(DownloadErrorException)
862 self.set_gui('error', str(f.value), [], False)
864 elif isinstance(f.value, DNSLookupError):
865 f.trap(DNSLookupError)
866 if common.settings['mirror'] != common.default_mirror:
867 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)
869 self.set_gui('error', str(f.value), [], False)
871 elif isinstance(f.value, ResponseFailed):
872 for reason in f.value.reasons:
873 if isinstance(reason.value, OpenSSL.SSL.Error):
874 # TODO: add the ability to report attack by posting bug to trac.torproject.org
875 if not self.common.settings['update_over_tor']:
876 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)
878 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
881 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
885 def download(self, name, url, path):
886 # keep track of current download
887 self.current_download_path = path
889 # initialize the progress bar
890 mirror_url = url.format(self.common.settings['mirror'])
891 self.progressbar.set_fraction(0)
892 self.progressbar.set_text(_('Downloading {0}').format(name))
893 self.progressbar.show()
896 if self.common.settings['update_over_tor']:
897 print _('Updating over Tor')
898 from twisted.internet.endpoints import TCP4ClientEndpoint
899 from txsocksx.http import SOCKS5Agent
901 torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
903 # default mirror gets certificate pinning, only for requests that use the mirror
904 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
905 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
907 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
909 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
910 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
912 agent = Agent(reactor)
914 # actually, agent needs to follow redirect
915 agent = RedirectAgent(agent)
918 d = agent.request('GET', mirror_url,
919 Headers({'User-Agent': ['torbrowser-launcher']}),
922 self.file_download = open(path, 'w')
923 d.addCallback(self.response_received).addErrback(self.download_error)
925 if not reactor.running:
928 def try_default_mirror(self, widget, data=None):
929 # change mirror to default and relaunch TBL
930 self.common.settings['mirror'] = self.common.default_mirror
931 self.common.save_settings()
932 subprocess.Popen([self.common.paths['tbl_bin']])
935 def try_tor(self, widget, data=None):
936 # set update_over_tor to true and relaunch TBL
937 self.common.settings['update_over_tor'] = True
938 self.common.save_settings()
939 subprocess.Popen([self.common.paths['tbl_bin']])
942 def attempt_update(self):
943 # load the update check file
945 versions = json.load(open(self.common.paths['update_check_file']))
948 # filter linux versions
950 for version in versions:
951 if '-Linux' in version:
952 valid.append(str(version))
958 self.common.settings['latest_version'] = latest[:-len('-Linux')]
959 self.common.settings['last_update_check_timestamp'] = int(time.time())
960 self.common.settings['check_for_updates'] = False
961 self.common.save_settings()
962 self.common.build_paths(self.common.settings['latest_version'])
963 self.start_launcher()
966 # failed to find the latest version
967 self.set_gui('error', _("Error checking for updates."), [], False)
970 # not a valid JSON object
971 self.set_gui('error', _("Error checking for updates."), [], False)
978 # initialize the progress bar
979 self.progressbar.set_fraction(0)
980 self.progressbar.set_text(_('Verifying Signature'))
981 self.progressbar.show()
984 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
985 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
986 self.pulse_until_process_exits(p)
987 if p.returncode == 0:
988 # compare with sha256 of the tarball
989 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
990 for line in open(self.common.paths['sha256_file'], 'r').readlines():
991 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
997 # TODO: add the ability to report attack by posting bug to trac.torproject.org
998 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)
1002 if not reactor.running:
1006 # initialize the progress bar
1007 self.progressbar.set_fraction(0)
1008 self.progressbar.set_text(_('Installing'))
1009 self.progressbar.show()
1014 if self.common.paths['tarball_file'][-2:] == 'xz':
1015 # if tarball is .tar.xz
1016 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1017 tf = tarfile.open(fileobj=xz)
1018 tf.extractall(self.common.paths['tbb']['dir'])
1021 # if tarball is .tar.gz
1022 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1023 tf = tarfile.open(self.common.paths['tarball_file'])
1024 tf.extractall(self.common.paths['tbb']['dir'])
1030 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1035 # installation is finished, so save installed_version
1036 self.common.settings['installed_version'] = self.common.settings['latest_version']
1037 self.common.save_settings()
1041 def run(self, run_next_task=True):
1042 devnull = open('/dev/null', 'w')
1043 subprocess.Popen([self.common.paths['tbb']['start']], stdout=devnull, stderr=devnull)
1046 if self.common.settings['modem_sound']:
1050 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1054 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."))
1055 md.set_position(gtk.WIN_POS_CENTER)
1062 # make the progress bar pulse until process p (a Popen object) finishes
1063 def pulse_until_process_exits(self, p):
1064 while p.poll() is None:
1066 self.progressbar.pulse()
1069 # start over and download TBB again
1070 def start_over(self):
1071 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1072 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1077 def refresh_gtk(self):
1078 while gtk.events_pending():
1079 gtk.main_iteration(False)
1082 def delete_event(self, widget, event, data=None):
1085 def destroy(self, widget, data=None):
1086 if hasattr(self, 'file_download'):
1087 self.file_download.close()
1088 if hasattr(self, 'current_download_path'):
1089 os.remove(self.current_download_path)
1090 delattr(self, 'current_download_path')
1094 if __name__ == "__main__":
1095 with open(os.path.join(SHARE, 'torbrowser-launcher/version')) as buf:
1096 tor_browser_launcher_version = buf.read().strip()
1098 print _('Tor Browser Launcher')
1099 print _('By Micah Lee, licensed under GPLv3')
1100 print _('version {0}').format(tor_browser_launcher_version)
1101 print 'https://github.com/micahflee/torbrowser-launcher'
1103 common = TBLCommon(tor_browser_launcher_version)
1105 # is torbrowser-launcher already running?
1106 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1108 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1109 common.bring_window_to_front(tbl_pid)
1112 if '-settings' in sys.argv:
1114 app = TBLSettings(common)
1118 app = TBLLauncher(common)