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 tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
147 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
148 self.paths['tarball_sig_file'] = tbb_data+'/download/'+tarball_filename+'.asc'
149 self.paths['tarball_url'] = '{0}torbrowser/linux/'+tarball_filename # {0} will be replaced with the mirror
150 self.paths['tarball_sig_url'] = '{0}torbrowser/linux/'+tarball_filename+'.asc'
151 self.paths['tarball_filename'] = tarball_filename
152 self.paths['tarball_sig_filename'] = tarball_filename+'.asc'
156 'tbl_bin': '/usr/bin/torbrowser-launcher',
157 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
158 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
159 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
160 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
161 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
162 'mirrors_txt': '/usr/share/torbrowser-launcher/mirrors.txt',
163 'data_dir': tbb_data,
164 'download_dir': tbb_data+'/download',
165 'gnupg_homedir': tbb_data+'/gnupg_homedir',
166 'settings_file': tbb_data+'/settings',
167 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
168 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
171 'dir': tbb_data+'/tbb/stable/'+self.architecture,
172 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
173 'vidalia_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
174 'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
175 'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
178 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
179 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
180 'vidalia_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
181 'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
182 'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
188 def mkdir(self, path):
190 if os.path.exists(path) == False:
191 os.makedirs(path, 0700)
194 print _("Cannot create directory {0}").format(path)
196 if not os.access(path, os.W_OK):
197 print _("{0} is not writable").format(path)
201 # if gnupg_homedir isn't set up, set it up
202 def init_gnupg(self):
203 if not os.path.exists(self.paths['gnupg_homedir']):
204 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
205 if self.mkdir(self.paths['gnupg_homedir']):
207 print _('Importing keys')
208 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key']]).wait()
209 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['sebastian_key']]).wait()
210 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['alexandre_key']]).wait()
213 def load_mirrors(self):
215 for mirror in open(self.paths['mirrors_txt'], 'r').readlines():
216 self.mirrors.append(mirror.strip())
219 def load_settings(self):
221 'tbl_version': self.tbl_version,
222 'preferred': 'stable',
223 'installed_version': {
231 'update_over_tor': True,
232 'check_for_updates': False,
233 'last_update_check_timestamp': 0,
234 'mirror': self.default_mirror
237 if os.path.isfile(self.paths['settings_file']):
238 settings = pickle.load(open(self.paths['settings_file']))
240 # what version settings is this?
241 if not 'tbl_version' in settings:
242 settings['tbl_version'] = '0.0.1'
244 # sanity checks for current version
245 if settings['tbl_version'] == self.tbl_version:
247 if not 'preferred' in settings:
248 good_settings = False
249 if not 'installed_version' in settings:
250 good_settings = False
251 if not 'stable' in settings['installed_version']:
252 good_settings = False
253 if not 'alpha' in settings['installed_version']:
254 good_settings = False
255 if not 'latest_version' in settings:
256 good_settings = False
257 if not 'stable' in settings['latest_version']:
258 good_settings = False
259 if not 'alpha' in settings['latest_version']:
260 good_settings = False
261 if not 'update_over_tor' in settings:
262 good_settings = False
263 if not 'check_for_updates' in settings:
264 good_settings = False
265 if not 'last_update_check_timestamp' in settings:
266 good_settings = False
267 if not 'mirror' in settings:
268 good_settings = False
271 self.settings = settings
273 self.settings = default_settings
275 # settings migrations for previous versions
276 elif settings['tbl_version'] == '0.0.1':
277 self.settings = default_settings
278 self.settings['installed_version']['alpha'] = settings['installed_version']
282 self.mkdir(self.paths['tbb']['alpha']['dir'])
283 # todo: move already-installed TBB alpha to new location
284 if os.path.exists(self.paths['data_dir']+'/tbb/x86_64'):
286 if os.path.exists(self.paths['data_dir']+'/tbb/i686'):
290 self.settings = default_settings
294 def save_settings(self):
295 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
298 # get the process id of a program
299 def get_pid(self, 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
323 def bring_window_to_front(self, pid):
324 # figure out the window id
326 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
327 for line in p.stdout.readlines():
328 line_split = line.split()
329 cur_win_id = line_split[0]
330 cur_win_pid = int(line_split[2])
331 if cur_win_pid == pid:
336 subprocess.call(['wmctrl', '-i', '-a', win_id])
339 def __init__(self, common):
340 print _('Starting settings dialog')
344 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
345 self.window.set_title(_("Tor Browser Launcher Settings"))
346 self.window.set_icon_from_file(self.common.paths['icon_file'])
347 self.window.set_position(gtk.WIN_POS_CENTER)
348 self.window.set_border_width(10)
349 self.window.connect("delete_event", self.delete_event)
350 self.window.connect("destroy", self.destroy)
352 # build the rest of the UI
353 self.box = gtk.VBox(False, 10)
354 self.window.add(self.box)
357 self.hbox = gtk.HBox(False, 10)
358 self.box.pack_start(self.hbox, True, True, 0)
361 self.settings_box = gtk.VBox(False, 10)
362 self.hbox.pack_start(self.settings_box, True, True, 0)
363 self.settings_box.show()
365 self.labels_box = gtk.VBox(False, 10)
366 self.hbox.pack_start(self.labels_box, True, True, 0)
367 self.labels_box.show()
370 self.preferred_box = gtk.HBox(False, 10)
371 self.settings_box.pack_start(self.preferred_box, True, True, 0)
372 self.preferred_box.show()
374 self.preferred_label = gtk.Label(_('I prefer'))
375 self.preferred_label.set_line_wrap(True)
376 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
377 self.preferred_label.show()
379 self.preferred_options = []
380 for i in self.common.available_versions:
381 self.preferred_options.append(self.common.available_versions[i])
382 self.preferred_options.sort()
384 self.preferred = gtk.combo_box_new_text()
385 for option in self.preferred_options:
386 self.preferred.append_text(option)
387 if self.common.settings['preferred'] in self.common.available_versions:
388 self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
390 self.preferred.set_active(0)
391 self.preferred_box.pack_start(self.preferred, True, True, 0)
392 self.preferred.show()
395 # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
397 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
398 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
399 if self.common.settings['update_over_tor']:
400 self.tor_update_checkbox.set_active(True)
402 self.tor_update_checkbox.set_active(False)
403 self.tor_update_checkbox.show()
407 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
408 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
409 if self.common.settings['check_for_updates']:
410 self.update_checkbox.set_active(True)
412 self.update_checkbox.set_active(False)
413 self.update_checkbox.show()
416 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
417 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
419 self.label1 = gtk.Label(_('Not installed'))
420 self.label1.set_line_wrap(True)
421 self.labels_box.pack_start(self.label1, True, True, 0)
424 if(self.common.settings['last_update_check_timestamp'] > 0):
425 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']))))
427 self.label1 = gtk.Label(_('Never checked for updates'))
428 self.label1.set_line_wrap(True)
429 self.labels_box.pack_start(self.label1, True, True, 0)
433 self.mirrors_box = gtk.HBox(False, 10)
434 self.box.pack_start(self.mirrors_box, True, True, 0)
435 self.mirrors_box.show()
437 self.mirrors_label = gtk.Label(_('Mirror'))
438 self.mirrors_label.set_line_wrap(True)
439 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
440 self.mirrors_label.show()
442 self.mirrors = gtk.combo_box_new_text()
443 for mirror in self.common.mirrors:
444 self.mirrors.append_text(mirror)
445 if self.common.settings['mirror'] in self.common.mirrors:
446 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
448 self.preferred.set_active(0)
449 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
453 self.button_box = gtk.HButtonBox()
454 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
455 self.box.pack_start(self.button_box, True, True, 0)
456 self.button_box.show()
458 # save and launch button
459 save_launch_image = gtk.Image()
460 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
461 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
462 self.save_launch_button.set_image(save_launch_image)
463 self.save_launch_button.connect("clicked", self.save_launch, None)
464 self.button_box.add(self.save_launch_button)
465 self.save_launch_button.show()
467 # save and exit button
468 save_exit_image = gtk.Image()
469 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
470 self.save_exit_button = gtk.Button(_("Save & Exit"))
471 self.save_exit_button.set_image(save_exit_image)
472 self.save_exit_button.connect("clicked", self.save_exit, None)
473 self.button_box.add(self.save_exit_button)
474 self.save_exit_button.show()
477 cancel_image = gtk.Image()
478 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
479 self.cancel_button = gtk.Button(_("Cancel"))
480 self.cancel_button.set_image(cancel_image)
481 self.cancel_button.connect("clicked", self.destroy, None)
482 self.button_box.add(self.cancel_button)
483 self.cancel_button.show()
492 def save_launch(self, widget, data=None):
494 p = subprocess.Popen([self.common.paths['tbl_bin']])
498 def save_exit(self, widget, data=None):
504 # figure out the selected preferred option
506 selected = self.preferred_options[self.preferred.get_active()]
507 for i in self.common.available_versions:
508 if self.common.available_versions[i] == selected:
511 self.common.settings['preferred'] = preferred
514 #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
515 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
517 # figure out the selected mirror
518 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
521 self.common.save_settings()
524 def delete_event(self, widget, event, data=None):
526 def destroy(self, widget, data=None):
531 def __init__(self, common):
532 print _('Starting launcher dialog')
536 self.set_gui(None, '', [])
537 self.launch_gui = True
538 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
540 # is vidalia already running and we just need to open a new firefox?
541 if self.common.settings['installed_version']:
542 vidalia_pid = self.common.get_pid('./App/vidalia')
543 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
545 if vidalia_pid and not firefox_pid:
546 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
547 self.common.bring_window_to_front(vidalia_pid)
548 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']])
550 elif vidalia_pid and firefox_pid:
551 print _('Vidalia and Firefox are already open, bringing them to focus')
553 # bring firefox to front, then vidalia
554 self.common.bring_window_to_front(firefox_pid)
555 self.common.bring_window_to_front(vidalia_pid)
559 check_for_updates = False
560 if self.common.settings['check_for_updates']:
561 check_for_updates = True
563 if not check_for_updates:
564 # how long was it since the last update check?
565 # 86400 seconds = 24 hours
566 current_timestamp = int(time.time())
567 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
568 check_for_updates = True
570 if check_for_updates:
572 print 'Checking for update'
573 self.set_gui('task', _("Checking for Tor Browser update."),
574 ['download_update_check',
577 # no need to check for update
578 print _('Checked for update within 24 hours, skipping')
579 self.start_launcher()
583 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
584 self.window.set_title(_("Tor Browser"))
585 self.window.set_icon_from_file(self.common.paths['icon_file'])
586 self.window.set_position(gtk.WIN_POS_CENTER)
587 self.window.set_border_width(10)
588 self.window.connect("delete_event", self.delete_event)
589 self.window.connect("destroy", self.destroy)
591 # build the rest of the UI
594 # download or run TBB
595 def start_launcher(self):
596 # is TBB already installed?
597 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
598 if os.path.isfile(start) and os.access(start, os.X_OK):
599 if self.common.settings['installed_version'][self.common.settings['preferred']] == self.common.settings['latest_version'][self.common.settings['preferred']]:
600 # current version of tbb is installed, launch it
602 self.launch_gui = False
603 elif self.common.settings['installed_version'][self.common.settings['preferred']] < self.common.settings['latest_version'][self.common.settings['preferred']]:
604 # there is a tbb upgrade available
605 self.set_gui('task', _("Your Tor Browser is out of date."),
606 ['download_tarball_sig',
612 # for some reason the installed tbb is newer than the current version?
613 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
617 # are the tarball and sig already downloaded?
618 if os.path.isfile(self.common.paths['tarball_file']) and os.path.isfile(self.common.paths['tarball_sig_file']):
619 # start the gui with verify
620 self.set_gui('task', _("Installing Tor Browser."),
627 self.set_gui('task', _("Downloading and installing Tor Browser."),
628 ['download_tarball_sig',
634 # there are different GUIs that might appear, this sets which one we want
635 def set_gui(self, gui, message, tasks, autostart=True):
637 self.gui_message = message
638 self.gui_tasks = tasks
640 self.gui_autostart = autostart
642 # set all gtk variables to False
644 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
649 self.progressbar = False
650 self.button_box = False
651 self.start_button = False
652 self.exit_button = False
654 # build the application's UI
658 self.box = gtk.VBox(False, 20)
659 self.window.add(self.box)
661 if 'error' in self.gui:
663 self.label = gtk.Label( self.gui_message )
664 self.label.set_line_wrap(True)
665 self.box.pack_start(self.label, True, True, 0)
669 self.button_box = gtk.HButtonBox()
670 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
671 self.box.pack_start(self.button_box, True, True, 0)
672 self.button_box.show()
674 if self.gui != 'error':
676 yes_image = gtk.Image()
677 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
678 self.yes_button = gtk.Button("Yes")
679 self.yes_button.set_image(yes_image)
680 if self.gui == 'error_try_stable':
681 self.yes_button.connect("clicked", self.try_stable, None)
682 elif self.gui == 'error_try_default_mirror':
683 self.yes_button.connect("clicked", self.try_default_mirror, None)
684 elif self.gui == 'error_try_tor':
685 self.yes_button.connect("clicked", self.try_tor, None)
686 self.button_box.add(self.yes_button)
687 self.yes_button.show()
690 exit_image = gtk.Image()
691 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
692 self.exit_button = gtk.Button("Exit")
693 self.exit_button.set_image(exit_image)
694 self.exit_button.connect("clicked", self.destroy, None)
695 self.button_box.add(self.exit_button)
696 self.exit_button.show()
698 elif self.gui == 'task':
700 self.label = gtk.Label( self.gui_message )
701 self.label.set_line_wrap(True)
702 self.box.pack_start(self.label, True, True, 0)
706 self.progressbar = gtk.ProgressBar(adjustment=None)
707 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
708 self.progressbar.set_pulse_step(0.01)
709 self.box.pack_start(self.progressbar, True, True, 0)
712 self.button_box = gtk.HButtonBox()
713 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
714 self.box.pack_start(self.button_box, True, True, 0)
715 self.button_box.show()
718 start_image = gtk.Image()
719 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
720 self.start_button = gtk.Button(_("Start"))
721 self.start_button.set_image(start_image)
722 self.start_button.connect("clicked", self.start, None)
723 self.button_box.add(self.start_button)
724 if not self.gui_autostart:
725 self.start_button.show()
728 exit_image = gtk.Image()
729 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
730 self.exit_button = gtk.Button(_("Exit"))
731 self.exit_button.set_image(exit_image)
732 self.exit_button.connect("clicked", self.destroy, None)
733 self.button_box.add(self.exit_button)
734 self.exit_button.show()
739 if self.gui_autostart:
742 # start button clicked, begin tasks
743 def start(self, widget, data=None):
744 # disable the start button
745 if self.start_button:
746 self.start_button.set_sensitive(False)
748 # start running tasks
751 # run the next task in the task list
755 if self.gui_task_i >= len(self.gui_tasks):
759 task = self.gui_tasks[self.gui_task_i]
761 # get ready for the next task
764 if task == 'download_update_check':
765 print _('Downloading'), self.common.paths['update_check_url']
766 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
768 if task == 'attempt_update':
769 print _('Checking to see if update is needed')
770 self.attempt_update()
772 elif task == 'download_tarball':
773 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
774 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
776 elif task == 'download_tarball_sig':
777 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
778 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
780 elif task == 'verify':
781 print _('Verifying signature')
784 elif task == 'extract':
785 print _('Extracting'), self.common.paths['tarball_filename']
789 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
792 elif task == 'start_over':
793 print _('Starting download over again')
796 def response_received(self, response):
797 class FileDownloader(Protocol):
798 def __init__(self, common, file, total, progress, done_cb):
802 self.progress = progress
803 self.all_done = done_cb
805 if response.code != 200:
808 if response.code == 404:
809 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
813 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?"))
815 if common.settings['mirror'] != common.default_mirror:
816 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']))
818 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
820 def dataReceived(self, bytes):
821 self.file.write(bytes)
822 self.so_far += len(bytes)
823 percent = float(self.so_far) / float(self.total)
824 self.progress.set_fraction(percent)
825 amount = float(self.so_far)
827 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
830 amount = amount / float(size)
833 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
835 def connectionLost(self, reason):
836 print _('Finished receiving body:'), reason.getErrorMessage()
837 self.all_done(reason)
839 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
840 response.deliverBody(dl)
842 def response_finished(self, msg):
843 if msg.check(ResponseDone):
844 self.file_download.close()
845 delattr(self, 'current_download_path')
851 print "FINISHED", msg
852 ## FIXME handle errors
854 def download_error(self, f):
855 print _("Download error:"), f.value, type(f.value)
857 if isinstance(f.value, TryStableException):
858 f.trap(TryStableException)
859 self.set_gui('error_try_stable', str(f.value), [], False)
861 elif isinstance(f.value, TryDefaultMirrorException):
862 f.trap(TryDefaultMirrorException)
863 self.set_gui('error_try_default_mirror', str(f.value), [], False)
865 elif isinstance(f.value, DownloadErrorException):
866 f.trap(DownloadErrorException)
867 self.set_gui('error', str(f.value), [], False)
869 elif isinstance(f.value, DNSLookupError):
870 f.trap(DNSLookupError)
871 if common.settings['mirror'] != common.default_mirror:
872 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)
874 self.set_gui('error', str(f.value), [], False)
876 elif isinstance(f.value, ResponseFailed):
877 for reason in f.value.reasons:
878 if isinstance(reason.value, OpenSSL.SSL.Error):
879 # TODO: add the ability to report attack by posting bug to trac.torproject.org
880 if not self.common.settings['update_over_tor']:
881 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)
883 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
886 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
890 def download(self, name, url, path):
891 # keep track of current download
892 self.current_download_path = path
894 # initialize the progress bar
895 mirror_url = url.format(self.common.settings['mirror'])
896 self.progressbar.set_fraction(0)
897 self.progressbar.set_text(_('Downloading {0}').format(name))
898 self.progressbar.show()
901 # default mirror gets certificate pinning, only for requests that use the mirror
902 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
903 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
905 agent = Agent(reactor)
907 # actually, agent needs to follow redirect
908 agent = RedirectAgent(agent)
911 d = agent.request('GET', mirror_url,
912 Headers({'User-Agent': ['torbrowser-launcher']}),
915 self.file_download = open(path, 'w')
916 d.addCallback(self.response_received).addErrback(self.download_error)
918 if not reactor.running:
921 def try_stable(self, widget, data=None):
922 # change preferred to stable and relaunch TBL
923 self.common.settings['preferred'] = 'stable'
924 self.common.save_settings()
925 p = subprocess.Popen([self.common.paths['tbl_bin']])
928 def try_default_mirror(self, widget, data=None):
929 # change preferred to stable and relaunch TBL
930 self.common.settings['mirror'] = self.common.default_mirror
931 self.common.save_settings()
932 p = subprocess.Popen([self.common.paths['tbl_bin']])
935 def try_tor(self, widget, data=None):
936 # set update_over_tor to true and relaunch TBL
937 self.common.settings['update_over_tor'] = True
938 self.common.save_settings()
939 p = subprocess.Popen([self.common.paths['tbl_bin']])
942 def attempt_update(self):
943 # load the update check file
945 versions = json.load(open(self.common.paths['update_check_file']))
949 # filter linux versions
952 for version in versions:
953 if str(version).find('-Linux') != -1:
954 if version.find('alpha') != -1:
955 valid_alphas.append(str(version))
957 valid_stables.append(str(version))
959 if len(valid_alphas):
960 latest_alpha = valid_alphas.pop()
962 if len(valid_stables):
963 latest_stable = valid_stables.pop()
965 if latest_stable or latest_alpha:
967 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
969 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
970 self.common.settings['last_update_check_timestamp'] = int(time.time())
971 self.common.settings['check_for_updates'] = False
972 self.common.save_settings()
973 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
974 self.start_launcher()
977 # failed to find the latest version
978 self.set_gui('error', _("Error checking for updates."), [], False)
981 # not a valid JSON object
982 self.set_gui('error', _("Error checking for updates."), [], False)
989 # initialize the progress bar
990 self.progressbar.set_fraction(0)
991 self.progressbar.set_text(_('Verifying Signature'))
992 self.progressbar.show()
994 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
995 self.pulse_until_process_exits(p)
997 if p.returncode == 0:
1000 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1001 self.set_gui('task', _("SIGNATURE VERIFICATION FAILED!\n\nYou might be under attack, or there might just be a networking problem. Click Start try the download again."), ['start_over'], False)
1005 if not reactor.running:
1009 # initialize the progress bar
1010 self.progressbar.set_fraction(0)
1011 self.progressbar.set_text(_('Installing'))
1012 self.progressbar.show()
1015 # make sure this file is a tarfile
1016 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1017 tf = tarfile.open(self.common.paths['tarball_file'])
1018 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1020 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
1024 # installation is finished, so save installed_version
1025 self.common.settings['installed_version'] = self.common.settings['latest_version']
1026 self.common.save_settings()
1030 def run(self, run_next_task = True):
1031 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1035 # make the progress bar pulse until process p (a Popen object) finishes
1036 def pulse_until_process_exits(self, p):
1037 while p.poll() == None:
1039 self.progressbar.pulse()
1042 # start over and download TBB again
1043 def start_over(self):
1044 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1045 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1050 def refresh_gtk(self):
1051 while gtk.events_pending():
1052 gtk.main_iteration(False)
1055 def delete_event(self, widget, event, data=None):
1057 def destroy(self, widget, data=None):
1058 if hasattr(self, 'file_download'):
1059 self.file_download.close()
1060 if hasattr(self, 'current_download_path'):
1061 os.remove(self.current_download_path)
1062 delattr(self, 'current_download_path')
1066 if __name__ == "__main__":
1067 tor_browser_launcher_version = '0.0.2'
1069 print _('Tor Browser Launcher')
1070 print _('By Micah Lee, licensed under GPLv3')
1071 print _('version {0}').format(tor_browser_launcher_version)
1072 print 'https://github.com/micahflee/torbrowser-launcher'
1074 common = TBLCommon(tor_browser_launcher_version)
1076 # is torbrowser-launcher already running?
1077 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1079 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1080 common.bring_window_to_front(tbl_pid)
1083 if '-settings' in sys.argv:
1085 app = TBLSettings(common)
1089 app = TBLLauncher(common)