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 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
396 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
397 if self.common.settings['update_over_tor']:
398 self.tor_update_checkbox.set_active(True)
400 self.tor_update_checkbox.set_active(False)
401 self.tor_update_checkbox.show()
404 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
405 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
406 if self.common.settings['check_for_updates']:
407 self.update_checkbox.set_active(True)
409 self.update_checkbox.set_active(False)
410 self.update_checkbox.show()
413 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
414 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
416 self.label1 = gtk.Label(_('Not installed'))
417 self.label1.set_line_wrap(True)
418 self.labels_box.pack_start(self.label1, True, True, 0)
421 if(self.common.settings['last_update_check_timestamp'] > 0):
422 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']))))
424 self.label1 = gtk.Label(_('Never checked for updates'))
425 self.label1.set_line_wrap(True)
426 self.labels_box.pack_start(self.label1, True, True, 0)
430 self.mirrors_box = gtk.HBox(False, 10)
431 self.box.pack_start(self.mirrors_box, True, True, 0)
432 self.mirrors_box.show()
434 self.mirrors_label = gtk.Label(_('Mirror'))
435 self.mirrors_label.set_line_wrap(True)
436 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
437 self.mirrors_label.show()
439 self.mirrors = gtk.combo_box_new_text()
440 for mirror in self.common.mirrors:
441 self.mirrors.append_text(mirror)
442 if self.common.settings['mirror'] in self.common.mirrors:
443 self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
445 self.preferred.set_active(0)
446 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
450 self.button_box = gtk.HButtonBox()
451 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
452 self.box.pack_start(self.button_box, True, True, 0)
453 self.button_box.show()
455 # save and launch button
456 save_launch_image = gtk.Image()
457 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
458 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
459 self.save_launch_button.set_image(save_launch_image)
460 self.save_launch_button.connect("clicked", self.save_launch, None)
461 self.button_box.add(self.save_launch_button)
462 self.save_launch_button.show()
464 # save and exit button
465 save_exit_image = gtk.Image()
466 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
467 self.save_exit_button = gtk.Button(_("Save & Exit"))
468 self.save_exit_button.set_image(save_exit_image)
469 self.save_exit_button.connect("clicked", self.save_exit, None)
470 self.button_box.add(self.save_exit_button)
471 self.save_exit_button.show()
474 cancel_image = gtk.Image()
475 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
476 self.cancel_button = gtk.Button(_("Cancel"))
477 self.cancel_button.set_image(cancel_image)
478 self.cancel_button.connect("clicked", self.destroy, None)
479 self.button_box.add(self.cancel_button)
480 self.cancel_button.show()
489 def save_launch(self, widget, data=None):
491 p = subprocess.Popen([self.common.paths['tbl_bin']])
495 def save_exit(self, widget, data=None):
501 # figure out the selected preferred option
503 selected = self.preferred_options[self.preferred.get_active()]
504 for i in self.common.available_versions:
505 if self.common.available_versions[i] == selected:
508 self.common.settings['preferred'] = preferred
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()
514 # figure out the selected mirror
515 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
518 self.common.save_settings()
521 def delete_event(self, widget, event, data=None):
523 def destroy(self, widget, data=None):
528 def __init__(self, common):
529 print _('Starting launcher dialog')
533 self.set_gui(None, '', [])
534 self.launch_gui = True
535 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
537 # is vidalia already running and we just need to open a new firefox?
538 if self.common.settings['installed_version']:
539 vidalia_pid = self.common.get_pid('./App/vidalia')
540 firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
542 if vidalia_pid and not firefox_pid:
543 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
544 self.common.bring_window_to_front(vidalia_pid)
545 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']])
547 elif vidalia_pid and firefox_pid:
548 print _('Vidalia and Firefox are already open, bringing them to focus')
550 # bring firefox to front, then vidalia
551 self.common.bring_window_to_front(firefox_pid)
552 self.common.bring_window_to_front(vidalia_pid)
556 check_for_updates = False
557 if self.common.settings['check_for_updates']:
558 check_for_updates = True
560 if not check_for_updates:
561 # how long was it since the last update check?
562 # 86400 seconds = 24 hours
563 current_timestamp = int(time.time())
564 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
565 check_for_updates = True
567 if check_for_updates:
569 print 'Checking for update'
570 self.set_gui('task', _("Checking for Tor Browser update."),
571 ['download_update_check',
574 # no need to check for update
575 print _('Checked for update within 24 hours, skipping')
576 self.start_launcher()
580 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
581 self.window.set_title(_("Tor Browser"))
582 self.window.set_icon_from_file(self.common.paths['icon_file'])
583 self.window.set_position(gtk.WIN_POS_CENTER)
584 self.window.set_border_width(10)
585 self.window.connect("delete_event", self.delete_event)
586 self.window.connect("destroy", self.destroy)
588 # build the rest of the UI
591 # download or run TBB
592 def start_launcher(self):
593 # is TBB already installed?
594 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
595 if os.path.isfile(start) and os.access(start, os.X_OK):
596 if self.common.settings['installed_version'][self.common.settings['preferred']] == self.common.settings['latest_version'][self.common.settings['preferred']]:
597 # current version of tbb is installed, launch it
599 self.launch_gui = False
600 elif self.common.settings['installed_version'][self.common.settings['preferred']] < self.common.settings['latest_version'][self.common.settings['preferred']]:
601 # there is a tbb upgrade available
602 self.set_gui('task', _("Your Tor Browser is out of date."),
603 ['download_tarball_sig',
609 # for some reason the installed tbb is newer than the current version?
610 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
614 # are the tarball and sig already downloaded?
615 if os.path.isfile(self.common.paths['tarball_file']) and os.path.isfile(self.common.paths['tarball_sig_file']):
616 # start the gui with verify
617 self.set_gui('task', _("Installing Tor Browser."),
624 self.set_gui('task', _("Downloading and installing Tor Browser."),
625 ['download_tarball_sig',
631 # there are different GUIs that might appear, this sets which one we want
632 def set_gui(self, gui, message, tasks, autostart=True):
634 self.gui_message = message
635 self.gui_tasks = tasks
637 self.gui_autostart = autostart
639 # set all gtk variables to False
641 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
646 self.progressbar = False
647 self.button_box = False
648 self.start_button = False
649 self.exit_button = False
651 # build the application's UI
655 self.box = gtk.VBox(False, 20)
656 self.window.add(self.box)
658 if 'error' in self.gui:
660 self.label = gtk.Label( self.gui_message )
661 self.label.set_line_wrap(True)
662 self.box.pack_start(self.label, True, True, 0)
666 self.button_box = gtk.HButtonBox()
667 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
668 self.box.pack_start(self.button_box, True, True, 0)
669 self.button_box.show()
671 if self.gui != 'error':
673 yes_image = gtk.Image()
674 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
675 self.yes_button = gtk.Button("Yes")
676 self.yes_button.set_image(yes_image)
677 if self.gui == 'error_try_stable':
678 self.yes_button.connect("clicked", self.try_stable, None)
679 elif self.gui == 'error_try_default_mirror':
680 self.yes_button.connect("clicked", self.try_default_mirror, None)
681 elif self.gui == 'error_try_tor':
682 self.yes_button.connect("clicked", self.try_tor, None)
683 self.button_box.add(self.yes_button)
684 self.yes_button.show()
687 exit_image = gtk.Image()
688 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
689 self.exit_button = gtk.Button("Exit")
690 self.exit_button.set_image(exit_image)
691 self.exit_button.connect("clicked", self.destroy, None)
692 self.button_box.add(self.exit_button)
693 self.exit_button.show()
695 elif self.gui == 'task':
697 self.label = gtk.Label( self.gui_message )
698 self.label.set_line_wrap(True)
699 self.box.pack_start(self.label, True, True, 0)
703 self.progressbar = gtk.ProgressBar(adjustment=None)
704 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
705 self.progressbar.set_pulse_step(0.01)
706 self.box.pack_start(self.progressbar, True, True, 0)
709 self.button_box = gtk.HButtonBox()
710 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
711 self.box.pack_start(self.button_box, True, True, 0)
712 self.button_box.show()
715 start_image = gtk.Image()
716 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
717 self.start_button = gtk.Button(_("Start"))
718 self.start_button.set_image(start_image)
719 self.start_button.connect("clicked", self.start, None)
720 self.button_box.add(self.start_button)
721 if not self.gui_autostart:
722 self.start_button.show()
725 exit_image = gtk.Image()
726 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
727 self.exit_button = gtk.Button(_("Exit"))
728 self.exit_button.set_image(exit_image)
729 self.exit_button.connect("clicked", self.destroy, None)
730 self.button_box.add(self.exit_button)
731 self.exit_button.show()
736 if self.gui_autostart:
739 # start button clicked, begin tasks
740 def start(self, widget, data=None):
741 # disable the start button
742 if self.start_button:
743 self.start_button.set_sensitive(False)
745 # start running tasks
748 # run the next task in the task list
752 if self.gui_task_i >= len(self.gui_tasks):
756 task = self.gui_tasks[self.gui_task_i]
758 # get ready for the next task
761 if task == 'download_update_check':
762 print _('Downloading'), self.common.paths['update_check_url']
763 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
765 if task == 'attempt_update':
766 print _('Checking to see if update it needed')
767 self.attempt_update()
769 elif task == 'download_tarball':
770 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
771 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
773 elif task == 'download_tarball_sig':
774 print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
775 self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
777 elif task == 'verify':
778 print _('Verifying signature')
781 elif task == 'extract':
782 print _('Extracting'), self.common.paths['tarball_filename']
786 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
789 elif task == 'start_over':
790 print _('Starting download over again')
793 def response_received(self, response):
794 class FileDownloader(Protocol):
795 def __init__(self, common, file, total, progress, done_cb):
799 self.progress = progress
800 self.all_done = done_cb
802 if response.code != 200:
805 if response.code == 404:
806 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
810 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?"))
812 if common.settings['mirror'] != common.default_mirror:
813 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']))
815 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
817 def dataReceived(self, bytes):
818 self.file.write(bytes)
819 self.so_far += len(bytes)
820 percent = float(self.so_far) / float(self.total)
821 self.progress.set_fraction(percent)
822 amount = float(self.so_far)
824 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
827 amount = amount / float(size)
830 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
832 def connectionLost(self, reason):
833 print _('Finished receiving body:'), reason.getErrorMessage()
834 self.all_done(reason)
836 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
837 response.deliverBody(dl)
839 def response_finished(self, msg):
840 if msg.check(ResponseDone):
841 self.file_download.close()
846 print "FINISHED", msg
847 ## FIXME handle errors
849 def download_error(self, f):
850 print _("Download error:"), f.value, type(f.value)
852 if isinstance(f.value, TryStableException):
853 f.trap(TryStableException)
854 self.set_gui('error_try_stable', str(f.value), [], False)
856 elif isinstance(f.value, TryDefaultMirrorException):
857 f.trap(TryDefaultMirrorException)
858 self.set_gui('error_try_default_mirror', str(f.value), [], False)
860 elif isinstance(f.value, DownloadErrorException):
861 f.trap(DownloadErrorException)
862 self.set_gui('error', str(f.value), [], False)
864 elif isinstance(f.value, DNSLookupError):
865 f.trap(DNSLookupError)
866 if common.settings['mirror'] != common.default_mirror:
867 self.set_gui('error_try_default_mirror', _("DNS Lookup Error\n\nYou are currently using a non-default mirror:\n{0}\n\nWould you like to switch back to the default?").format(common.settings['mirror']), [], False)
869 self.set_gui('error', str(f.value), [], False)
871 elif isinstance(f.value, ResponseFailed):
872 for reason in f.value.reasons:
873 if isinstance(reason.value, OpenSSL.SSL.Error):
874 # TODO: add the ability to report attack by posting bug to trac.torproject.org
875 if not self.common.settings['update_over_tor']:
876 self.set_gui('error_try_tor', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack. Try the download again using Tor?'), [], False)
878 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
881 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
885 def download(self, name, url, path):
886 # initialize the progress bar
887 mirror_url = url.format(self.common.settings['mirror'])
888 self.progressbar.set_fraction(0)
889 self.progressbar.set_text(_('Downloading {0}').format(name))
890 self.progressbar.show()
893 # default mirror gets certificate pinning, only for requests that use the mirror
894 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
895 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
897 agent = Agent(reactor)
899 # actually, agent needs to follow redirect
900 agent = RedirectAgent(agent)
903 d = agent.request('GET', mirror_url,
904 Headers({'User-Agent': ['torbrowser-launcher']}),
907 self.file_download = open(path, 'w')
908 d.addCallback(self.response_received).addErrback(self.download_error)
910 if not reactor.running:
913 def try_stable(self, widget, data=None):
914 # change preferred to stable and relaunch TBL
915 self.common.settings['preferred'] = 'stable'
916 self.common.save_settings()
917 p = subprocess.Popen([self.common.paths['tbl_bin']])
920 def try_default_mirror(self, widget, data=None):
921 # change preferred to stable and relaunch TBL
922 self.common.settings['mirror'] = self.common.default_mirror
923 self.common.save_settings()
924 p = subprocess.Popen([self.common.paths['tbl_bin']])
927 def try_tor(self, widget, data=None):
928 # set update_over_tor to true and relaunch TBL
929 self.common.settings['update_over_tor'] = True
930 self.common.save_settings()
931 p = subprocess.Popen([self.common.paths['tbl_bin']])
934 def attempt_update(self):
935 # load the update check file
937 versions = json.load(open(self.common.paths['update_check_file']))
941 # filter linux versions
944 for version in versions:
945 if str(version).find('-Linux') != -1:
946 if version.find('alpha') != -1:
947 valid_alphas.append(str(version))
949 valid_stables.append(str(version))
951 if len(valid_alphas):
952 latest_alpha = valid_alphas.pop()
954 if len(valid_stables):
955 latest_stable = valid_stables.pop()
957 if latest_stable or latest_alpha:
959 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
961 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
962 self.common.settings['last_update_check_timestamp'] = int(time.time())
963 self.common.settings['check_for_updates'] = False
964 self.common.save_settings()
965 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
966 self.start_launcher()
969 # failed to find the latest version
970 self.set_gui('error', _("Error checking for updates."), [], False)
973 # not a valid JSON object
974 self.set_gui('error', _("Error checking for updates."), [], False)
981 # initialize the progress bar
982 self.progressbar.set_fraction(0)
983 self.progressbar.set_text(_('Verifying Signature'))
984 self.progressbar.show()
986 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
987 self.pulse_until_process_exits(p)
989 if p.returncode == 0:
992 # TODO: add the ability to report attack by posting bug to trac.torproject.org
993 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)
997 if not reactor.running:
1001 # initialize the progress bar
1002 self.progressbar.set_fraction(0)
1003 self.progressbar.set_text(_('Installing'))
1004 self.progressbar.show()
1007 # make sure this file is a tarfile
1008 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1009 tf = tarfile.open(self.common.paths['tarball_file'])
1010 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1012 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
1016 # installation is finished, so save installed_version
1017 self.common.settings['installed_version'] = self.common.settings['latest_version']
1018 self.common.save_settings()
1022 def run(self, run_next_task = True):
1023 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1027 # make the progress bar pulse until process p (a Popen object) finishes
1028 def pulse_until_process_exits(self, p):
1029 while p.poll() == None:
1031 self.progressbar.pulse()
1034 # start over and download TBB again
1035 def start_over(self):
1036 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1037 self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1042 def refresh_gtk(self):
1043 while gtk.events_pending():
1044 gtk.main_iteration(False)
1047 def delete_event(self, widget, event, data=None):
1049 def destroy(self, widget, data=None):
1050 if hasattr(self, 'file_download'):
1051 self.file_download.close()
1055 if __name__ == "__main__":
1056 tor_browser_launcher_version = '0.0.2'
1058 print _('Tor Browser Launcher')
1059 print _('By Micah Lee, licensed under GPLv3')
1060 print _('version {0}').format(tor_browser_launcher_version)
1061 print 'https://github.com/micahflee/torbrowser-launcher'
1063 common = TBLCommon(tor_browser_launcher_version)
1065 # is torbrowser-launcher already running?
1066 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1068 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1069 common.bring_window_to_front(tbl_pid)
1072 if '-settings' in sys.argv:
1074 app = TBLSettings(common)
1078 app = TBLLauncher(common)