4 https://github.com/micahflee/torbrowser-launcher/
6 Copyright (c) 2013 Micah Lee <micahflee@riseup.net>
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 sys.path.append('/usr/share/torbrowser-launcher/lib/')
34 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
36 from twisted.internet import gtk2reactor
38 from twisted.internet import reactor
44 import os, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil
46 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
47 from twisted.web.http_headers import Headers
48 from twisted.internet.protocol import Protocol
49 from twisted.internet.ssl import ClientContextFactory
50 from twisted.internet.endpoints import TCP4ClientEndpoint
51 from twisted.internet.error import DNSLookupError
53 from txsocksx.client import SOCKS5ClientEndpoint
57 class TryStableException(Exception):
59 class TryDefaultMirrorException(Exception):
61 class DownloadErrorException(Exception):
64 class VerifyTorProjectCert(ClientContextFactory):
66 def __init__(self, torproject_pem):
67 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
69 def getContext(self, host, port):
70 ctx = ClientContextFactory.getContext(self)
71 ctx.set_verify_depth(0)
72 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
75 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
76 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
80 def __init__(self, tbl_version):
81 print _('Initializing Tor Browser Launcher')
82 self.tbl_version = tbl_version
85 self.available_versions = {
86 'stable': _('Tor Browser Bundle - stable'),
87 'alpha': _('Tor Browser Bundle - alpha')
89 self.default_mirror = 'https://www.torproject.org/dist/'
91 self.discover_arch_lang()
93 self.mkdir(self.paths['data_dir'])
96 self.mkdir(self.paths['download_dir'])
97 self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
100 # allow buttons to have icons
102 gtk_settings = gtk.settings_get_default()
103 gtk_settings.props.gtk_button_images = True
107 # discover the architecture and language
108 def discover_arch_lang(self):
109 # figure out the architecture
110 (sysname, nodename, release, version, machine) = os.uname()
111 self.architecture = machine
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 == 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 os.path.exists(homedir) == False:
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
146 if tbb_version >= '3.':
148 dirname = tbb_version.replace('-alpha-', 'a')
149 dirname = tbb_version.replace('-beta-', 'b')
150 if self.architecture == 'x86_64':
154 tarball_filename = dirname+'/tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
157 self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
158 self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
159 self.paths['sha256_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/sha256sums.txt'
160 self.paths['sha256_sig_url'] = 'https://archive.torproject.org/tor-package-archive/torbrowser/'+dirname+'/sha256sums.txt.mp-asc'
163 tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
166 self.paths['tarball_sig_file'] = tbb_data+'/download/'+tarball_filename+'.asc'
167 self.paths['tarball_sig_url'] = '{0}torbrowser/linux/'+tarball_filename+'.asc'
168 self.paths['tarball_sig_filename'] = tarball_filename+'.asc'
170 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
171 self.paths['tarball_url'] = '{0}torbrowser/linux/'+tarball_filename # {0} will be replaced with the mirror
172 self.paths['tarball_filename'] = tarball_filename
176 'tbl_bin': '/usr/bin/torbrowser-launcher',
177 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
178 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
179 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
180 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
181 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
182 'mike_key': '/usr/share/torbrowser-launcher/mike.asc',
183 'mirrors_txt': '/usr/share/torbrowser-launcher/mirrors.txt',
184 'data_dir': tbb_data,
185 'download_dir': tbb_data+'/download',
186 'gnupg_homedir': tbb_data+'/gnupg_homedir',
187 'settings_file': tbb_data+'/settings',
188 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
189 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
192 'dir': tbb_data+'/tbb/stable/'+self.architecture,
193 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
194 'vidalia_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
195 'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
196 'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
199 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
200 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
201 'vidalia_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
202 'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
203 'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
209 def mkdir(self, path):
211 if os.path.exists(path) == False:
212 os.makedirs(path, 0700)
215 print _("Cannot create directory {0}").format(path)
217 if not os.access(path, os.W_OK):
218 print _("{0} is not writable").format(path)
222 # if gnupg_homedir isn't set up, set it up
223 def init_gnupg(self):
224 if not os.path.exists(self.paths['gnupg_homedir']):
225 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
226 if self.mkdir(self.paths['gnupg_homedir']):
228 print _('Importing keys')
229 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key']]).wait()
230 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['sebastian_key']]).wait()
231 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['alexandre_key']]).wait()
234 def load_mirrors(self):
236 for mirror in open(self.paths['mirrors_txt'], 'r').readlines():
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 'last_update_check_timestamp': 0,
255 'mirror': self.default_mirror
258 if os.path.isfile(self.paths['settings_file']):
259 settings = pickle.load(open(self.paths['settings_file']))
261 # what version settings is this?
262 if not 'tbl_version' in settings:
263 settings['tbl_version'] = '0.0.1'
265 # sanity checks for current version
266 if settings['tbl_version'] == self.tbl_version:
268 if not 'preferred' in settings:
269 good_settings = False
270 if not 'installed_version' in settings:
271 good_settings = False
272 if not 'stable' in settings['installed_version']:
273 good_settings = False
274 if not 'alpha' in settings['installed_version']:
275 good_settings = False
276 if not 'latest_version' in settings:
277 good_settings = False
278 if not 'stable' in settings['latest_version']:
279 good_settings = False
280 if not 'alpha' in settings['latest_version']:
281 good_settings = False
282 if not 'update_over_tor' in settings:
283 good_settings = False
284 if not 'check_for_updates' in settings:
285 good_settings = False
286 if not 'last_update_check_timestamp' in settings:
287 good_settings = False
288 if not 'mirror' in settings:
289 good_settings = False
292 self.settings = settings
294 self.settings = default_settings
296 # settings migrations for previous versions
297 elif settings['tbl_version'] == '0.0.1':
298 self.settings = default_settings
299 self.settings['installed_version']['alpha'] = settings['installed_version']
303 self.mkdir(self.paths['tbb']['alpha']['dir'])
304 # todo: move already-installed TBB alpha to new location
305 if os.path.exists(self.paths['data_dir']+'/tbb/x86_64'):
307 if os.path.exists(self.paths['data_dir']+'/tbb/i686'):
311 self.settings = default_settings
315 def save_settings(self):
316 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
319 # get the process id of a program
320 def get_pid(self, bin_path, python = False):
323 for p in psutil.process_iter():
325 if p.pid != os.getpid():
328 if len(p.cmdline) > 1:
329 if 'python' in p.cmdline[0]:
332 if len(p.cmdline) > 0:
343 # bring program's x window to front
344 def bring_window_to_front(self, pid):
345 # figure out the window id
347 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
348 for line in p.stdout.readlines():
349 line_split = line.split()
350 cur_win_id = line_split[0]
351 cur_win_pid = int(line_split[2])
352 if cur_win_pid == pid:
357 subprocess.call(['wmctrl', '-i', '-a', win_id])
360 def __init__(self, common):
361 print _('Starting settings dialog')
365 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
366 self.window.set_title(_("Tor Browser Launcher Settings"))
367 self.window.set_icon_from_file(self.common.paths['icon_file'])
368 self.window.set_position(gtk.WIN_POS_CENTER)
369 self.window.set_border_width(10)
370 self.window.connect("delete_event", self.delete_event)
371 self.window.connect("destroy", self.destroy)
373 # build the rest of the UI
374 self.box = gtk.VBox(False, 10)
375 self.window.add(self.box)
378 self.hbox = gtk.HBox(False, 10)
379 self.box.pack_start(self.hbox, True, True, 0)
382 self.settings_box = gtk.VBox(False, 10)
383 self.hbox.pack_start(self.settings_box, True, True, 0)
384 self.settings_box.show()
386 self.labels_box = gtk.VBox(False, 10)
387 self.hbox.pack_start(self.labels_box, True, True, 0)
388 self.labels_box.show()
391 self.preferred_box = gtk.HBox(False, 10)
392 self.settings_box.pack_start(self.preferred_box, True, True, 0)
393 self.preferred_box.show()
395 self.preferred_label = gtk.Label(_('I prefer'))
396 self.preferred_label.set_line_wrap(True)
397 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
398 self.preferred_label.show()
400 self.preferred_options = []
401 for i in self.common.available_versions:
402 self.preferred_options.append(self.common.available_versions[i])
403 self.preferred_options.sort()
405 self.preferred = gtk.combo_box_new_text()
406 for option in self.preferred_options:
407 self.preferred.append_text(option)
408 if self.common.settings['preferred'] in self.common.available_versions:
409 self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
411 self.preferred.set_active(0)
412 self.preferred_box.pack_start(self.preferred, True, True, 0)
413 self.preferred.show()
416 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
418 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
419 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
420 if self.common.settings['update_over_tor']:
421 self.tor_update_checkbox.set_active(True)
423 self.tor_update_checkbox.set_active(False)
424 self.tor_update_checkbox.show()
428 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
429 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
430 if self.common.settings['check_for_updates']:
431 self.update_checkbox.set_active(True)
433 self.update_checkbox.set_active(False)
434 self.update_checkbox.show()
437 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
438 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
440 self.label1 = gtk.Label(_('Not installed'))
441 self.label1.set_line_wrap(True)
442 self.labels_box.pack_start(self.label1, True, True, 0)
445 if(self.common.settings['last_update_check_timestamp'] > 0):
446 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']))))
448 self.label1 = gtk.Label(_('Never checked for updates'))
449 self.label1.set_line_wrap(True)
450 self.labels_box.pack_start(self.label1, True, True, 0)
454 self.mirrors_box = gtk.HBox(False, 10)
455 self.box.pack_start(self.mirrors_box, True, True, 0)
456 self.mirrors_box.show()
458 self.mirrors_label = gtk.Label(_('Mirror'))
459 self.mirrors_label.set_line_wrap(True)
460 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
461 self.mirrors_label.show()
463 self.mirrors = gtk.combo_box_new_text()
464 for mirror in self.common.mirrors:
465 self.mirrors.append_text(mirror)
466 if self.common.settings['mirror'] in self.common.mirrors:
467 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
469 self.preferred.set_active(0)
470 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
474 self.button_box = gtk.HButtonBox()
475 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
476 self.box.pack_start(self.button_box, True, True, 0)
477 self.button_box.show()
479 # save and launch button
480 save_launch_image = gtk.Image()
481 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
482 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
483 self.save_launch_button.set_image(save_launch_image)
484 self.save_launch_button.connect("clicked", self.save_launch, None)
485 self.button_box.add(self.save_launch_button)
486 self.save_launch_button.show()
488 # save and exit button
489 save_exit_image = gtk.Image()
490 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
491 self.save_exit_button = gtk.Button(_("Save & Exit"))
492 self.save_exit_button.set_image(save_exit_image)
493 self.save_exit_button.connect("clicked", self.save_exit, None)
494 self.button_box.add(self.save_exit_button)
495 self.save_exit_button.show()
498 cancel_image = gtk.Image()
499 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
500 self.cancel_button = gtk.Button(_("Cancel"))
501 self.cancel_button.set_image(cancel_image)
502 self.cancel_button.connect("clicked", self.destroy, None)
503 self.button_box.add(self.cancel_button)
504 self.cancel_button.show()
513 def save_launch(self, widget, data=None):
515 p = subprocess.Popen([self.common.paths['tbl_bin']])
519 def save_exit(self, widget, data=None):
525 # figure out the selected preferred option
527 selected = self.preferred_options[self.preferred.get_active()]
528 for i in self.common.available_versions:
529 if self.common.available_versions[i] == selected:
532 self.common.settings['preferred'] = preferred
535 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
536 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
538 # figure out the selected mirror
539 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
542 self.common.save_settings()
545 def delete_event(self, widget, event, data=None):
547 def destroy(self, widget, data=None):
552 def __init__(self, common):
553 print _('Starting launcher dialog')
557 self.set_gui(None, '', [])
558 self.launch_gui = True
559 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
561 # is vidalia already running and we just need to open a new firefox?
562 if self.common.settings['installed_version']:
563 vidalia_pid = self.common.get_pid('./App/vidalia')
564 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
566 if vidalia_pid and not firefox_pid:
567 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
568 self.common.bring_window_to_front(vidalia_pid)
569 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'], '-no-remote', '-profile', self.common.paths['tbb'][self.common.settings['preferred']]['firefox_profile']])
571 elif vidalia_pid and firefox_pid:
572 print _('Vidalia and Firefox are already open, bringing them to focus')
574 # bring firefox to front, then vidalia
575 self.common.bring_window_to_front(firefox_pid)
576 self.common.bring_window_to_front(vidalia_pid)
580 check_for_updates = False
581 if self.common.settings['check_for_updates']:
582 check_for_updates = True
584 if not check_for_updates:
585 # how long was it since the last update check?
586 # 86400 seconds = 24 hours
587 current_timestamp = int(time.time())
588 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
589 check_for_updates = True
591 if check_for_updates:
593 print 'Checking for update'
594 self.set_gui('task', _("Checking for Tor Browser update."),
595 ['download_update_check',
598 # no need to check for update
599 print _('Checked for update within 24 hours, skipping')
600 self.start_launcher()
604 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
605 self.window.set_title(_("Tor Browser"))
606 self.window.set_icon_from_file(self.common.paths['icon_file'])
607 self.window.set_position(gtk.WIN_POS_CENTER)
608 self.window.set_border_width(10)
609 self.window.connect("delete_event", self.delete_event)
610 self.window.connect("destroy", self.destroy)
612 # build the rest of the UI
615 # download or run TBB
616 def start_launcher(self):
617 # is TBB already installed?
618 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
619 if os.path.isfile(start) and os.access(start, os.X_OK):
620 if self.common.settings['installed_version'][self.common.settings['preferred']] == self.common.settings['latest_version'][self.common.settings['preferred']]:
621 # current version of tbb is installed, launch it
623 self.launch_gui = False
624 elif self.common.settings['installed_version'][self.common.settings['preferred']] < self.common.settings['latest_version'][self.common.settings['preferred']]:
625 # there is a tbb upgrade available
626 self.set_gui('task', _("Your Tor Browser is out of date."),
627 ['download_tarball_sig',
633 # for some reason the installed tbb is newer than the current version?
634 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
638 # are the tarball and sig already downloaded?
639 if os.path.isfile(self.common.paths['tarball_file']) and os.path.isfile(self.common.paths['tarball_sig_file']):
640 # start the gui with verify
641 self.set_gui('task', _("Installing Tor Browser."),
648 self.set_gui('task', _("Downloading and installing Tor Browser."),
649 ['download_tarball_sig',
655 # there are different GUIs that might appear, this sets which one we want
656 def set_gui(self, gui, message, tasks, autostart=True):
658 self.gui_message = message
659 self.gui_tasks = tasks
661 self.gui_autostart = autostart
663 # set all gtk variables to False
665 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
670 self.progressbar = False
671 self.button_box = False
672 self.start_button = False
673 self.exit_button = False
675 # build the application's UI
679 self.box = gtk.VBox(False, 20)
680 self.window.add(self.box)
682 if 'error' in self.gui:
684 self.label = gtk.Label( self.gui_message )
685 self.label.set_line_wrap(True)
686 self.box.pack_start(self.label, True, True, 0)
690 self.button_box = gtk.HButtonBox()
691 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
692 self.box.pack_start(self.button_box, True, True, 0)
693 self.button_box.show()
695 if self.gui != 'error':
697 yes_image = gtk.Image()
698 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
699 self.yes_button = gtk.Button("Yes")
700 self.yes_button.set_image(yes_image)
701 if self.gui == 'error_try_stable':
702 self.yes_button.connect("clicked", self.try_stable, None)
703 elif self.gui == 'error_try_default_mirror':
704 self.yes_button.connect("clicked", self.try_default_mirror, None)
705 elif self.gui == 'error_try_tor':
706 self.yes_button.connect("clicked", self.try_tor, None)
707 self.button_box.add(self.yes_button)
708 self.yes_button.show()
711 exit_image = gtk.Image()
712 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
713 self.exit_button = gtk.Button("Exit")
714 self.exit_button.set_image(exit_image)
715 self.exit_button.connect("clicked", self.destroy, None)
716 self.button_box.add(self.exit_button)
717 self.exit_button.show()
719 elif self.gui == 'task':
721 self.label = gtk.Label( self.gui_message )
722 self.label.set_line_wrap(True)
723 self.box.pack_start(self.label, True, True, 0)
727 self.progressbar = gtk.ProgressBar(adjustment=None)
728 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
729 self.progressbar.set_pulse_step(0.01)
730 self.box.pack_start(self.progressbar, True, True, 0)
733 self.button_box = gtk.HButtonBox()
734 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
735 self.box.pack_start(self.button_box, True, True, 0)
736 self.button_box.show()
739 start_image = gtk.Image()
740 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
741 self.start_button = gtk.Button(_("Start"))
742 self.start_button.set_image(start_image)
743 self.start_button.connect("clicked", self.start, None)
744 self.button_box.add(self.start_button)
745 if not self.gui_autostart:
746 self.start_button.show()
749 exit_image = gtk.Image()
750 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
751 self.exit_button = gtk.Button(_("Exit"))
752 self.exit_button.set_image(exit_image)
753 self.exit_button.connect("clicked", self.destroy, None)
754 self.button_box.add(self.exit_button)
755 self.exit_button.show()
760 if self.gui_autostart:
763 # start button clicked, begin tasks
764 def start(self, widget, data=None):
765 # disable the start button
766 if self.start_button:
767 self.start_button.set_sensitive(False)
769 # start running tasks
772 # run the next task in the task list
776 if self.gui_task_i >= len(self.gui_tasks):
780 task = self.gui_tasks[self.gui_task_i]
782 # get ready for the next task
785 if task == 'download_update_check':
786 print _('Downloading'), self.common.paths['update_check_url']
787 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
789 if task == 'attempt_update':
790 print _('Checking to see if update is needed')
791 self.attempt_update()
793 elif task == 'download_tarball_sig':
794 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
795 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
797 elif task == 'download_tarball':
798 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
799 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
801 elif task == 'verify':
802 print _('Verifying signature')
805 elif task == 'extract':
806 print _('Extracting'), self.common.paths['tarball_filename']
810 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
813 elif task == 'start_over':
814 print _('Starting download over again')
817 def response_received(self, response):
818 class FileDownloader(Protocol):
819 def __init__(self, common, file, total, progress, done_cb):
823 self.progress = progress
824 self.all_done = done_cb
826 if response.code != 200:
829 if response.code == 404:
830 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
834 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?"))
836 if common.settings['mirror'] != common.default_mirror:
837 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']))
839 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
841 def dataReceived(self, bytes):
842 self.file.write(bytes)
843 self.so_far += len(bytes)
844 percent = float(self.so_far) / float(self.total)
845 self.progress.set_fraction(percent)
846 amount = float(self.so_far)
848 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
851 amount = amount / float(size)
854 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
856 def connectionLost(self, reason):
857 print _('Finished receiving body:'), reason.getErrorMessage()
858 self.all_done(reason)
860 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
861 response.deliverBody(dl)
863 def response_finished(self, msg):
864 if msg.check(ResponseDone):
865 self.file_download.close()
866 delattr(self, 'current_download_path')
872 print "FINISHED", msg
873 ## FIXME handle errors
875 def download_error(self, f):
876 print _("Download error:"), f.value, type(f.value)
878 if isinstance(f.value, TryStableException):
879 f.trap(TryStableException)
880 self.set_gui('error_try_stable', str(f.value), [], False)
882 elif isinstance(f.value, TryDefaultMirrorException):
883 f.trap(TryDefaultMirrorException)
884 self.set_gui('error_try_default_mirror', str(f.value), [], False)
886 elif isinstance(f.value, DownloadErrorException):
887 f.trap(DownloadErrorException)
888 self.set_gui('error', str(f.value), [], False)
890 elif isinstance(f.value, DNSLookupError):
891 f.trap(DNSLookupError)
892 if common.settings['mirror'] != common.default_mirror:
893 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)
895 self.set_gui('error', str(f.value), [], False)
897 elif isinstance(f.value, ResponseFailed):
898 for reason in f.value.reasons:
899 if isinstance(reason.value, OpenSSL.SSL.Error):
900 # TODO: add the ability to report attack by posting bug to trac.torproject.org
901 if not self.common.settings['update_over_tor']:
902 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)
904 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
907 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
911 def download(self, name, url, path):
912 # keep track of current download
913 self.current_download_path = path
915 # initialize the progress bar
916 mirror_url = url.format(self.common.settings['mirror'])
917 self.progressbar.set_fraction(0)
918 self.progressbar.set_text(_('Downloading {0}').format(name))
919 self.progressbar.show()
922 # default mirror gets certificate pinning, only for requests that use the mirror
923 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
924 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
926 agent = Agent(reactor)
928 # actually, agent needs to follow redirect
929 agent = RedirectAgent(agent)
932 d = agent.request('GET', mirror_url,
933 Headers({'User-Agent': ['torbrowser-launcher']}),
936 self.file_download = open(path, 'w')
937 d.addCallback(self.response_received).addErrback(self.download_error)
939 if not reactor.running:
942 def try_stable(self, widget, data=None):
943 # change preferred to stable and relaunch TBL
944 self.common.settings['preferred'] = 'stable'
945 self.common.save_settings()
946 p = subprocess.Popen([self.common.paths['tbl_bin']])
949 def try_default_mirror(self, widget, data=None):
950 # change preferred to stable and relaunch TBL
951 self.common.settings['mirror'] = self.common.default_mirror
952 self.common.save_settings()
953 p = subprocess.Popen([self.common.paths['tbl_bin']])
956 def try_tor(self, widget, data=None):
957 # set update_over_tor to true and relaunch TBL
958 self.common.settings['update_over_tor'] = True
959 self.common.save_settings()
960 p = subprocess.Popen([self.common.paths['tbl_bin']])
963 def attempt_update(self):
964 # load the update check file
966 versions = json.load(open(self.common.paths['update_check_file']))
970 # filter linux versions
973 for version in versions:
974 if str(version).find('-Linux') != -1:
975 if version.find('alpha') != -1:
976 valid_alphas.append(str(version))
978 valid_stables.append(str(version))
980 if len(valid_alphas):
981 latest_alpha = valid_alphas.pop()
983 if len(valid_stables):
984 latest_stable = valid_stables.pop()
986 if latest_stable or latest_alpha:
988 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
990 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
991 self.common.settings['last_update_check_timestamp'] = int(time.time())
992 self.common.settings['check_for_updates'] = False
993 self.common.save_settings()
994 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
995 self.start_launcher()
998 # failed to find the latest version
999 self.set_gui('error', _("Error checking for updates."), [], False)
1002 # not a valid JSON object
1003 self.set_gui('error', _("Error checking for updates."), [], False)
1010 # initialize the progress bar
1011 self.progressbar.set_fraction(0)
1012 self.progressbar.set_text(_('Verifying Signature'))
1013 self.progressbar.show()
1015 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
1016 self.pulse_until_process_exits(p)
1018 if p.returncode == 0:
1021 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1022 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)
1026 if not reactor.running:
1030 # initialize the progress bar
1031 self.progressbar.set_fraction(0)
1032 self.progressbar.set_text(_('Installing'))
1033 self.progressbar.show()
1036 # make sure this file is a tarfile
1037 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1038 tf = tarfile.open(self.common.paths['tarball_file'])
1039 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1041 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
1045 # installation is finished, so save installed_version
1046 self.common.settings['installed_version'] = self.common.settings['latest_version']
1047 self.common.save_settings()
1051 def run(self, run_next_task = True):
1052 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1056 # make the progress bar pulse until process p (a Popen object) finishes
1057 def pulse_until_process_exits(self, p):
1058 while p.poll() == None:
1060 self.progressbar.pulse()
1063 # start over and download TBB again
1064 def start_over(self):
1065 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1066 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1071 def refresh_gtk(self):
1072 while gtk.events_pending():
1073 gtk.main_iteration(False)
1076 def delete_event(self, widget, event, data=None):
1078 def destroy(self, widget, data=None):
1079 if hasattr(self, 'file_download'):
1080 self.file_download.close()
1081 if hasattr(self, 'current_download_path'):
1082 os.remove(self.current_download_path)
1083 delattr(self, 'current_download_path')
1087 if __name__ == "__main__":
1088 tor_browser_launcher_version = '0.0.2'
1090 print _('Tor Browser Launcher')
1091 print _('By Micah Lee, licensed under GPLv3')
1092 print _('version {0}').format(tor_browser_launcher_version)
1093 print 'https://github.com/micahflee/torbrowser-launcher'
1095 common = TBLCommon(tor_browser_launcher_version)
1097 # is torbrowser-launcher already running?
1098 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1100 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1101 common.bring_window_to_front(tbl_pid)
1104 if '-settings' in sys.argv:
1106 app = TBLSettings(common)
1110 app = TBLLauncher(common)