]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
Made pygame and modem sound optiomal, the option is simply disabled if you do not...
[torbrowser-launcher.git] / torbrowser-launcher
1 #!/usr/bin/env python
2 """
3 Tor Browser Launcher
4 https://github.com/micahflee/torbrowser-launcher/
5
6 Copyright (c) 2013 Micah Lee <micah@micahflee.com>
7
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
15 conditions:
16
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
19
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.
28 """
29
30 import sys
31 import platform
32
33 import gettext
34 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
35
36 from twisted.internet import gtk2reactor
37 gtk2reactor.install()
38 from twisted.internet import reactor
39
40 import pygtk
41 pygtk.require('2.0')
42 import gtk
43
44 import os, subprocess, locale, time, pickle, json, tarfile, psutil, hashlib, lzma
45
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.error import DNSLookupError
51
52 import OpenSSL
53
54
55 class TryStableException(Exception):
56     pass
57
58
59 class TryDefaultMirrorException(Exception):
60     pass
61
62
63 class DownloadErrorException(Exception):
64     pass
65
66
67 class VerifyTorProjectCert(ClientContextFactory):
68
69     def __init__(self, torproject_pem):
70         self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
71
72     def getContext(self, host, port):
73         ctx = ClientContextFactory.getContext(self)
74         ctx.set_verify_depth(0)
75         ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
76         return ctx
77
78     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
79         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
80
81
82 class TBLCommon:
83
84     def __init__(self, tbl_version):
85         print _('Initializing Tor Browser Launcher')
86         self.tbl_version = tbl_version
87
88         # initialize the app
89         self.available_versions = {
90             'stable': _('Tor Browser Bundle - stable'),
91             'alpha': _('Tor Browser Bundle - alpha')
92         }
93         self.default_mirror = 'https://www.torproject.org/dist/'
94
95         self.discover_arch_lang()
96         self.build_paths()
97         self.mkdir(self.paths['data_dir'])
98         self.load_mirrors()
99         self.load_settings()
100         self.mkdir(self.paths['download_dir'])
101         self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
102         self.init_gnupg()
103
104         # allow buttons to have icons
105         try:
106             gtk_settings = gtk.settings_get_default()
107             gtk_settings.props.gtk_button_images = True
108         except:
109             pass
110
111     # discover the architecture and language
112     def discover_arch_lang(self):
113         # figure out the architecture
114         self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
115
116         # figure out the language
117         available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
118         default_locale = locale.getdefaultlocale()[0]
119         if default_locale is None:
120             self.language = 'en-US'
121         else:
122             self.language = default_locale.replace('_', '-')
123             if self.language not in available_languages:
124                 self.language = self.language.split('-')[0]
125                 if self.language not in available_languages:
126                     for l in available_languages:
127                         if l[0:2] == self.language:
128                             self.language = l
129             # if language isn't available, default to english
130             if self.language not in available_languages:
131                 self.language = 'en-US'
132
133     # build all relevant paths
134     def build_paths(self, tbb_version=None):
135         homedir = os.getenv('HOME')
136         if not homedir:
137             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
138             if not os.path.exists(homedir):
139                 try:
140                     os.mkdir(homedir, 0700)
141                 except:
142                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
143         if not os.access(homedir, os.W_OK):
144             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
145
146         tbb_data = '%s/.torbrowser' % homedir
147
148         if tbb_version:
149             # tarball filename
150             dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b').replace('-rc-', 'rc')
151             if self.architecture == 'x86_64':
152                 arch = 'linux64'
153             else:
154                 arch = 'linux32'
155             tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
156
157             # tarball
158             self.paths['tarball_url'] = '{0}torbrowser/'+dirname+'/'+tarball_filename
159             self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
160             self.paths['tarball_filename'] = tarball_filename
161
162             # sig
163             self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
164             self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
165             self.paths['sha256_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt'
166             self.paths['sha256_sig_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt-mikeperry.asc'
167         else:
168             self.paths = {
169                 'tbl_bin': '/usr/bin/torbrowser-launcher',
170                 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
171                 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
172                 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
173                 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
174                 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
175                 'mike_key': '/usr/share/torbrowser-launcher/mike-2013-09.asc',
176                 'mirrors_txt': ['/usr/share/torbrowser-launcher/mirrors.txt',
177                                 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
178                 'modem_sound': '/usr/share/torbrowser-launcher/modem.ogg',
179                 'data_dir': tbb_data,
180                 'download_dir': tbb_data+'/download',
181                 'gnupg_homedir': tbb_data+'/gnupg_homedir',
182                 'settings_file': tbb_data+'/settings',
183                 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
184                 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
185                 'tbb': {
186                     'stable': {
187                         'dir': tbb_data+'/tbb/stable/'+self.architecture,
188                         'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
189                         'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
190                     },
191                     'alpha': {
192                         'dir': tbb_data+'/tbb/alpha/'+self.architecture,
193                         'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
194                         'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
195                     }
196                 }
197             }
198
199     # create a directory
200     @staticmethod
201     def mkdir(path):
202         try:
203             if not os.path.exists(path):
204                 os.makedirs(path, 0700)
205                 return True
206         except:
207             print _("Cannot create directory {0}").format(path)
208             return False
209         if not os.access(path, os.W_OK):
210             print _("{0} is not writable").format(path)
211             return False
212         return True
213
214     # if gnupg_homedir isn't set up, set it up
215     def init_gnupg(self):
216         if not os.path.exists(self.paths['gnupg_homedir']):
217             print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
218             self.mkdir(self.paths['gnupg_homedir'])
219         self.import_keys()
220
221     # import gpg keys
222     def import_keys(self):
223         print _('Importing keys')
224         subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key'], self.paths['sebastian_key'], self.paths['alexandre_key'], self.paths['mike_key']]).wait()
225
226     # load mirrors
227     def load_mirrors(self):
228         self.mirrors = []
229         for srcfile in self.paths['mirrors_txt']:
230             if not os.path.exists(srcfile):
231                 print "Warning: can't load mirrors from %s" % srcfile
232                 continue
233             for mirror in open(srcfile, 'r').readlines():
234                 if mirror.strip() not in self.mirrors:
235                     self.mirrors.append(mirror.strip())
236
237     # load settings
238     def load_settings(self):
239         default_settings = {
240             'tbl_version': self.tbl_version,
241             'preferred': 'stable',
242             'installed_version': {
243                 'stable': False,
244                 'alpha': False
245             },
246             'latest_version': {
247                 'stable': '0',
248                 'alpha': '0'
249             },
250             'update_over_tor': True,
251             'check_for_updates': False,
252             'modem_sound': False,
253             'last_update_check_timestamp': 0,
254             'mirror': self.default_mirror
255         }
256
257         if os.path.isfile(self.paths['settings_file']):
258             settings = pickle.load(open(self.paths['settings_file']))
259             resave = False
260
261             # settings migrations
262             if settings['tbl_version'] == '0.0.1':
263                 print '0.0.1 migration'
264                 self.settings = default_settings
265                 self.settings['installed_version']['alpha'] = settings['installed_version']
266                 resave = True
267
268                 # move tbb alpha
269                 self.mkdir(self.paths['tbb']['alpha']['dir'])
270
271             # make sure settings file is up-to-date
272             for setting in default_settings:
273                 if setting not in settings:
274                     settings[setting] = default_settings[setting]
275                     resave = True
276
277             # make sure the version is current
278             if settings['tbl_version'] != self.tbl_version:
279                 settings['tbl_version'] = self.tbl_version
280                 resave = True
281
282             self.settings = settings
283             if resave:
284                 self.save_settings()
285
286         else:
287             self.settings = default_settings
288             self.save_settings()
289
290     # save settings
291     def save_settings(self):
292         pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
293         return True
294
295     # get the process id of a program
296     @staticmethod
297     def get_pid(bin_path, python=False):
298         pid = None
299
300         for p in psutil.process_iter():
301             try:
302                 if p.pid != os.getpid():
303                     exe = None
304                     if python:
305                         if len(p.cmdline) > 1:
306                             if 'python' in p.cmdline[0]:
307                                 exe = p.cmdline[1]
308                     else:
309                         if len(p.cmdline) > 0:
310                             exe = p.cmdline[0]
311
312                     if exe == bin_path:
313                         pid = p.pid
314
315             except:
316                 pass
317
318         return pid
319
320     # bring program's x window to front
321     @staticmethod
322     def bring_window_to_front(pid):
323         # figure out the window id
324         win_id = None
325         p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
326         for line in p.stdout.readlines():
327             line_split = line.split()
328             cur_win_id = line_split[0]
329             cur_win_pid = int(line_split[2])
330             if cur_win_pid == pid:
331                 win_id = cur_win_id
332
333         # bring to front
334         if win_id:
335             subprocess.call(['wmctrl', '-i', '-a', win_id])
336
337
338 class TBLSettings:
339     def __init__(self, common):
340         print _('Starting settings dialog')
341         self.common = common
342
343         # set up the window
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)
351
352         # build the rest of the UI
353         self.box = gtk.VBox(False, 10)
354         self.window.add(self.box)
355         self.box.show()
356
357         self.hbox = gtk.HBox(False, 10)
358         self.box.pack_start(self.hbox, True, True, 0)
359         self.hbox.show()
360
361         self.settings_box = gtk.VBox(False, 10)
362         self.hbox.pack_start(self.settings_box, True, True, 0)
363         self.settings_box.show()
364
365         self.labels_box = gtk.VBox(False, 10)
366         self.hbox.pack_start(self.labels_box, True, True, 0)
367         self.labels_box.show()
368
369         # preferred version
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()
373
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()
378
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()
383
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']]))
389         else:
390             self.preferred.set_active(0)
391         self.preferred_box.pack_start(self.preferred, True, True, 0)
392         self.preferred.show()
393
394         # download over tor
395         # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
396         """
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)
401         else:
402             self.tor_update_checkbox.set_active(False)
403         self.tor_update_checkbox.show()
404         """
405
406         # check for updates
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)
411         else:
412             self.update_checkbox.set_active(False)
413         self.update_checkbox.show()
414
415         # modem sound
416         self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
417         self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
418
419         try:
420             import pygame
421             if self.common.settings['modem_sound']:
422                 self.modem_checkbox.set_active(True)
423             else:
424                 self.modem_checkbox.set_active(False)
425         except ImportError:
426             self.modem_checkbox.set_active(False)
427             self.modem_checkbox.set_sensitive(False)
428             self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
429         self.modem_checkbox.show()
430
431         # labels
432         if(self.common.settings['installed_version'][self.common.settings['preferred']]):
433             self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
434         else:
435             self.label1 = gtk.Label(_('Not installed'))
436         self.label1.set_line_wrap(True)
437         self.labels_box.pack_start(self.label1, True, True, 0)
438         self.label1.show()
439
440         if(self.common.settings['last_update_check_timestamp'] > 0):
441             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']))))
442         else:
443             self.label1 = gtk.Label(_('Never checked for updates'))
444         self.label1.set_line_wrap(True)
445         self.labels_box.pack_start(self.label1, True, True, 0)
446         self.label1.show()
447
448         # mirrors
449         self.mirrors_box = gtk.HBox(False, 10)
450         self.box.pack_start(self.mirrors_box, True, True, 0)
451         self.mirrors_box.show()
452
453         self.mirrors_label = gtk.Label(_('Mirror'))
454         self.mirrors_label.set_line_wrap(True)
455         self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
456         self.mirrors_label.show()
457
458         self.mirrors = gtk.combo_box_new_text()
459         for mirror in self.common.mirrors:
460             self.mirrors.append_text(mirror)
461         if self.common.settings['mirror'] in self.common.mirrors:
462             self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
463         else:
464             self.preferred.set_active(0)
465         self.mirrors_box.pack_start(self.mirrors, True, True, 0)
466         self.mirrors.show()
467
468         # button box
469         self.button_box = gtk.HButtonBox()
470         self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
471         self.box.pack_start(self.button_box, True, True, 0)
472         self.button_box.show()
473
474         # save and launch button
475         save_launch_image = gtk.Image()
476         save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
477         self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
478         self.save_launch_button.set_image(save_launch_image)
479         self.save_launch_button.connect("clicked", self.save_launch, None)
480         self.button_box.add(self.save_launch_button)
481         self.save_launch_button.show()
482
483         # save and exit button
484         save_exit_image = gtk.Image()
485         save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
486         self.save_exit_button = gtk.Button(_("Save & Exit"))
487         self.save_exit_button.set_image(save_exit_image)
488         self.save_exit_button.connect("clicked", self.save_exit, None)
489         self.button_box.add(self.save_exit_button)
490         self.save_exit_button.show()
491
492         # cancel button
493         cancel_image = gtk.Image()
494         cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
495         self.cancel_button = gtk.Button(_("Cancel"))
496         self.cancel_button.set_image(cancel_image)
497         self.cancel_button.connect("clicked", self.destroy, None)
498         self.button_box.add(self.cancel_button)
499         self.cancel_button.show()
500
501         # show the window
502         self.window.show()
503
504         # start gtk
505         gtk.main()
506
507     # save and launch
508     def save_launch(self, widget, data=None):
509         self.save()
510         subprocess.Popen([self.common.paths['tbl_bin']])
511         self.destroy(False)
512
513     # save and exit
514     def save_exit(self, widget, data=None):
515         self.save()
516         self.destroy(False)
517
518     # save settings
519     def save(self):
520         # figure out the selected preferred option
521         preferred = None
522         selected = self.preferred_options[self.preferred.get_active()]
523         for i in self.common.available_versions:
524             if self.common.available_versions[i] == selected:
525                 preferred = i
526         if preferred:
527             self.common.settings['preferred'] = preferred
528
529         # checkbox options
530         #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
531         self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
532         self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
533
534         # figure out the selected mirror
535         self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
536
537         # save them
538         self.common.save_settings()
539
540     # exit
541     def delete_event(self, widget, event, data=None):
542         return False
543
544     def destroy(self, widget, data=None):
545         gtk.main_quit()
546
547
548 class TBLLauncher:
549     def __init__(self, common):
550         print _('Starting launcher dialog')
551         self.common = common
552
553         # init launcher
554         self.set_gui(None, '', [])
555         self.launch_gui = True
556         self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
557
558         # is firefox already running?
559         if self.common.settings['installed_version']:
560             firefox_pid = self.common.get_pid('./Browser/firefox')
561             if firefox_pid:
562                 print _('Firefox are is open, bringing to focus')
563                 # bring firefox to front
564                 self.common.bring_window_to_front(firefox_pid)
565                 return
566
567         # check for updates?
568         check_for_updates = False
569         if self.common.settings['check_for_updates']:
570             check_for_updates = True
571
572         if not check_for_updates:
573             # how long was it since the last update check?
574             # 86400 seconds = 24 hours
575             current_timestamp = int(time.time())
576             if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
577                 check_for_updates = True
578
579         if check_for_updates:
580             # check for update
581             print 'Checking for update'
582             self.set_gui('task', _("Checking for Tor Browser update."),
583                          ['download_update_check',
584                           'attempt_update'])
585         else:
586             # no need to check for update
587             print _('Checked for update within 24 hours, skipping')
588             self.start_launcher()
589
590         if self.launch_gui:
591             # set up the window
592             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
593             self.window.set_title(_("Tor Browser"))
594             self.window.set_icon_from_file(self.common.paths['icon_file'])
595             self.window.set_position(gtk.WIN_POS_CENTER)
596             self.window.set_border_width(10)
597             self.window.connect("delete_event", self.delete_event)
598             self.window.connect("destroy", self.destroy)
599
600             # build the rest of the UI
601             self.build_ui()
602
603     # download or run TBB
604     def start_launcher(self):
605         # is TBB already installed?
606         latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
607         installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
608
609         # verify installed version for newer versions of TBB (#58)
610         if installed_version >= '3.0':
611             versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
612             if os.path.exists(versions_filename):
613                 for line in open(versions_filename):
614                     if 'TORBROWSER_VERSION' in line:
615                         installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
616
617         start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
618         if os.path.isfile(start) and os.access(start, os.X_OK):
619             if installed_version == latest_version:
620                 print _('Latest version of TBB is installed, launching')
621                 # current version of tbb is installed, launch it
622                 self.run(False)
623                 self.launch_gui = False
624             elif installed_version < latest_version:
625                 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
626                 # there is a tbb upgrade available
627                 self.set_gui('task', _("Your Tor Browser is out of date."),
628                              ['download_sha256',
629                               'download_sha256_sig',
630                               'download_tarball',
631                               'verify',
632                               'extract',
633                               'run'])
634             else:
635                 # for some reason the installed tbb is newer than the current version?
636                 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
637
638         # not installed
639         else:
640             print _('TBB is not installed, attempting to install {0}'.format(latest_version))
641             self.set_gui('task', _("Downloading and installing Tor Browser."),
642                          ['download_sha256',
643                           'download_sha256_sig',
644                           'download_tarball',
645                           'verify',
646                           'extract',
647                           'run'])
648
649     # there are different GUIs that might appear, this sets which one we want
650     def set_gui(self, gui, message, tasks, autostart=True):
651         self.gui = gui
652         self.gui_message = message
653         self.gui_tasks = tasks
654         self.gui_task_i = 0
655         self.gui_autostart = autostart
656
657     # set all gtk variables to False
658     def clear_ui(self):
659         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
660             self.box.destroy()
661         self.box = False
662
663         self.label = False
664         self.progressbar = False
665         self.button_box = False
666         self.start_button = False
667         self.exit_button = False
668
669     # build the application's UI
670     def build_ui(self):
671         self.clear_ui()
672
673         self.box = gtk.VBox(False, 20)
674         self.window.add(self.box)
675
676         if 'error' in self.gui:
677             # labels
678             self.label = gtk.Label(self.gui_message)
679             self.label.set_line_wrap(True)
680             self.box.pack_start(self.label, True, True, 0)
681             self.label.show()
682
683             # button box
684             self.button_box = gtk.HButtonBox()
685             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
686             self.box.pack_start(self.button_box, True, True, 0)
687             self.button_box.show()
688
689             if self.gui != 'error':
690                 # yes button
691                 yes_image = gtk.Image()
692                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
693                 self.yes_button = gtk.Button("Yes")
694                 self.yes_button.set_image(yes_image)
695                 if self.gui == 'error_try_stable':
696                     self.yes_button.connect("clicked", self.try_stable, None)
697                 elif self.gui == 'error_try_default_mirror':
698                     self.yes_button.connect("clicked", self.try_default_mirror, None)
699                 elif self.gui == 'error_try_tor':
700                     self.yes_button.connect("clicked", self.try_tor, None)
701                 self.button_box.add(self.yes_button)
702                 self.yes_button.show()
703
704             # exit button
705             exit_image = gtk.Image()
706             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
707             self.exit_button = gtk.Button("Exit")
708             self.exit_button.set_image(exit_image)
709             self.exit_button.connect("clicked", self.destroy, None)
710             self.button_box.add(self.exit_button)
711             self.exit_button.show()
712
713         elif self.gui == 'task':
714             # label
715             self.label = gtk.Label(self.gui_message)
716             self.label.set_line_wrap(True)
717             self.box.pack_start(self.label, True, True, 0)
718             self.label.show()
719
720             # progress bar
721             self.progressbar = gtk.ProgressBar(adjustment=None)
722             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
723             self.progressbar.set_pulse_step(0.01)
724             self.box.pack_start(self.progressbar, True, True, 0)
725
726             # button box
727             self.button_box = gtk.HButtonBox()
728             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
729             self.box.pack_start(self.button_box, True, True, 0)
730             self.button_box.show()
731
732             # start button
733             start_image = gtk.Image()
734             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
735             self.start_button = gtk.Button(_("Start"))
736             self.start_button.set_image(start_image)
737             self.start_button.connect("clicked", self.start, None)
738             self.button_box.add(self.start_button)
739             if not self.gui_autostart:
740                 self.start_button.show()
741
742             # exit button
743             exit_image = gtk.Image()
744             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
745             self.exit_button = gtk.Button(_("Exit"))
746             self.exit_button.set_image(exit_image)
747             self.exit_button.connect("clicked", self.destroy, None)
748             self.button_box.add(self.exit_button)
749             self.exit_button.show()
750
751         self.box.show()
752         self.window.show()
753
754         if self.gui_autostart:
755             self.start(None)
756
757     # start button clicked, begin tasks
758     def start(self, widget, data=None):
759         # disable the start button
760         if self.start_button:
761             self.start_button.set_sensitive(False)
762
763         # start running tasks
764         self.run_task()
765
766     # run the next task in the task list
767     def run_task(self):
768         self.refresh_gtk()
769
770         if self.gui_task_i >= len(self.gui_tasks):
771             self.destroy(False)
772             return
773
774         task = self.gui_tasks[self.gui_task_i]
775
776         # get ready for the next task
777         self.gui_task_i += 1
778
779         print _('Running task: {0}'.format(task))
780         if task == 'download_update_check':
781             print _('Downloading'), self.common.paths['update_check_url']
782             self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
783
784         if task == 'attempt_update':
785             print _('Checking to see if update is needed')
786             self.attempt_update()
787
788         elif task == 'download_sha256':
789             print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
790             self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
791
792         elif task == 'download_sha256_sig':
793             print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
794             self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
795
796         elif task == 'download_tarball':
797             print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
798             self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
799
800         elif task == 'verify':
801             print _('Verifying signature')
802             self.verify()
803
804         elif task == 'extract':
805             print _('Extracting'), self.common.paths['tarball_filename']
806             self.extract()
807
808         elif task == 'run':
809             print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
810             self.run()
811
812         elif task == 'start_over':
813             print _('Starting download over again')
814             self.start_over()
815
816     def response_received(self, response):
817         class FileDownloader(Protocol):
818             def __init__(self, common, file, total, progress, done_cb):
819                 self.file = file
820                 self.total = total
821                 self.so_far = 0
822                 self.progress = progress
823                 self.all_done = done_cb
824
825                 if response.code != 200:
826                     try_stable = False
827
828                     if response.code == 404:
829                         if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
830                             try_stable = True
831
832                     if try_stable:
833                         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?"))
834                     else:
835                         if common.settings['mirror'] != common.default_mirror:
836                             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']))
837                         else:
838                             raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
839
840             def dataReceived(self, bytes):
841                 self.file.write(bytes)
842                 self.so_far += len(bytes)
843                 percent = float(self.so_far) / float(self.total)
844                 self.progress.set_fraction(percent)
845                 amount = float(self.so_far)
846                 units = "bytes"
847                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
848                     if amount > size:
849                         units = unit
850                         amount = amount / float(size)
851                         break
852
853                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
854
855             def connectionLost(self, reason):
856                 print _('Finished receiving body:'), reason.getErrorMessage()
857                 self.all_done(reason)
858
859         dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
860         response.deliverBody(dl)
861
862     def response_finished(self, msg):
863         if msg.check(ResponseDone):
864             self.file_download.close()
865             delattr(self, 'current_download_path')
866
867             # next task!
868             self.run_task()
869
870         else:
871             print "FINISHED", msg
872             ## FIXME handle errors
873
874     def download_error(self, f):
875         print _("Download error:"), f.value, type(f.value)
876
877         if isinstance(f.value, TryStableException):
878             f.trap(TryStableException)
879             self.set_gui('error_try_stable', str(f.value), [], False)
880
881         elif isinstance(f.value, TryDefaultMirrorException):
882             f.trap(TryDefaultMirrorException)
883             self.set_gui('error_try_default_mirror', str(f.value), [], False)
884
885         elif isinstance(f.value, DownloadErrorException):
886             f.trap(DownloadErrorException)
887             self.set_gui('error', str(f.value), [], False)
888
889         elif isinstance(f.value, DNSLookupError):
890             f.trap(DNSLookupError)
891             if common.settings['mirror'] != common.default_mirror:
892                 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)
893             else:
894                 self.set_gui('error', str(f.value), [], False)
895
896         elif isinstance(f.value, ResponseFailed):
897             for reason in f.value.reasons:
898                 if isinstance(reason.value, OpenSSL.SSL.Error):
899                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
900                     if not self.common.settings['update_over_tor']:
901                         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)
902                     else:
903                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
904
905         else:
906             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
907
908         self.build_ui()
909
910     def download(self, name, url, path):
911         # keep track of current download
912         self.current_download_path = path
913
914         # initialize the progress bar
915         mirror_url = url.format(self.common.settings['mirror'])
916         self.progressbar.set_fraction(0)
917         self.progressbar.set_text(_('Downloading {0}').format(name))
918         self.progressbar.show()
919         self.refresh_gtk()
920
921         # default mirror gets certificate pinning, only for requests that use the mirror
922         if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
923             agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
924         else:
925             agent = Agent(reactor)
926
927         # actually, agent needs to follow redirect
928         agent = RedirectAgent(agent)
929
930         # start the request
931         d = agent.request('GET', mirror_url,
932                           Headers({'User-Agent': ['torbrowser-launcher']}),
933                           None)
934
935         self.file_download = open(path, 'w')
936         d.addCallback(self.response_received).addErrback(self.download_error)
937
938         if not reactor.running:
939             reactor.run()
940
941     def try_stable(self, widget, data=None):
942         # change preferred to stable and relaunch TBL
943         self.common.settings['preferred'] = 'stable'
944         self.common.save_settings()
945         p = subprocess.Popen([self.common.paths['tbl_bin']])
946         self.destroy(False)
947
948     def try_default_mirror(self, widget, data=None):
949         # change preferred to stable and relaunch TBL
950         self.common.settings['mirror'] = self.common.default_mirror
951         self.common.save_settings()
952         subprocess.Popen([self.common.paths['tbl_bin']])
953         self.destroy(False)
954
955     def try_tor(self, widget, data=None):
956         # set update_over_tor to true and relaunch TBL
957         self.common.settings['update_over_tor'] = True
958         self.common.save_settings()
959         subprocess.Popen([self.common.paths['tbl_bin']])
960         self.destroy(False)
961
962     def attempt_update(self):
963         # load the update check file
964         try:
965             versions = json.load(open(self.common.paths['update_check_file']))
966             latest_stable = None
967             latest_alpha = None
968
969             # filter linux versions
970             valid_alphas = []
971             valid_stables = []
972             for version in versions:
973                 if '-Linux' in version:
974                     if 'alpha' in version or 'beta' in version or '-rc' in version:
975                         valid_alphas.append(str(version))
976                     else:
977                         valid_stables.append(str(version))
978             valid_alphas.sort()
979             if len(valid_alphas):
980                 latest_alpha = valid_alphas.pop()
981             valid_stables.sort()
982             if len(valid_stables):
983                 latest_stable = valid_stables.pop()
984
985             if latest_stable or latest_alpha:
986                 if latest_stable:
987                     self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
988                 if latest_alpha:
989                     self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
990                 self.common.settings['last_update_check_timestamp'] = int(time.time())
991                 self.common.settings['check_for_updates'] = False
992                 self.common.save_settings()
993                 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
994                 self.start_launcher()
995
996             else:
997                 # failed to find the latest version
998                 self.set_gui('error', _("Error checking for updates."), [], False)
999
1000         except:
1001             # not a valid JSON object
1002             self.set_gui('error', _("Error checking for updates."), [], False)
1003
1004         # now start over
1005         self.clear_ui()
1006         self.build_ui()
1007
1008     def verify(self):
1009         # initialize the progress bar
1010         self.progressbar.set_fraction(0)
1011         self.progressbar.set_text(_('Verifying Signature'))
1012         self.progressbar.show()
1013
1014         verified = False
1015         # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1016         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1017         self.pulse_until_process_exits(p)
1018         if p.returncode == 0:
1019             # compare with sha256 of the tarball
1020             tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1021             for line in open(self.common.paths['sha256_file'], 'r').readlines():
1022                 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1023                     verified = True
1024
1025         if verified:
1026             self.run_task()
1027         else:
1028             # TODO: add the ability to report attack by posting bug to trac.torproject.org
1029             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)
1030             self.clear_ui()
1031             self.build_ui()
1032
1033             if not reactor.running:
1034                 reactor.run()
1035
1036     def extract(self):
1037         # initialize the progress bar
1038         self.progressbar.set_fraction(0)
1039         self.progressbar.set_text(_('Installing'))
1040         self.progressbar.show()
1041         self.refresh_gtk()
1042
1043         extracted = False
1044         try:
1045             if self.common.paths['tarball_file'][-2:] == 'xz':
1046                 # if tarball is .tar.xz
1047                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1048                 tf = tarfile.open(fileobj=xz)
1049                 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1050                 extracted = True
1051             else:
1052                 # if tarball is .tar.gz
1053                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1054                     tf = tarfile.open(self.common.paths['tarball_file'])
1055                     tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1056                     extracted = True
1057         except:
1058             pass
1059
1060         if not extracted:
1061             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1062             self.clear_ui()
1063             self.build_ui()
1064             return
1065
1066         # installation is finished, so save installed_version
1067         self.common.settings['installed_version'] = self.common.settings['latest_version']
1068         self.common.save_settings()
1069
1070         self.run_task()
1071
1072     def run(self, run_next_task=True):
1073         subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1074
1075         # play modem sound?
1076         if self.common.settings['modem_sound']:
1077             try:
1078                 import pygame
1079                 pygame.mixer.init()
1080                 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1081                 sound.play()
1082                 time.sleep(10)
1083             except ImportError:
1084                 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-pygame package is missing, the modem sound is unavailable."))
1085                 md.set_position(gtk.WIN_POS_CENTER)
1086                 md.run()
1087                 md.destroy()
1088
1089
1090         if run_next_task:
1091             self.run_task()
1092
1093     # make the progress bar pulse until process p (a Popen object) finishes
1094     def pulse_until_process_exits(self, p):
1095         while p.poll() is None:
1096             time.sleep(0.01)
1097             self.progressbar.pulse()
1098             self.refresh_gtk()
1099
1100     # start over and download TBB again
1101     def start_over(self):
1102         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1103         self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1104         self.gui_task_i = 0
1105         self.start(None)
1106
1107     # refresh gtk
1108     def refresh_gtk(self):
1109         while gtk.events_pending():
1110             gtk.main_iteration(False)
1111
1112     # exit
1113     def delete_event(self, widget, event, data=None):
1114         return False
1115
1116     def destroy(self, widget, data=None):
1117         if hasattr(self, 'file_download'):
1118             self.file_download.close()
1119         if hasattr(self, 'current_download_path'):
1120             os.remove(self.current_download_path)
1121             delattr(self, 'current_download_path')
1122         if reactor.running:
1123             reactor.stop()
1124
1125 if __name__ == "__main__":
1126     tor_browser_launcher_version = open('/usr/share/torbrowser-launcher/version').read().strip()
1127
1128     print _('Tor Browser Launcher')
1129     print _('By Micah Lee, licensed under GPLv3')
1130     print _('version {0}').format(tor_browser_launcher_version)
1131     print 'https://github.com/micahflee/torbrowser-launcher'
1132
1133     common = TBLCommon(tor_browser_launcher_version)
1134
1135     # is torbrowser-launcher already running?
1136     tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1137     if tbl_pid:
1138         print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1139         common.bring_window_to_front(tbl_pid)
1140         sys.exit()
1141
1142     if '-settings' in sys.argv:
1143         # settings mode
1144         app = TBLSettings(common)
1145
1146     else:
1147         # launcher mode
1148         app = TBLLauncher(common)