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