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.available_versions = {
92 'stable': _('Tor Browser Bundle - stable'),
93 'alpha': _('Tor Browser Bundle - alpha')
95 self.default_mirror = 'https://www.torproject.org/dist/'
97 self.discover_arch_lang()
99 self.mkdir(self.paths['data_dir'])
102 self.mkdir(self.paths['download_dir'])
103 self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
106 # allow buttons to have icons
108 gtk_settings = gtk.settings_get_default()
109 gtk_settings.props.gtk_button_images = True
113 # discover the architecture and language
114 def discover_arch_lang(self):
115 # figure out the architecture
116 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
118 # figure out the language
119 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
120 default_locale = locale.getdefaultlocale()[0]
121 if default_locale is None:
122 self.language = 'en-US'
124 self.language = default_locale.replace('_', '-')
125 if self.language not in available_languages:
126 self.language = self.language.split('-')[0]
127 if self.language not in available_languages:
128 for l in available_languages:
129 if l[0:2] == self.language:
131 # if language isn't available, default to english
132 if self.language not in available_languages:
133 self.language = 'en-US'
135 # build all relevant paths
136 def build_paths(self, tbb_version=None):
137 homedir = os.getenv('HOME')
139 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
140 if not os.path.exists(homedir):
142 os.mkdir(homedir, 0700)
144 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
145 if not os.access(homedir, os.W_OK):
146 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
148 tbb_data = '%s/.torbrowser' % homedir
152 dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b').replace('-rc-', 'rc')
153 if self.architecture == 'x86_64':
157 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
160 self.paths['tarball_url'] = '{0}torbrowser/'+dirname+'/'+tarball_filename
161 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
162 self.paths['tarball_filename'] = tarball_filename
165 self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
166 self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
167 self.paths['sha256_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt'
168 self.paths['sha256_sig_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt-mikeperry.asc'
171 'tbl_bin': '/usr/bin/torbrowser-launcher',
172 'icon_file': os.path.join(SHARE, 'pixmaps/torbrowser80.xpm'),
173 'torproject_pem': os.path.join(SHARE, 'torbrowser-launcher/torproject.pem'),
174 'erinn_key': os.path.join(SHARE, 'torbrowser-launcher/erinn.asc'),
175 'sebastian_key': os.path.join(SHARE, 'torbrowser-launcher/sebastian.asc'),
176 'alexandre_key': os.path.join(SHARE, 'torbrowser-launcher/alexandre.asc'),
177 'mike_key': os.path.join(SHARE, 'torbrowser-launcher/mike-2013-09.asc'),
178 'mirrors_txt': [os.path.join(SHARE, 'torbrowser-launcher/mirrors.txt'),
179 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
180 'modem_sound': os.path.join(SHARE, 'torbrowser-launcher/modem.ogg'),
181 'data_dir': tbb_data,
182 'download_dir': tbb_data+'/download',
183 'gnupg_homedir': tbb_data+'/gnupg_homedir',
184 'settings_file': tbb_data+'/settings',
185 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
186 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
189 'dir': tbb_data+'/tbb/stable/'+self.architecture,
190 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
191 'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
194 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
195 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
196 'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
205 if not os.path.exists(path):
206 os.makedirs(path, 0700)
209 print _("Cannot create directory {0}").format(path)
211 if not os.access(path, os.W_OK):
212 print _("{0} is not writable").format(path)
216 # if gnupg_homedir isn't set up, set it up
217 def init_gnupg(self):
218 if not os.path.exists(self.paths['gnupg_homedir']):
219 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
220 self.mkdir(self.paths['gnupg_homedir'])
224 def import_keys(self):
225 print _('Importing keys')
226 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()
229 def load_mirrors(self):
231 for srcfile in self.paths['mirrors_txt']:
232 if not os.path.exists(srcfile):
233 print "Warning: can't load mirrors from %s" % srcfile
235 for mirror in open(srcfile, 'r').readlines():
236 if mirror.strip() not in self.mirrors:
237 self.mirrors.append(mirror.strip())
240 def load_settings(self):
242 'tbl_version': self.tbl_version,
243 'preferred': 'stable',
244 'installed_version': {
252 'update_over_tor': True,
253 'check_for_updates': False,
254 'modem_sound': False,
255 'last_update_check_timestamp': 0,
256 'mirror': self.default_mirror
259 if os.path.isfile(self.paths['settings_file']):
260 settings = pickle.load(open(self.paths['settings_file']))
263 # settings migrations
264 if settings['tbl_version'] == '0.0.1':
265 print '0.0.1 migration'
266 self.settings = default_settings
267 self.settings['installed_version']['alpha'] = settings['installed_version']
271 self.mkdir(self.paths['tbb']['alpha']['dir'])
273 # make sure settings file is up-to-date
274 for setting in default_settings:
275 if setting not in settings:
276 settings[setting] = default_settings[setting]
279 # make sure the version is current
280 if settings['tbl_version'] != self.tbl_version:
281 settings['tbl_version'] = self.tbl_version
284 self.settings = settings
289 self.settings = default_settings
293 def save_settings(self):
294 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
297 # get the process id of a program
299 def get_pid(bin_path, python=False):
302 for p in psutil.process_iter():
304 if p.pid != os.getpid():
307 if len(p.cmdline) > 1:
308 if 'python' in p.cmdline[0]:
311 if len(p.cmdline) > 0:
322 # bring program's x window to front
324 def bring_window_to_front(pid):
325 # figure out the window id
327 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
328 for line in p.stdout.readlines():
329 line_split = line.split()
330 cur_win_id = line_split[0]
331 cur_win_pid = int(line_split[2])
332 if cur_win_pid == pid:
337 subprocess.call(['wmctrl', '-i', '-a', win_id])
341 def __init__(self, common):
342 print _('Starting settings dialog')
346 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
347 self.window.set_title(_("Tor Browser Launcher Settings"))
348 self.window.set_icon_from_file(self.common.paths['icon_file'])
349 self.window.set_position(gtk.WIN_POS_CENTER)
350 self.window.set_border_width(10)
351 self.window.connect("delete_event", self.delete_event)
352 self.window.connect("destroy", self.destroy)
354 # build the rest of the UI
355 self.box = gtk.VBox(False, 10)
356 self.window.add(self.box)
359 self.hbox = gtk.HBox(False, 10)
360 self.box.pack_start(self.hbox, True, True, 0)
363 self.settings_box = gtk.VBox(False, 10)
364 self.hbox.pack_start(self.settings_box, True, True, 0)
365 self.settings_box.show()
367 self.labels_box = gtk.VBox(False, 10)
368 self.hbox.pack_start(self.labels_box, True, True, 0)
369 self.labels_box.show()
372 self.preferred_box = gtk.HBox(False, 10)
373 self.settings_box.pack_start(self.preferred_box, True, True, 0)
374 self.preferred_box.show()
376 self.preferred_label = gtk.Label(_('I prefer'))
377 self.preferred_label.set_line_wrap(True)
378 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
379 self.preferred_label.show()
381 self.preferred_options = []
382 for i in self.common.available_versions:
383 self.preferred_options.append(self.common.available_versions[i])
384 self.preferred_options.sort()
386 self.preferred = gtk.combo_box_new_text()
387 for option in self.preferred_options:
388 self.preferred.append_text(option)
389 if self.common.settings['preferred'] in self.common.available_versions:
390 self.preferred.set_active(self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]))
392 self.preferred.set_active(0)
393 self.preferred_box.pack_start(self.preferred, True, True, 0)
394 self.preferred.show()
399 self.txsocks_found = True
401 self.txsocks_found = False
402 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
403 if self.txsocks_found:
404 self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
406 self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))
408 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
409 if self.common.settings['update_over_tor'] and self.txsocks_found:
410 self.tor_update_checkbox.set_active(True)
412 self.tor_update_checkbox.set_active(False)
414 if self.txsocks_found == False:
415 self.tor_update_checkbox.set_sensitive(False)
417 self.tor_update_checkbox.show()
420 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
421 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
422 if self.common.settings['check_for_updates']:
423 self.update_checkbox.set_active(True)
425 self.update_checkbox.set_active(False)
426 self.update_checkbox.show()
429 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
430 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
434 if self.common.settings['modem_sound']:
435 self.modem_checkbox.set_active(True)
437 self.modem_checkbox.set_active(False)
439 self.modem_checkbox.set_active(False)
440 self.modem_checkbox.set_sensitive(False)
441 self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
442 self.modem_checkbox.show()
445 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
446 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
448 self.label1 = gtk.Label(_('Not installed'))
449 self.label1.set_line_wrap(True)
450 self.labels_box.pack_start(self.label1, True, True, 0)
453 if(self.common.settings['last_update_check_timestamp'] > 0):
454 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']))))
456 self.label1 = gtk.Label(_('Never checked for updates'))
457 self.label1.set_line_wrap(True)
458 self.labels_box.pack_start(self.label1, True, True, 0)
462 self.mirrors_box = gtk.HBox(False, 10)
463 self.box.pack_start(self.mirrors_box, True, True, 0)
464 self.mirrors_box.show()
466 self.mirrors_label = gtk.Label(_('Mirror'))
467 self.mirrors_label.set_line_wrap(True)
468 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
469 self.mirrors_label.show()
471 self.mirrors = gtk.combo_box_new_text()
472 for mirror in self.common.mirrors:
473 self.mirrors.append_text(mirror)
474 if self.common.settings['mirror'] in self.common.mirrors:
475 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
477 self.preferred.set_active(0)
478 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
482 self.button_box = gtk.HButtonBox()
483 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
484 self.box.pack_start(self.button_box, True, True, 0)
485 self.button_box.show()
487 # save and launch button
488 save_launch_image = gtk.Image()
489 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
490 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
491 self.save_launch_button.set_image(save_launch_image)
492 self.save_launch_button.connect("clicked", self.save_launch, None)
493 self.button_box.add(self.save_launch_button)
494 self.save_launch_button.show()
496 # save and exit button
497 save_exit_image = gtk.Image()
498 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
499 self.save_exit_button = gtk.Button(_("Save & Exit"))
500 self.save_exit_button.set_image(save_exit_image)
501 self.save_exit_button.connect("clicked", self.save_exit, None)
502 self.button_box.add(self.save_exit_button)
503 self.save_exit_button.show()
506 cancel_image = gtk.Image()
507 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
508 self.cancel_button = gtk.Button(_("Cancel"))
509 self.cancel_button.set_image(cancel_image)
510 self.cancel_button.connect("clicked", self.destroy, None)
511 self.button_box.add(self.cancel_button)
512 self.cancel_button.show()
520 # UI Callback for update over tor/use system tor
521 def on_system_tor_clicked(self, event):
522 if self.txsocks_found:
523 value = self.system_tor_checkbox.get_active()
527 self.tor_update_checkbox.set_active(value)
528 self.tor_update_checkbox.set_sensitive(value)
531 def save_launch(self, widget, data=None):
533 subprocess.Popen([self.common.paths['tbl_bin']])
537 def save_exit(self, widget, data=None):
543 # figure out the selected preferred option
545 selected = self.preferred_options[self.preferred.get_active()]
546 for i in self.common.available_versions:
547 if self.common.available_versions[i] == selected:
550 self.common.settings['preferred'] = preferred
553 self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
554 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
555 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
557 # figure out the selected mirror
558 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
561 self.common.save_settings()
564 def delete_event(self, widget, event, data=None):
567 def destroy(self, widget, data=None):
572 def __init__(self, common):
573 print _('Starting launcher dialog')
577 self.set_gui(None, '', [])
578 self.launch_gui = True
579 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
581 if self.common.settings['update_over_tor']:
585 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"))
586 md.set_position(gtk.WIN_POS_CENTER)
589 self.common.settings['update_over_tor'] = False
590 self.common.save_settings()
592 # is firefox already running?
593 if self.common.settings['installed_version']:
594 firefox_pid = self.common.get_pid('./Browser/firefox')
596 print _('Firefox are is open, bringing to focus')
597 # bring firefox to front
598 self.common.bring_window_to_front(firefox_pid)
602 check_for_updates = False
603 if self.common.settings['check_for_updates']:
604 check_for_updates = True
606 if not check_for_updates:
607 # how long was it since the last update check?
608 # 86400 seconds = 24 hours
609 current_timestamp = int(time.time())
610 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
611 check_for_updates = True
613 if check_for_updates:
615 print 'Checking for update'
616 self.set_gui('task', _("Checking for Tor Browser update."),
617 ['download_update_check',
620 # no need to check for update
621 print _('Checked for update within 24 hours, skipping')
622 self.start_launcher()
626 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
627 self.window.set_title(_("Tor Browser"))
628 self.window.set_icon_from_file(self.common.paths['icon_file'])
629 self.window.set_position(gtk.WIN_POS_CENTER)
630 self.window.set_border_width(10)
631 self.window.connect("delete_event", self.delete_event)
632 self.window.connect("destroy", self.destroy)
634 # build the rest of the UI
637 # download or run TBB
638 def start_launcher(self):
639 # is TBB already installed?
640 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
641 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
643 # verify installed version for newer versions of TBB (#58)
644 if installed_version >= '3.0':
645 versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
646 if os.path.exists(versions_filename):
647 for line in open(versions_filename):
648 if 'TORBROWSER_VERSION' in line:
649 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
651 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
652 if os.path.isfile(start) and os.access(start, os.X_OK):
653 if installed_version == latest_version:
654 print _('Latest version of TBB is installed, launching')
655 # current version of tbb is installed, launch it
657 self.launch_gui = False
658 elif installed_version < latest_version:
659 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
660 # there is a tbb upgrade available
661 self.set_gui('task', _("Your Tor Browser is out of date."),
663 'download_sha256_sig',
669 # for some reason the installed tbb is newer than the current version?
670 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
674 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
675 self.set_gui('task', _("Downloading and installing Tor Browser."),
677 'download_sha256_sig',
683 # there are different GUIs that might appear, this sets which one we want
684 def set_gui(self, gui, message, tasks, autostart=True):
686 self.gui_message = message
687 self.gui_tasks = tasks
689 self.gui_autostart = autostart
691 # set all gtk variables to False
693 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
698 self.progressbar = False
699 self.button_box = False
700 self.start_button = False
701 self.exit_button = False
703 # build the application's UI
707 self.box = gtk.VBox(False, 20)
708 self.window.add(self.box)
710 if 'error' in self.gui:
712 self.label = gtk.Label(self.gui_message)
713 self.label.set_line_wrap(True)
714 self.box.pack_start(self.label, True, True, 0)
718 self.button_box = gtk.HButtonBox()
719 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
720 self.box.pack_start(self.button_box, True, True, 0)
721 self.button_box.show()
723 if self.gui != 'error':
725 yes_image = gtk.Image()
726 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
727 self.yes_button = gtk.Button("Yes")
728 self.yes_button.set_image(yes_image)
729 if self.gui == 'error_try_stable':
730 self.yes_button.connect("clicked", self.try_stable, None)
731 elif self.gui == 'error_try_default_mirror':
732 self.yes_button.connect("clicked", self.try_default_mirror, None)
733 elif self.gui == 'error_try_tor':
734 self.yes_button.connect("clicked", self.try_tor, None)
735 self.button_box.add(self.yes_button)
736 self.yes_button.show()
739 exit_image = gtk.Image()
740 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
741 self.exit_button = gtk.Button("Exit")
742 self.exit_button.set_image(exit_image)
743 self.exit_button.connect("clicked", self.destroy, None)
744 self.button_box.add(self.exit_button)
745 self.exit_button.show()
747 elif self.gui == 'task':
749 self.label = gtk.Label(self.gui_message)
750 self.label.set_line_wrap(True)
751 self.box.pack_start(self.label, True, True, 0)
755 self.progressbar = gtk.ProgressBar(adjustment=None)
756 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
757 self.progressbar.set_pulse_step(0.01)
758 self.box.pack_start(self.progressbar, True, True, 0)
761 self.button_box = gtk.HButtonBox()
762 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
763 self.box.pack_start(self.button_box, True, True, 0)
764 self.button_box.show()
767 start_image = gtk.Image()
768 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
769 self.start_button = gtk.Button(_("Start"))
770 self.start_button.set_image(start_image)
771 self.start_button.connect("clicked", self.start, None)
772 self.button_box.add(self.start_button)
773 if not self.gui_autostart:
774 self.start_button.show()
777 exit_image = gtk.Image()
778 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
779 self.exit_button = gtk.Button(_("Exit"))
780 self.exit_button.set_image(exit_image)
781 self.exit_button.connect("clicked", self.destroy, None)
782 self.button_box.add(self.exit_button)
783 self.exit_button.show()
788 if self.gui_autostart:
791 # start button clicked, begin tasks
792 def start(self, widget, data=None):
793 # disable the start button
794 if self.start_button:
795 self.start_button.set_sensitive(False)
797 # start running tasks
800 # run the next task in the task list
804 if self.gui_task_i >= len(self.gui_tasks):
808 task = self.gui_tasks[self.gui_task_i]
810 # get ready for the next task
813 print _('Running task: {0}'.format(task))
814 if task == 'download_update_check':
815 print _('Downloading'), self.common.paths['update_check_url']
816 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
818 if task == 'attempt_update':
819 print _('Checking to see if update is needed')
820 self.attempt_update()
822 elif task == 'download_sha256':
823 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
824 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
826 elif task == 'download_sha256_sig':
827 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
828 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
830 elif task == 'download_tarball':
831 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
832 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
834 elif task == 'verify':
835 print _('Verifying signature')
838 elif task == 'extract':
839 print _('Extracting'), self.common.paths['tarball_filename']
843 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
846 elif task == 'start_over':
847 print _('Starting download over again')
850 def response_received(self, response):
851 class FileDownloader(Protocol):
852 def __init__(self, common, file, total, progress, done_cb):
856 self.progress = progress
857 self.all_done = done_cb
859 if response.code != 200:
862 if response.code == 404:
863 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
867 raise TryStableException(_("It looks like the alpha version of Tor Browser Bundle isn't available for your language. Would you like to try the stable version instead?"))
869 if common.settings['mirror'] != common.default_mirror:
870 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']))
872 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
874 def dataReceived(self, bytes):
875 self.file.write(bytes)
876 self.so_far += len(bytes)
877 percent = float(self.so_far) / float(self.total)
878 self.progress.set_fraction(percent)
879 amount = float(self.so_far)
881 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
884 amount = amount / float(size)
887 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
889 def connectionLost(self, reason):
890 print _('Finished receiving body:'), reason.getErrorMessage()
891 self.all_done(reason)
893 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
894 response.deliverBody(dl)
896 def response_finished(self, msg):
897 if msg.check(ResponseDone):
898 self.file_download.close()
899 delattr(self, 'current_download_path')
905 print "FINISHED", msg
906 ## FIXME handle errors
908 def download_error(self, f):
909 print _("Download error:"), f.value, type(f.value)
911 if isinstance(f.value, TryStableException):
912 f.trap(TryStableException)
913 self.set_gui('error_try_stable', str(f.value), [], False)
915 elif isinstance(f.value, TryDefaultMirrorException):
916 f.trap(TryDefaultMirrorException)
917 self.set_gui('error_try_default_mirror', str(f.value), [], False)
919 elif isinstance(f.value, DownloadErrorException):
920 f.trap(DownloadErrorException)
921 self.set_gui('error', str(f.value), [], False)
923 elif isinstance(f.value, DNSLookupError):
924 f.trap(DNSLookupError)
925 if common.settings['mirror'] != common.default_mirror:
926 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)
928 self.set_gui('error', str(f.value), [], False)
930 elif isinstance(f.value, ResponseFailed):
931 for reason in f.value.reasons:
932 if isinstance(reason.value, OpenSSL.SSL.Error):
933 # TODO: add the ability to report attack by posting bug to trac.torproject.org
934 if not self.common.settings['update_over_tor']:
935 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)
937 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
940 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
944 def download(self, name, url, path):
945 # keep track of current download
946 self.current_download_path = path
948 # initialize the progress bar
949 mirror_url = url.format(self.common.settings['mirror'])
950 self.progressbar.set_fraction(0)
951 self.progressbar.set_text(_('Downloading {0}').format(name))
952 self.progressbar.show()
955 if self.common.settings['update_over_tor']:
956 print _('Updating over Tor')
957 from twisted.internet.endpoints import TCP4ClientEndpoint
958 from txsocksx.http import SOCKS5Agent
960 torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
962 # default mirror gets certificate pinning, only for requests that use the mirror
963 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
964 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
966 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
968 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
969 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
971 agent = Agent(reactor)
973 # actually, agent needs to follow redirect
974 agent = RedirectAgent(agent)
977 d = agent.request('GET', mirror_url,
978 Headers({'User-Agent': ['torbrowser-launcher']}),
981 self.file_download = open(path, 'w')
982 d.addCallback(self.response_received).addErrback(self.download_error)
984 if not reactor.running:
987 def try_stable(self, widget, data=None):
988 # change preferred to stable and relaunch TBL
989 self.common.settings['preferred'] = 'stable'
990 self.common.save_settings()
991 p = subprocess.Popen([self.common.paths['tbl_bin']])
994 def try_default_mirror(self, widget, data=None):
995 # change preferred to stable and relaunch TBL
996 self.common.settings['mirror'] = self.common.default_mirror
997 self.common.save_settings()
998 subprocess.Popen([self.common.paths['tbl_bin']])
1001 def try_tor(self, widget, data=None):
1002 # set update_over_tor to true and relaunch TBL
1003 self.common.settings['update_over_tor'] = True
1004 self.common.save_settings()
1005 subprocess.Popen([self.common.paths['tbl_bin']])
1008 def attempt_update(self):
1009 # load the update check file
1011 versions = json.load(open(self.common.paths['update_check_file']))
1012 latest_stable = None
1015 # filter linux versions
1018 for version in versions:
1019 if '-Linux' in version:
1020 if 'alpha' in version or 'beta' in version or '-rc' in version:
1021 valid_alphas.append(str(version))
1023 valid_stables.append(str(version))
1025 if len(valid_alphas):
1026 latest_alpha = valid_alphas.pop()
1027 valid_stables.sort()
1028 if len(valid_stables):
1029 latest_stable = valid_stables.pop()
1031 if latest_stable or latest_alpha:
1033 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
1035 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
1036 self.common.settings['last_update_check_timestamp'] = int(time.time())
1037 self.common.settings['check_for_updates'] = False
1038 self.common.save_settings()
1039 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
1040 self.start_launcher()
1043 # failed to find the latest version
1044 self.set_gui('error', _("Error checking for updates."), [], False)
1047 # not a valid JSON object
1048 self.set_gui('error', _("Error checking for updates."), [], False)
1055 # initialize the progress bar
1056 self.progressbar.set_fraction(0)
1057 self.progressbar.set_text(_('Verifying Signature'))
1058 self.progressbar.show()
1061 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1062 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1063 self.pulse_until_process_exits(p)
1064 if p.returncode == 0:
1065 # compare with sha256 of the tarball
1066 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1067 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1068 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1074 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1075 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)
1079 if not reactor.running:
1083 # initialize the progress bar
1084 self.progressbar.set_fraction(0)
1085 self.progressbar.set_text(_('Installing'))
1086 self.progressbar.show()
1091 if self.common.paths['tarball_file'][-2:] == 'xz':
1092 # if tarball is .tar.xz
1093 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1094 tf = tarfile.open(fileobj=xz)
1095 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1098 # if tarball is .tar.gz
1099 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1100 tf = tarfile.open(self.common.paths['tarball_file'])
1101 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1107 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1112 # installation is finished, so save installed_version
1113 self.common.settings['installed_version'] = self.common.settings['latest_version']
1114 self.common.save_settings()
1118 def run(self, run_next_task=True):
1119 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1122 if self.common.settings['modem_sound']:
1126 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1130 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."))
1131 md.set_position(gtk.WIN_POS_CENTER)
1139 # make the progress bar pulse until process p (a Popen object) finishes
1140 def pulse_until_process_exits(self, p):
1141 while p.poll() is None:
1143 self.progressbar.pulse()
1146 # start over and download TBB again
1147 def start_over(self):
1148 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1149 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1154 def refresh_gtk(self):
1155 while gtk.events_pending():
1156 gtk.main_iteration(False)
1159 def delete_event(self, widget, event, data=None):
1162 def destroy(self, widget, data=None):
1163 if hasattr(self, 'file_download'):
1164 self.file_download.close()
1165 if hasattr(self, 'current_download_path'):
1166 os.remove(self.current_download_path)
1167 delattr(self, 'current_download_path')
1171 if __name__ == "__main__":
1172 with open(os.path.join(SHARE, 'torbrowser-launcher/version')) as buf:
1173 tor_browser_launcher_version = buf.read().strip()
1175 print _('Tor Browser Launcher')
1176 print _('By Micah Lee, licensed under GPLv3')
1177 print _('version {0}').format(tor_browser_launcher_version)
1178 print 'https://github.com/micahflee/torbrowser-launcher'
1180 common = TBLCommon(tor_browser_launcher_version)
1182 # is torbrowser-launcher already running?
1183 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1185 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1186 common.bring_window_to_front(tbl_pid)
1189 if '-settings' in sys.argv:
1191 app = TBLSettings(common)
1195 app = TBLLauncher(common)