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 for d in self.paths['dirs']:
95 self.mkdir(self.paths['dirs'][d])
98 self.mkdir(self.paths['download_dir'])
99 self.mkdir(self.paths['tbb']['dir'])
102 # allow buttons to have icons
104 gtk_settings = gtk.settings_get_default()
105 gtk_settings.props.gtk_button_images = True
109 # discover the architecture and language
110 def discover_arch_lang(self):
111 # figure out the architecture
112 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
114 # figure out the language
115 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
116 default_locale = locale.getdefaultlocale()[0]
117 if default_locale is None:
118 self.language = 'en-US'
120 self.language = default_locale.replace('_', '-')
121 if self.language not in available_languages:
122 self.language = self.language.split('-')[0]
123 if self.language not in available_languages:
124 for l in available_languages:
125 if l[0:2] == self.language:
127 # if language isn't available, default to english
128 if self.language not in available_languages:
129 self.language = 'en-US'
131 # build all relevant paths
132 def build_paths(self, tbb_version=None):
133 homedir = os.getenv('HOME')
135 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
136 if not os.path.exists(homedir):
138 os.mkdir(homedir, 0700)
140 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
141 if not os.access(homedir, os.W_OK):
142 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
144 tbb_config = '{0}/.config/torbrowser'.format(homedir)
145 tbb_cache = '{0}/.cache/torbrowser'.format(homedir)
146 tbb_local = '{0}/.local/share/torbrowser'.format(homedir)
147 old_tbb_data = '{0}/.torbrowser'.format(homedir)
151 if self.architecture == 'x86_64':
155 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
158 self.paths['tarball_url'] = '{0}torbrowser/'+tbb_version+'/'+tarball_filename
159 self.paths['tarball_file'] = tbb_cache+'/download/'+tarball_filename
160 self.paths['tarball_filename'] = tarball_filename
163 self.paths['sha256_file'] = tbb_cache+'/download/sha256sums.txt'
164 self.paths['sha256_sig_file'] = tbb_cache+'/download/sha256sums.txt.asc'
165 self.paths['sha256_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt'
166 self.paths['sha256_sig_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt.asc'
170 'config': tbb_config,
174 'old_data_dir': old_tbb_data,
175 'tbl_bin': '/usr/bin/torbrowser-launcher',
176 'icon_file': os.path.join(SHARE, 'pixmaps/torbrowser80.xpm'),
177 'torproject_pem': os.path.join(SHARE, 'torbrowser-launcher/torproject.pem'),
178 'erinn_key': os.path.join(SHARE, 'torbrowser-launcher/erinn.asc'),
179 'mirrors_txt': [os.path.join(SHARE, 'torbrowser-launcher/mirrors.txt'),
180 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
181 'modem_sound': os.path.join(SHARE, 'torbrowser-launcher/modem.ogg'),
182 'download_dir': tbb_cache+'/download',
183 'gnupg_homedir': tbb_local+'/gnupg_homedir',
184 'settings_file': tbb_config+'/settings',
185 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
186 'update_check_file': tbb_cache+'/download/RecommendedTBBVersions',
188 'dir': tbb_local+'/tbb/'+self.architecture,
189 'start': tbb_local+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
190 'versions': tbb_local+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
198 if not os.path.exists(path):
199 os.makedirs(path, 0700)
202 print _("Cannot create directory {0}").format(path)
204 if not os.access(path, os.W_OK):
205 print _("{0} is not writable").format(path)
209 # if gnupg_homedir isn't set up, set it up
210 def init_gnupg(self):
211 if not os.path.exists(self.paths['gnupg_homedir']):
212 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
213 self.mkdir(self.paths['gnupg_homedir'])
217 def import_keys(self):
218 print _('Importing keys')
219 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key']]).wait()
222 def load_mirrors(self):
224 for srcfile in self.paths['mirrors_txt']:
225 if os.path.exists(srcfile):
226 print "Successfully loaded mirrors from %s" % srcfile
227 elif not os.path.exists(srcfile):
228 print "Warning: can't load mirrors from %s" % srcfile
230 for mirror in open(srcfile, 'r').readlines():
231 if mirror.strip() not in self.mirrors:
232 self.mirrors.append(mirror.strip())
235 def load_settings(self):
237 'tbl_version': self.tbl_version,
238 'installed_version': False,
239 'latest_version': '0',
240 'update_over_tor': True,
241 'check_for_updates': False,
242 'modem_sound': False,
243 'last_update_check_timestamp': 0,
244 'mirror': self.default_mirror
247 if os.path.isfile(self.paths['settings_file']):
248 settings = pickle.load(open(self.paths['settings_file']))
251 # settings migrations
252 if settings['tbl_version'] <= '0.1.0':
253 print '0.1.0 migration'
254 settings['installed_version'] = settings['installed_version']['stable']
255 settings['latest_version'] = settings['latest_version']['stable']
258 # make new tbb folder
259 self.mkdir(self.paths['tbb']['dir'])
260 old_tbb_dir = self.paths['old_data_dir']+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language
261 new_tbb_dir = self.paths['tbb']['dir']+'/tor-browser_'+self.language
262 if os.path.isdir(old_tbb_dir):
263 os.rename(old_tbb_dir, new_tbb_dir)
265 # make sure settings file is up-to-date
266 for setting in default_settings:
267 if setting not in settings:
268 settings[setting] = default_settings[setting]
271 # make sure the version is current
272 if settings['tbl_version'] != self.tbl_version:
273 settings['tbl_version'] = self.tbl_version
276 self.settings = settings
281 self.settings = default_settings
285 def save_settings(self):
286 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
289 # get the process id of a program
291 def get_pid(bin_path, python=False):
294 for p in psutil.process_iter():
296 if p.pid != os.getpid():
299 if len(p.cmdline) > 1:
300 if 'python' in p.cmdline[0]:
303 if len(p.cmdline) > 0:
314 # bring program's x window to front
316 def bring_window_to_front(pid):
317 # figure out the window id
319 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
320 for line in p.stdout.readlines():
321 line_split = line.split()
322 cur_win_id = line_split[0]
323 cur_win_pid = int(line_split[2])
324 if cur_win_pid == pid:
329 subprocess.call(['wmctrl', '-i', '-a', win_id])
333 def __init__(self, common):
334 print _('Starting settings dialog')
338 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
339 self.window.set_title(_("Tor Browser Launcher Settings"))
340 self.window.set_icon_from_file(self.common.paths['icon_file'])
341 self.window.set_position(gtk.WIN_POS_CENTER)
342 self.window.set_border_width(10)
343 self.window.connect("delete_event", self.delete_event)
344 self.window.connect("destroy", self.destroy)
346 # build the rest of the UI
347 self.box = gtk.VBox(False, 10)
348 self.window.add(self.box)
351 self.hbox = gtk.HBox(False, 10)
352 self.box.pack_start(self.hbox, True, True, 0)
355 self.settings_box = gtk.VBox(False, 10)
356 self.hbox.pack_start(self.settings_box, True, True, 0)
357 self.settings_box.show()
359 self.labels_box = gtk.VBox(False, 10)
360 self.hbox.pack_start(self.labels_box, True, True, 0)
361 self.labels_box.show()
366 self.txsocks_found = True
368 self.txsocks_found = False
369 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
370 if self.txsocks_found:
371 self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
373 self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))
375 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
376 if self.common.settings['update_over_tor'] and self.txsocks_found:
377 self.tor_update_checkbox.set_active(True)
379 self.tor_update_checkbox.set_active(False)
381 if self.txsocks_found == False:
382 self.tor_update_checkbox.set_sensitive(False)
384 self.tor_update_checkbox.show()
387 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
388 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
389 if self.common.settings['check_for_updates']:
390 self.update_checkbox.set_active(True)
392 self.update_checkbox.set_active(False)
393 self.update_checkbox.show()
396 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
397 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
401 if self.common.settings['modem_sound']:
402 self.modem_checkbox.set_active(True)
404 self.modem_checkbox.set_active(False)
406 self.modem_checkbox.set_active(False)
407 self.modem_checkbox.set_sensitive(False)
408 self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
409 self.modem_checkbox.show()
412 if(self.common.settings['installed_version']):
413 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version']))
415 self.label1 = gtk.Label(_('Not installed'))
416 self.label1.set_line_wrap(True)
417 self.labels_box.pack_start(self.label1, True, True, 0)
420 if(self.common.settings['last_update_check_timestamp'] > 0):
421 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']))))
423 self.label1 = gtk.Label(_('Never checked for updates'))
424 self.label1.set_line_wrap(True)
425 self.labels_box.pack_start(self.label1, True, True, 0)
429 self.mirrors_box = gtk.HBox(False, 10)
430 self.box.pack_start(self.mirrors_box, True, True, 0)
431 self.mirrors_box.show()
433 self.mirrors_label = gtk.Label(_('Mirror'))
434 self.mirrors_label.set_line_wrap(True)
435 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
436 self.mirrors_label.show()
438 self.mirrors = gtk.combo_box_new_text()
439 for mirror in self.common.mirrors:
440 self.mirrors.append_text(mirror)
441 if self.common.settings['mirror'] in self.common.mirrors:
442 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
444 self.mirrors.set_active(0)
445 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
449 self.button_box = gtk.HButtonBox()
450 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
451 self.box.pack_start(self.button_box, True, True, 0)
452 self.button_box.show()
454 # save and launch button
455 save_launch_image = gtk.Image()
456 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
457 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
458 self.save_launch_button.set_image(save_launch_image)
459 self.save_launch_button.connect("clicked", self.save_launch, None)
460 self.button_box.add(self.save_launch_button)
461 self.save_launch_button.show()
463 # save and exit button
464 save_exit_image = gtk.Image()
465 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
466 self.save_exit_button = gtk.Button(_("Save & Exit"))
467 self.save_exit_button.set_image(save_exit_image)
468 self.save_exit_button.connect("clicked", self.save_exit, None)
469 self.button_box.add(self.save_exit_button)
470 self.save_exit_button.show()
473 cancel_image = gtk.Image()
474 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
475 self.cancel_button = gtk.Button(_("Cancel"))
476 self.cancel_button.set_image(cancel_image)
477 self.cancel_button.connect("clicked", self.destroy, None)
478 self.button_box.add(self.cancel_button)
479 self.cancel_button.show()
487 # UI Callback for update over tor/use system tor
488 def on_system_tor_clicked(self, event):
489 if self.txsocks_found:
490 value = self.system_tor_checkbox.get_active()
494 self.tor_update_checkbox.set_active(value)
495 self.tor_update_checkbox.set_sensitive(value)
498 def save_launch(self, widget, data=None):
500 subprocess.Popen([self.common.paths['tbl_bin']])
504 def save_exit(self, widget, data=None):
511 self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
512 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
513 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
515 # figure out the selected mirror
516 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
519 self.common.save_settings()
522 def delete_event(self, widget, event, data=None):
525 def destroy(self, widget, data=None):
530 def __init__(self, common):
531 print _('Starting launcher dialog')
535 self.set_gui(None, '', [])
536 self.launch_gui = True
537 print "LATEST VERSION", self.common.settings['latest_version']
538 self.common.build_paths(self.common.settings['latest_version'])
540 if self.common.settings['update_over_tor']:
544 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"))
545 md.set_position(gtk.WIN_POS_CENTER)
548 self.common.settings['update_over_tor'] = False
549 self.common.save_settings()
551 # is firefox already running?
552 if self.common.settings['installed_version']:
553 firefox_pid = self.common.get_pid('./Browser/firefox')
555 print _('Firefox are is open, bringing to focus')
556 # bring firefox to front
557 self.common.bring_window_to_front(firefox_pid)
561 check_for_updates = False
562 if self.common.settings['check_for_updates']:
563 check_for_updates = True
565 if not check_for_updates:
566 # how long was it since the last update check?
567 # 86400 seconds = 24 hours
568 current_timestamp = int(time.time())
569 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
570 check_for_updates = True
572 if check_for_updates:
574 print 'Checking for update'
575 self.set_gui('task', _("Checking for Tor Browser update."),
576 ['download_update_check',
579 # no need to check for update
580 print _('Checked for update within 24 hours, skipping')
581 self.start_launcher()
585 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
586 self.window.set_title(_("Tor Browser"))
587 self.window.set_icon_from_file(self.common.paths['icon_file'])
588 self.window.set_position(gtk.WIN_POS_CENTER)
589 self.window.set_border_width(10)
590 self.window.connect("delete_event", self.delete_event)
591 self.window.connect("destroy", self.destroy)
593 # build the rest of the UI
596 # download or run TBB
597 def start_launcher(self):
598 # is TBB already installed?
599 latest_version = self.common.settings['latest_version']
600 installed_version = self.common.settings['installed_version']
602 # verify installed version for newer versions of TBB (#58)
603 if installed_version >= '3.0':
604 versions_filename = self.common.paths['tbb']['versions']
605 if os.path.exists(versions_filename):
606 for line in open(versions_filename):
607 if 'TORBROWSER_VERSION' in line:
608 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
610 start = self.common.paths['tbb']['start']
611 if os.path.isfile(start) and os.access(start, os.X_OK):
612 if installed_version == latest_version:
613 print _('Latest version of TBB is installed, launching')
614 # current version of tbb is installed, launch it
616 self.launch_gui = False
617 elif installed_version < latest_version:
618 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
619 # there is a tbb upgrade available
620 self.set_gui('task', _("Your Tor Browser is out of date."),
622 'download_sha256_sig',
628 # for some reason the installed tbb is newer than the current version?
629 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
633 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
634 self.set_gui('task', _("Downloading and installing Tor Browser."),
636 'download_sha256_sig',
642 # there are different GUIs that might appear, this sets which one we want
643 def set_gui(self, gui, message, tasks, autostart=True):
645 self.gui_message = message
646 self.gui_tasks = tasks
648 self.gui_autostart = autostart
650 # set all gtk variables to False
652 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
657 self.progressbar = False
658 self.button_box = False
659 self.start_button = False
660 self.exit_button = False
662 # build the application's UI
666 self.box = gtk.VBox(False, 20)
667 self.window.add(self.box)
669 if 'error' in self.gui:
671 self.label = gtk.Label(self.gui_message)
672 self.label.set_line_wrap(True)
673 self.box.pack_start(self.label, True, True, 0)
677 self.button_box = gtk.HButtonBox()
678 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
679 self.box.pack_start(self.button_box, True, True, 0)
680 self.button_box.show()
682 if self.gui != 'error':
684 yes_image = gtk.Image()
685 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
686 self.yes_button = gtk.Button("Yes")
687 self.yes_button.set_image(yes_image)
688 if self.gui == 'error_try_stable':
689 self.yes_button.connect("clicked", self.try_stable, None)
690 elif self.gui == 'error_try_default_mirror':
691 self.yes_button.connect("clicked", self.try_default_mirror, None)
692 elif self.gui == 'error_try_tor':
693 self.yes_button.connect("clicked", self.try_tor, None)
694 self.button_box.add(self.yes_button)
695 self.yes_button.show()
698 exit_image = gtk.Image()
699 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
700 self.exit_button = gtk.Button("Exit")
701 self.exit_button.set_image(exit_image)
702 self.exit_button.connect("clicked", self.destroy, None)
703 self.button_box.add(self.exit_button)
704 self.exit_button.show()
706 elif self.gui == 'task':
708 self.label = gtk.Label(self.gui_message)
709 self.label.set_line_wrap(True)
710 self.box.pack_start(self.label, True, True, 0)
714 self.progressbar = gtk.ProgressBar(adjustment=None)
715 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
716 self.progressbar.set_pulse_step(0.01)
717 self.box.pack_start(self.progressbar, True, True, 0)
720 self.button_box = gtk.HButtonBox()
721 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
722 self.box.pack_start(self.button_box, True, True, 0)
723 self.button_box.show()
726 start_image = gtk.Image()
727 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
728 self.start_button = gtk.Button(_("Start"))
729 self.start_button.set_image(start_image)
730 self.start_button.connect("clicked", self.start, None)
731 self.button_box.add(self.start_button)
732 if not self.gui_autostart:
733 self.start_button.show()
736 exit_image = gtk.Image()
737 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
738 self.exit_button = gtk.Button(_("Exit"))
739 self.exit_button.set_image(exit_image)
740 self.exit_button.connect("clicked", self.destroy, None)
741 self.button_box.add(self.exit_button)
742 self.exit_button.show()
747 if self.gui_autostart:
750 # start button clicked, begin tasks
751 def start(self, widget, data=None):
752 # disable the start button
753 if self.start_button:
754 self.start_button.set_sensitive(False)
756 # start running tasks
759 # run the next task in the task list
763 if self.gui_task_i >= len(self.gui_tasks):
767 task = self.gui_tasks[self.gui_task_i]
769 # get ready for the next task
772 print _('Running task: {0}'.format(task))
773 if task == 'download_update_check':
774 print _('Downloading'), self.common.paths['update_check_url']
775 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
777 if task == 'attempt_update':
778 print _('Checking to see if update is needed')
779 self.attempt_update()
781 elif task == 'download_sha256':
782 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
783 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
785 elif task == 'download_sha256_sig':
786 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
787 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
789 elif task == 'download_tarball':
790 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
791 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
793 elif task == 'verify':
794 print _('Verifying signature')
797 elif task == 'extract':
798 print _('Extracting'), self.common.paths['tarball_filename']
802 print _('Running'), self.common.paths['tbb']['start']
805 elif task == 'start_over':
806 print _('Starting download over again')
809 def response_received(self, response):
810 class FileDownloader(Protocol):
811 def __init__(self, common, file, total, progress, done_cb):
815 self.progress = progress
816 self.all_done = done_cb
818 if response.code != 200:
819 if common.settings['mirror'] != common.default_mirror:
820 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']))
822 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
824 def dataReceived(self, bytes):
825 self.file.write(bytes)
826 self.so_far += len(bytes)
827 percent = float(self.so_far) / float(self.total)
828 self.progress.set_fraction(percent)
829 amount = float(self.so_far)
831 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
834 amount = amount / float(size)
837 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
839 def connectionLost(self, reason):
840 print _('Finished receiving body:'), reason.getErrorMessage()
841 self.all_done(reason)
843 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
844 response.deliverBody(dl)
846 def response_finished(self, msg):
847 if msg.check(ResponseDone):
848 self.file_download.close()
849 delattr(self, 'current_download_path')
855 print "FINISHED", msg
856 ## FIXME handle errors
858 def download_error(self, f):
859 print _("Download error:"), f.value, type(f.value)
861 if isinstance(f.value, TryStableException):
862 f.trap(TryStableException)
863 self.set_gui('error_try_stable', str(f.value), [], False)
865 elif isinstance(f.value, TryDefaultMirrorException):
866 f.trap(TryDefaultMirrorException)
867 self.set_gui('error_try_default_mirror', str(f.value), [], False)
869 elif isinstance(f.value, DownloadErrorException):
870 f.trap(DownloadErrorException)
871 self.set_gui('error', str(f.value), [], False)
873 elif isinstance(f.value, DNSLookupError):
874 f.trap(DNSLookupError)
875 if common.settings['mirror'] != common.default_mirror:
876 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)
878 self.set_gui('error', str(f.value), [], False)
880 elif isinstance(f.value, ResponseFailed):
881 for reason in f.value.reasons:
882 if isinstance(reason.value, OpenSSL.SSL.Error):
883 # TODO: add the ability to report attack by posting bug to trac.torproject.org
884 if not self.common.settings['update_over_tor']:
885 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)
887 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
890 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
894 def download(self, name, url, path):
895 # keep track of current download
896 self.current_download_path = path
898 # initialize the progress bar
899 mirror_url = url.format(self.common.settings['mirror'])
900 self.progressbar.set_fraction(0)
901 self.progressbar.set_text(_('Downloading {0}').format(name))
902 self.progressbar.show()
905 if self.common.settings['update_over_tor']:
906 print _('Updating over Tor')
907 from twisted.internet.endpoints import TCP4ClientEndpoint
908 from txsocksx.http import SOCKS5Agent
910 torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
912 # default mirror gets certificate pinning, only for requests that use the mirror
913 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
914 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
916 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
918 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
919 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
921 agent = Agent(reactor)
923 # actually, agent needs to follow redirect
924 agent = RedirectAgent(agent)
927 d = agent.request('GET', mirror_url,
928 Headers({'User-Agent': ['torbrowser-launcher']}),
931 self.file_download = open(path, 'w')
932 d.addCallback(self.response_received).addErrback(self.download_error)
934 if not reactor.running:
937 def try_default_mirror(self, widget, data=None):
938 # change mirror to default and relaunch TBL
939 self.common.settings['mirror'] = self.common.default_mirror
940 self.common.save_settings()
941 subprocess.Popen([self.common.paths['tbl_bin']])
944 def try_tor(self, widget, data=None):
945 # set update_over_tor to true and relaunch TBL
946 self.common.settings['update_over_tor'] = True
947 self.common.save_settings()
948 subprocess.Popen([self.common.paths['tbl_bin']])
951 def attempt_update(self):
952 # load the update check file
954 versions = json.load(open(self.common.paths['update_check_file']))
957 # filter linux versions
959 for version in versions:
960 if '-Linux' in version:
961 valid.append(str(version))
968 # remove alphas/betas
969 for version in valid:
970 if '-alpha-' not in version and '-beta-' not in version:
971 stable.append(version)
973 latest = stable.pop()
978 self.common.settings['latest_version'] = latest[:-len('-Linux')]
979 self.common.settings['last_update_check_timestamp'] = int(time.time())
980 self.common.settings['check_for_updates'] = False
981 self.common.save_settings()
982 self.common.build_paths(self.common.settings['latest_version'])
983 self.start_launcher()
986 # failed to find the latest version
987 self.set_gui('error', _("Error checking for updates."), [], False)
990 # not a valid JSON object
991 self.set_gui('error', _("Error checking for updates."), [], False)
998 # initialize the progress bar
999 self.progressbar.set_fraction(0)
1000 self.progressbar.set_text(_('Verifying Signature'))
1001 self.progressbar.show()
1004 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1005 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1006 self.pulse_until_process_exits(p)
1007 if p.returncode == 0:
1008 # compare with sha256 of the tarball
1009 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1010 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1011 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1017 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1018 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)
1022 if not reactor.running:
1026 # initialize the progress bar
1027 self.progressbar.set_fraction(0)
1028 self.progressbar.set_text(_('Installing'))
1029 self.progressbar.show()
1034 if self.common.paths['tarball_file'][-2:] == 'xz':
1035 # if tarball is .tar.xz
1036 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1037 tf = tarfile.open(fileobj=xz)
1038 tf.extractall(self.common.paths['tbb']['dir'])
1041 # if tarball is .tar.gz
1042 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1043 tf = tarfile.open(self.common.paths['tarball_file'])
1044 tf.extractall(self.common.paths['tbb']['dir'])
1050 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1055 # installation is finished, so save installed_version
1056 self.common.settings['installed_version'] = self.common.settings['latest_version']
1057 self.common.save_settings()
1061 def run(self, run_next_task=True):
1062 devnull = open('/dev/null', 'w')
1063 subprocess.Popen([self.common.paths['tbb']['start']], stdout=devnull, stderr=devnull)
1066 if self.common.settings['modem_sound']:
1070 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1074 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."))
1075 md.set_position(gtk.WIN_POS_CENTER)
1082 # make the progress bar pulse until process p (a Popen object) finishes
1083 def pulse_until_process_exits(self, p):
1084 while p.poll() is None:
1086 self.progressbar.pulse()
1089 # start over and download TBB again
1090 def start_over(self):
1091 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1092 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1097 def refresh_gtk(self):
1098 while gtk.events_pending():
1099 gtk.main_iteration(False)
1102 def delete_event(self, widget, event, data=None):
1105 def destroy(self, widget, data=None):
1106 if hasattr(self, 'file_download'):
1107 self.file_download.close()
1108 if hasattr(self, 'current_download_path'):
1109 os.remove(self.current_download_path)
1110 delattr(self, 'current_download_path')
1114 if __name__ == "__main__":
1115 with open(os.path.join(SHARE, 'torbrowser-launcher/version')) as buf:
1116 tor_browser_launcher_version = buf.read().strip()
1118 print _('Tor Browser Launcher')
1119 print _('By Micah Lee, licensed under GPLv3')
1120 print _('version {0}').format(tor_browser_launcher_version)
1121 print 'https://github.com/micahflee/torbrowser-launcher'
1123 common = TBLCommon(tor_browser_launcher_version)
1125 # is torbrowser-launcher already running?
1126 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1128 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1129 common.bring_window_to_front(tbl_pid)
1132 if '-settings' in sys.argv:
1134 app = TBLSettings(common)
1138 app = TBLLauncher(common)